blob: 5442e5b0413f6e6040e9c9e54f4461398c121430 [file] [log] [blame]
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.server.speech;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.app.AppGlobals;
import android.content.AttributionSource;
import android.content.ComponentName;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
import android.content.pm.ServiceInfo;
import android.os.Binder;
import android.os.IBinder;
import android.os.RemoteException;
import android.permission.PermissionManager;
import android.speech.IRecognitionListener;
import android.speech.IRecognitionService;
import android.speech.IRecognitionServiceManagerCallback;
import android.speech.RecognitionService;
import android.speech.SpeechRecognizer;
import android.util.Slog;
import com.android.internal.annotations.GuardedBy;
import com.android.server.infra.AbstractPerUserSystemService;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
final class SpeechRecognitionManagerServiceImpl extends
AbstractPerUserSystemService<SpeechRecognitionManagerServiceImpl,
SpeechRecognitionManagerService> {
private static final String TAG = SpeechRecognitionManagerServiceImpl.class.getSimpleName();
private static final int MAX_CONCURRENT_CONNECTIONS_BY_CLIENT = 10;
private final Object mLock = new Object();
@NonNull
@GuardedBy("mLock")
private final Map<Integer, Set<RemoteSpeechRecognitionService>> mRemoteServicesByUid =
new HashMap<>();
SpeechRecognitionManagerServiceImpl(
@NonNull SpeechRecognitionManagerService master,
@NonNull Object lock, @UserIdInt int userId) {
super(master, lock, userId);
}
@GuardedBy("mLock")
@Override // from PerUserSystemService
protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent)
throws PackageManager.NameNotFoundException {
try {
return AppGlobals.getPackageManager().getServiceInfo(serviceComponent,
PackageManager.GET_META_DATA, mUserId);
} catch (RemoteException e) {
throw new PackageManager.NameNotFoundException(
"Could not get service for " + serviceComponent);
}
}
@GuardedBy("mLock")
@Override // from PerUserSystemService
protected boolean updateLocked(boolean disabled) {
final boolean enabledChanged = super.updateLocked(disabled);
return enabledChanged;
}
void createSessionLocked(
ComponentName componentName,
IBinder clientToken,
boolean onDevice,
IRecognitionServiceManagerCallback callback) {
if (mMaster.debug) {
Slog.i(TAG, String.format("#createSessionLocked, component=%s, onDevice=%s",
componentName, onDevice));
}
ComponentName serviceComponent = componentName;
if (onDevice) {
serviceComponent = getOnDeviceComponentNameLocked();
}
if (serviceComponent == null) {
if (mMaster.debug) {
Slog.i(TAG, "Service component is undefined, responding with error.");
}
tryRespondWithError(callback, SpeechRecognizer.ERROR_CLIENT);
return;
}
final int creatorCallingUid = Binder.getCallingUid();
RemoteSpeechRecognitionService service = createService(creatorCallingUid, serviceComponent);
if (service == null) {
tryRespondWithError(callback, SpeechRecognizer.ERROR_TOO_MANY_REQUESTS);
return;
}
IBinder.DeathRecipient deathRecipient =
() -> handleClientDeath(creatorCallingUid, service, true /* invoke #cancel */);
try {
clientToken.linkToDeath(deathRecipient, 0);
} catch (RemoteException e) {
// RemoteException == binder already died, schedule disconnect anyway.
handleClientDeath(creatorCallingUid, service, true /* invoke #cancel */);
return;
}
service.connect().thenAccept(binderService -> {
if (binderService != null) {
try {
callback.onSuccess(new IRecognitionService.Stub() {
@Override
public void startListening(
Intent recognizerIntent,
IRecognitionListener listener,
@NonNull AttributionSource attributionSource)
throws RemoteException {
attributionSource.enforceCallingUid();
if (!attributionSource.isTrusted(mMaster.getContext())) {
attributionSource = mMaster.getContext()
.getSystemService(PermissionManager.class)
.registerAttributionSource(attributionSource);
}
service.startListening(recognizerIntent, listener, attributionSource);
}
@Override
public void stopListening(
IRecognitionListener listener) throws RemoteException {
service.stopListening(listener);
}
@Override
public void cancel(
IRecognitionListener listener,
boolean isShutdown) throws RemoteException {
service.cancel(listener, isShutdown);
if (isShutdown) {
handleClientDeath(
creatorCallingUid,
service,
false /* invoke #cancel */);
clientToken.unlinkToDeath(deathRecipient, 0);
}
}
});
} catch (RemoteException e) {
Slog.e(TAG, "Error creating a speech recognition session", e);
tryRespondWithError(callback, SpeechRecognizer.ERROR_CLIENT);
}
} else {
tryRespondWithError(callback, SpeechRecognizer.ERROR_CLIENT);
}
});
}
private void handleClientDeath(
int callingUid,
RemoteSpeechRecognitionService service, boolean invokeCancel) {
if (invokeCancel) {
service.shutdown();
}
removeService(callingUid, service);
}
@GuardedBy("mLock")
@Nullable
private ComponentName getOnDeviceComponentNameLocked() {
final String serviceName = getComponentNameLocked();
if (mMaster.debug) {
Slog.i(TAG, "Resolved component name: " + serviceName);
}
if (serviceName == null) {
if (mMaster.verbose) {
Slog.v(TAG, "ensureRemoteServiceLocked(): no service component name.");
}
return null;
}
return ComponentName.unflattenFromString(serviceName);
}
private RemoteSpeechRecognitionService createService(
int callingUid, ComponentName serviceComponent) {
synchronized (mLock) {
Set<RemoteSpeechRecognitionService> servicesForClient =
mRemoteServicesByUid.get(callingUid);
if (servicesForClient != null
&& servicesForClient.size() >= MAX_CONCURRENT_CONNECTIONS_BY_CLIENT) {
return null;
}
if (servicesForClient != null) {
Optional<RemoteSpeechRecognitionService> existingService =
servicesForClient
.stream()
.filter(service ->
service.getServiceComponentName().equals(serviceComponent))
.findFirst();
if (existingService.isPresent()) {
if (mMaster.debug) {
Slog.i(TAG, "Reused existing connection to " + serviceComponent);
}
return existingService.get();
}
}
if (serviceComponent != null && !componentMapsToRecognitionService(serviceComponent)) {
return null;
}
RemoteSpeechRecognitionService service =
new RemoteSpeechRecognitionService(
getContext(), serviceComponent, getUserId(), callingUid);
Set<RemoteSpeechRecognitionService> valuesByCaller =
mRemoteServicesByUid.computeIfAbsent(callingUid, key -> new HashSet<>());
valuesByCaller.add(service);
if (mMaster.debug) {
Slog.i(TAG, "Creating a new connection to " + serviceComponent);
}
return service;
}
}
private boolean componentMapsToRecognitionService(@NonNull ComponentName serviceComponent) {
List<ResolveInfo> resolveInfos =
getContext().getPackageManager().queryIntentServicesAsUser(
new Intent(RecognitionService.SERVICE_INTERFACE), 0, getUserId());
if (resolveInfos == null) {
return false;
}
for (ResolveInfo ri : resolveInfos) {
if (ri.serviceInfo != null
&& serviceComponent.equals(ri.serviceInfo.getComponentName())) {
return true;
}
}
Slog.w(TAG, "serviceComponent is not RecognitionService: " + serviceComponent);
return false;
}
private void removeService(int callingUid, RemoteSpeechRecognitionService service) {
synchronized (mLock) {
Set<RemoteSpeechRecognitionService> valuesByCaller =
mRemoteServicesByUid.get(callingUid);
if (valuesByCaller != null) {
valuesByCaller.remove(service);
}
}
}
private static void tryRespondWithError(IRecognitionServiceManagerCallback callback,
int errorCode) {
try {
callback.onError(errorCode);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to respond with error");
}
}
}