|  | /* | 
|  | * 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.IBinder; | 
|  | import android.os.RemoteException; | 
|  | import android.os.UserHandle; | 
|  | import android.service.textclassifier.ITextClassificationCallback; | 
|  | import android.service.textclassifier.ITextClassifierService; | 
|  | import android.service.textclassifier.ITextLinksCallback; | 
|  | import android.service.textclassifier.ITextSelectionCallback; | 
|  | import android.service.textclassifier.TextClassifierService; | 
|  | import android.util.Slog; | 
|  | import android.util.SparseArray; | 
|  | import android.view.textclassifier.SelectionEvent; | 
|  | import android.view.textclassifier.TextClassification; | 
|  | import android.view.textclassifier.TextClassificationContext; | 
|  | import android.view.textclassifier.TextClassificationSessionId; | 
|  | import android.view.textclassifier.TextLinks; | 
|  | import android.view.textclassifier.TextSelection; | 
|  |  | 
|  | import com.android.internal.annotations.GuardedBy; | 
|  | import com.android.internal.util.FunctionalUtils; | 
|  | import com.android.internal.util.FunctionalUtils.ThrowingRunnable; | 
|  | import com.android.internal.util.Preconditions; | 
|  | import com.android.server.SystemService; | 
|  |  | 
|  | import java.util.ArrayDeque; | 
|  | 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<UserState> mUserStates = new SparseArray<>(); | 
|  |  | 
|  | private TextClassificationManagerService(Context context) { | 
|  | mContext = Preconditions.checkNotNull(context); | 
|  | mLock = new Object(); | 
|  | } | 
|  |  | 
|  | @Override | 
|  | public void onSuggestSelection( | 
|  | TextClassificationSessionId sessionId, | 
|  | TextSelection.Request request, ITextSelectionCallback callback) | 
|  | throws RemoteException { | 
|  | Preconditions.checkNotNull(request); | 
|  | Preconditions.checkNotNull(callback); | 
|  |  | 
|  | synchronized (mLock) { | 
|  | UserState userState = getCallingUserStateLocked(); | 
|  | 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( | 
|  | TextClassificationSessionId sessionId, | 
|  | TextClassification.Request request, ITextClassificationCallback callback) | 
|  | throws RemoteException { | 
|  | Preconditions.checkNotNull(request); | 
|  | Preconditions.checkNotNull(callback); | 
|  |  | 
|  | synchronized (mLock) { | 
|  | UserState userState = getCallingUserStateLocked(); | 
|  | 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( | 
|  | TextClassificationSessionId sessionId, | 
|  | TextLinks.Request request, ITextLinksCallback callback) | 
|  | throws RemoteException { | 
|  | Preconditions.checkNotNull(request); | 
|  | Preconditions.checkNotNull(callback); | 
|  |  | 
|  | synchronized (mLock) { | 
|  | UserState userState = getCallingUserStateLocked(); | 
|  | 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( | 
|  | TextClassificationSessionId sessionId, SelectionEvent event) throws RemoteException { | 
|  | Preconditions.checkNotNull(event); | 
|  | validateInput(event.getPackageName(), mContext); | 
|  |  | 
|  | synchronized (mLock) { | 
|  | UserState userState = getCallingUserStateLocked(); | 
|  | 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 onCreateTextClassificationSession( | 
|  | TextClassificationContext classificationContext, TextClassificationSessionId sessionId) | 
|  | throws RemoteException { | 
|  | Preconditions.checkNotNull(sessionId); | 
|  | Preconditions.checkNotNull(classificationContext); | 
|  | validateInput(classificationContext.getPackageName(), mContext); | 
|  |  | 
|  | synchronized (mLock) { | 
|  | UserState userState = getCallingUserStateLocked(); | 
|  | if (userState.isBoundLocked()) { | 
|  | userState.mService.onCreateTextClassificationSession( | 
|  | classificationContext, sessionId); | 
|  | } 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) { | 
|  | UserState userState = getCallingUserStateLocked(); | 
|  | if (userState.isBoundLocked()) { | 
|  | userState.mService.onDestroyTextClassificationSession(sessionId); | 
|  | } else { | 
|  | userState.mPendingRequests.add(new PendingRequest( | 
|  | () -> onDestroyTextClassificationSession(sessionId), | 
|  | null /* onServiceFailure */, null /* binder */, this, userState)); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private UserState getCallingUserStateLocked() { | 
|  | return getUserStateLocked(UserHandle.getCallingUserId()); | 
|  | } | 
|  |  | 
|  | 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; | 
|  | } | 
|  |  | 
|  | UserState peekUserStateLocked(int userId) { | 
|  | return mUserStates.get(userId); | 
|  | } | 
|  |  | 
|  | 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(String packageName, Context context) | 
|  | throws RemoteException { | 
|  | try { | 
|  | final int uid = context.getPackageManager() | 
|  | .getPackageUid(packageName, 0); | 
|  | Preconditions.checkArgument(Binder.getCallingUid() == uid); | 
|  | } catch (IllegalArgumentException | NullPointerException | | 
|  | PackageManager.NameNotFoundException e) { | 
|  | throw new RemoteException(e.getMessage()); | 
|  | } | 
|  | } | 
|  |  | 
|  | private static final class UserState { | 
|  | @UserIdInt final int mUserId; | 
|  | final TextClassifierServiceConnection mConnection = new TextClassifierServiceConnection(); | 
|  | @GuardedBy("mLock") | 
|  | final Queue<PendingRequest> 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); | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | private boolean bindIfHasPendingRequestsLocked() { | 
|  | return !mPendingRequests.isEmpty() && bindLocked(); | 
|  | } | 
|  |  | 
|  | /** | 
|  | * @return true if the service is bound or in the process of being bound. | 
|  | *      Returns false otherwise. | 
|  | */ | 
|  | 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, | 
|  | UserHandle.of(mUserId)); | 
|  | mBinding = willBind; | 
|  | } finally { | 
|  | Binder.restoreCallingIdentity(identity); | 
|  | } | 
|  | return willBind; | 
|  | } | 
|  |  | 
|  | 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(); | 
|  | } | 
|  | } | 
|  | } | 
|  | } | 
|  | } |