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 the Android a system-provided content capture service 47 * 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 whitelist 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 174 /** @hide */ 175 @IntDef(prefix = { "FLUSH_REASON_" }, value = { 176 FLUSH_REASON_FULL, 177 FLUSH_REASON_VIEW_ROOT_ENTERED, 178 FLUSH_REASON_SESSION_STARTED, 179 FLUSH_REASON_SESSION_FINISHED, 180 FLUSH_REASON_IDLE_TIMEOUT, 181 FLUSH_REASON_TEXT_CHANGE_TIMEOUT, 182 FLUSH_REASON_SESSION_CONNECTED 183 }) 184 @Retention(RetentionPolicy.SOURCE) 185 public @interface FlushReason{} 186 187 private final Object mLock = new Object(); 188 189 /** 190 * Guard use to ignore events after it's destroyed. 191 */ 192 @NonNull 193 @GuardedBy("mLock") 194 private boolean mDestroyed; 195 196 /** @hide */ 197 @Nullable 198 protected final int mId; 199 200 private int mState = UNKNOWN_STATE; 201 202 // Lazily created on demand. 203 private ContentCaptureSessionId mContentCaptureSessionId; 204 205 /** 206 * {@link ContentCaptureContext} set by client, or {@code null} when it's the 207 * {@link ContentCaptureManager#getMainContentCaptureSession() default session} for the 208 * context. 209 */ 210 @Nullable 211 private ContentCaptureContext mClientContext; 212 213 /** 214 * List of children session. 215 */ 216 @Nullable 217 @GuardedBy("mLock") 218 private ArrayList<ContentCaptureSession> mChildren; 219 220 /** @hide */ ContentCaptureSession()221 protected ContentCaptureSession() { 222 this(getRandomSessionId()); 223 } 224 225 /** @hide */ 226 @VisibleForTesting ContentCaptureSession(int id)227 public ContentCaptureSession(int id) { 228 Preconditions.checkArgument(id != NO_SESSION_ID); 229 mId = id; 230 } 231 232 // Used by ChildCOntentCaptureSession ContentCaptureSession(@onNull ContentCaptureContext initialContext)233 ContentCaptureSession(@NonNull ContentCaptureContext initialContext) { 234 this(); 235 mClientContext = Preconditions.checkNotNull(initialContext); 236 } 237 238 /** @hide */ 239 @NonNull getMainCaptureSession()240 abstract MainContentCaptureSession getMainCaptureSession(); 241 242 /** 243 * Gets the id used to identify this session. 244 */ 245 @NonNull getContentCaptureSessionId()246 public final ContentCaptureSessionId getContentCaptureSessionId() { 247 if (mContentCaptureSessionId == null) { 248 mContentCaptureSessionId = new ContentCaptureSessionId(mId); 249 } 250 return mContentCaptureSessionId; 251 } 252 253 /** @hide */ 254 @NonNull getId()255 public int getId() { 256 return mId; 257 } 258 259 /** 260 * Creates a new {@link ContentCaptureSession}. 261 * 262 * <p>See {@link View#setContentCaptureSession(ContentCaptureSession)} for more info. 263 */ 264 @NonNull createContentCaptureSession( @onNull ContentCaptureContext context)265 public final ContentCaptureSession createContentCaptureSession( 266 @NonNull ContentCaptureContext context) { 267 final ContentCaptureSession child = newChild(context); 268 if (sDebug) { 269 Log.d(TAG, "createContentCaptureSession(" + context + ": parent=" + mId + ", child=" 270 + child.mId); 271 } 272 synchronized (mLock) { 273 if (mChildren == null) { 274 mChildren = new ArrayList<>(INITIAL_CHILDREN_CAPACITY); 275 } 276 mChildren.add(child); 277 } 278 return child; 279 } 280 newChild(@onNull ContentCaptureContext context)281 abstract ContentCaptureSession newChild(@NonNull ContentCaptureContext context); 282 283 /** 284 * Flushes the buffered events to the service. 285 */ flush(@lushReason int reason)286 abstract void flush(@FlushReason int reason); 287 288 /** 289 * Sets the {@link ContentCaptureContext} associated with the session. 290 * 291 * <p>Typically used to change the context associated with the default session from an activity. 292 */ setContentCaptureContext(@ullable ContentCaptureContext context)293 public final void setContentCaptureContext(@Nullable ContentCaptureContext context) { 294 mClientContext = context; 295 updateContentCaptureContext(context); 296 } 297 updateContentCaptureContext(@ullable ContentCaptureContext context)298 abstract void updateContentCaptureContext(@Nullable ContentCaptureContext context); 299 300 /** 301 * Gets the {@link ContentCaptureContext} associated with the session. 302 * 303 * @return context set on constructor or by 304 * {@link #setContentCaptureContext(ContentCaptureContext)}, or {@code null} if never 305 * explicitly set. 306 */ 307 @Nullable getContentCaptureContext()308 public final ContentCaptureContext getContentCaptureContext() { 309 return mClientContext; 310 } 311 312 /** 313 * Destroys this session, flushing out all pending notifications to the service. 314 * 315 * <p>Once destroyed, any new notification will be dropped. 316 */ destroy()317 public final void destroy() { 318 synchronized (mLock) { 319 if (mDestroyed) { 320 if (sDebug) Log.d(TAG, "destroy(" + mId + "): already destroyed"); 321 return; 322 } 323 mDestroyed = true; 324 325 // TODO(b/111276913): check state (for example, how to handle if it's waiting for remote 326 // id) and send it to the cache of batched commands 327 if (sVerbose) { 328 Log.v(TAG, "destroy(): state=" + getStateAsString(mState) + ", mId=" + mId); 329 } 330 // Finish children first 331 if (mChildren != null) { 332 final int numberChildren = mChildren.size(); 333 if (sVerbose) Log.v(TAG, "Destroying " + numberChildren + " children first"); 334 for (int i = 0; i < numberChildren; i++) { 335 final ContentCaptureSession child = mChildren.get(i); 336 try { 337 child.destroy(); 338 } catch (Exception e) { 339 Log.w(TAG, "exception destroying child session #" + i + ": " + e); 340 } 341 } 342 } 343 } 344 345 try { 346 flush(FLUSH_REASON_SESSION_FINISHED); 347 } finally { 348 onDestroy(); 349 } 350 } 351 onDestroy()352 abstract void onDestroy(); 353 354 /** @hide */ 355 @Override close()356 public void close() { 357 destroy(); 358 } 359 360 /** 361 * Notifies the Content Capture Service that a node has been added to the view structure. 362 * 363 * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or 364 * automatically by the Android System for views that return {@code true} on 365 * {@link View#onProvideContentCaptureStructure(ViewStructure, int)}. 366 * 367 * @param node node that has been added. 368 */ notifyViewAppeared(@onNull ViewStructure node)369 public final void notifyViewAppeared(@NonNull ViewStructure node) { 370 Preconditions.checkNotNull(node); 371 if (!isContentCaptureEnabled()) return; 372 373 if (!(node instanceof ViewNode.ViewStructureImpl)) { 374 throw new IllegalArgumentException("Invalid node class: " + node.getClass()); 375 } 376 377 internalNotifyViewAppeared((ViewStructureImpl) node); 378 } 379 internalNotifyViewAppeared(@onNull ViewNode.ViewStructureImpl node)380 abstract void internalNotifyViewAppeared(@NonNull ViewNode.ViewStructureImpl node); 381 382 /** 383 * Notifies the Content Capture Service that a node has been removed from the view structure. 384 * 385 * <p>Typically called "manually" by views that handle their own virtual view hierarchy, or 386 * automatically by the Android System for standard views. 387 * 388 * @param id id of the node that has been removed. 389 */ notifyViewDisappeared(@onNull AutofillId id)390 public final void notifyViewDisappeared(@NonNull AutofillId id) { 391 Preconditions.checkNotNull(id); 392 if (!isContentCaptureEnabled()) return; 393 394 internalNotifyViewDisappeared(id); 395 } 396 internalNotifyViewDisappeared(@onNull AutofillId id)397 abstract void internalNotifyViewDisappeared(@NonNull AutofillId id); 398 399 /** 400 * Notifies the Content Capture Service that many nodes has been removed from a virtual view 401 * structure. 402 * 403 * <p>Should only be called by views that handle their own virtual view hierarchy. 404 * 405 * @param hostId id of the non-virtual view hosting the virtual view hierarchy (it can be 406 * obtained by calling {@link ViewStructure#getAutofillId()}). 407 * @param virtualIds ids of the virtual children. 408 * 409 * @throws IllegalArgumentException if the {@code hostId} is an autofill id for a virtual view. 410 * @throws IllegalArgumentException if {@code virtualIds} is empty 411 */ notifyViewsDisappeared(@onNull AutofillId hostId, @NonNull long[] virtualIds)412 public final void notifyViewsDisappeared(@NonNull AutofillId hostId, 413 @NonNull long[] virtualIds) { 414 Preconditions.checkArgument(hostId.isNonVirtual(), "hostId cannot be virtual: %s", hostId); 415 Preconditions.checkArgument(!ArrayUtils.isEmpty(virtualIds), "virtual ids cannot be empty"); 416 if (!isContentCaptureEnabled()) return; 417 418 // TODO(b/123036895): use a internalNotifyViewsDisappeared that optimizes how the event is 419 // parcelized 420 for (long id : virtualIds) { 421 internalNotifyViewDisappeared(new AutofillId(hostId, id, mId)); 422 } 423 } 424 425 /** 426 * Notifies the Intelligence Service that the value of a text node has been changed. 427 * 428 * @param id of the node. 429 * @param text new text. 430 */ notifyViewTextChanged(@onNull AutofillId id, @Nullable CharSequence text)431 public final void notifyViewTextChanged(@NonNull AutofillId id, @Nullable CharSequence text) { 432 Preconditions.checkNotNull(id); 433 434 if (!isContentCaptureEnabled()) return; 435 436 internalNotifyViewTextChanged(id, text); 437 } 438 internalNotifyViewTextChanged(@onNull AutofillId id, @Nullable CharSequence text)439 abstract void internalNotifyViewTextChanged(@NonNull AutofillId id, 440 @Nullable CharSequence text); 441 442 /** 443 * Notifies the Intelligence Service that the insets of a view have changed. 444 */ notifyViewInsetsChanged(@onNull Insets viewInsets)445 public final void notifyViewInsetsChanged(@NonNull Insets viewInsets) { 446 Preconditions.checkNotNull(viewInsets); 447 448 if (!isContentCaptureEnabled()) return; 449 450 internalNotifyViewInsetsChanged(viewInsets); 451 } 452 internalNotifyViewInsetsChanged(@onNull Insets viewInsets)453 abstract void internalNotifyViewInsetsChanged(@NonNull Insets viewInsets); 454 455 /** @hide */ internalNotifyViewTreeEvent(boolean started)456 public abstract void internalNotifyViewTreeEvent(boolean started); 457 458 /** 459 * Notifies the Content Capture Service that a session has resumed. 460 */ notifySessionResumed()461 public final void notifySessionResumed() { 462 if (!isContentCaptureEnabled()) return; 463 464 internalNotifySessionResumed(); 465 } 466 internalNotifySessionResumed()467 abstract void internalNotifySessionResumed(); 468 469 /** 470 * Notifies the Content Capture Service that a session has paused. 471 */ notifySessionPaused()472 public final void notifySessionPaused() { 473 if (!isContentCaptureEnabled()) return; 474 475 internalNotifySessionPaused(); 476 } 477 internalNotifySessionPaused()478 abstract void internalNotifySessionPaused(); 479 480 /** 481 * Creates a {@link ViewStructure} for a "standard" view. 482 * 483 * <p>This method should be called after a visible view is laid out; the view then must populate 484 * the structure and pass it to {@link #notifyViewAppeared(ViewStructure)}. 485 * 486 * <b>Note: </b>views that manage a virtual structure under this view must populate just the 487 * node representing this view and return right away, then asynchronously report (not 488 * necessarily in the UI thread) when the children nodes appear, disappear or have their text 489 * changed by calling {@link ContentCaptureSession#notifyViewAppeared(ViewStructure)}, 490 * {@link ContentCaptureSession#notifyViewDisappeared(AutofillId)}, and 491 * {@link ContentCaptureSession#notifyViewTextChanged(AutofillId, CharSequence)} respectively. 492 * The structure for the a child must be created using 493 * {@link ContentCaptureSession#newVirtualViewStructure(AutofillId, long)}, and the 494 * {@code autofillId} for a child can be obtained either through 495 * {@code childStructure.getAutofillId()} or 496 * {@link ContentCaptureSession#newAutofillId(AutofillId, long)}. 497 * 498 * <p>When the virtual view hierarchy represents a web page, you should also: 499 * 500 * <ul> 501 * <li>Call {@link ContentCaptureManager#getContentCaptureConditions()} to infer content capture 502 * events should be generate for that URL. 503 * <li>Create a new {@link ContentCaptureSession} child for every HTML element that renders a 504 * new URL (like an {@code IFRAME}) and use that session to notify events from that subtree. 505 * </ul> 506 * 507 * <p><b>Note: </b>the following methods of the {@code structure} will be ignored: 508 * <ul> 509 * <li>{@link ViewStructure#setChildCount(int)} 510 * <li>{@link ViewStructure#addChildCount(int)} 511 * <li>{@link ViewStructure#getChildCount()} 512 * <li>{@link ViewStructure#newChild(int)} 513 * <li>{@link ViewStructure#asyncNewChild(int)} 514 * <li>{@link ViewStructure#asyncCommit()} 515 * <li>{@link ViewStructure#setWebDomain(String)} 516 * <li>{@link ViewStructure#newHtmlInfoBuilder(String)} 517 * <li>{@link ViewStructure#setHtmlInfo(android.view.ViewStructure.HtmlInfo)} 518 * <li>{@link ViewStructure#setDataIsSensitive(boolean)} 519 * <li>{@link ViewStructure#setAlpha(float)} 520 * <li>{@link ViewStructure#setElevation(float)} 521 * <li>{@link ViewStructure#setTransformation(android.graphics.Matrix)} 522 * </ul> 523 */ 524 @NonNull newViewStructure(@onNull View view)525 public final ViewStructure newViewStructure(@NonNull View view) { 526 return new ViewNode.ViewStructureImpl(view); 527 } 528 529 /** 530 * Creates a new {@link AutofillId} for a virtual child, so it can be used to uniquely identify 531 * the children in the session. 532 * 533 * @param hostId id of the non-virtual view hosting the virtual view hierarchy (it can be 534 * obtained by calling {@link ViewStructure#getAutofillId()}). 535 * @param virtualChildId id of the virtual child, relative to the parent. 536 * 537 * @return if for the virtual child 538 * 539 * @throws IllegalArgumentException if the {@code parentId} is a virtual child id. 540 */ newAutofillId(@onNull AutofillId hostId, long virtualChildId)541 public @NonNull AutofillId newAutofillId(@NonNull AutofillId hostId, long virtualChildId) { 542 Preconditions.checkNotNull(hostId); 543 Preconditions.checkArgument(hostId.isNonVirtual(), "hostId cannot be virtual: %s", hostId); 544 return new AutofillId(hostId, virtualChildId, mId); 545 } 546 547 /** 548 * Creates a {@link ViewStructure} for a "virtual" view, so it can be passed to 549 * {@link #notifyViewAppeared(ViewStructure)} by the view managing the virtual view hierarchy. 550 * 551 * @param parentId id of the virtual view parent (it can be obtained by calling 552 * {@link ViewStructure#getAutofillId()} on the parent). 553 * @param virtualId id of the virtual child, relative to the parent. 554 * 555 * @return a new {@link ViewStructure} that can be used for Content Capture purposes. 556 */ 557 @NonNull newVirtualViewStructure(@onNull AutofillId parentId, long virtualId)558 public final ViewStructure newVirtualViewStructure(@NonNull AutofillId parentId, 559 long virtualId) { 560 return new ViewNode.ViewStructureImpl(parentId, virtualId, mId); 561 } 562 isContentCaptureEnabled()563 boolean isContentCaptureEnabled() { 564 synchronized (mLock) { 565 return !mDestroyed; 566 } 567 } 568 569 @CallSuper dump(@onNull String prefix, @NonNull PrintWriter pw)570 void dump(@NonNull String prefix, @NonNull PrintWriter pw) { 571 pw.print(prefix); pw.print("id: "); pw.println(mId); 572 if (mClientContext != null) { 573 pw.print(prefix); mClientContext.dump(pw); pw.println(); 574 } 575 synchronized (mLock) { 576 pw.print(prefix); pw.print("destroyed: "); pw.println(mDestroyed); 577 if (mChildren != null && !mChildren.isEmpty()) { 578 final String prefix2 = prefix + " "; 579 final int numberChildren = mChildren.size(); 580 pw.print(prefix); pw.print("number children: "); pw.println(numberChildren); 581 for (int i = 0; i < numberChildren; i++) { 582 final ContentCaptureSession child = mChildren.get(i); 583 pw.print(prefix); pw.print(i); pw.println(": "); child.dump(prefix2, pw); 584 } 585 } 586 } 587 } 588 589 @Override toString()590 public String toString() { 591 return Integer.toString(mId); 592 } 593 594 /** @hide */ 595 @NonNull getStateAsString(int state)596 protected static String getStateAsString(int state) { 597 return state + " (" + (state == UNKNOWN_STATE ? "UNKNOWN" 598 : DebugUtils.flagsToString(ContentCaptureSession.class, "STATE_", state)) + ")"; 599 } 600 601 /** @hide */ 602 @NonNull getFlushReasonAsString(@lushReason int reason)603 public static String getFlushReasonAsString(@FlushReason int reason) { 604 switch (reason) { 605 case FLUSH_REASON_FULL: 606 return "FULL"; 607 case FLUSH_REASON_VIEW_ROOT_ENTERED: 608 return "VIEW_ROOT"; 609 case FLUSH_REASON_SESSION_STARTED: 610 return "STARTED"; 611 case FLUSH_REASON_SESSION_FINISHED: 612 return "FINISHED"; 613 case FLUSH_REASON_IDLE_TIMEOUT: 614 return "IDLE"; 615 case FLUSH_REASON_TEXT_CHANGE_TIMEOUT: 616 return "TEXT_CHANGE"; 617 case FLUSH_REASON_SESSION_CONNECTED: 618 return "CONNECTED"; 619 default: 620 return "UNKOWN-" + reason; 621 } 622 } 623 getRandomSessionId()624 private static int getRandomSessionId() { 625 int id; 626 do { 627 id = ID_GENERATOR.nextInt(); 628 } while (id == NO_SESSION_ID); 629 return id; 630 } 631 } 632