/* * 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.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; import android.service.textclassifier.ITextClassifierCallback; import android.service.textclassifier.ITextClassifierService; import android.service.textclassifier.TextClassifierService; 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.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.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.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"; 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); } 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) { userState.mConnection.cleanupService(); mManagerService.mUserStates.remove(userId); } } } } private final Context mContext; private final Object mLock; @GuardedBy("mLock") final SparseArray mUserStates = new SparseArray<>(); @GuardedBy("mLock") private final Map mSessionUserIds = new ArrayMap<>(); private TextClassificationManagerService(Context context) { mContext = Preconditions.checkNotNull(context); mLock = new Object(); } @Override public void onSuggestSelection( @Nullable TextClassificationSessionId sessionId, TextSelection.Request request, ITextClassifierCallback callback) throws RemoteException { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); final int userId = request.getUserId(); validateInput(mContext, request.getCallingPackageName(), userId); synchronized (mLock) { UserState userState = getUserStateLocked(userId); if (!userState.bindLocked()) { callback.onFailure(); } else if (userState.isBoundLocked()) { userState.mService.onSuggestSelection(sessionId, request, callback); } else { userState.mPendingRequests.add(new PendingRequest( () -> onSuggestSelection(sessionId, request, callback), callback::onFailure, callback.asBinder(), this, userState)); } } } @Override public void onClassifyText( @Nullable TextClassificationSessionId sessionId, TextClassification.Request request, ITextClassifierCallback callback) throws RemoteException { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); final int userId = request.getUserId(); validateInput(mContext, request.getCallingPackageName(), userId); synchronized (mLock) { UserState userState = getUserStateLocked(userId); if (!userState.bindLocked()) { callback.onFailure(); } else if (userState.isBoundLocked()) { userState.mService.onClassifyText(sessionId, request, callback); } else { userState.mPendingRequests.add(new PendingRequest( () -> onClassifyText(sessionId, request, callback), callback::onFailure, callback.asBinder(), this, userState)); } } } @Override public void onGenerateLinks( @Nullable TextClassificationSessionId sessionId, TextLinks.Request request, ITextClassifierCallback callback) throws RemoteException { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); final int userId = request.getUserId(); validateInput(mContext, request.getCallingPackageName(), userId); synchronized (mLock) { UserState userState = getUserStateLocked(userId); if (!userState.bindLocked()) { callback.onFailure(); } else if (userState.isBoundLocked()) { userState.mService.onGenerateLinks(sessionId, request, callback); } else { userState.mPendingRequests.add(new PendingRequest( () -> onGenerateLinks(sessionId, request, callback), callback::onFailure, callback.asBinder(), this, userState)); } } } @Override public void onSelectionEvent( @Nullable TextClassificationSessionId sessionId, SelectionEvent event) throws RemoteException { Preconditions.checkNotNull(event); final int userId = event.getUserId(); validateInput(mContext, event.getPackageName(), userId); synchronized (mLock) { UserState userState = getUserStateLocked(userId); if (userState.isBoundLocked()) { userState.mService.onSelectionEvent(sessionId, event); } else { userState.mPendingRequests.add(new PendingRequest( () -> onSelectionEvent(sessionId, event), null /* onServiceFailure */, null /* binder */, this, userState)); } } } @Override public void onTextClassifierEvent( @Nullable TextClassificationSessionId sessionId, TextClassifierEvent event) throws RemoteException { Preconditions.checkNotNull(event); final String packageName = event.getEventContext() == null ? null : event.getEventContext().getPackageName(); final int userId = event.getEventContext() == null ? UserHandle.getCallingUserId() : event.getEventContext().getUserId(); validateInput(mContext, packageName, userId); synchronized (mLock) { UserState userState = getUserStateLocked(userId); if (userState.isBoundLocked()) { userState.mService.onTextClassifierEvent(sessionId, event); } else { userState.mPendingRequests.add(new PendingRequest( () -> onTextClassifierEvent(sessionId, event), null /* onServiceFailure */, null /* binder */, this, userState)); } } } @Override public void onDetectLanguage( @Nullable TextClassificationSessionId sessionId, TextLanguage.Request request, ITextClassifierCallback callback) throws RemoteException { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); final int userId = request.getUserId(); validateInput(mContext, request.getCallingPackageName(), userId); synchronized (mLock) { UserState userState = getUserStateLocked(userId); if (!userState.bindLocked()) { callback.onFailure(); } else if (userState.isBoundLocked()) { userState.mService.onDetectLanguage(sessionId, request, callback); } else { userState.mPendingRequests.add(new PendingRequest( () -> onDetectLanguage(sessionId, request, callback), callback::onFailure, callback.asBinder(), this, userState)); } } } @Override public void onSuggestConversationActions( @Nullable TextClassificationSessionId sessionId, ConversationActions.Request request, ITextClassifierCallback callback) throws RemoteException { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); final int userId = request.getUserId(); validateInput(mContext, request.getCallingPackageName(), userId); synchronized (mLock) { UserState userState = getUserStateLocked(userId); if (!userState.bindLocked()) { callback.onFailure(); } else if (userState.isBoundLocked()) { userState.mService.onSuggestConversationActions(sessionId, request, callback); } else { userState.mPendingRequests.add(new PendingRequest( () -> onSuggestConversationActions(sessionId, request, callback), callback::onFailure, callback.asBinder(), this, userState)); } } } @Override public void onCreateTextClassificationSession( TextClassificationContext classificationContext, TextClassificationSessionId sessionId) throws RemoteException { Preconditions.checkNotNull(sessionId); Preconditions.checkNotNull(classificationContext); final int userId = classificationContext.getUserId(); validateInput(mContext, classificationContext.getPackageName(), userId); synchronized (mLock) { UserState userState = getUserStateLocked(userId); if (userState.isBoundLocked()) { userState.mService.onCreateTextClassificationSession( classificationContext, sessionId); mSessionUserIds.put(sessionId, userId); } else { userState.mPendingRequests.add(new PendingRequest( () -> onCreateTextClassificationSession(classificationContext, sessionId), null /* onServiceFailure */, null /* binder */, this, userState)); } } } @Override public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) throws RemoteException { Preconditions.checkNotNull(sessionId); synchronized (mLock) { final int userId = mSessionUserIds.containsKey(sessionId) ? mSessionUserIds.get(sessionId) : UserHandle.getCallingUserId(); validateInput(mContext, null /* packageName */, userId); UserState userState = getUserStateLocked(userId); if (userState.isBoundLocked()) { userState.mService.onDestroyTextClassificationSession(sessionId); mSessionUserIds.remove(sessionId); } else { userState.mPendingRequests.add(new PendingRequest( () -> onDestroyTextClassificationSession(sessionId), null /* onServiceFailure */, null /* binder */, this, userState)); } } } @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, " "); TextClassificationManager tcm = mContext.getSystemService(TextClassificationManager.class); tcm.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 static final class PendingRequest implements IBinder.DeathRecipient { @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( @NonNull ThrowingRunnable request, @Nullable ThrowingRunnable onServiceFailure, @Nullable IBinder binder, TextClassificationManagerService service, UserState owningUser) { mRequest = logOnFailure(Preconditions.checkNotNull(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(); } } } @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 static final class UserState { @UserIdInt final int mUserId; final TextClassifierServiceConnection mConnection = new TextClassifierServiceConnection(); @GuardedBy("mLock") final Queue mPendingRequests = new ArrayDeque<>(); @GuardedBy("mLock") ITextClassifierService mService; @GuardedBy("mLock") boolean mBinding; private final Context mContext; private final Object mLock; private UserState(int userId, Context context, Object lock) { mUserId = userId; mContext = Preconditions.checkNotNull(context); mLock = Preconditions.checkNotNull(lock); } @GuardedBy("mLock") boolean isBoundLocked() { return mService != null; } @GuardedBy("mLock") private void handlePendingRequestsLocked() { PendingRequest request; while ((request = mPendingRequests.poll()) != null) { if (isBoundLocked()) { request.mRequest.run(); } else { if (request.mOnServiceFailure != null) { request.mOnServiceFailure.run(); } } if (request.mBinder != null) { request.mBinder.unlinkToDeath(request, 0); } } } @GuardedBy("mLock") private boolean bindIfHasPendingRequestsLocked() { return !mPendingRequests.isEmpty() && bindLocked(); } /** * @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 { 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()); 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("binding", mBinding); pw.printPair("numberRequests", mPendingRequests.size()); } } private final class TextClassifierServiceConnection implements ServiceConnection { @Override public void onServiceConnected(ComponentName name, IBinder service) { init(ITextClassifierService.Stub.asInterface(service)); } @Override public void onServiceDisconnected(ComponentName name) { cleanupService(); } @Override public void onBindingDied(ComponentName name) { cleanupService(); } @Override public void onNullBinding(ComponentName name) { cleanupService(); } void cleanupService() { init(null); } private void init(@Nullable ITextClassifierService service) { synchronized (mLock) { mService = service; mBinding = false; handlePendingRequestsLocked(); } } } } }