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