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