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.os.Build.VERSION_CODES.UPSIDE_DOWN_CAKE; 19 import static android.view.contentcapture.ContentCaptureHelper.sDebug; 20 import static android.view.contentcapture.ContentCaptureHelper.sVerbose; 21 import static android.view.contentcapture.ContentCaptureManager.NO_SESSION_ID; 22 import static android.view.contentcapture.flags.Flags.FLAG_CCAPI_BAKLAVA_ENABLED; 23 24 import android.annotation.CallSuper; 25 import android.annotation.FlaggedApi; 26 import android.annotation.IntDef; 27 import android.annotation.NonNull; 28 import android.annotation.Nullable; 29 import android.app.compat.CompatChanges; 30 import android.compat.annotation.ChangeId; 31 import android.compat.annotation.EnabledSince; 32 import android.content.ComponentName; 33 import android.graphics.Insets; 34 import android.graphics.Rect; 35 import android.os.IBinder; 36 import android.util.DebugUtils; 37 import android.util.Log; 38 import android.util.SparseArray; 39 import android.view.View; 40 import android.view.ViewStructure; 41 import android.view.autofill.AutofillId; 42 import android.view.contentcapture.ViewNode.ViewStructureImpl; 43 44 import com.android.internal.annotations.GuardedBy; 45 import com.android.internal.annotations.VisibleForTesting; 46 import com.android.internal.os.IResultReceiver; 47 import com.android.internal.util.ArrayUtils; 48 import com.android.internal.util.Preconditions; 49 50 import java.io.PrintWriter; 51 import java.lang.annotation.Retention; 52 import java.lang.annotation.RetentionPolicy; 53 import java.security.SecureRandom; 54 import java.util.ArrayList; 55 import java.util.List; 56 import java.util.Objects; 57 58 /** 59 * Session used when notifying the Android system about events associated with views. 60 */ 61 public abstract class ContentCaptureSession implements AutoCloseable { 62 63 private static final String TAG = ContentCaptureSession.class.getSimpleName(); 64 65 // TODO(b/158778794): to make the session ids truly globally unique across 66 // processes, we may need to explore other options. 67 private static final SecureRandom ID_GENERATOR = new SecureRandom(); 68 69 /** 70 * Name of the {@link IResultReceiver} extra used to pass the binder interface to the service. 71 * @hide 72 */ 73 public static final String EXTRA_BINDER = "binder"; 74 75 /** 76 * Name of the {@link IResultReceiver} extra used to pass the content capture enabled state. 77 * @hide 78 */ 79 public static final String EXTRA_ENABLED_STATE = "enabled"; 80 81 /** 82 * Initial state, when there is no session. 83 * 84 * @hide 85 */ 86 // NOTE: not prefixed by STATE_ so it's not printed on getStateAsString() 87 public static final int UNKNOWN_STATE = 0x0; 88 89 /** 90 * Service's startSession() was called, but server didn't confirm it was created yet. 91 * 92 * @hide 93 */ 94 public static final int STATE_WAITING_FOR_SERVER = 0x1; 95 96 /** 97 * Session is active. 98 * 99 * @hide 100 */ 101 public static final int STATE_ACTIVE = 0x2; 102 103 /** 104 * Session is disabled because there is no service for this user. 105 * 106 * @hide 107 */ 108 public static final int STATE_DISABLED = 0x4; 109 110 /** 111 * Session is disabled because its id already existed on server. 112 * 113 * @hide 114 */ 115 public static final int STATE_DUPLICATED_ID = 0x8; 116 117 /** 118 * Session is disabled because service is not set for user. 119 * 120 * @hide 121 */ 122 public static final int STATE_NO_SERVICE = 0x10; 123 124 /** 125 * Session is disabled by FLAG_SECURE 126 * 127 * @hide 128 */ 129 public static final int STATE_FLAG_SECURE = 0x20; 130 131 /** 132 * Session is disabled manually by the specific app 133 * (through {@link ContentCaptureManager#setContentCaptureEnabled(boolean)}). 134 * 135 * @hide 136 */ 137 public static final int STATE_BY_APP = 0x40; 138 139 /** 140 * Session is disabled because session start was never replied. 141 * 142 * @hide 143 */ 144 public static final int STATE_NO_RESPONSE = 0x80; 145 146 /** 147 * Session is disabled because an internal error. 148 * 149 * @hide 150 */ 151 public static final int STATE_INTERNAL_ERROR = 0x100; 152 153 /** 154 * Session is disabled because service didn't allowlist package or activity. 155 * 156 * @hide 157 */ 158 public static final int STATE_NOT_WHITELISTED = 0x200; 159 160 /** 161 * Session is disabled because the service died. 162 * 163 * @hide 164 */ 165 public static final int STATE_SERVICE_DIED = 0x400; 166 167 /** 168 * Session is disabled because the service package is being udpated. 169 * 170 * @hide 171 */ 172 public static final int STATE_SERVICE_UPDATING = 0x800; 173 174 /** 175 * Session is enabled, after the service died and came back to live. 176 * 177 * @hide 178 */ 179 public static final int STATE_SERVICE_RESURRECTED = 0x1000; 180 181 private static final int INITIAL_CHILDREN_CAPACITY = 5; 182 183 /** @hide */ 184 public static final int FLUSH_REASON_FULL = 1; 185 /** @hide */ 186 public static final int FLUSH_REASON_VIEW_ROOT_ENTERED = 2; 187 /** @hide */ 188 public static final int FLUSH_REASON_SESSION_STARTED = 3; 189 /** @hide */ 190 public static final int FLUSH_REASON_SESSION_FINISHED = 4; 191 /** @hide */ 192 public static final int FLUSH_REASON_IDLE_TIMEOUT = 5; 193 /** @hide */ 194 public static final int FLUSH_REASON_TEXT_CHANGE_TIMEOUT = 6; 195 /** @hide */ 196 public static final int FLUSH_REASON_SESSION_CONNECTED = 7; 197 /** @hide */ 198 public static final int FLUSH_REASON_FORCE_FLUSH = 8; 199 /** @hide */ 200 public static final int FLUSH_REASON_VIEW_TREE_APPEARING = 9; 201 /** @hide */ 202 public static final int FLUSH_REASON_VIEW_TREE_APPEARED = 10; 203 204 /** 205 * After {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE}, 206 * {@link #notifyViewsDisappeared(AutofillId, long[])} wraps 207 * the virtual children with a pair of view tree appearing and view tree appeared events. 208 */ 209 @ChangeId 210 @EnabledSince(targetSdkVersion = UPSIDE_DOWN_CAKE) 211 static final long NOTIFY_NODES_DISAPPEAR_NOW_SENDS_TREE_EVENTS = 258825825L; 212 213 /** @hide */ 214 @IntDef( 215 prefix = {"FLUSH_REASON_"}, 216 value = { 217 FLUSH_REASON_FULL, 218 FLUSH_REASON_VIEW_ROOT_ENTERED, 219 FLUSH_REASON_SESSION_STARTED, 220 FLUSH_REASON_SESSION_FINISHED, 221 FLUSH_REASON_IDLE_TIMEOUT, 222 FLUSH_REASON_TEXT_CHANGE_TIMEOUT, 223 FLUSH_REASON_SESSION_CONNECTED, 224 FLUSH_REASON_FORCE_FLUSH, 225 FLUSH_REASON_VIEW_TREE_APPEARING, 226 FLUSH_REASON_VIEW_TREE_APPEARED 227 }) 228 @Retention(RetentionPolicy.SOURCE) 229 public @interface FlushReason {} 230 231 private final Object mLock = new Object(); 232 233 /** 234 * Guard use to ignore events after it's destroyed. 235 */ 236 @NonNull 237 @GuardedBy("mLock") 238 private boolean mDestroyed; 239 240 /** @hide */ 241 @Nullable 242 protected final int mId; 243 244 private int mState = UNKNOWN_STATE; 245 246 // Lazily created on demand. 247 private ContentCaptureSessionId mContentCaptureSessionId; 248 249 /** 250 * {@link ContentCaptureContext} set by client, or {@code null} when it's the 251 * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the 252 * context. 253 */ 254 @Nullable 255 private ContentCaptureContext mClientContext; 256 257 /** 258 * List of children session. 259 */ 260 @Nullable 261 @GuardedBy("mLock") 262 private ArrayList<ContentCaptureSession> mChildren; 263 264 /** @hide */ ContentCaptureSession()265 protected ContentCaptureSession() { 266 this(getRandomSessionId()); 267 } 268 269 /** @hide */ 270 @VisibleForTesting ContentCaptureSession(int id)271 public ContentCaptureSession(int id) { 272 Preconditions.checkArgument(id != NO_SESSION_ID); 273 mId = id; 274 } 275 276 // Used by ChildContentCaptureSession ContentCaptureSession(@onNull ContentCaptureContext initialContext)277 ContentCaptureSession(@NonNull ContentCaptureContext initialContext) { 278 this(); 279 mClientContext = Objects.requireNonNull(initialContext); 280 } 281 282 /** @hide */ 283 @NonNull getMainCaptureSession()284 abstract ContentCaptureSession getMainCaptureSession(); 285 start(@onNull IBinder token, @NonNull IBinder shareableActivityToken, @NonNull ComponentName component, int flags)286 abstract void start(@NonNull IBinder token, @NonNull IBinder shareableActivityToken, 287 @NonNull ComponentName component, int flags); 288 isDisabled()289 abstract boolean isDisabled(); 290 291 /** 292 * Sets the disabled state of content capture. 293 * 294 * @return whether disabled state was changed. 295 */ setDisabled(boolean disabled)296 abstract boolean setDisabled(boolean disabled); 297 298 /** 299 * Gets the id used to identify this session. 300 */ 301 @NonNull getContentCaptureSessionId()302 public final ContentCaptureSessionId getContentCaptureSessionId() { 303 if (mContentCaptureSessionId == null) { 304 mContentCaptureSessionId = new ContentCaptureSessionId(mId); 305 } 306 return mContentCaptureSessionId; 307 } 308 309 /** @hide */ 310 @NonNull getId()311 public int getId() { 312 return mId; 313 } 314 315 /** 316 * Creates a new {@link ContentCaptureSession}. 317 * 318 * <p>See {@link View#setContentCaptureSession(ContentCaptureSession)} for more info. 319 */ 320 @NonNull createContentCaptureSession( @onNull ContentCaptureContext context)321 public final ContentCaptureSession createContentCaptureSession( 322 @NonNull ContentCaptureContext context) { 323 final ContentCaptureSession child = newChild(context); 324 if (sDebug) { 325 Log.d(TAG, "createContentCaptureSession(" + context + ": parent=" + mId + ", child=" 326 + child.mId); 327 } 328 synchronized (mLock) { 329 if (mChildren == null) { 330 mChildren = new ArrayList<>(INITIAL_CHILDREN_CAPACITY); 331 } 332 mChildren.add(child); 333 } 334 return child; 335 } 336 newChild(@onNull ContentCaptureContext context)337 abstract ContentCaptureSession newChild(@NonNull ContentCaptureContext context); 338 339 /** 340 * Flushes the buffered events to the service. 341 */ flush(@lushReason int reason)342 abstract void flush(@FlushReason int reason); 343 344 /** 345 * Sets the {@link ContentCaptureContext} associated with the session. 346 * 347 * <p>Typically used to change the context associated with the default session from an activity. 348 */ setContentCaptureContext(@ullable ContentCaptureContext context)349 public final void setContentCaptureContext(@Nullable ContentCaptureContext context) { 350 if (!isContentCaptureEnabled()) return; 351 352 mClientContext = context; 353 updateContentCaptureContext(context); 354 } 355 updateContentCaptureContext(@ullable ContentCaptureContext context)356 abstract void updateContentCaptureContext(@Nullable ContentCaptureContext context); 357 358 /** 359 * Gets the {@link ContentCaptureContext} associated with the session. 360 * 361 * @return context set on constructor or by 362 * {@link #setContentCaptureContext(ContentCaptureContext)}, or {@code null} if never 363 * explicitly set. 364 */ 365 @Nullable getContentCaptureContext()366 public final ContentCaptureContext getContentCaptureContext() { 367 return mClientContext; 368 } 369 370 /** 371 * Destroys this session, flushing out all pending notifications to the service. 372 * 373 * <p>Once destroyed, any new notification will be dropped. 374 */ destroy()375 public final void destroy() { 376 synchronized (mLock) { 377 if (mDestroyed) { 378 if (sDebug) Log.d(TAG, "destroy(" + mId + "): already destroyed"); 379 return; 380 } 381 mDestroyed = true; 382 383 // TODO(b/111276913): check state (for example, how to handle if it's waiting for remote 384 // id) and send it to the cache of batched commands 385 if (sVerbose) { 386 Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId); 387 } 388 // Finish children first 389 if (mChildren != null) { 390 final int numberChildren = mChildren.size(); 391 if (sVerbose) Log.v(TAG, "Destroying " + numberChildren + " children first"); 392 for (int i = 0; i < numberChildren; i++) { 393 final ContentCaptureSession child = mChildren.get(i); 394 try { 395 child.destroy(); 396 } catch (Exception e) { 397 Log.w(TAG, "exception destroying child session #" + i + ": " + e); 398 } 399 } 400 } 401 } 402 403 onDestroy(); 404 } 405 onDestroy()406 abstract void onDestroy(); 407 408 /** @hide */ 409 @Override close()410 public void close() { 411 destroy(); 412 } 413 414 /** 415 * Notifies the Content Capture Service that a node has been added to the view structure. 416 * 417 * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or 418 * automatically by the Android System for views that return {@code true} on 419 * {@link View#onProvideContentCaptureStructure(ViewStructure, int)}. 420 * 421 * <p>Consider use {@link #notifyViewsAppeared} which has a better performance when notifying 422 * a list of nodes has appeared. 423 * 424 * @param node node that has been added. 425 */ notifyViewAppeared(@onNull ViewStructure node)426 public final void notifyViewAppeared(@NonNull ViewStructure node) { 427 Objects.requireNonNull(node); 428 if (!isContentCaptureEnabled()) return; 429 430 if (!(node instanceof ViewNode.ViewStructureImpl)) { 431 throw new IllegalArgumentException("Invalid node class: " + node.getClass()); 432 } 433 434 internalNotifyViewAppeared(mId, (ViewStructureImpl) node); 435 } 436 internalNotifyViewAppeared( int sessionId, @NonNull ViewNode.ViewStructureImpl node)437 abstract void internalNotifyViewAppeared( 438 int sessionId, @NonNull ViewNode.ViewStructureImpl node); 439 440 /** 441 * Notifies the Content Capture Service that a node has been removed from the view structure. 442 * 443 * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or 444 * automatically by the Android System for standard views. 445 * 446 * <p>Consider use {@link #notifyViewsDisappeared} which has a better performance when notifying 447 * a list of nodes has disappeared. 448 * 449 * @param id id of the node that has been removed. 450 */ notifyViewDisappeared(@onNull AutofillId id)451 public final void notifyViewDisappeared(@NonNull AutofillId id) { 452 Objects.requireNonNull(id); 453 if (!isContentCaptureEnabled()) return; 454 455 internalNotifyViewDisappeared(mId, id); 456 } 457 internalNotifyViewDisappeared(int sessionId, @NonNull AutofillId id)458 abstract void internalNotifyViewDisappeared(int sessionId, @NonNull AutofillId id); 459 460 /** 461 * Notifies the Content Capture Service that a list of nodes has appeared in the view structure. 462 * 463 * <p>Typically called manually by views that handle their own virtual view hierarchy. 464 * 465 * @param appearedNodes nodes that have appeared. Each element represents a view node that has 466 * been added to the view structure. The order of the elements is important, which should be 467 * preserved as the attached order of when the node is attached to the virtual view hierarchy. 468 */ notifyViewsAppeared(@onNull List<ViewStructure> appearedNodes)469 public final void notifyViewsAppeared(@NonNull List<ViewStructure> appearedNodes) { 470 Preconditions.checkCollectionElementsNotNull(appearedNodes, "appearedNodes"); 471 if (!isContentCaptureEnabled()) return; 472 473 for (int i = 0; i < appearedNodes.size(); i++) { 474 ViewStructure v = appearedNodes.get(i); 475 if (!(v instanceof ViewNode.ViewStructureImpl)) { 476 throw new IllegalArgumentException("Invalid class: " + v.getClass()); 477 } 478 } 479 480 internalNotifyViewTreeEvent(mId, /* started= */ true); 481 for (int i = 0; i < appearedNodes.size(); i++) { 482 ViewStructure v = appearedNodes.get(i); 483 internalNotifyViewAppeared(mId, (ViewStructureImpl) v); 484 } 485 internalNotifyViewTreeEvent(mId, /* started= */ false); 486 } 487 488 /** 489 * Notifies the Content Capture Service that many nodes has been removed from a virtual view 490 * structure. 491 * 492 * <p>Should only be called by views that handle their own virtual view hierarchy. 493 * 494 * <p>After UPSIDE_DOWN_CAKE, this method wraps the virtual children with a pair of view tree 495 * appearing and view tree appeared events. 496 * 497 * @param hostId id of the non-virtual view hosting the virtual view hierarchy (it can be 498 * obtained by calling {@link ViewStructure#getAutofillId()}). 499 * @param virtualIds ids of the virtual children. 500 * 501 * @throws IllegalArgumentException if the {@code hostId} is an autofill id for a virtual view. 502 * @throws IllegalArgumentException if {@code virtualIds} is empty 503 */ notifyViewsDisappeared(@onNull AutofillId hostId, @NonNull long[] virtualIds)504 public final void notifyViewsDisappeared(@NonNull AutofillId hostId, 505 @NonNull long[] virtualIds) { 506 Preconditions.checkArgument(hostId.isNonVirtual(), "hostId cannot be virtual: %s", hostId); 507 Preconditions.checkArgument(!ArrayUtils.isEmpty(virtualIds), "virtual ids cannot be empty"); 508 if (!isContentCaptureEnabled()) return; 509 510 if (CompatChanges.isChangeEnabled(NOTIFY_NODES_DISAPPEAR_NOW_SENDS_TREE_EVENTS)) { 511 internalNotifyViewTreeEvent(mId, /* started= */ true); 512 } 513 // TODO(b/123036895): use a internalNotifyViewsDisappeared that optimizes how the event is 514 // parcelized 515 for (long id : virtualIds) { 516 internalNotifyViewDisappeared(mId, new AutofillId(hostId, id, mId)); 517 } 518 if (CompatChanges.isChangeEnabled(NOTIFY_NODES_DISAPPEAR_NOW_SENDS_TREE_EVENTS)) { 519 internalNotifyViewTreeEvent(mId, /* started= */ false); 520 } 521 } 522 523 /** 524 * Notifies the Intelligence Service that the value of a text node has been changed. 525 * 526 * @param id of the node. 527 * @param text new text. 528 */ notifyViewTextChanged(@onNull AutofillId id, @Nullable CharSequence text)529 public final void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) { 530 Objects.requireNonNull(id); 531 532 if (!isContentCaptureEnabled()) return; 533 534 internalNotifyViewTextChanged(mId, id, text); 535 } 536 internalNotifyViewTextChanged(int sessionId, @NonNull AutofillId id, @Nullable CharSequence text)537 abstract void internalNotifyViewTextChanged(int sessionId, @NonNull AutofillId id, 538 @Nullable CharSequence text); 539 540 /** 541 * Notifies the Intelligence Service that the insets of a view have changed. 542 */ notifyViewInsetsChanged(@onNull Insets viewInsets)543 public final void notifyViewInsetsChanged(@NonNull Insets viewInsets) { 544 Objects.requireNonNull(viewInsets); 545 546 if (!isContentCaptureEnabled()) return; 547 548 internalNotifyViewInsetsChanged(mId, viewInsets); 549 } 550 internalNotifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets)551 abstract void internalNotifyViewInsetsChanged(int sessionId, @NonNull Insets viewInsets); 552 553 /** 554 * Flushes an internal buffer of UI events and signals System Intelligence (SI) that a 555 * semantically meaningful state has been reached. SI uses this signal to potentially 556 * rebuild the view hierarchy and understand the current state of the UI. 557 * 558 * <p>UI events are often batched together for performance reasons. A semantic batch 559 * represents a series of events that, when applied sequentially, result in a 560 * meaningful and complete UI state. 561 * 562 * <p>It is crucial to call {@code flush()} after completing a semantic batch to ensure 563 * SI can accurately reconstruct the view hierarchy. 564 * 565 * <p><b>Premature Flushing:</b> Calling {@code flush()} within a semantic batch may 566 * lead to SI failing to rebuild the view hierarchy correctly. This could manifest as 567 * incorrect ordering of sibling nodes. 568 * 569 * <p><b>Delayed Flushing:</b> While not immediately flushing after a semantic batch is 570 * generally safe, it's recommended to do so as soon as possible. In the worst-case 571 * scenario where a {@code flush()} is never called, SI will attempt to process the 572 * events after a short delay based on view appearance and disappearance events. 573 */ 574 @FlaggedApi(FLAG_CCAPI_BAKLAVA_ENABLED) flush()575 public void flush() { 576 internalNotifySessionFlushEvent(mId); 577 } 578 579 /** @hide */ internalNotifySessionFlushEvent(int sessionId)580 abstract void internalNotifySessionFlushEvent(int sessionId); 581 582 /** @hide */ notifyViewTreeEvent(boolean started)583 public void notifyViewTreeEvent(boolean started) { 584 internalNotifyViewTreeEvent(mId, started); 585 } 586 587 /** @hide */ internalNotifyViewTreeEvent(int sessionId, boolean started)588 abstract void internalNotifyViewTreeEvent(int sessionId, boolean started); 589 590 /** 591 * Notifies the Content Capture Service that a session has resumed. 592 */ notifySessionResumed()593 public final void notifySessionResumed() { 594 if (!isContentCaptureEnabled()) return; 595 596 internalNotifySessionResumed(); 597 } 598 internalNotifySessionResumed()599 abstract void internalNotifySessionResumed(); 600 601 /** 602 * Notifies the Content Capture Service that a session has paused. 603 */ notifySessionPaused()604 public final void notifySessionPaused() { 605 if (!isContentCaptureEnabled()) return; 606 607 internalNotifySessionPaused(); 608 } 609 internalNotifySessionPaused()610 abstract void internalNotifySessionPaused(); 611 internalNotifyChildSessionStarted(int parentSessionId, int childSessionId, @NonNull ContentCaptureContext clientContext)612 abstract void internalNotifyChildSessionStarted(int parentSessionId, int childSessionId, 613 @NonNull ContentCaptureContext clientContext); 614 internalNotifyChildSessionFinished(int parentSessionId, int childSessionId)615 abstract void internalNotifyChildSessionFinished(int parentSessionId, int childSessionId); 616 internalNotifyContextUpdated( int sessionId, @Nullable ContentCaptureContext context)617 abstract void internalNotifyContextUpdated( 618 int sessionId, @Nullable ContentCaptureContext context); 619 620 /** @hide */ notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds)621 public abstract void notifyWindowBoundsChanged(int sessionId, @NonNull Rect bounds); 622 623 /** @hide */ notifyContentCaptureEvents( @onNull SparseArray<ArrayList<Object>> contentCaptureEvents)624 public abstract void notifyContentCaptureEvents( 625 @NonNull SparseArray<ArrayList<Object>> contentCaptureEvents); 626 627 /** 628 * Creates a {@link ViewStructure} for a "standard" view. 629 * 630 * <p>This method should be called after a visible view is laid out; the view then must populate 631 * the structure and pass it to {@link #notifyViewAppeared(ViewStructure)}. 632 * 633 * <b>Note: </b>views that manage a virtual structure under this view must populate just the 634 * node representing this view and return right away, then asynchronously report (not 635 * necessarily in the UI thread) when the children nodes appear, disappear or have their text 636 * changed by calling {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)}, 637 * {@link ContentCaptureSession#notifyViewDisappeared(AutofillId)}, and 638 * {@link ContentCaptureSession#notifyViewTextChanged(AutofillId, CharSequence)} respectively. 639 * The structure for the a child must be created using 640 * {@link ContentCaptureSession#newVirtualViewStructure(AutofillId, long)}, and the 641 * {@code autofillId} for a child can be obtained either through 642 * {@code childStructure.getAutofillId()} or 643 * {@link ContentCaptureSession#newAutofillId(AutofillId, long)}. 644 * 645 * <p>When the virtual view hierarchy represents a web page, you should also: 646 * 647 * <ul> 648 * <li>Call {@link ContentCaptureManager#getContentCaptureConditions()} to infer content capture 649 * events should be generate for that URL. 650 * <li>Create a new {@link ContentCaptureSession} child for every HTML element that renders a 651 * new URL (like an {@code IFRAME}) and use that session to notify events from that subtree. 652 * </ul> 653 * 654 * <p><b>Note: </b>the following methods of the {@code structure} will be ignored: 655 * <ul> 656 * <li>{@link ViewStructure#setChildCount(int)} 657 * <li>{@link ViewStructure#addChildCount(int)} 658 * <li>{@link ViewStructure#getChildCount()} 659 * <li>{@link ViewStructure#newChild(int)} 660 * <li>{@link ViewStructure#asyncNewChild(int)} 661 * <li>{@link ViewStructure#asyncCommit()} 662 * <li>{@link ViewStructure#setWebDomain(String)} 663 * <li>{@link ViewStructure#newHtmlInfoBuilder(String)} 664 * <li>{@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)} 665 * <li>{@link ViewStructure#setDataIsSensitive(boolean)} 666 * <li>{@link ViewStructure#setAlpha(float)} 667 * <li>{@link ViewStructure#setElevation(float)} 668 * <li>{@link ViewStructure#setTransformation(android.graphics.Matrix)} 669 * </ul> 670 */ 671 @NonNull newViewStructure(@onNull View view)672 public final ViewStructure newViewStructure(@NonNull View view) { 673 return new ViewNode.ViewStructureImpl(view); 674 } 675 676 /** 677 * Creates a new {@link AutofillId} for a virtual child, so it can be used to uniquely identify 678 * the children in the session. 679 * 680 * @param hostId id of the non-virtual view hosting the virtual view hierarchy (it can be 681 * obtained by calling {@link ViewStructure#getAutofillId()}). 682 * @param virtualChildId id of the virtual child, relative to the parent. 683 * 684 * @return if for the virtual child 685 * 686 * @throws IllegalArgumentException if the {@code parentId} is a virtual child id. 687 */ newAutofillId(@onNull AutofillId hostId, long virtualChildId)688 public @NonNull AutofillId newAutofillId(@NonNull AutofillId hostId, long virtualChildId) { 689 Objects.requireNonNull(hostId); 690 Preconditions.checkArgument(hostId.isNonVirtual(), "hostId cannot be virtual: %s", hostId); 691 return new AutofillId(hostId, virtualChildId, mId); 692 } 693 694 /** 695 * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to 696 * {@link #notifyViewAppeared(ViewStructure)} by the view managing the virtual view hierarchy. 697 * 698 * @param parentId id of the virtual view parent (it can be obtained by calling 699 * {@link ViewStructure#getAutofillId()} on the parent). 700 * @param virtualId id of the virtual child, relative to the parent. 701 * 702 * @return a new {@link ViewStructure} that can be used for Content Capture purposes. 703 */ 704 @NonNull newVirtualViewStructure(@onNull AutofillId parentId, long virtualId)705 public final ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId, 706 long virtualId) { 707 return new ViewNode.ViewStructureImpl(parentId, virtualId, mId); 708 } 709 isContentCaptureEnabled()710 boolean isContentCaptureEnabled() { 711 synchronized (mLock) { 712 return !mDestroyed; 713 } 714 } 715 716 @CallSuper dump(@onNull String prefix, @NonNull PrintWriter pw)717 void dump(@NonNull String prefix, @NonNull PrintWriter pw) { 718 pw.print(prefix); pw.print("id: "); pw.println(mId); 719 if (mClientContext != null) { 720 pw.print(prefix); mClientContext.dump(pw); pw.println(); 721 } 722 synchronized (mLock) { 723 pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); 724 if (mChildren != null && !mChildren.isEmpty()) { 725 final String prefix2 = prefix + " "; 726 final int numberChildren = mChildren.size(); 727 pw.print(prefix); pw.print("number children: "); pw.println(numberChildren); 728 for (int i = 0; i < numberChildren; i++) { 729 final ContentCaptureSession child = mChildren.get(i); 730 pw.print(prefix); pw.print(i); pw.println(": "); child.dump(prefix2, pw); 731 } 732 } 733 } 734 } 735 736 @Override toString()737 public String toString() { 738 return Integer.toString(mId); 739 } 740 741 /** @hide */ 742 @NonNull getStateAsString(int state)743 protected static String getStateAsString(int state) { 744 return state + " (" + (state == UNKNOWN_STATE ? "UNKNOWN" 745 : DebugUtils.flagsToString(ContentCaptureSession.class, "STATE_", state)) + ")"; 746 } 747 748 /** @hide */ 749 @NonNull getFlushReasonAsString(@lushReason int reason)750 public static String getFlushReasonAsString(@FlushReason int reason) { 751 switch (reason) { 752 case FLUSH_REASON_FULL: 753 return "FULL"; 754 case FLUSH_REASON_VIEW_ROOT_ENTERED: 755 return "VIEW_ROOT"; 756 case FLUSH_REASON_SESSION_STARTED: 757 return "STARTED"; 758 case FLUSH_REASON_SESSION_FINISHED: 759 return "FINISHED"; 760 case FLUSH_REASON_IDLE_TIMEOUT: 761 return "IDLE"; 762 case FLUSH_REASON_TEXT_CHANGE_TIMEOUT: 763 return "TEXT_CHANGE"; 764 case FLUSH_REASON_SESSION_CONNECTED: 765 return "CONNECTED"; 766 case FLUSH_REASON_FORCE_FLUSH: 767 return "FORCE_FLUSH"; 768 case FLUSH_REASON_VIEW_TREE_APPEARING: 769 return "VIEW_TREE_APPEARING"; 770 case FLUSH_REASON_VIEW_TREE_APPEARED: 771 return "VIEW_TREE_APPEARED"; 772 default: 773 return "UNKNOWN-" + reason; 774 } 775 } 776 getRandomSessionId()777 private static int getRandomSessionId() { 778 int id; 779 do { 780 id = ID_GENERATOR.nextInt(); 781 } while (id == NO_SESSION_ID); 782 return id; 783 } 784 } 785