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