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 package android.service.contentcapture; 17 18 import static android.view.contentcapture.ContentCaptureHelper.sDebug; 19 import static android.view.contentcapture.ContentCaptureHelper.sVerbose; 20 import static android.view.contentcapture.ContentCaptureHelper.toList; 21 import static android.view.contentcapture.ContentCaptureSession.NO_SESSION_ID; 22 23 import static com.android.internal.util.function.pooled.PooledLambda.obtainMessage; 24 25 import android.annotation.CallSuper; 26 import android.annotation.NonNull; 27 import android.annotation.Nullable; 28 import android.annotation.SystemApi; 29 import android.annotation.TestApi; 30 import android.app.Service; 31 import android.content.ComponentName; 32 import android.content.ContentCaptureOptions; 33 import android.content.Intent; 34 import android.content.pm.ParceledListSlice; 35 import android.os.Binder; 36 import android.os.Bundle; 37 import android.os.Handler; 38 import android.os.IBinder; 39 import android.os.Looper; 40 import android.os.RemoteException; 41 import android.util.Log; 42 import android.util.Slog; 43 import android.util.SparseIntArray; 44 import android.util.StatsLog; 45 import android.view.contentcapture.ContentCaptureCondition; 46 import android.view.contentcapture.ContentCaptureContext; 47 import android.view.contentcapture.ContentCaptureEvent; 48 import android.view.contentcapture.ContentCaptureManager; 49 import android.view.contentcapture.ContentCaptureSession; 50 import android.view.contentcapture.ContentCaptureSessionId; 51 import android.view.contentcapture.DataRemovalRequest; 52 import android.view.contentcapture.IContentCaptureDirectManager; 53 import android.view.contentcapture.MainContentCaptureSession; 54 55 import com.android.internal.os.IResultReceiver; 56 57 import java.io.FileDescriptor; 58 import java.io.PrintWriter; 59 import java.util.List; 60 import java.util.Set; 61 62 /** 63 * A service used to capture the content of the screen to provide contextual data in other areas of 64 * the system such as Autofill. 65 * 66 * @hide 67 */ 68 @SystemApi 69 @TestApi 70 public abstract class ContentCaptureService extends Service { 71 72 private static final String TAG = ContentCaptureService.class.getSimpleName(); 73 74 /** 75 * The {@link Intent} that must be declared as handled by the service. 76 * 77 * <p>To be supported, the service must also require the 78 * {@link android.Manifest.permission#BIND_CONTENT_CAPTURE_SERVICE} permission so 79 * that other applications can not abuse it. 80 */ 81 public static final String SERVICE_INTERFACE = 82 "android.service.contentcapture.ContentCaptureService"; 83 84 /** 85 * Name under which a ContentCaptureService component publishes information about itself. 86 * 87 * <p>This meta-data should reference an XML resource containing a 88 * <code><{@link 89 * android.R.styleable#ContentCaptureService content-capture-service}></code> tag. 90 * 91 * <p>Here's an example of how to use it on {@code AndroidManifest.xml}: 92 * 93 * <pre> 94 * <service android:name=".MyContentCaptureService" 95 * android:permission="android.permission.BIND_CONTENT_CAPTURE_SERVICE"> 96 * <intent-filter> 97 * <action android:name="android.service.contentcapture.ContentCaptureService" /> 98 * </intent-filter> 99 * 100 * <meta-data 101 * android:name="android.content_capture" 102 * android:resource="@xml/my_content_capture_service"/> 103 * </service> 104 * </pre> 105 * 106 * <p>And then on {@code res/xml/my_content_capture_service.xml}: 107 * 108 * <pre> 109 * <content-capture-service xmlns:android="http://schemas.android.com/apk/res/android" 110 * android:settingsActivity="my.package.MySettingsActivity"> 111 * </content-capture-service> 112 * </pre> 113 */ 114 public static final String SERVICE_META_DATA = "android.content_capture"; 115 116 private Handler mHandler; 117 private IContentCaptureServiceCallback mCallback; 118 119 private long mCallerMismatchTimeout = 1000; 120 private long mLastCallerMismatchLog; 121 122 /** 123 * Binder that receives calls from the system server. 124 */ 125 private final IContentCaptureService mServerInterface = new IContentCaptureService.Stub() { 126 127 @Override 128 public void onConnected(IBinder callback, boolean verbose, boolean debug) { 129 sVerbose = verbose; 130 sDebug = debug; 131 mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnConnected, 132 ContentCaptureService.this, callback)); 133 } 134 135 @Override 136 public void onDisconnected() { 137 mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnDisconnected, 138 ContentCaptureService.this)); 139 } 140 141 @Override 142 public void onSessionStarted(ContentCaptureContext context, int sessionId, int uid, 143 IResultReceiver clientReceiver, int initialState) { 144 mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnCreateSession, 145 ContentCaptureService.this, context, sessionId, uid, clientReceiver, 146 initialState)); 147 } 148 149 @Override 150 public void onActivitySnapshot(int sessionId, SnapshotData snapshotData) { 151 mHandler.sendMessage( 152 obtainMessage(ContentCaptureService::handleOnActivitySnapshot, 153 ContentCaptureService.this, sessionId, snapshotData)); 154 } 155 156 @Override 157 public void onSessionFinished(int sessionId) { 158 mHandler.sendMessage(obtainMessage(ContentCaptureService::handleFinishSession, 159 ContentCaptureService.this, sessionId)); 160 } 161 162 @Override 163 public void onDataRemovalRequest(DataRemovalRequest request) { 164 mHandler.sendMessage( 165 obtainMessage(ContentCaptureService::handleOnDataRemovalRequest, 166 ContentCaptureService.this, request)); 167 } 168 169 @Override 170 public void onActivityEvent(ActivityEvent event) { 171 mHandler.sendMessage(obtainMessage(ContentCaptureService::handleOnActivityEvent, 172 ContentCaptureService.this, event)); 173 174 } 175 }; 176 177 /** 178 * Binder that receives calls from the app. 179 */ 180 private final IContentCaptureDirectManager mClientInterface = 181 new IContentCaptureDirectManager.Stub() { 182 183 @Override 184 public void sendEvents(@SuppressWarnings("rawtypes") ParceledListSlice events, int reason, 185 ContentCaptureOptions options) { 186 mHandler.sendMessage(obtainMessage(ContentCaptureService::handleSendEvents, 187 ContentCaptureService.this, Binder.getCallingUid(), events, reason, options)); 188 } 189 }; 190 191 /** 192 * UIDs associated with each session. 193 * 194 * <p>This map is populated when an session is started, which is called by the system server 195 * and can be trusted. Then subsequent calls made by the app are verified against this map. 196 */ 197 private final SparseIntArray mSessionUids = new SparseIntArray(); 198 199 @CallSuper 200 @Override onCreate()201 public void onCreate() { 202 super.onCreate(); 203 mHandler = new Handler(Looper.getMainLooper(), null, true); 204 } 205 206 /** @hide */ 207 @Override onBind(Intent intent)208 public final IBinder onBind(Intent intent) { 209 if (SERVICE_INTERFACE.equals(intent.getAction())) { 210 return mServerInterface.asBinder(); 211 } 212 Log.w(TAG, "Tried to bind to wrong intent (should be " + SERVICE_INTERFACE + ": " + intent); 213 return null; 214 } 215 216 /** 217 * Explicitly limits content capture to the given packages and activities. 218 * 219 * <p>To reset the whitelist, call it passing {@code null} to both arguments. 220 * 221 * <p>Useful when the service wants to restrict content capture to a category of apps, like 222 * chat apps. For example, if the service wants to support view captures on all activities of 223 * app {@code ChatApp1} and just activities {@code act1} and {@code act2} of {@code ChatApp2}, 224 * it would call: {@code setContentCaptureWhitelist(Sets.newArraySet("ChatApp1"), 225 * Sets.newArraySet(new ComponentName("ChatApp2", "act1"), 226 * new ComponentName("ChatApp2", "act2")));} 227 */ setContentCaptureWhitelist(@ullable Set<String> packages, @Nullable Set<ComponentName> activities)228 public final void setContentCaptureWhitelist(@Nullable Set<String> packages, 229 @Nullable Set<ComponentName> activities) { 230 final IContentCaptureServiceCallback callback = mCallback; 231 if (callback == null) { 232 Log.w(TAG, "setContentCaptureWhitelist(): no server callback"); 233 return; 234 } 235 236 try { 237 callback.setContentCaptureWhitelist(toList(packages), toList(activities)); 238 } catch (RemoteException e) { 239 e.rethrowFromSystemServer(); 240 } 241 } 242 243 /** 244 * Explicitly sets the conditions for which content capture should be available by an app. 245 * 246 * <p>Typically used to restrict content capture to a few websites on browser apps. Example: 247 * 248 * <code> 249 * ArraySet<ContentCaptureCondition> conditions = new ArraySet<>(1); 250 * conditions.add(new ContentCaptureCondition(new LocusId("^https://.*\\.example\\.com$"), 251 * ContentCaptureCondition.FLAG_IS_REGEX)); 252 * service.setContentCaptureConditions("com.example.browser_app", conditions); 253 * 254 * </code> 255 * 256 * <p>NOTE: </p> this method doesn't automatically disable content capture for the given 257 * conditions; it's up to the {@code packageName} implementation to call 258 * {@link ContentCaptureManager#getContentCaptureConditions()} and disable it accordingly. 259 * 260 * @param packageName name of the packages where the restrictions are set. 261 * @param conditions list of conditions, or {@code null} to reset the conditions for the 262 * package. 263 */ setContentCaptureConditions(@onNull String packageName, @Nullable Set<ContentCaptureCondition> conditions)264 public final void setContentCaptureConditions(@NonNull String packageName, 265 @Nullable Set<ContentCaptureCondition> conditions) { 266 final IContentCaptureServiceCallback callback = mCallback; 267 if (callback == null) { 268 Log.w(TAG, "setContentCaptureConditions(): no server callback"); 269 return; 270 } 271 272 try { 273 callback.setContentCaptureConditions(packageName, toList(conditions)); 274 } catch (RemoteException e) { 275 e.rethrowFromSystemServer(); 276 } 277 } 278 279 /** 280 * Called when the Android system connects to service. 281 * 282 * <p>You should generally do initialization here rather than in {@link #onCreate}. 283 */ onConnected()284 public void onConnected() { 285 Slog.i(TAG, "bound to " + getClass().getName()); 286 } 287 288 /** 289 * Creates a new content capture session. 290 * 291 * @param context content capture context 292 * @param sessionId the session's Id 293 */ onCreateContentCaptureSession(@onNull ContentCaptureContext context, @NonNull ContentCaptureSessionId sessionId)294 public void onCreateContentCaptureSession(@NonNull ContentCaptureContext context, 295 @NonNull ContentCaptureSessionId sessionId) { 296 if (sVerbose) { 297 Log.v(TAG, "onCreateContentCaptureSession(id=" + sessionId + ", ctx=" + context + ")"); 298 } 299 } 300 301 /** 302 * Notifies the service of {@link ContentCaptureEvent events} associated with a content capture 303 * session. 304 * 305 * @param sessionId the session's Id 306 * @param event the event 307 */ onContentCaptureEvent(@onNull ContentCaptureSessionId sessionId, @NonNull ContentCaptureEvent event)308 public void onContentCaptureEvent(@NonNull ContentCaptureSessionId sessionId, 309 @NonNull ContentCaptureEvent event) { 310 if (sVerbose) Log.v(TAG, "onContentCaptureEventsRequest(id=" + sessionId + ")"); 311 } 312 313 /** 314 * Notifies the service that the app requested to remove content capture data. 315 * 316 * @param request the content capture data requested to be removed 317 */ onDataRemovalRequest(@onNull DataRemovalRequest request)318 public void onDataRemovalRequest(@NonNull DataRemovalRequest request) { 319 if (sVerbose) Log.v(TAG, "onDataRemovalRequest()"); 320 } 321 322 /** 323 * Notifies the service of {@link SnapshotData snapshot data} associated with a session. 324 * 325 * @param sessionId the session's Id 326 * @param snapshotData the data 327 */ onActivitySnapshot(@onNull ContentCaptureSessionId sessionId, @NonNull SnapshotData snapshotData)328 public void onActivitySnapshot(@NonNull ContentCaptureSessionId sessionId, 329 @NonNull SnapshotData snapshotData) { 330 if (sVerbose) Log.v(TAG, "onActivitySnapshot(id=" + sessionId + ")"); 331 } 332 333 /** 334 * Notifies the service of an activity-level event that is not associated with a session. 335 * 336 * <p>This method can be used to track some high-level events for all activities, even those 337 * that are not whitelisted for Content Capture. 338 * 339 * @param event high-level activity event 340 */ onActivityEvent(@onNull ActivityEvent event)341 public void onActivityEvent(@NonNull ActivityEvent event) { 342 if (sVerbose) Log.v(TAG, "onActivityEvent(): " + event); 343 } 344 345 /** 346 * Destroys the content capture session. 347 * 348 * @param sessionId the id of the session to destroy 349 * */ onDestroyContentCaptureSession(@onNull ContentCaptureSessionId sessionId)350 public void onDestroyContentCaptureSession(@NonNull ContentCaptureSessionId sessionId) { 351 if (sVerbose) Log.v(TAG, "onDestroyContentCaptureSession(id=" + sessionId + ")"); 352 } 353 354 /** 355 * Disables the Content Capture service for the given user. 356 */ disableSelf()357 public final void disableSelf() { 358 if (sDebug) Log.d(TAG, "disableSelf()"); 359 360 final IContentCaptureServiceCallback callback = mCallback; 361 if (callback == null) { 362 Log.w(TAG, "disableSelf(): no server callback"); 363 return; 364 } 365 try { 366 callback.disableSelf(); 367 } catch (RemoteException e) { 368 e.rethrowFromSystemServer(); 369 } 370 } 371 372 /** 373 * Called when the Android system disconnects from the service. 374 * 375 * <p> At this point this service may no longer be an active {@link ContentCaptureService}. 376 * It should not make calls on {@link ContentCaptureManager} that requires the caller to be 377 * the current service. 378 */ onDisconnected()379 public void onDisconnected() { 380 Slog.i(TAG, "unbinding from " + getClass().getName()); 381 } 382 383 @Override 384 @CallSuper dump(FileDescriptor fd, PrintWriter pw, String[] args)385 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 386 pw.print("Debug: "); pw.print(sDebug); pw.print(" Verbose: "); pw.println(sVerbose); 387 final int size = mSessionUids.size(); 388 pw.print("Number sessions: "); pw.println(size); 389 if (size > 0) { 390 final String prefix = " "; 391 for (int i = 0; i < size; i++) { 392 pw.print(prefix); pw.print(mSessionUids.keyAt(i)); 393 pw.print(": uid="); pw.println(mSessionUids.valueAt(i)); 394 } 395 } 396 } 397 handleOnConnected(@onNull IBinder callback)398 private void handleOnConnected(@NonNull IBinder callback) { 399 mCallback = IContentCaptureServiceCallback.Stub.asInterface(callback); 400 onConnected(); 401 } 402 handleOnDisconnected()403 private void handleOnDisconnected() { 404 onDisconnected(); 405 mCallback = null; 406 } 407 408 //TODO(b/111276913): consider caching the InteractionSessionId for the lifetime of the session, 409 // so we don't need to create a temporary InteractionSessionId for each event. 410 handleOnCreateSession(@onNull ContentCaptureContext context, int sessionId, int uid, IResultReceiver clientReceiver, int initialState)411 private void handleOnCreateSession(@NonNull ContentCaptureContext context, 412 int sessionId, int uid, IResultReceiver clientReceiver, int initialState) { 413 mSessionUids.put(sessionId, uid); 414 onCreateContentCaptureSession(context, new ContentCaptureSessionId(sessionId)); 415 416 final int clientFlags = context.getFlags(); 417 int stateFlags = 0; 418 if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE) != 0) { 419 stateFlags |= ContentCaptureSession.STATE_FLAG_SECURE; 420 } 421 if ((clientFlags & ContentCaptureContext.FLAG_DISABLED_BY_APP) != 0) { 422 stateFlags |= ContentCaptureSession.STATE_BY_APP; 423 } 424 if (stateFlags == 0) { 425 stateFlags = initialState; 426 } else { 427 stateFlags |= ContentCaptureSession.STATE_DISABLED; 428 } 429 setClientState(clientReceiver, stateFlags, mClientInterface.asBinder()); 430 } 431 handleSendEvents(int uid, @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason, @Nullable ContentCaptureOptions options)432 private void handleSendEvents(int uid, 433 @NonNull ParceledListSlice<ContentCaptureEvent> parceledEvents, int reason, 434 @Nullable ContentCaptureOptions options) { 435 final List<ContentCaptureEvent> events = parceledEvents.getList(); 436 if (events.isEmpty()) { 437 Log.w(TAG, "handleSendEvents() received empty list of events"); 438 return; 439 } 440 441 // Metrics. 442 final FlushMetrics metrics = new FlushMetrics(); 443 ComponentName activityComponent = null; 444 445 // Most events belong to the same session, so we can keep a reference to the last one 446 // to avoid creating too many ContentCaptureSessionId objects 447 int lastSessionId = NO_SESSION_ID; 448 ContentCaptureSessionId sessionId = null; 449 450 for (int i = 0; i < events.size(); i++) { 451 final ContentCaptureEvent event = events.get(i); 452 if (!handleIsRightCallerFor(event, uid)) continue; 453 int sessionIdInt = event.getSessionId(); 454 if (sessionIdInt != lastSessionId) { 455 sessionId = new ContentCaptureSessionId(sessionIdInt); 456 lastSessionId = sessionIdInt; 457 if (i != 0) { 458 writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason); 459 metrics.reset(); 460 } 461 } 462 final ContentCaptureContext clientContext = event.getContentCaptureContext(); 463 if (activityComponent == null && clientContext != null) { 464 activityComponent = clientContext.getActivityComponent(); 465 } 466 switch (event.getType()) { 467 case ContentCaptureEvent.TYPE_SESSION_STARTED: 468 clientContext.setParentSessionId(event.getParentSessionId()); 469 mSessionUids.put(sessionIdInt, uid); 470 onCreateContentCaptureSession(clientContext, sessionId); 471 metrics.sessionStarted++; 472 break; 473 case ContentCaptureEvent.TYPE_SESSION_FINISHED: 474 mSessionUids.delete(sessionIdInt); 475 onDestroyContentCaptureSession(sessionId); 476 metrics.sessionFinished++; 477 break; 478 case ContentCaptureEvent.TYPE_VIEW_APPEARED: 479 onContentCaptureEvent(sessionId, event); 480 metrics.viewAppearedCount++; 481 break; 482 case ContentCaptureEvent.TYPE_VIEW_DISAPPEARED: 483 onContentCaptureEvent(sessionId, event); 484 metrics.viewDisappearedCount++; 485 break; 486 case ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED: 487 onContentCaptureEvent(sessionId, event); 488 metrics.viewTextChangedCount++; 489 break; 490 default: 491 onContentCaptureEvent(sessionId, event); 492 } 493 } 494 writeFlushMetrics(lastSessionId, activityComponent, metrics, options, reason); 495 } 496 handleOnActivitySnapshot(int sessionId, @NonNull SnapshotData snapshotData)497 private void handleOnActivitySnapshot(int sessionId, @NonNull SnapshotData snapshotData) { 498 onActivitySnapshot(new ContentCaptureSessionId(sessionId), snapshotData); 499 } 500 handleFinishSession(int sessionId)501 private void handleFinishSession(int sessionId) { 502 mSessionUids.delete(sessionId); 503 onDestroyContentCaptureSession(new ContentCaptureSessionId(sessionId)); 504 } 505 handleOnDataRemovalRequest(@onNull DataRemovalRequest request)506 private void handleOnDataRemovalRequest(@NonNull DataRemovalRequest request) { 507 onDataRemovalRequest(request); 508 } 509 handleOnActivityEvent(@onNull ActivityEvent event)510 private void handleOnActivityEvent(@NonNull ActivityEvent event) { 511 onActivityEvent(event); 512 } 513 514 /** 515 * Checks if the given {@code uid} owns the session associated with the event. 516 */ handleIsRightCallerFor(@onNull ContentCaptureEvent event, int uid)517 private boolean handleIsRightCallerFor(@NonNull ContentCaptureEvent event, int uid) { 518 final int sessionId; 519 switch (event.getType()) { 520 case ContentCaptureEvent.TYPE_SESSION_STARTED: 521 case ContentCaptureEvent.TYPE_SESSION_FINISHED: 522 sessionId = event.getParentSessionId(); 523 break; 524 default: 525 sessionId = event.getSessionId(); 526 } 527 if (mSessionUids.indexOfKey(sessionId) < 0) { 528 if (sVerbose) { 529 Log.v(TAG, "handleIsRightCallerFor(" + event + "): no session for " + sessionId 530 + ": " + mSessionUids); 531 } 532 // Just ignore, as the session could have been finished already 533 return false; 534 } 535 final int rightUid = mSessionUids.get(sessionId); 536 if (rightUid != uid) { 537 Log.e(TAG, "invalid call from UID " + uid + ": session " + sessionId + " belongs to " 538 + rightUid); 539 long now = System.currentTimeMillis(); 540 if (now - mLastCallerMismatchLog > mCallerMismatchTimeout) { 541 StatsLog.write(StatsLog.CONTENT_CAPTURE_CALLER_MISMATCH_REPORTED, 542 getPackageManager().getNameForUid(rightUid), 543 getPackageManager().getNameForUid(uid)); 544 mLastCallerMismatchLog = now; 545 } 546 return false; 547 } 548 return true; 549 550 } 551 552 /** 553 * Sends the state of the {@link ContentCaptureManager} in the client app. 554 * 555 * @param clientReceiver receiver in the client app. 556 * @param sessionState state of the session 557 * @param binder handle to the {@code IContentCaptureDirectManager} object that resides in the 558 * service. 559 * @hide 560 */ setClientState(@onNull IResultReceiver clientReceiver, int sessionState, @Nullable IBinder binder)561 public static void setClientState(@NonNull IResultReceiver clientReceiver, 562 int sessionState, @Nullable IBinder binder) { 563 try { 564 final Bundle extras; 565 if (binder != null) { 566 extras = new Bundle(); 567 extras.putBinder(MainContentCaptureSession.EXTRA_BINDER, binder); 568 } else { 569 extras = null; 570 } 571 clientReceiver.send(sessionState, extras); 572 } catch (RemoteException e) { 573 Slog.w(TAG, "Error async reporting result to client: " + e); 574 } 575 } 576 577 /** 578 * Logs the metrics for content capture events flushing. 579 */ writeFlushMetrics(int sessionId, @Nullable ComponentName app, @NonNull FlushMetrics flushMetrics, @Nullable ContentCaptureOptions options, int flushReason)580 private void writeFlushMetrics(int sessionId, @Nullable ComponentName app, 581 @NonNull FlushMetrics flushMetrics, @Nullable ContentCaptureOptions options, 582 int flushReason) { 583 if (mCallback == null) { 584 Log.w(TAG, "writeSessionFlush(): no server callback"); 585 return; 586 } 587 588 try { 589 mCallback.writeSessionFlush(sessionId, app, flushMetrics, options, flushReason); 590 } catch (RemoteException e) { 591 Log.e(TAG, "failed to write flush metrics: " + e); 592 } 593 } 594 } 595