| /* |
| * 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.SystemTextClassifierMetadata; |
| 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.ArrayList; |
| import java.util.List; |
| 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) { |
| userState.cleanupServiceLocked(); |
| mManagerService.mUserStates.remove(userId); |
| } |
| } |
| } |
| |
| } |
| |
| private final TextClassifierSettingsListener mSettingsListener; |
| private final Context mContext; |
| private final Object mLock; |
| @GuardedBy("mLock") |
| final SparseArray<UserState> mUserStates = new SparseArray<>(); |
| private final SessionCache mSessionCache; |
| private final TextClassificationConstants mSettings; |
| @Nullable |
| private final String mDefaultTextClassifierPackage; |
| @Nullable |
| private final String mSystemTextClassifierPackage; |
| |
| private TextClassificationManagerService(Context context) { |
| mContext = Objects.requireNonNull(context); |
| mLock = new Object(); |
| mSettings = new TextClassificationConstants(); |
| mSettingsListener = new TextClassifierSettingsListener(mContext); |
| PackageManager packageManager = mContext.getPackageManager(); |
| mDefaultTextClassifierPackage = packageManager.getDefaultTextClassifierPackageName(); |
| mSystemTextClassifierPackage = packageManager.getSystemTextClassifierPackageName(); |
| mSessionCache = new SessionCache(mLock); |
| } |
| |
| 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); |
| Objects.requireNonNull(request.getSystemTextClassifierMetadata()); |
| |
| handleRequest( |
| request.getSystemTextClassifierMetadata(), |
| /* verifyCallingPackage= */ true, |
| /* 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); |
| Objects.requireNonNull(request.getSystemTextClassifierMetadata()); |
| |
| handleRequest( |
| request.getSystemTextClassifierMetadata(), |
| /* verifyCallingPackage= */ true, |
| /* 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); |
| Objects.requireNonNull(request.getSystemTextClassifierMetadata()); |
| |
| handleRequest( |
| request.getSystemTextClassifierMetadata(), |
| /* verifyCallingPackage= */ true, |
| /* attemptToBind= */ true, |
| service -> service.onGenerateLinks(sessionId, request, callback), |
| "onGenerateLinks", |
| callback); |
| } |
| |
| @Override |
| public void onSelectionEvent( |
| @Nullable TextClassificationSessionId sessionId, SelectionEvent event) |
| throws RemoteException { |
| Objects.requireNonNull(event); |
| Objects.requireNonNull(event.getSystemTextClassifierMetadata()); |
| |
| handleRequest( |
| event.getSystemTextClassifierMetadata(), |
| /* verifyCallingPackage= */ false, |
| /* 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 TextClassificationContext eventContext = event.getEventContext(); |
| final SystemTextClassifierMetadata systemTcMetadata = |
| eventContext != null ? eventContext.getSystemTextClassifierMetadata() : null; |
| |
| handleRequest( |
| systemTcMetadata, |
| /* verifyCallingPackage= */ false, |
| /* 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); |
| Objects.requireNonNull(request.getSystemTextClassifierMetadata()); |
| |
| handleRequest( |
| request.getSystemTextClassifierMetadata(), |
| /* verifyCallingPackage= */ true, |
| /* 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); |
| Objects.requireNonNull(request.getSystemTextClassifierMetadata()); |
| |
| handleRequest( |
| request.getSystemTextClassifierMetadata(), |
| /* verifyCallingPackage= */ true, |
| /* 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); |
| Objects.requireNonNull(classificationContext.getSystemTextClassifierMetadata()); |
| |
| handleRequest( |
| classificationContext.getSystemTextClassifierMetadata(), |
| /* verifyCallingPackage= */ true, |
| /* attemptToBind= */ false, |
| service -> { |
| service.onCreateTextClassificationSession(classificationContext, sessionId); |
| mSessionCache.put(sessionId, classificationContext); |
| }, |
| "onCreateTextClassificationSession", |
| NO_OP_CALLBACK); |
| } |
| |
| @Override |
| public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) |
| throws RemoteException { |
| Objects.requireNonNull(sessionId); |
| |
| synchronized (mLock) { |
| final StrippedTextClassificationContext textClassificationContext = |
| mSessionCache.get(sessionId); |
| final int userId = textClassificationContext != null |
| ? textClassificationContext.userId |
| : UserHandle.getCallingUserId(); |
| final boolean useDefaultTextClassifier = |
| textClassificationContext != null |
| ? textClassificationContext.useDefaultTextClassifier |
| : true; |
| final SystemTextClassifierMetadata sysTcMetadata = new SystemTextClassifierMetadata( |
| "", userId, useDefaultTextClassifier); |
| |
| handleRequest( |
| sysTcMetadata, |
| /* verifyCallingPackage= */ false, |
| /* attemptToBind= */ false, |
| service -> { |
| service.onDestroyTextClassificationSession(sessionId); |
| mSessionCache.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); |
| mUserStates.put(userId, result); |
| } |
| return result; |
| } |
| |
| @GuardedBy("mLock") |
| UserState peekUserStateLocked(int userId) { |
| return mUserStates.get(userId); |
| } |
| |
| private int resolvePackageToUid(@Nullable String packageName, @UserIdInt int userId) { |
| if (packageName == null) { |
| return Process.INVALID_UID; |
| } |
| final PackageManager pm = mContext.getPackageManager(); |
| try { |
| return pm.getPackageUidAsUser(packageName, userId); |
| } catch (PackageManager.NameNotFoundException e) { |
| Slog.e(LOG_TAG, "Could not get the UID for " + packageName); |
| } |
| return Process.INVALID_UID; |
| } |
| |
| @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(); |
| pw.printPair("defaultTextClassifierPackage", mDefaultTextClassifierPackage); |
| pw.println(); |
| pw.printPair("systemTextClassifierPackage", mSystemTextClassifierPackage); |
| 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.printPair("User", mUserStates.keyAt(i)); |
| pw.println(); |
| userState.dump(pw); |
| pw.decreaseIndent(); |
| } |
| } |
| pw.println("Number of active sessions: " + mSessionCache.size()); |
| } |
| } |
| |
| private void handleRequest( |
| @Nullable SystemTextClassifierMetadata sysTcMetadata, |
| boolean verifyCallingPackage, |
| boolean attemptToBind, |
| @NonNull ThrowingConsumer<ITextClassifierService> textClassifierServiceConsumer, |
| @NonNull String methodName, |
| @NonNull ITextClassifierCallback callback) throws RemoteException { |
| Objects.requireNonNull(textClassifierServiceConsumer); |
| Objects.requireNonNull(methodName); |
| Objects.requireNonNull(callback); |
| |
| final int userId = |
| sysTcMetadata == null ? UserHandle.getCallingUserId() : sysTcMetadata.getUserId(); |
| final String callingPackageName = |
| sysTcMetadata == null ? null : sysTcMetadata.getCallingPackageName(); |
| final boolean useDefaultTextClassifier = |
| sysTcMetadata == null ? true : sysTcMetadata.useDefaultTextClassifier(); |
| |
| try { |
| if (verifyCallingPackage) { |
| validateCallingPackage(callingPackageName); |
| } |
| validateUser(userId); |
| } catch (Exception e) { |
| throw new RemoteException("Invalid request: " + e.getMessage(), e, |
| /* enableSuppression */ true, /* writableStackTrace */ true); |
| } |
| synchronized (mLock) { |
| UserState userState = getUserStateLocked(userId); |
| ServiceState serviceState = |
| userState.getServiceStateLocked(useDefaultTextClassifier); |
| if (serviceState == null) { |
| Slog.d(LOG_TAG, "No configured system TextClassifierService"); |
| callback.onFailure(); |
| } else if (attemptToBind && !serviceState.bindLocked()) { |
| Slog.d(LOG_TAG, "Unable to bind TextClassifierService at " + methodName); |
| callback.onFailure(); |
| } else if (serviceState.isBoundLocked()) { |
| if (!serviceState.checkRequestAcceptedLocked(Binder.getCallingUid(), methodName)) { |
| return; |
| } |
| textClassifierServiceConsumer.accept(serviceState.mService); |
| } else { |
| serviceState.mPendingRequests.add( |
| new PendingRequest( |
| methodName, |
| () -> textClassifierServiceConsumer.accept(serviceState.mService), |
| callback::onFailure, callback.asBinder(), |
| this, |
| serviceState, |
| Binder.getCallingUid())); |
| } |
| } |
| } |
| |
| private void onTextClassifierServicePackageOverrideChanged(String overriddenPackage) { |
| synchronized (mLock) { |
| final int size = mUserStates.size(); |
| for (int i = 0; i < size; i++) { |
| UserState userState = mUserStates.valueAt(i); |
| userState.onTextClassifierServicePackageOverrideChangedLocked(overriddenPackage); |
| } |
| } |
| } |
| |
| 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 ServiceState mServiceState; |
| @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 |
| * @parm service the TCMS instance. |
| * @param serviceState the service state of the service that will execute the request. |
| * @param uid the calling uid of the request. |
| */ |
| PendingRequest(@Nullable String name, |
| @NonNull ThrowingRunnable request, @Nullable ThrowingRunnable onServiceFailure, |
| @Nullable IBinder binder, |
| @NonNull TextClassificationManagerService service, |
| @NonNull ServiceState serviceState, int uid) { |
| mName = name; |
| mRequest = |
| logOnFailure(Objects.requireNonNull(request), "handling pending request"); |
| mOnServiceFailure = |
| logOnFailure(onServiceFailure, "notifying callback of service failure"); |
| mBinder = binder; |
| mService = service; |
| mServiceState = Objects.requireNonNull(serviceState); |
| 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() { |
| mServiceState.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 void validateCallingPackage(@Nullable String callingPackage) |
| throws PackageManager.NameNotFoundException { |
| if (callingPackage != null) { |
| final int packageUid = mContext.getPackageManager() |
| .getPackageUidAsUser(callingPackage, UserHandle.getCallingUserId()); |
| final int callingUid = Binder.getCallingUid(); |
| Preconditions.checkArgument( |
| callingUid == packageUid |
| // Trust the system process: |
| || callingUid == android.os.Process.SYSTEM_UID, |
| "Invalid package name. callingPackage=" + callingPackage |
| + ", callingUid=" + callingUid); |
| } |
| } |
| |
| private void validateUser(@UserIdInt int userId) { |
| Preconditions.checkArgument(userId != UserHandle.USER_NULL, "Null userId"); |
| final int callingUserId = UserHandle.getCallingUserId(); |
| if (callingUserId != userId) { |
| mContext.enforceCallingOrSelfPermission( |
| android.Manifest.permission.INTERACT_ACROSS_USERS_FULL, |
| "Invalid userId. UserId=" + userId + ", CallingUserId=" + callingUserId); |
| } |
| } |
| |
| /** |
| * Stores the stripped down version of {@link TextClassificationContext}s, i.e. {@link |
| * StrippedTextClassificationContext}, keyed by {@link TextClassificationSessionId}. Sessions |
| * are cleaned up automatically when the client process is dead. |
| */ |
| static final class SessionCache { |
| @NonNull |
| private final Object mLock; |
| @NonNull |
| @GuardedBy("mLock") |
| private final Map<TextClassificationSessionId, StrippedTextClassificationContext> mCache = |
| new ArrayMap<>(); |
| @NonNull |
| @GuardedBy("mLock") |
| private final Map<TextClassificationSessionId, DeathRecipient> mDeathRecipients = |
| new ArrayMap<>(); |
| |
| SessionCache(@NonNull Object lock) { |
| mLock = Objects.requireNonNull(lock); |
| } |
| |
| void put(@NonNull TextClassificationSessionId sessionId, |
| @NonNull TextClassificationContext textClassificationContext) { |
| synchronized (mLock) { |
| mCache.put(sessionId, |
| new StrippedTextClassificationContext(textClassificationContext)); |
| try { |
| DeathRecipient deathRecipient = () -> remove(sessionId); |
| sessionId.getToken().linkToDeath(deathRecipient, /* flags= */ 0); |
| mDeathRecipients.put(sessionId, deathRecipient); |
| } catch (RemoteException e) { |
| Slog.w(LOG_TAG, "SessionCache: Failed to link to death", e); |
| } |
| } |
| } |
| |
| @Nullable |
| StrippedTextClassificationContext get(@NonNull TextClassificationSessionId sessionId) { |
| Objects.requireNonNull(sessionId); |
| synchronized (mLock) { |
| return mCache.get(sessionId); |
| } |
| } |
| |
| void remove(@NonNull TextClassificationSessionId sessionId) { |
| Objects.requireNonNull(sessionId); |
| synchronized (mLock) { |
| DeathRecipient deathRecipient = mDeathRecipients.get(sessionId); |
| if (deathRecipient != null) { |
| sessionId.getToken().unlinkToDeath(deathRecipient, /* flags= */ 0); |
| } |
| mDeathRecipients.remove(sessionId); |
| mCache.remove(sessionId); |
| } |
| } |
| |
| int size() { |
| synchronized (mLock) { |
| return mCache.size(); |
| } |
| } |
| } |
| |
| /** A stripped down version of {@link TextClassificationContext}. */ |
| static class StrippedTextClassificationContext { |
| @UserIdInt |
| public final int userId; |
| public final boolean useDefaultTextClassifier; |
| |
| StrippedTextClassificationContext(TextClassificationContext textClassificationContext) { |
| SystemTextClassifierMetadata sysTcMetadata = |
| textClassificationContext.getSystemTextClassifierMetadata(); |
| userId = sysTcMetadata.getUserId(); |
| useDefaultTextClassifier = sysTcMetadata.useDefaultTextClassifier(); |
| } |
| } |
| |
| private final class UserState { |
| @UserIdInt |
| final int mUserId; |
| @Nullable |
| private final ServiceState mDefaultServiceState; |
| @Nullable |
| private final ServiceState mSystemServiceState; |
| @GuardedBy("mLock") |
| @Nullable |
| private ServiceState mUntrustedServiceState; |
| |
| private UserState(int userId) { |
| mUserId = userId; |
| mDefaultServiceState = TextUtils.isEmpty(mDefaultTextClassifierPackage) |
| ? null |
| : new ServiceState(userId, mDefaultTextClassifierPackage, /* isTrusted= */true); |
| mSystemServiceState = TextUtils.isEmpty(mSystemTextClassifierPackage) |
| ? null |
| : new ServiceState(userId, mSystemTextClassifierPackage, /* isTrusted= */ true); |
| } |
| |
| @GuardedBy("mLock") |
| @Nullable |
| ServiceState getServiceStateLocked(boolean useDefaultTextClassifier) { |
| if (useDefaultTextClassifier) { |
| return mDefaultServiceState; |
| } |
| String textClassifierServicePackageOverride = |
| Binder.withCleanCallingIdentity( |
| mSettings::getTextClassifierServicePackageOverride); |
| if (!TextUtils.isEmpty(textClassifierServicePackageOverride)) { |
| if (textClassifierServicePackageOverride.equals(mDefaultTextClassifierPackage)) { |
| return mDefaultServiceState; |
| } |
| if (textClassifierServicePackageOverride.equals(mSystemTextClassifierPackage) |
| && mSystemServiceState != null) { |
| return mSystemServiceState; |
| } |
| if (mUntrustedServiceState == null) { |
| mUntrustedServiceState = |
| new ServiceState( |
| mUserId, |
| textClassifierServicePackageOverride, |
| /* isTrusted= */false); |
| } |
| return mUntrustedServiceState; |
| } |
| return mSystemServiceState != null ? mSystemServiceState : mDefaultServiceState; |
| } |
| |
| @GuardedBy("mLock") |
| void onTextClassifierServicePackageOverrideChangedLocked(String overriddenPackageName) { |
| // The override config is just used for testing, and the flag value is not expected |
| // to change often. So, let's keep it simple and just unbind all the services here. The |
| // right service will be bound when the next request comes. |
| for (ServiceState serviceState : getAllServiceStatesLocked()) { |
| serviceState.unbindIfBoundLocked(); |
| } |
| mUntrustedServiceState = null; |
| } |
| |
| @GuardedBy("mLock") |
| void bindIfHasPendingRequestsLocked() { |
| for (ServiceState serviceState : getAllServiceStatesLocked()) { |
| serviceState.bindIfHasPendingRequestsLocked(); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| void cleanupServiceLocked() { |
| for (ServiceState serviceState : getAllServiceStatesLocked()) { |
| if (serviceState.mConnection != null) { |
| serviceState.mConnection.cleanupService(); |
| } |
| } |
| } |
| |
| @GuardedBy("mLock") |
| @NonNull |
| private List<ServiceState> getAllServiceStatesLocked() { |
| List<ServiceState> serviceStates = new ArrayList<>(); |
| if (mDefaultServiceState != null) { |
| serviceStates.add(mDefaultServiceState); |
| } |
| if (mSystemServiceState != null) { |
| serviceStates.add(mSystemServiceState); |
| } |
| if (mUntrustedServiceState != null) { |
| serviceStates.add(mUntrustedServiceState); |
| } |
| return serviceStates; |
| } |
| |
| void dump(IndentingPrintWriter pw) { |
| synchronized (mLock) { |
| pw.increaseIndent(); |
| dump(pw, mDefaultServiceState, "Default"); |
| dump(pw, mSystemServiceState, "System"); |
| dump(pw, mUntrustedServiceState, "Untrusted"); |
| pw.decreaseIndent(); |
| } |
| } |
| |
| private void dump( |
| IndentingPrintWriter pw, @Nullable ServiceState serviceState, String name) { |
| synchronized (mLock) { |
| if (serviceState != null) { |
| pw.print(name + ": "); |
| serviceState.dump(pw); |
| pw.println(); |
| } |
| } |
| } |
| } |
| |
| private final class ServiceState { |
| @UserIdInt |
| final int mUserId; |
| @NonNull |
| final String mPackageName; |
| @NonNull |
| final TextClassifierServiceConnection mConnection; |
| final boolean mIsTrusted; |
| @NonNull |
| @GuardedBy("mLock") |
| final Queue<PendingRequest> mPendingRequests = new ArrayDeque<>(); |
| @Nullable |
| @GuardedBy("mLock") |
| ITextClassifierService mService; |
| @GuardedBy("mLock") |
| boolean mBinding; |
| @Nullable |
| @GuardedBy("mLock") |
| ComponentName mBoundComponentName = null; |
| @GuardedBy("mLock") |
| int mBoundServiceUid = Process.INVALID_UID; |
| |
| private ServiceState(@UserIdInt int userId, String packageName, boolean isTrusted) { |
| mUserId = userId; |
| mPackageName = packageName; |
| mConnection = new TextClassifierServiceConnection(mUserId); |
| mIsTrusted = isTrusted; |
| } |
| |
| @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") |
| void unbindIfBoundLocked() { |
| if (isBoundLocked()) { |
| Slog.v(LOG_TAG, "Unbinding " + mBoundComponentName + " for " + mUserId); |
| mContext.unbindService(mConnection); |
| mConnection.cleanupService(); |
| } |
| } |
| |
| /** |
| * @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 = getTextClassifierServiceComponent(); |
| 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; |
| } |
| |
| @Nullable |
| private ComponentName getTextClassifierServiceComponent() { |
| return TextClassifierService.getServiceComponentName( |
| mContext, |
| mPackageName, |
| mIsTrusted ? PackageManager.MATCH_SYSTEM_ONLY : 0); |
| } |
| |
| private void dump(IndentingPrintWriter pw) { |
| pw.printPair("context", mContext); |
| pw.printPair("userId", mUserId); |
| synchronized (mLock) { |
| pw.printPair("packageName", mPackageName); |
| pw.printPair("boundComponentName", mBoundComponentName); |
| pw.printPair("isTrusted", mIsTrusted); |
| pw.printPair("boundServiceUid", mBoundServiceUid); |
| pw.printPair("binding", mBinding); |
| pw.printPair("numberRequests", mPendingRequests.size()); |
| } |
| } |
| |
| @GuardedBy("mLock") |
| private boolean checkRequestAcceptedLocked(int requestUid, @NonNull String methodName) { |
| if (mIsTrusted || (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; |
| } |
| |
| @GuardedBy("mLock") |
| private void updateServiceInfoLocked(int userId, @Nullable ComponentName componentName) { |
| mBoundComponentName = componentName; |
| mBoundServiceUid = |
| mBoundComponentName == null |
| ? Process.INVALID_UID |
| : resolvePackageToUid(mBoundComponentName.getPackageName(), 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; |
| @Nullable |
| private String mServicePackageOverride; |
| |
| |
| TextClassifierSettingsListener(Context context) { |
| mContext = context; |
| mServicePackageOverride = mSettings.getTextClassifierServicePackageOverride(); |
| } |
| |
| void registerObserver() { |
| DeviceConfig.addOnPropertiesChangedListener( |
| DeviceConfig.NAMESPACE_TEXTCLASSIFIER, |
| mContext.getMainExecutor(), |
| this); |
| } |
| |
| @Override |
| public void onPropertiesChanged(DeviceConfig.Properties properties) { |
| final String currentServicePackageOverride = |
| mSettings.getTextClassifierServicePackageOverride(); |
| if (TextUtils.equals(currentServicePackageOverride, mServicePackageOverride)) { |
| return; |
| } |
| mServicePackageOverride = currentServicePackageOverride; |
| onTextClassifierServicePackageOverrideChanged(currentServicePackageOverride); |
| } |
| } |
| } |