Home | History | Annotate | Download | only in textclassifier
      1 /*
      2  * Copyright (C) 2018 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.server.textclassifier;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.annotation.UserIdInt;
     22 import android.content.ComponentName;
     23 import android.content.Context;
     24 import android.content.Intent;
     25 import android.content.ServiceConnection;
     26 import android.content.pm.PackageManager;
     27 import android.os.Binder;
     28 import android.os.IBinder;
     29 import android.os.RemoteException;
     30 import android.os.UserHandle;
     31 import android.service.textclassifier.ITextClassificationCallback;
     32 import android.service.textclassifier.ITextClassifierService;
     33 import android.service.textclassifier.ITextLinksCallback;
     34 import android.service.textclassifier.ITextSelectionCallback;
     35 import android.service.textclassifier.TextClassifierService;
     36 import android.util.Slog;
     37 import android.util.SparseArray;
     38 import android.view.textclassifier.SelectionEvent;
     39 import android.view.textclassifier.TextClassification;
     40 import android.view.textclassifier.TextClassificationContext;
     41 import android.view.textclassifier.TextClassificationSessionId;
     42 import android.view.textclassifier.TextLinks;
     43 import android.view.textclassifier.TextSelection;
     44 
     45 import com.android.internal.annotations.GuardedBy;
     46 import com.android.internal.util.FunctionalUtils;
     47 import com.android.internal.util.FunctionalUtils.ThrowingRunnable;
     48 import com.android.internal.util.Preconditions;
     49 import com.android.server.SystemService;
     50 
     51 import java.util.ArrayDeque;
     52 import java.util.Queue;
     53 
     54 /**
     55  * A manager for TextClassifier services.
     56  * Apps bind to the TextClassificationManagerService for text classification. This service
     57  * reroutes calls to it to a {@link TextClassifierService} that it manages.
     58  */
     59 public final class TextClassificationManagerService extends ITextClassifierService.Stub {
     60 
     61     private static final String LOG_TAG = "TextClassificationManagerService";
     62 
     63     public static final class Lifecycle extends SystemService {
     64 
     65         private final TextClassificationManagerService mManagerService;
     66 
     67         public Lifecycle(Context context) {
     68             super(context);
     69             mManagerService = new TextClassificationManagerService(context);
     70         }
     71 
     72         @Override
     73         public void onStart() {
     74             try {
     75                 publishBinderService(Context.TEXT_CLASSIFICATION_SERVICE, mManagerService);
     76             } catch (Throwable t) {
     77                 // Starting this service is not critical to the running of this device and should
     78                 // therefore not crash the device. If it fails, log the error and continue.
     79                 Slog.e(LOG_TAG, "Could not start the TextClassificationManagerService.", t);
     80             }
     81         }
     82 
     83         @Override
     84         public void onStartUser(int userId) {
     85             processAnyPendingWork(userId);
     86         }
     87 
     88         @Override
     89         public void onUnlockUser(int userId) {
     90             // Rebind if we failed earlier due to locked encrypted user
     91             processAnyPendingWork(userId);
     92         }
     93 
     94         private void processAnyPendingWork(int userId) {
     95             synchronized (mManagerService.mLock) {
     96                 mManagerService.getUserStateLocked(userId).bindIfHasPendingRequestsLocked();
     97             }
     98         }
     99 
    100         @Override
    101         public void onStopUser(int userId) {
    102             synchronized (mManagerService.mLock) {
    103                 UserState userState = mManagerService.peekUserStateLocked(userId);
    104                 if (userState != null) {
    105                     userState.mConnection.cleanupService();
    106                     mManagerService.mUserStates.remove(userId);
    107                 }
    108             }
    109         }
    110 
    111     }
    112 
    113     private final Context mContext;
    114     private final Object mLock;
    115     @GuardedBy("mLock")
    116     final SparseArray<UserState> mUserStates = new SparseArray<>();
    117 
    118     private TextClassificationManagerService(Context context) {
    119         mContext = Preconditions.checkNotNull(context);
    120         mLock = new Object();
    121     }
    122 
    123     @Override
    124     public void onSuggestSelection(
    125             TextClassificationSessionId sessionId,
    126             TextSelection.Request request, ITextSelectionCallback callback)
    127             throws RemoteException {
    128         Preconditions.checkNotNull(request);
    129         Preconditions.checkNotNull(callback);
    130 
    131         synchronized (mLock) {
    132             UserState userState = getCallingUserStateLocked();
    133             if (!userState.bindLocked()) {
    134                 callback.onFailure();
    135             } else if (userState.isBoundLocked()) {
    136                 userState.mService.onSuggestSelection(sessionId, request, callback);
    137             } else {
    138                 userState.mPendingRequests.add(new PendingRequest(
    139                         () -> onSuggestSelection(sessionId, request, callback),
    140                         callback::onFailure, callback.asBinder(), this, userState));
    141             }
    142         }
    143     }
    144 
    145     @Override
    146     public void onClassifyText(
    147             TextClassificationSessionId sessionId,
    148             TextClassification.Request request, ITextClassificationCallback callback)
    149             throws RemoteException {
    150         Preconditions.checkNotNull(request);
    151         Preconditions.checkNotNull(callback);
    152 
    153         synchronized (mLock) {
    154             UserState userState = getCallingUserStateLocked();
    155             if (!userState.bindLocked()) {
    156                 callback.onFailure();
    157             } else if (userState.isBoundLocked()) {
    158                 userState.mService.onClassifyText(sessionId, request, callback);
    159             } else {
    160                 userState.mPendingRequests.add(new PendingRequest(
    161                         () -> onClassifyText(sessionId, request, callback),
    162                         callback::onFailure, callback.asBinder(), this, userState));
    163             }
    164         }
    165     }
    166 
    167     @Override
    168     public void onGenerateLinks(
    169             TextClassificationSessionId sessionId,
    170             TextLinks.Request request, ITextLinksCallback callback)
    171             throws RemoteException {
    172         Preconditions.checkNotNull(request);
    173         Preconditions.checkNotNull(callback);
    174 
    175         synchronized (mLock) {
    176             UserState userState = getCallingUserStateLocked();
    177             if (!userState.bindLocked()) {
    178                 callback.onFailure();
    179             } else if (userState.isBoundLocked()) {
    180                 userState.mService.onGenerateLinks(sessionId, request, callback);
    181             } else {
    182                 userState.mPendingRequests.add(new PendingRequest(
    183                         () -> onGenerateLinks(sessionId, request, callback),
    184                         callback::onFailure, callback.asBinder(), this, userState));
    185             }
    186         }
    187     }
    188 
    189     @Override
    190     public void onSelectionEvent(
    191             TextClassificationSessionId sessionId, SelectionEvent event) throws RemoteException {
    192         Preconditions.checkNotNull(event);
    193         validateInput(event.getPackageName(), mContext);
    194 
    195         synchronized (mLock) {
    196             UserState userState = getCallingUserStateLocked();
    197             if (userState.isBoundLocked()) {
    198                 userState.mService.onSelectionEvent(sessionId, event);
    199             } else {
    200                 userState.mPendingRequests.add(new PendingRequest(
    201                         () -> onSelectionEvent(sessionId, event),
    202                         null /* onServiceFailure */, null /* binder */, this, userState));
    203             }
    204         }
    205     }
    206 
    207     @Override
    208     public void onCreateTextClassificationSession(
    209             TextClassificationContext classificationContext, TextClassificationSessionId sessionId)
    210             throws RemoteException {
    211         Preconditions.checkNotNull(sessionId);
    212         Preconditions.checkNotNull(classificationContext);
    213         validateInput(classificationContext.getPackageName(), mContext);
    214 
    215         synchronized (mLock) {
    216             UserState userState = getCallingUserStateLocked();
    217             if (userState.isBoundLocked()) {
    218                 userState.mService.onCreateTextClassificationSession(
    219                         classificationContext, sessionId);
    220             } else {
    221                 userState.mPendingRequests.add(new PendingRequest(
    222                         () -> onCreateTextClassificationSession(classificationContext, sessionId),
    223                         null /* onServiceFailure */, null /* binder */, this, userState));
    224             }
    225         }
    226     }
    227 
    228     @Override
    229     public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId)
    230             throws RemoteException {
    231         Preconditions.checkNotNull(sessionId);
    232 
    233         synchronized (mLock) {
    234             UserState userState = getCallingUserStateLocked();
    235             if (userState.isBoundLocked()) {
    236                 userState.mService.onDestroyTextClassificationSession(sessionId);
    237             } else {
    238                 userState.mPendingRequests.add(new PendingRequest(
    239                         () -> onDestroyTextClassificationSession(sessionId),
    240                         null /* onServiceFailure */, null /* binder */, this, userState));
    241             }
    242         }
    243     }
    244 
    245     private UserState getCallingUserStateLocked() {
    246         return getUserStateLocked(UserHandle.getCallingUserId());
    247     }
    248 
    249     private UserState getUserStateLocked(int userId) {
    250         UserState result = mUserStates.get(userId);
    251         if (result == null) {
    252             result = new UserState(userId, mContext, mLock);
    253             mUserStates.put(userId, result);
    254         }
    255         return result;
    256     }
    257 
    258     UserState peekUserStateLocked(int userId) {
    259         return mUserStates.get(userId);
    260     }
    261 
    262     private static final class PendingRequest implements IBinder.DeathRecipient {
    263 
    264         @Nullable private final IBinder mBinder;
    265         @NonNull private final Runnable mRequest;
    266         @Nullable private final Runnable mOnServiceFailure;
    267         @GuardedBy("mLock")
    268         @NonNull private final UserState mOwningUser;
    269         @NonNull private final TextClassificationManagerService mService;
    270 
    271         /**
    272          * Initializes a new pending request.
    273          * @param request action to perform when the service is bound
    274          * @param onServiceFailure action to perform when the service dies or disconnects
    275          * @param binder binder to the process that made this pending request
    276          * @param service
    277          * @param owningUser
    278          */
    279         PendingRequest(
    280                 @NonNull ThrowingRunnable request, @Nullable ThrowingRunnable onServiceFailure,
    281                 @Nullable IBinder binder,
    282                 TextClassificationManagerService service,
    283                 UserState owningUser) {
    284             mRequest =
    285                     logOnFailure(Preconditions.checkNotNull(request), "handling pending request");
    286             mOnServiceFailure =
    287                     logOnFailure(onServiceFailure, "notifying callback of service failure");
    288             mBinder = binder;
    289             mService = service;
    290             mOwningUser = owningUser;
    291             if (mBinder != null) {
    292                 try {
    293                     mBinder.linkToDeath(this, 0);
    294                 } catch (RemoteException e) {
    295                     e.printStackTrace();
    296                 }
    297             }
    298         }
    299 
    300         @Override
    301         public void binderDied() {
    302             synchronized (mService.mLock) {
    303                 // No need to handle this pending request anymore. Remove.
    304                 removeLocked();
    305             }
    306         }
    307 
    308         @GuardedBy("mLock")
    309         private void removeLocked() {
    310             mOwningUser.mPendingRequests.remove(this);
    311             if (mBinder != null) {
    312                 mBinder.unlinkToDeath(this, 0);
    313             }
    314         }
    315     }
    316 
    317     private static Runnable logOnFailure(@Nullable ThrowingRunnable r, String opDesc) {
    318         if (r == null) return null;
    319         return FunctionalUtils.handleExceptions(r,
    320                 e -> Slog.d(LOG_TAG, "Error " + opDesc + ": " + e.getMessage()));
    321     }
    322 
    323     private static void validateInput(String packageName, Context context)
    324             throws RemoteException {
    325         try {
    326             final int uid = context.getPackageManager()
    327                     .getPackageUid(packageName, 0);
    328             Preconditions.checkArgument(Binder.getCallingUid() == uid);
    329         } catch (IllegalArgumentException | NullPointerException |
    330                 PackageManager.NameNotFoundException e) {
    331             throw new RemoteException(e.getMessage());
    332         }
    333     }
    334 
    335     private static final class UserState {
    336         @UserIdInt final int mUserId;
    337         final TextClassifierServiceConnection mConnection = new TextClassifierServiceConnection();
    338         @GuardedBy("mLock")
    339         final Queue<PendingRequest> mPendingRequests = new ArrayDeque<>();
    340         @GuardedBy("mLock")
    341         ITextClassifierService mService;
    342         @GuardedBy("mLock")
    343         boolean mBinding;
    344 
    345         private final Context mContext;
    346         private final Object mLock;
    347 
    348         private UserState(int userId, Context context, Object lock) {
    349             mUserId = userId;
    350             mContext = Preconditions.checkNotNull(context);
    351             mLock = Preconditions.checkNotNull(lock);
    352         }
    353 
    354         @GuardedBy("mLock")
    355         boolean isBoundLocked() {
    356             return mService != null;
    357         }
    358 
    359         @GuardedBy("mLock")
    360         private void handlePendingRequestsLocked() {
    361             PendingRequest request;
    362             while ((request = mPendingRequests.poll()) != null) {
    363                 if (isBoundLocked()) {
    364                     request.mRequest.run();
    365                 } else {
    366                     if (request.mOnServiceFailure != null) {
    367                         request.mOnServiceFailure.run();
    368                     }
    369                 }
    370 
    371                 if (request.mBinder != null) {
    372                     request.mBinder.unlinkToDeath(request, 0);
    373                 }
    374             }
    375         }
    376 
    377         private boolean bindIfHasPendingRequestsLocked() {
    378             return !mPendingRequests.isEmpty() && bindLocked();
    379         }
    380 
    381         /**
    382          * @return true if the service is bound or in the process of being bound.
    383          *      Returns false otherwise.
    384          */
    385         private boolean bindLocked() {
    386             if (isBoundLocked() || mBinding) {
    387                 return true;
    388             }
    389 
    390             // TODO: Handle bind timeout.
    391             final boolean willBind;
    392             final long identity = Binder.clearCallingIdentity();
    393             try {
    394                 ComponentName componentName =
    395                         TextClassifierService.getServiceComponentName(mContext);
    396                 if (componentName == null) {
    397                     // Might happen if the storage is encrypted and the user is not unlocked
    398                     return false;
    399                 }
    400                 Intent serviceIntent = new Intent(TextClassifierService.SERVICE_INTERFACE)
    401                         .setComponent(componentName);
    402                 Slog.d(LOG_TAG, "Binding to " + serviceIntent.getComponent());
    403                 willBind = mContext.bindServiceAsUser(
    404                         serviceIntent, mConnection,
    405                         Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
    406                         UserHandle.of(mUserId));
    407                 mBinding = willBind;
    408             } finally {
    409                 Binder.restoreCallingIdentity(identity);
    410             }
    411             return willBind;
    412         }
    413 
    414         private final class TextClassifierServiceConnection implements ServiceConnection {
    415             @Override
    416             public void onServiceConnected(ComponentName name, IBinder service) {
    417                 init(ITextClassifierService.Stub.asInterface(service));
    418             }
    419 
    420             @Override
    421             public void onServiceDisconnected(ComponentName name) {
    422                 cleanupService();
    423             }
    424 
    425             @Override
    426             public void onBindingDied(ComponentName name) {
    427                 cleanupService();
    428             }
    429 
    430             @Override
    431             public void onNullBinding(ComponentName name) {
    432                 cleanupService();
    433             }
    434 
    435             void cleanupService() {
    436                 init(null);
    437             }
    438 
    439             private void init(@Nullable ITextClassifierService service) {
    440                 synchronized (mLock) {
    441                     mService = service;
    442                     mBinding = false;
    443                     handlePendingRequestsLocked();
    444                 }
    445             }
    446         }
    447     }
    448 }
    449