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