blob: 3dee853240a6bb04d207c8cbff36d829552f2f07 [file] [log] [blame]
/*
* Copyright (C) 2018 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.textclassifier;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.pm.PackageManager;
import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Process;
import android.os.RemoteException;
import android.os.UserHandle;
import android.provider.DeviceConfig;
import android.service.textclassifier.ITextClassifierCallback;
import android.service.textclassifier.ITextClassifierService;
import android.service.textclassifier.TextClassifierService;
import android.service.textclassifier.TextClassifierService.ConnectionState;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.Slog;
import android.util.SparseArray;
import android.view.textclassifier.ConversationActions;
import android.view.textclassifier.SelectionEvent;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationConstants;
import android.view.textclassifier.TextClassificationContext;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassificationSessionId;
import android.view.textclassifier.TextClassifierEvent;
import android.view.textclassifier.TextLanguage;
import android.view.textclassifier.TextLinks;
import android.view.textclassifier.TextSelection;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.util.DumpUtils;
import com.android.internal.util.FunctionalUtils;
import com.android.internal.util.FunctionalUtils.ThrowingConsumer;
import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
import com.android.internal.util.IndentingPrintWriter;
import com.android.internal.util.Preconditions;
import com.android.server.SystemService;
import java.io.FileDescriptor;
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.Objects;
import java.util.Queue;
/**
* A manager for TextClassifier services.
* Apps bind to the TextClassificationManagerService for text classification. This service
* reroutes calls to it to a {@link TextClassifierService} that it manages.
*/
public final class TextClassificationManagerService extends ITextClassifierService.Stub {
private static final String LOG_TAG = "TextClassificationManagerService";
private static final ITextClassifierCallback NO_OP_CALLBACK = new ITextClassifierCallback() {
@Override
public void onSuccess(Bundle result) {}
@Override
public void onFailure() {}
@Override
public IBinder asBinder() {
return null;
}
};
public static final class Lifecycle extends SystemService {
private final TextClassificationManagerService mManagerService;
public Lifecycle(Context context) {
super(context);
mManagerService = new TextClassificationManagerService(context);
}
@Override
public void onStart() {
try {
publishBinderService(Context.TEXT_CLASSIFICATION_SERVICE, mManagerService);
mManagerService.startListenSettings();
} catch (Throwable t) {
// Starting this service is not critical to the running of this device and should
// therefore not crash the device. If it fails, log the error and continue.
Slog.e(LOG_TAG, "Could not start the TextClassificationManagerService.", t);
}
}
@Override
public void onStartUser(int userId) {
processAnyPendingWork(userId);
}
@Override
public void onUnlockUser(int userId) {
// Rebind if we failed earlier due to locked encrypted user
processAnyPendingWork(userId);
}
private void processAnyPendingWork(int userId) {
synchronized (mManagerService.mLock) {
mManagerService.getUserStateLocked(userId).bindIfHasPendingRequestsLocked();
}
}
@Override
public void onStopUser(int userId) {
synchronized (mManagerService.mLock) {
UserState userState = mManagerService.peekUserStateLocked(userId);
if (userState != null) {
if (userState.mConnection != null) {
userState.mConnection.cleanupService();
}
mManagerService.mUserStates.remove(userId);
}
}
}
}
private final TextClassifierSettingsListener mSettingsListener;
private final Context mContext;
private final Object mLock;
@GuardedBy("mLock")
final SparseArray<UserState> mUserStates = new SparseArray<>();
@GuardedBy("mLock")
private final Map<TextClassificationSessionId, Integer> mSessionUserIds = new ArrayMap<>();
@GuardedBy("mLock")
private TextClassificationConstants mSettings;
private TextClassificationManagerService(Context context) {
mContext = Objects.requireNonNull(context);
mLock = new Object();
mSettingsListener = new TextClassifierSettingsListener(mContext);
}
private void startListenSettings() {
mSettingsListener.registerObserver();
}
@Override
public void onConnectedStateChanged(@ConnectionState int connected) {
}
@Override
public void onSuggestSelection(
@Nullable TextClassificationSessionId sessionId,
TextSelection.Request request, ITextClassifierCallback callback)
throws RemoteException {
Objects.requireNonNull(request);
handleRequest(
request.getUserId(),
request.getCallingPackageName(),
/* attemptToBind= */ true,
service -> service.onSuggestSelection(sessionId, request, callback),
"onSuggestSelection",
callback);
}
@Override
public void onClassifyText(
@Nullable TextClassificationSessionId sessionId,
TextClassification.Request request, ITextClassifierCallback callback)
throws RemoteException {
Objects.requireNonNull(request);
handleRequest(
request.getUserId(),
request.getCallingPackageName(),
/* attemptToBind= */ true,
service -> service.onClassifyText(sessionId, request, callback),
"onClassifyText",
callback);
}
@Override
public void onGenerateLinks(
@Nullable TextClassificationSessionId sessionId,
TextLinks.Request request, ITextClassifierCallback callback)
throws RemoteException {
Objects.requireNonNull(request);
handleRequest(
request.getUserId(),
request.getCallingPackageName(),
/* attemptToBind= */ true,
service -> service.onGenerateLinks(sessionId, request, callback),
"onGenerateLinks",
callback);
}
@Override
public void onSelectionEvent(
@Nullable TextClassificationSessionId sessionId, SelectionEvent event)
throws RemoteException {
Objects.requireNonNull(event);
handleRequest(
event.getUserId(),
event.getPackageName(),
/* attemptToBind= */ false,
service -> service.onSelectionEvent(sessionId, event),
"onSelectionEvent",
NO_OP_CALLBACK);
}
@Override
public void onTextClassifierEvent(
@Nullable TextClassificationSessionId sessionId,
TextClassifierEvent event) throws RemoteException {
Objects.requireNonNull(event);
final String packageName = event.getEventContext() == null
? null
: event.getEventContext().getPackageName();
final int userId = event.getEventContext() == null
? UserHandle.getCallingUserId()
: event.getEventContext().getUserId();
handleRequest(
userId,
packageName,
/* attemptToBind= */ false,
service -> service.onTextClassifierEvent(sessionId, event),
"onTextClassifierEvent",
NO_OP_CALLBACK);
}
@Override
public void onDetectLanguage(
@Nullable TextClassificationSessionId sessionId,
TextLanguage.Request request,
ITextClassifierCallback callback) throws RemoteException {
Objects.requireNonNull(request);
handleRequest(
request.getUserId(),
request.getCallingPackageName(),
/* attemptToBind= */ true,
service -> service.onDetectLanguage(sessionId, request, callback),
"onDetectLanguage",
callback);
}
@Override
public void onSuggestConversationActions(
@Nullable TextClassificationSessionId sessionId,
ConversationActions.Request request,
ITextClassifierCallback callback) throws RemoteException {
Objects.requireNonNull(request);
handleRequest(
request.getUserId(),
request.getCallingPackageName(),
/* attemptToBind= */ true,
service -> service.onSuggestConversationActions(sessionId, request, callback),
"onSuggestConversationActions",
callback);
}
@Override
public void onCreateTextClassificationSession(
TextClassificationContext classificationContext, TextClassificationSessionId sessionId)
throws RemoteException {
Objects.requireNonNull(sessionId);
Objects.requireNonNull(classificationContext);
final int userId = classificationContext.getUserId();
handleRequest(
userId,
classificationContext.getPackageName(),
/* attemptToBind= */ false,
service -> {
service.onCreateTextClassificationSession(classificationContext, sessionId);
mSessionUserIds.put(sessionId, userId);
},
"onCreateTextClassificationSession",
NO_OP_CALLBACK);
}
@Override
public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId)
throws RemoteException {
Objects.requireNonNull(sessionId);
synchronized (mLock) {
final int userId = mSessionUserIds.containsKey(sessionId)
? mSessionUserIds.get(sessionId)
: UserHandle.getCallingUserId();
handleRequest(
userId,
/* callingPackageName= */ null,
/* attemptToBind= */ false,
service -> {
service.onDestroyTextClassificationSession(sessionId);
mSessionUserIds.remove(sessionId);
},
"onDestroyTextClassificationSession",
NO_OP_CALLBACK);
}
}
@GuardedBy("mLock")
private UserState getUserStateLocked(int userId) {
UserState result = mUserStates.get(userId);
if (result == null) {
result = new UserState(userId, mContext, mLock);
mUserStates.put(userId, result);
}
return result;
}
@GuardedBy("mLock")
UserState peekUserStateLocked(int userId) {
return mUserStates.get(userId);
}
@Override
protected void dump(FileDescriptor fd, PrintWriter fout, String[] args) {
if (!DumpUtils.checkDumpPermission(mContext, LOG_TAG, fout)) return;
IndentingPrintWriter pw = new IndentingPrintWriter(fout, " ");
// Create a TCM instance with the system server identity. TCM creates a ContentObserver
// to listen for settings changes. It does not pass the checkContentProviderAccess check
// if we are using the shell identity, because AMS does not track of processes spawn from
// shell.
Binder.withCleanCallingIdentity(
() -> mContext.getSystemService(TextClassificationManager.class).dump(pw));
pw.printPair("context", mContext);
pw.println();
synchronized (mLock) {
int size = mUserStates.size();
pw.print("Number user states: "); pw.println(size);
if (size > 0) {
for (int i = 0; i < size; i++) {
pw.increaseIndent();
UserState userState = mUserStates.valueAt(i);
pw.print(i); pw.print(":"); userState.dump(pw); pw.println();
pw.decreaseIndent();
}
}
pw.println("Number of active sessions: " + mSessionUserIds.size());
}
}
private void handleRequest(
@UserIdInt int userId,
@Nullable String callingPackageName,
boolean attemptToBind,
@NonNull ThrowingConsumer<ITextClassifierService> textClassifierServiceConsumer,
@NonNull String methodName,
@NonNull ITextClassifierCallback callback)
throws RemoteException {
Objects.requireNonNull(textClassifierServiceConsumer);
Objects.requireNonNull(methodName);
Objects.requireNonNull(callback);
validateInput(mContext, callingPackageName, userId);
synchronized (mLock) {
UserState userState = getUserStateLocked(userId);
if (attemptToBind && !userState.bindLocked()) {
Slog.d(LOG_TAG, "Unable to bind TextClassifierService at " + methodName);
callback.onFailure();
} else if (userState.isBoundLocked()) {
if (!userState.checkRequestAcceptedLocked(Binder.getCallingUid(), methodName)) {
return;
}
textClassifierServiceConsumer.accept(userState.mService);
} else {
userState.mPendingRequests.add(
new PendingRequest(
methodName,
() -> textClassifierServiceConsumer.accept(userState.mService),
callback::onFailure, callback.asBinder(),
this,
userState,
Binder.getCallingUid()));
}
}
}
private void unbindServiceIfNecessary() {
final ComponentName serviceComponentName =
TextClassifierService.getServiceComponentName(mContext);
if (serviceComponentName == null) {
// It should not occur if we had defined default service names in config.xml
Slog.w(LOG_TAG, "No default configured system TextClassifierService.");
return;
}
synchronized (mLock) {
final int size = mUserStates.size();
for (int i = 0; i < size; i++) {
UserState userState = mUserStates.valueAt(i);
// Only unbind for a new service
if (userState.isCurrentlyBoundToComponentLocked(serviceComponentName)) {
return;
}
if (userState.isBoundLocked()) {
userState.unbindLocked();
}
}
}
}
private static final class PendingRequest implements IBinder.DeathRecipient {
private final int mUid;
@Nullable private final String mName;
@Nullable private final IBinder mBinder;
@NonNull private final Runnable mRequest;
@Nullable private final Runnable mOnServiceFailure;
@GuardedBy("mLock")
@NonNull private final UserState mOwningUser;
@NonNull private final TextClassificationManagerService mService;
/**
* Initializes a new pending request.
* @param request action to perform when the service is bound
* @param onServiceFailure action to perform when the service dies or disconnects
* @param binder binder to the process that made this pending request
* @param service
* @param owningUser
*/
PendingRequest(@Nullable String name,
@NonNull ThrowingRunnable request, @Nullable ThrowingRunnable onServiceFailure,
@Nullable IBinder binder,
TextClassificationManagerService service,
UserState owningUser, int uid) {
mName = name;
mRequest =
logOnFailure(Objects.requireNonNull(request), "handling pending request");
mOnServiceFailure =
logOnFailure(onServiceFailure, "notifying callback of service failure");
mBinder = binder;
mService = service;
mOwningUser = owningUser;
if (mBinder != null) {
try {
mBinder.linkToDeath(this, 0);
} catch (RemoteException e) {
e.printStackTrace();
}
}
mUid = uid;
}
@Override
public void binderDied() {
synchronized (mService.mLock) {
// No need to handle this pending request anymore. Remove.
removeLocked();
}
}
@GuardedBy("mLock")
private void removeLocked() {
mOwningUser.mPendingRequests.remove(this);
if (mBinder != null) {
mBinder.unlinkToDeath(this, 0);
}
}
}
private static Runnable logOnFailure(@Nullable ThrowingRunnable r, String opDesc) {
if (r == null) return null;
return FunctionalUtils.handleExceptions(r,
e -> Slog.d(LOG_TAG, "Error " + opDesc + ": " + e.getMessage()));
}
private static void validateInput(
Context context, @Nullable String packageName, @UserIdInt int userId)
throws RemoteException {
try {
if (packageName != null) {
final int packageUid = context.getPackageManager()
.getPackageUidAsUser(packageName, UserHandle.getCallingUserId());
final int callingUid = Binder.getCallingUid();
Preconditions.checkArgument(callingUid == packageUid
// Trust the system process:
|| callingUid == android.os.Process.SYSTEM_UID,
"Invalid package name. Package=" + packageName
+ ", CallingUid=" + callingUid);
}
Preconditions.checkArgument(userId != UserHandle.USER_NULL, "Null userId");
final int callingUserId = UserHandle.getCallingUserId();
if (callingUserId != userId) {
context.enforceCallingOrSelfPermission(
android.Manifest.permission.INTERACT_ACROSS_USERS_FULL,
"Invalid userId. UserId=" + userId + ", CallingUserId=" + callingUserId);
}
} catch (Exception e) {
throw new RemoteException("Invalid request: " + e.getMessage(), e,
/* enableSuppression */ true, /* writableStackTrace */ true);
}
}
private final class UserState {
@UserIdInt final int mUserId;
@GuardedBy("mLock")
TextClassifierServiceConnection mConnection = null;
@GuardedBy("mLock")
final Queue<PendingRequest> mPendingRequests = new ArrayDeque<>();
@GuardedBy("mLock")
ITextClassifierService mService;
@GuardedBy("mLock")
boolean mBinding;
@GuardedBy("mLock")
ComponentName mBoundComponentName = null;
@GuardedBy("mLock")
boolean mBoundToDefaultTrustService;
@GuardedBy("mLock")
int mBoundServiceUid;
private final Context mContext;
private final Object mLock;
private UserState(int userId, Context context, Object lock) {
mUserId = userId;
mContext = Objects.requireNonNull(context);
mLock = Objects.requireNonNull(lock);
}
@GuardedBy("mLock")
boolean isBoundLocked() {
return mService != null;
}
@GuardedBy("mLock")
private void handlePendingRequestsLocked() {
PendingRequest request;
while ((request = mPendingRequests.poll()) != null) {
if (isBoundLocked()) {
if (!checkRequestAcceptedLocked(request.mUid, request.mName)) {
return;
}
request.mRequest.run();
} else {
if (request.mOnServiceFailure != null) {
Slog.d(LOG_TAG, "Unable to bind TextClassifierService for PendingRequest "
+ request.mName);
request.mOnServiceFailure.run();
}
}
if (request.mBinder != null) {
request.mBinder.unlinkToDeath(request, 0);
}
}
}
@GuardedBy("mLock")
private boolean bindIfHasPendingRequestsLocked() {
return !mPendingRequests.isEmpty() && bindLocked();
}
@GuardedBy("mLock")
private boolean isCurrentlyBoundToComponentLocked(@NonNull ComponentName componentName) {
return (mBoundComponentName != null
&& mBoundComponentName.getPackageName().equals(
componentName.getPackageName()));
}
@GuardedBy("mLock")
private void unbindLocked() {
Slog.v(LOG_TAG, "unbinding from " + mBoundComponentName + " for " + mUserId);
mContext.unbindService(mConnection);
mConnection.cleanupService();
mConnection = null;
}
/**
* @return true if the service is bound or in the process of being bound.
* Returns false otherwise.
*/
@GuardedBy("mLock")
private boolean bindLocked() {
if (isBoundLocked() || mBinding) {
return true;
}
// TODO: Handle bind timeout.
final boolean willBind;
final long identity = Binder.clearCallingIdentity();
try {
final ComponentName componentName =
TextClassifierService.getServiceComponentName(mContext);
if (componentName == null) {
// Might happen if the storage is encrypted and the user is not unlocked
return false;
}
Intent serviceIntent = new Intent(TextClassifierService.SERVICE_INTERFACE)
.setComponent(componentName);
Slog.d(LOG_TAG, "Binding to " + serviceIntent.getComponent());
mConnection = new TextClassifierServiceConnection(mUserId);
willBind = mContext.bindServiceAsUser(
serviceIntent, mConnection,
Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE
| Context.BIND_RESTRICT_ASSOCIATIONS,
UserHandle.of(mUserId));
mBinding = willBind;
} finally {
Binder.restoreCallingIdentity(identity);
}
return willBind;
}
private void dump(IndentingPrintWriter pw) {
pw.printPair("context", mContext);
pw.printPair("userId", mUserId);
synchronized (mLock) {
pw.printPair("boundComponentName", mBoundComponentName);
pw.printPair("boundToDefaultTrustService", mBoundToDefaultTrustService);
pw.printPair("boundServiceUid", mBoundServiceUid);
pw.printPair("binding", mBinding);
pw.printPair("numberRequests", mPendingRequests.size());
}
}
@GuardedBy("mLock")
private boolean checkRequestAcceptedLocked(int requestUid, @NonNull String methodName) {
if (mBoundToDefaultTrustService || (requestUid == mBoundServiceUid)) {
return true;
}
Slog.w(LOG_TAG, String.format(
"[%s] Non-default TextClassifierServices may only see text from the same uid.",
methodName));
return false;
}
private boolean isDefaultTrustService(@NonNull ComponentName currentService) {
final String[] defaultServiceNames =
mContext.getPackageManager().getSystemTextClassifierPackages();
final String servicePackageName = currentService.getPackageName();
for (int i = 0; i < defaultServiceNames.length; i++) {
if (defaultServiceNames[i].equals(servicePackageName)) {
return true;
}
}
return false;
}
private int getServiceUid(@Nullable ComponentName service, int userId) {
if (service == null) {
return Process.INVALID_UID;
}
final String servicePackageName = service.getPackageName();
final PackageManager pm = mContext.getPackageManager();
final int serviceUid;
try {
serviceUid = pm.getPackageUidAsUser(servicePackageName, userId);
} catch (PackageManager.NameNotFoundException e) {
Slog.e(LOG_TAG, "Could not verify UID for " + service);
return Process.INVALID_UID;
}
return serviceUid;
}
@GuardedBy("mLock")
private void updateServiceInfoLocked(int userId, @Nullable ComponentName componentName) {
mBoundComponentName = componentName;
mBoundToDefaultTrustService = (mBoundComponentName != null && isDefaultTrustService(
mBoundComponentName));
mBoundServiceUid = getServiceUid(mBoundComponentName, userId);
}
private final class TextClassifierServiceConnection implements ServiceConnection {
@UserIdInt private final int mUserId;
TextClassifierServiceConnection(int userId) {
mUserId = userId;
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
final ITextClassifierService tcService = ITextClassifierService.Stub.asInterface(
service);
try {
tcService.onConnectedStateChanged(TextClassifierService.CONNECTED);
} catch (RemoteException e) {
Slog.e(LOG_TAG, "error in onConnectedStateChanged");
}
init(tcService, name);
}
@Override
public void onServiceDisconnected(ComponentName name) {
cleanupService();
}
@Override
public void onBindingDied(ComponentName name) {
cleanupService();
}
@Override
public void onNullBinding(ComponentName name) {
cleanupService();
}
void cleanupService() {
init(/* service */ null, /* name */ null);
}
private void init(@Nullable ITextClassifierService service,
@Nullable ComponentName name) {
synchronized (mLock) {
mService = service;
mBinding = false;
updateServiceInfoLocked(mUserId, name);
handlePendingRequestsLocked();
}
}
}
}
private final class TextClassifierSettingsListener implements
DeviceConfig.OnPropertiesChangedListener {
@NonNull private final Context mContext;
@NonNull private final TextClassificationConstants mSettings;
@Nullable private String mServicePackageName;
TextClassifierSettingsListener(Context context) {
mContext = context;
mSettings = TextClassificationManager.getSettings(mContext);
mServicePackageName = mSettings.getTextClassifierServicePackageOverride();
}
public void registerObserver() {
DeviceConfig.addOnPropertiesChangedListener(
DeviceConfig.NAMESPACE_TEXTCLASSIFIER,
mContext.getMainExecutor(),
this);
}
@Override
public void onPropertiesChanged(DeviceConfig.Properties properties) {
final String overrideServiceName = mSettings.getTextClassifierServicePackageOverride();
if (TextUtils.equals(overrideServiceName, mServicePackageName)) {
return;
}
mServicePackageName = overrideServiceName;
unbindServiceIfNecessary();
}
}
}