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.view.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.toSet; 21 22 import android.annotation.IntDef; 23 import android.annotation.NonNull; 24 import android.annotation.Nullable; 25 import android.annotation.SystemApi; 26 import android.annotation.SystemService; 27 import android.annotation.TestApi; 28 import android.annotation.UiThread; 29 import android.content.ComponentName; 30 import android.content.ContentCaptureOptions; 31 import android.content.Context; 32 import android.os.Handler; 33 import android.os.IBinder; 34 import android.os.Looper; 35 import android.os.RemoteException; 36 import android.os.ServiceManager; 37 import android.util.Log; 38 import android.view.View; 39 import android.view.ViewStructure; 40 import android.view.WindowManager; 41 import android.view.contentcapture.ContentCaptureSession.FlushReason; 42 43 import com.android.internal.annotations.GuardedBy; 44 import com.android.internal.util.Preconditions; 45 import com.android.internal.util.SyncResultReceiver; 46 47 import java.io.PrintWriter; 48 import java.lang.annotation.Retention; 49 import java.lang.annotation.RetentionPolicy; 50 import java.util.ArrayList; 51 import java.util.Set; 52 53 /** 54 * Content capture is mechanism used to let apps notify the Android system of events associated with 55 * views. 56 * 57 * <p>Before using this manager, you should check if it's available. Example: 58 * <pre><code> 59 * ContentCaptureManager mgr = context.getSystemService(ContentCaptureManager.class); 60 * if (mgr != null && mgr.isContentCaptureEnabled()) { 61 * // ... 62 * } 63 * </code></pre> 64 * 65 * <p>To support content capture, you must notifiy the Android system of the following events: 66 * 67 * <ul> 68 * <li>When a visible view is laid out, call 69 * {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)}. 70 * <li>When a view becomes invisible or is removed from the view hierarchy, call 71 * {@link ContentCaptureSession#notifyViewDisappeared(android.view.autofill.AutofillId)}. 72 * <li>When the view represents text and the text value changed, call {@link 73 * ContentCaptureSession#notifyViewTextChanged(android.view.autofill.AutofillId, CharSequence)}. 74 * </ul> 75 * 76 * <p>You can get a blank content capture structure using 77 * {@link ContentCaptureSession#newViewStructure(View)}, then populate its relevant fields. 78 * Here's an example of the relevant methods for an {@code EditText}-like view: 79 * 80 * <pre><code> 81 * public class MyEditText extends View { 82 * 83 * private void populateContentCaptureStructure(@NonNull ViewStructure structure) { 84 * structure.setText(getText(), getSelectionStart(), getSelectionEnd()); 85 * structure.setHint(getHint()); 86 * structure.setInputType(getInputType()); 87 * // set other properties like setTextIdEntry(), setTextLines(), setTextStyle(), 88 * // setMinTextEms(), setMaxTextEms(), setMaxTextLength() 89 * } 90 * 91 * private void onTextChanged() { 92 * if (isLaidOut() && isTextEditable()) { 93 * ContentCaptureManager mgr = mContext.getSystemService(ContentCaptureManager.class); 94 * if (cm != null && cm.isContentCaptureEnabled()) { 95 * ContentCaptureSession session = getContentCaptureSession(); 96 * if (session != null) { 97 * session.notifyViewTextChanged(getAutofillId(), getText()); 98 * } 99 * } 100 * } 101 * </code></pre> 102 * 103 * <p>The main integration point with content capture is the {@link ContentCaptureSession}. A "main" 104 * session is automatically created by the Android system when content capture is enabled for the 105 * activity. The session could have a {@link ContentCaptureContext} to provide more contextual info 106 * about it, such as the locus associated with the view hierarchy 107 * (see {@link android.content.LocusId} for more info about locus). By default, the main session 108 * doesn't have a {@code ContentCaptureContext}, but you can change it after its created. Example: 109 * 110 * <pre><code> 111 * protected void onCreate(Bundle savedInstanceState) { 112 * // Initialize view structure 113 * ContentCaptureSession session = rootView.getContentCaptureSession(); 114 * if (session != null) { 115 * session.setContentCaptureContext(ContentCaptureContext.forLocusId("chat_UserA_UserB")); 116 * } 117 * } 118 * </code></pre> 119 * 120 * <p>If your activity contains view hierarchies with a different contextual meaning, you should 121 * created child sessions for each view hierarchy root. For example, if your activity is a browser, 122 * you could use the main session for the main URL being rendered, then child sessions for each 123 * {@code IFRAME}: 124 * 125 * <pre><code> 126 * ContentCaptureSession mMainSession; 127 * 128 * protected void onCreate(Bundle savedInstanceState) { 129 * // Initialize view structure... 130 * mMainSession = rootView.getContentCaptureSession(); 131 * if (mMainSession != null) { 132 * mMainSession.setContentCaptureContext( 133 * ContentCaptureContext.forLocusId("https://example.com")); 134 * } 135 * } 136 * 137 * private void loadIFrame(View iframeRootView, String url) { 138 * if (mMainSession != null) { 139 * ContentCaptureSession iFrameSession = mMainSession.newChild( 140 * ContentCaptureContext.forLocusId(url)); 141 * } 142 * iframeRootView.setContentCaptureSession(iFrameSession); 143 * } 144 * // Load iframe... 145 * } 146 * </code></pre> 147 * 148 */ 149 @SystemService(Context.CONTENT_CAPTURE_MANAGER_SERVICE) 150 public final class ContentCaptureManager { 151 152 private static final String TAG = ContentCaptureManager.class.getSimpleName(); 153 154 /** @hide */ 155 public static final int RESULT_CODE_OK = 0; 156 /** @hide */ 157 public static final int RESULT_CODE_TRUE = 1; 158 /** @hide */ 159 public static final int RESULT_CODE_FALSE = 2; 160 /** @hide */ 161 public static final int RESULT_CODE_SECURITY_EXCEPTION = -1; 162 163 /** 164 * Timeout for calls to system_server. 165 */ 166 private static final int SYNC_CALLS_TIMEOUT_MS = 5000; 167 168 /** 169 * DeviceConfig property used by {@code com.android.server.SystemServer} on start to decide 170 * whether the content capture service should be created or not 171 * 172 * <p>By default it should *NOT* be set (or set to {@code "default"}, so the decision is based 173 * on whether the OEM provides an implementation for the service), but it can be overridden to: 174 * 175 * <ul> 176 * <li>Provide a "kill switch" so OEMs can disable it remotely in case of emergency (when 177 * it's set to {@code "false"}). 178 * <li>Enable the CTS tests to be run on AOSP builds (when it's set to {@code "true"}). 179 * </ul> 180 * 181 * @hide 182 */ 183 @TestApi 184 public static final String DEVICE_CONFIG_PROPERTY_SERVICE_EXPLICITLY_ENABLED = 185 "service_explicitly_enabled"; 186 187 /** 188 * Maximum number of events that are buffered before sent to the app. 189 * 190 * @hide 191 */ 192 @TestApi 193 public static final String DEVICE_CONFIG_PROPERTY_MAX_BUFFER_SIZE = "max_buffer_size"; 194 195 /** 196 * Frequency (in ms) of buffer flushes when no events are received. 197 * 198 * @hide 199 */ 200 @TestApi 201 public static final String DEVICE_CONFIG_PROPERTY_IDLE_FLUSH_FREQUENCY = "idle_flush_frequency"; 202 203 /** 204 * Frequency (in ms) of buffer flushes when no events are received and the last one was a 205 * text change event. 206 * 207 * @hide 208 */ 209 @TestApi 210 public static final String DEVICE_CONFIG_PROPERTY_TEXT_CHANGE_FLUSH_FREQUENCY = 211 "text_change_flush_frequency"; 212 213 /** 214 * Size of events that are logging on {@code dump}. 215 * 216 * <p>Set it to {@code 0} or less to disable history. 217 * 218 * @hide 219 */ 220 @TestApi 221 public static final String DEVICE_CONFIG_PROPERTY_LOG_HISTORY_SIZE = "log_history_size"; 222 223 /** 224 * Sets the logging level for {@code logcat} statements. 225 * 226 * <p>Valid values are: {@link #LOGGING_LEVEL_OFF}, {@value #LOGGING_LEVEL_DEBUG}, and 227 * {@link #LOGGING_LEVEL_VERBOSE}. 228 * 229 * @hide 230 */ 231 @TestApi 232 public static final String DEVICE_CONFIG_PROPERTY_LOGGING_LEVEL = "logging_level"; 233 234 /** 235 * Sets how long (in ms) the service is bound while idle. 236 * 237 * <p>Use {@code 0} to keep it permanently bound. 238 * 239 * @hide 240 */ 241 public static final String DEVICE_CONFIG_PROPERTY_IDLE_UNBIND_TIMEOUT = "idle_unbind_timeout"; 242 243 /** @hide */ 244 @TestApi 245 public static final int LOGGING_LEVEL_OFF = 0; 246 247 /** @hide */ 248 @TestApi 249 public static final int LOGGING_LEVEL_DEBUG = 1; 250 251 /** @hide */ 252 @TestApi 253 public static final int LOGGING_LEVEL_VERBOSE = 2; 254 255 /** @hide */ 256 @IntDef(flag = false, value = { 257 LOGGING_LEVEL_OFF, 258 LOGGING_LEVEL_DEBUG, 259 LOGGING_LEVEL_VERBOSE 260 }) 261 @Retention(RetentionPolicy.SOURCE) 262 public @interface LoggingLevel {} 263 264 265 /** @hide */ 266 public static final int DEFAULT_MAX_BUFFER_SIZE = 100; 267 /** @hide */ 268 public static final int DEFAULT_IDLE_FLUSHING_FREQUENCY_MS = 5_000; 269 /** @hide */ 270 public static final int DEFAULT_TEXT_CHANGE_FLUSHING_FREQUENCY_MS = 1_000; 271 /** @hide */ 272 public static final int DEFAULT_LOG_HISTORY_SIZE = 10; 273 274 private final Object mLock = new Object(); 275 276 @NonNull 277 private final Context mContext; 278 279 @NonNull 280 private final IContentCaptureManager mService; 281 282 @NonNull 283 final ContentCaptureOptions mOptions; 284 285 // Flags used for starting session. 286 @GuardedBy("mLock") 287 private int mFlags; 288 289 // TODO(b/119220549): use UI Thread directly (as calls are one-way) or a shared thread / handler 290 // held at the Application level 291 @NonNull 292 private final Handler mHandler; 293 294 @GuardedBy("mLock") 295 private MainContentCaptureSession mMainSession; 296 297 /** @hide */ 298 public interface ContentCaptureClient { 299 /** 300 * Gets the component name of the client. 301 */ 302 @NonNull contentCaptureClientGetComponentName()303 ComponentName contentCaptureClientGetComponentName(); 304 } 305 306 /** @hide */ ContentCaptureManager(@onNull Context context, @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options)307 public ContentCaptureManager(@NonNull Context context, 308 @NonNull IContentCaptureManager service, @NonNull ContentCaptureOptions options) { 309 mContext = Preconditions.checkNotNull(context, "context cannot be null"); 310 mService = Preconditions.checkNotNull(service, "service cannot be null"); 311 mOptions = Preconditions.checkNotNull(options, "options cannot be null"); 312 313 ContentCaptureHelper.setLoggingLevel(mOptions.loggingLevel); 314 315 if (sVerbose) Log.v(TAG, "Constructor for " + context.getPackageName()); 316 317 // TODO(b/119220549): we might not even need a handler, as the IPCs are oneway. But if we 318 // do, then we should optimize it to run the tests after the Choreographer finishes the most 319 // important steps of the frame. 320 mHandler = Handler.createAsync(Looper.getMainLooper()); 321 } 322 323 /** 324 * Gets the main session associated with the context. 325 * 326 * <p>By default there's just one (associated with the activity lifecycle), but apps could 327 * explicitly add more using 328 * {@link ContentCaptureSession#createContentCaptureSession(ContentCaptureContext)}. 329 * 330 * @hide 331 */ 332 @NonNull 333 @UiThread getMainContentCaptureSession()334 public MainContentCaptureSession getMainContentCaptureSession() { 335 synchronized (mLock) { 336 if (mMainSession == null) { 337 mMainSession = new MainContentCaptureSession(mContext, this, mHandler, mService); 338 if (sVerbose) Log.v(TAG, "getMainContentCaptureSession(): created " + mMainSession); 339 } 340 return mMainSession; 341 } 342 } 343 344 /** @hide */ 345 @UiThread onActivityCreated(@onNull IBinder applicationToken, @NonNull ComponentName activityComponent)346 public void onActivityCreated(@NonNull IBinder applicationToken, 347 @NonNull ComponentName activityComponent) { 348 if (mOptions.lite) return; 349 synchronized (mLock) { 350 getMainContentCaptureSession().start(applicationToken, activityComponent, mFlags); 351 } 352 } 353 354 /** @hide */ 355 @UiThread onActivityResumed()356 public void onActivityResumed() { 357 if (mOptions.lite) return; 358 getMainContentCaptureSession().notifySessionLifecycle(/* started= */ true); 359 } 360 361 /** @hide */ 362 @UiThread onActivityPaused()363 public void onActivityPaused() { 364 if (mOptions.lite) return; 365 getMainContentCaptureSession().notifySessionLifecycle(/* started= */ false); 366 } 367 368 /** @hide */ 369 @UiThread onActivityDestroyed()370 public void onActivityDestroyed() { 371 if (mOptions.lite) return; 372 getMainContentCaptureSession().destroy(); 373 } 374 375 /** 376 * Flushes the content of all sessions. 377 * 378 * <p>Typically called by {@code Activity} when it's paused / resumed. 379 * 380 * @hide 381 */ 382 @UiThread flush(@lushReason int reason)383 public void flush(@FlushReason int reason) { 384 if (mOptions.lite) return; 385 getMainContentCaptureSession().flush(reason); 386 } 387 388 /** 389 * Returns the component name of the system service that is consuming the captured events for 390 * the current user. 391 */ 392 @Nullable getServiceComponentName()393 public ComponentName getServiceComponentName() { 394 if (!isContentCaptureEnabled() && !mOptions.lite) return null; 395 396 final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); 397 try { 398 mService.getServiceComponentName(resultReceiver); 399 return resultReceiver.getParcelableResult(); 400 } catch (RemoteException e) { 401 throw e.rethrowFromSystemServer(); 402 } 403 } 404 405 /** 406 * Gets the (optional) intent used to launch the service-specific settings. 407 * 408 * <p>This method is static because it's called by Settings, which might not be whitelisted 409 * for content capture (in which case the ContentCaptureManager on its context would be null). 410 * 411 * @hide 412 */ 413 // TODO: use "lite" options as it's done by activities from the content capture service 414 @Nullable getServiceSettingsComponentName()415 public static ComponentName getServiceSettingsComponentName() { 416 final IBinder binder = ServiceManager 417 .checkService(Context.CONTENT_CAPTURE_MANAGER_SERVICE); 418 if (binder == null) return null; 419 420 final IContentCaptureManager service = IContentCaptureManager.Stub.asInterface(binder); 421 final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); 422 try { 423 service.getServiceSettingsActivity(resultReceiver); 424 final int resultCode = resultReceiver.getIntResult(); 425 if (resultCode == RESULT_CODE_SECURITY_EXCEPTION) { 426 throw new SecurityException(resultReceiver.getStringResult()); 427 } 428 return resultReceiver.getParcelableResult(); 429 } catch (RemoteException e) { 430 throw e.rethrowFromSystemServer(); 431 } 432 } 433 434 /** 435 * Checks whether content capture is enabled for this activity. 436 */ isContentCaptureEnabled()437 public boolean isContentCaptureEnabled() { 438 if (mOptions.lite) return false; 439 440 final MainContentCaptureSession mainSession; 441 synchronized (mLock) { 442 mainSession = mMainSession; 443 } 444 // The main session is only set when the activity starts, so we need to return true until 445 // then. 446 if (mainSession != null && mainSession.isDisabled()) return false; 447 448 return true; 449 } 450 451 /** 452 * Gets the list of conditions for when content capture should be allowed. 453 * 454 * <p>This method is typically used by web browsers so they don't generate unnecessary content 455 * capture events for some websites. 456 * 457 * @return list of conditions, or {@code null} if there isn't any restriction 458 * (in which case content capture events should always be generated). If the list is empty, 459 * then it should not generate any event at all. 460 */ 461 @Nullable getContentCaptureConditions()462 public Set<ContentCaptureCondition> getContentCaptureConditions() { 463 // NOTE: we could cache the conditions on ContentCaptureOptions, but then it would be stick 464 // to the lifetime of the app. OTOH, by dynamically calling the server every time, we allow 465 // the service to fine tune how long-lived apps (like browsers) are whitelisted. 466 if (!isContentCaptureEnabled() && !mOptions.lite) return null; 467 468 final SyncResultReceiver resultReceiver = syncRun( 469 (r) -> mService.getContentCaptureConditions(mContext.getPackageName(), r)); 470 471 final ArrayList<ContentCaptureCondition> result = resultReceiver 472 .getParcelableListResult(); 473 return toSet(result); 474 } 475 476 /** 477 * Called by apps to explicitly enable or disable content capture. 478 * 479 * <p><b>Note: </b> this call is not persisted accross reboots, so apps should typically call 480 * it on {@link android.app.Activity#onCreate(android.os.Bundle, android.os.PersistableBundle)}. 481 */ setContentCaptureEnabled(boolean enabled)482 public void setContentCaptureEnabled(boolean enabled) { 483 if (sDebug) { 484 Log.d(TAG, "setContentCaptureEnabled(): setting to " + enabled + " for " + mContext); 485 } 486 487 MainContentCaptureSession mainSession; 488 synchronized (mLock) { 489 if (enabled) { 490 mFlags &= ~ContentCaptureContext.FLAG_DISABLED_BY_APP; 491 } else { 492 mFlags |= ContentCaptureContext.FLAG_DISABLED_BY_APP; 493 } 494 mainSession = mMainSession; 495 } 496 if (mainSession != null) { 497 mainSession.setDisabled(!enabled); 498 } 499 } 500 501 /** 502 * Called by apps to update flag secure when window attributes change. 503 * 504 * @hide 505 */ updateWindowAttributes(@onNull WindowManager.LayoutParams params)506 public void updateWindowAttributes(@NonNull WindowManager.LayoutParams params) { 507 if (sDebug) { 508 Log.d(TAG, "updateWindowAttributes(): window flags=" + params.flags); 509 } 510 final boolean flagSecureEnabled = 511 (params.flags & WindowManager.LayoutParams.FLAG_SECURE) != 0; 512 513 MainContentCaptureSession mainSession; 514 synchronized (mLock) { 515 if (flagSecureEnabled) { 516 mFlags |= ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE; 517 } else { 518 mFlags &= ~ContentCaptureContext.FLAG_DISABLED_BY_FLAG_SECURE; 519 } 520 mainSession = mMainSession; 521 } 522 if (mainSession != null) { 523 mainSession.setDisabled(flagSecureEnabled); 524 } 525 } 526 527 /** 528 * Gets whether content capture is enabled for the given user. 529 * 530 * <p>This method is typically used by the content capture service settings page, so it can 531 * provide a toggle to enable / disable it. 532 * 533 * @throws SecurityException if caller is not the app that owns the content capture service 534 * associated with the user. 535 * 536 * @hide 537 */ 538 @SystemApi 539 @TestApi isContentCaptureFeatureEnabled()540 public boolean isContentCaptureFeatureEnabled() { 541 final SyncResultReceiver resultReceiver = syncRun( 542 (r) -> mService.isContentCaptureFeatureEnabled(r)); 543 final int resultCode = resultReceiver.getIntResult(); 544 switch (resultCode) { 545 case RESULT_CODE_TRUE: 546 return true; 547 case RESULT_CODE_FALSE: 548 return false; 549 default: 550 Log.wtf(TAG, "received invalid result: " + resultCode); 551 return false; 552 } 553 } 554 555 /** 556 * Called by the app to remove content capture data associated with some context. 557 * 558 * @param request object specifying what data should be removed. 559 */ removeData(@onNull DataRemovalRequest request)560 public void removeData(@NonNull DataRemovalRequest request) { 561 Preconditions.checkNotNull(request); 562 563 try { 564 mService.removeData(request); 565 } catch (RemoteException e) { 566 e.rethrowFromSystemServer(); 567 } 568 } 569 570 /** 571 * Runs a sync method in the service, properly handling exceptions. 572 * 573 * @throws SecurityException if caller is not allowed to execute the method. 574 */ 575 @NonNull syncRun(@onNull MyRunnable r)576 private SyncResultReceiver syncRun(@NonNull MyRunnable r) { 577 final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); 578 try { 579 r.run(resultReceiver); 580 final int resultCode = resultReceiver.getIntResult(); 581 if (resultCode == RESULT_CODE_SECURITY_EXCEPTION) { 582 throw new SecurityException(resultReceiver.getStringResult()); 583 } 584 return resultReceiver; 585 } catch (RemoteException e) { 586 throw e.rethrowFromSystemServer(); 587 } 588 } 589 590 /** @hide */ dump(String prefix, PrintWriter pw)591 public void dump(String prefix, PrintWriter pw) { 592 pw.print(prefix); pw.println("ContentCaptureManager"); 593 final String prefix2 = prefix + " "; 594 synchronized (mLock) { 595 pw.print(prefix2); pw.print("isContentCaptureEnabled(): "); 596 pw.println(isContentCaptureEnabled()); 597 pw.print(prefix2); pw.print("Debug: "); pw.print(sDebug); 598 pw.print(" Verbose: "); pw.println(sVerbose); 599 pw.print(prefix2); pw.print("Context: "); pw.println(mContext); 600 pw.print(prefix2); pw.print("User: "); pw.println(mContext.getUserId()); 601 pw.print(prefix2); pw.print("Service: "); pw.println(mService); 602 pw.print(prefix2); pw.print("Flags: "); pw.println(mFlags); 603 pw.print(prefix2); pw.print("Options: "); mOptions.dumpShort(pw); pw.println(); 604 if (mMainSession != null) { 605 final String prefix3 = prefix2 + " "; 606 pw.print(prefix2); pw.println("Main session:"); 607 mMainSession.dump(prefix3, pw); 608 } else { 609 pw.print(prefix2); pw.println("No sessions"); 610 } 611 } 612 } 613 614 private interface MyRunnable { run(@onNull SyncResultReceiver receiver)615 void run(@NonNull SyncResultReceiver receiver) throws RemoteException; 616 } 617 } 618