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.getSanitizedString; 19 import static android.view.contentcapture.ContentCaptureSession.NO_SESSION_ID; 20 21 import android.annotation.IntDef; 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.annotation.SystemApi; 25 import android.annotation.TestApi; 26 import android.os.Parcel; 27 import android.os.Parcelable; 28 import android.util.Log; 29 import android.view.autofill.AutofillId; 30 31 import com.android.internal.util.Preconditions; 32 33 import java.io.PrintWriter; 34 import java.lang.annotation.Retention; 35 import java.lang.annotation.RetentionPolicy; 36 import java.util.ArrayList; 37 import java.util.List; 38 39 /** @hide */ 40 @SystemApi 41 @TestApi 42 public final class ContentCaptureEvent implements Parcelable { 43 44 private static final String TAG = ContentCaptureEvent.class.getSimpleName(); 45 46 /** @hide */ 47 public static final int TYPE_SESSION_FINISHED = -2; 48 /** @hide */ 49 public static final int TYPE_SESSION_STARTED = -1; 50 51 /** 52 * Called when a node has been added to the screen and is visible to the user. 53 * 54 * <p>The metadata of the node is available through {@link #getViewNode()}. 55 */ 56 public static final int TYPE_VIEW_APPEARED = 1; 57 58 /** 59 * Called when one or more nodes have been removed from the screen and is not visible to the 60 * user anymore. 61 * 62 * <p>To get the id(s), first call {@link #getIds()} - if it returns {@code null}, then call 63 * {@link #getId()}. 64 */ 65 public static final int TYPE_VIEW_DISAPPEARED = 2; 66 67 /** 68 * Called when the text of a node has been changed. 69 * 70 * <p>The id of the node is available through {@link #getId()}, and the new text is 71 * available through {@link #getText()}. 72 */ 73 public static final int TYPE_VIEW_TEXT_CHANGED = 3; 74 75 /** 76 * Called before events (such as {@link #TYPE_VIEW_APPEARED} and/or 77 * {@link #TYPE_VIEW_DISAPPEARED}) representing a view hierarchy are sent. 78 * 79 * <p><b>NOTE</b>: there is no guarantee this event will be sent. For example, it's not sent 80 * if the initial view hierarchy doesn't initially have any view that's important for content 81 * capture. 82 */ 83 public static final int TYPE_VIEW_TREE_APPEARING = 4; 84 85 /** 86 * Called after events (such as {@link #TYPE_VIEW_APPEARED} and/or 87 * {@link #TYPE_VIEW_DISAPPEARED}) representing a view hierarchy were sent. 88 * 89 * <p><b>NOTE</b>: there is no guarantee this event will be sent. For example, it's not sent 90 * if the initial view hierarchy doesn't initially have any view that's important for content 91 * capture. 92 */ 93 public static final int TYPE_VIEW_TREE_APPEARED = 5; 94 95 /** 96 * Called after a call to 97 * {@link ContentCaptureSession#setContentCaptureContext(ContentCaptureContext)}. 98 * 99 * <p>The passed context is available through {@link #getContentCaptureContext()}. 100 */ 101 public static final int TYPE_CONTEXT_UPDATED = 6; 102 103 /** 104 * Called after the session is ready, typically after the activity resumed and the 105 * initial views appeared 106 */ 107 public static final int TYPE_SESSION_RESUMED = 7; 108 109 /** 110 * Called after the session is paused, typically after the activity paused and the 111 * views disappeared. 112 */ 113 public static final int TYPE_SESSION_PAUSED = 8; 114 115 116 /** @hide */ 117 @IntDef(prefix = { "TYPE_" }, value = { 118 TYPE_VIEW_APPEARED, 119 TYPE_VIEW_DISAPPEARED, 120 TYPE_VIEW_TEXT_CHANGED, 121 TYPE_VIEW_TREE_APPEARING, 122 TYPE_VIEW_TREE_APPEARED, 123 TYPE_CONTEXT_UPDATED, 124 TYPE_SESSION_PAUSED, 125 TYPE_SESSION_RESUMED 126 }) 127 @Retention(RetentionPolicy.SOURCE) 128 public @interface EventType{} 129 130 private final int mSessionId; 131 private final int mType; 132 private final long mEventTime; 133 private @Nullable AutofillId mId; 134 private @Nullable ArrayList<AutofillId> mIds; 135 private @Nullable ViewNode mNode; 136 private @Nullable CharSequence mText; 137 private int mParentSessionId = NO_SESSION_ID; 138 private @Nullable ContentCaptureContext mClientContext; 139 140 /** @hide */ ContentCaptureEvent(int sessionId, int type, long eventTime)141 public ContentCaptureEvent(int sessionId, int type, long eventTime) { 142 mSessionId = sessionId; 143 mType = type; 144 mEventTime = eventTime; 145 } 146 147 /** @hide */ ContentCaptureEvent(int sessionId, int type)148 public ContentCaptureEvent(int sessionId, int type) { 149 this(sessionId, type, System.currentTimeMillis()); 150 } 151 152 /** @hide */ setAutofillId(@onNull AutofillId id)153 public ContentCaptureEvent setAutofillId(@NonNull AutofillId id) { 154 mId = Preconditions.checkNotNull(id); 155 return this; 156 } 157 158 /** @hide */ setAutofillIds(@onNull ArrayList<AutofillId> ids)159 public ContentCaptureEvent setAutofillIds(@NonNull ArrayList<AutofillId> ids) { 160 mIds = Preconditions.checkNotNull(ids); 161 return this; 162 } 163 164 /** 165 * Adds an autofill id to the this event, merging the single id into a list if necessary. 166 * 167 * @hide 168 */ addAutofillId(@onNull AutofillId id)169 public ContentCaptureEvent addAutofillId(@NonNull AutofillId id) { 170 Preconditions.checkNotNull(id); 171 if (mIds == null) { 172 mIds = new ArrayList<>(); 173 if (mId == null) { 174 Log.w(TAG, "addAutofillId(" + id + ") called without an initial id"); 175 } else { 176 mIds.add(mId); 177 mId = null; 178 } 179 } 180 mIds.add(id); 181 return this; 182 } 183 184 /** 185 * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}. 186 * 187 * @hide 188 */ setParentSessionId(int parentSessionId)189 public ContentCaptureEvent setParentSessionId(int parentSessionId) { 190 mParentSessionId = parentSessionId; 191 return this; 192 } 193 194 /** 195 * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}. 196 * 197 * @hide 198 */ setClientContext(@onNull ContentCaptureContext clientContext)199 public ContentCaptureEvent setClientContext(@NonNull ContentCaptureContext clientContext) { 200 mClientContext = clientContext; 201 return this; 202 } 203 204 /** @hide */ 205 @NonNull getSessionId()206 public int getSessionId() { 207 return mSessionId; 208 } 209 210 /** 211 * Used by {@link #TYPE_SESSION_STARTED} and {@link #TYPE_SESSION_FINISHED}. 212 * 213 * @hide 214 */ 215 @Nullable getParentSessionId()216 public int getParentSessionId() { 217 return mParentSessionId; 218 } 219 220 /** 221 * Gets the {@link ContentCaptureContext} set calls to 222 * {@link ContentCaptureSession#setContentCaptureContext(ContentCaptureContext)}. 223 * 224 * <p>Only set on {@link #TYPE_CONTEXT_UPDATED} events. 225 */ 226 @Nullable getContentCaptureContext()227 public ContentCaptureContext getContentCaptureContext() { 228 return mClientContext; 229 } 230 231 /** @hide */ 232 @NonNull setViewNode(@onNull ViewNode node)233 public ContentCaptureEvent setViewNode(@NonNull ViewNode node) { 234 mNode = Preconditions.checkNotNull(node); 235 return this; 236 } 237 238 /** @hide */ 239 @NonNull setText(@ullable CharSequence text)240 public ContentCaptureEvent setText(@Nullable CharSequence text) { 241 mText = text; 242 return this; 243 } 244 245 /** 246 * Gets the type of the event. 247 * 248 * @return one of {@link #TYPE_VIEW_APPEARED}, {@link #TYPE_VIEW_DISAPPEARED}, 249 * {@link #TYPE_VIEW_TEXT_CHANGED}, {@link #TYPE_VIEW_TREE_APPEARING}, 250 * {@link #TYPE_VIEW_TREE_APPEARED}, {@link #TYPE_CONTEXT_UPDATED}, 251 * {@link #TYPE_SESSION_RESUMED}, or {@link #TYPE_SESSION_PAUSED}. 252 */ getType()253 public @EventType int getType() { 254 return mType; 255 } 256 257 /** 258 * Gets when the event was generated, in millis since epoch. 259 */ getEventTime()260 public long getEventTime() { 261 return mEventTime; 262 } 263 264 /** 265 * Gets the whole metadata of the node associated with the event. 266 * 267 * <p>Only set on {@link #TYPE_VIEW_APPEARED} events. 268 */ 269 @Nullable getViewNode()270 public ViewNode getViewNode() { 271 return mNode; 272 } 273 274 /** 275 * Gets the {@link AutofillId} of the node associated with the event. 276 * 277 * <p>Only set on {@link #TYPE_VIEW_DISAPPEARED} (when the event contains just one node - if 278 * it contains more than one, this method returns {@code null} and the actual ids should be 279 * retrived by {@link #getIds()}) and {@link #TYPE_VIEW_TEXT_CHANGED} events. 280 */ 281 @Nullable getId()282 public AutofillId getId() { 283 return mId; 284 } 285 286 /** 287 * Gets the {@link AutofillId AutofillIds} of the nodes associated with the event. 288 * 289 * <p>Only set on {@link #TYPE_VIEW_DISAPPEARED}, when the event contains more than one node 290 * (if it contains just one node, it's returned by {@link #getId()} instead. 291 */ 292 @Nullable getIds()293 public List<AutofillId> getIds() { 294 return mIds; 295 } 296 297 /** 298 * Gets the current text of the node associated with the event. 299 * 300 * <p>Only set on {@link #TYPE_VIEW_TEXT_CHANGED} events. 301 */ 302 @Nullable getText()303 public CharSequence getText() { 304 return mText; 305 } 306 307 /** 308 * Merges event of the same type, either {@link #TYPE_VIEW_TEXT_CHANGED} 309 * or {@link #TYPE_VIEW_DISAPPEARED}. 310 * 311 * @hide 312 */ mergeEvent(@onNull ContentCaptureEvent event)313 public void mergeEvent(@NonNull ContentCaptureEvent event) { 314 Preconditions.checkNotNull(event); 315 final int eventType = event.getType(); 316 if (mType != eventType) { 317 Log.e(TAG, "mergeEvent(" + getTypeAsString(eventType) + ") cannot be merged " 318 + "with different eventType=" + getTypeAsString(mType)); 319 return; 320 } 321 322 if (eventType == TYPE_VIEW_DISAPPEARED) { 323 final List<AutofillId> ids = event.getIds(); 324 final AutofillId id = event.getId(); 325 if (ids != null) { 326 if (id != null) { 327 Log.w(TAG, "got TYPE_VIEW_DISAPPEARED event with both id and ids: " + event); 328 } 329 for (int i = 0; i < ids.size(); i++) { 330 addAutofillId(ids.get(i)); 331 } 332 return; 333 } 334 if (id != null) { 335 addAutofillId(id); 336 return; 337 } 338 throw new IllegalArgumentException("mergeEvent(): got " 339 + "TYPE_VIEW_DISAPPEARED event with neither id or ids: " + event); 340 } else if (eventType == TYPE_VIEW_TEXT_CHANGED) { 341 setText(event.getText()); 342 } else { 343 Log.e(TAG, "mergeEvent(" + getTypeAsString(eventType) 344 + ") does not support this event type."); 345 } 346 } 347 348 /** @hide */ dump(@onNull PrintWriter pw)349 public void dump(@NonNull PrintWriter pw) { 350 pw.print("type="); pw.print(getTypeAsString(mType)); 351 pw.print(", time="); pw.print(mEventTime); 352 if (mId != null) { 353 pw.print(", id="); pw.print(mId); 354 } 355 if (mIds != null) { 356 pw.print(", ids="); pw.print(mIds); 357 } 358 if (mNode != null) { 359 pw.print(", mNode.id="); pw.print(mNode.getAutofillId()); 360 } 361 if (mSessionId != NO_SESSION_ID) { 362 pw.print(", sessionId="); pw.print(mSessionId); 363 } 364 if (mParentSessionId != NO_SESSION_ID) { 365 pw.print(", parentSessionId="); pw.print(mParentSessionId); 366 } 367 if (mText != null) { 368 pw.print(", text="); pw.println(getSanitizedString(mText)); 369 } 370 if (mClientContext != null) { 371 pw.print(", context="); mClientContext.dump(pw); pw.println(); 372 373 } 374 } 375 376 @NonNull 377 @Override toString()378 public String toString() { 379 final StringBuilder string = new StringBuilder("ContentCaptureEvent[type=") 380 .append(getTypeAsString(mType)); 381 string.append(", session=").append(mSessionId); 382 if (mType == TYPE_SESSION_STARTED && mParentSessionId != NO_SESSION_ID) { 383 string.append(", parent=").append(mParentSessionId); 384 } 385 if (mId != null) { 386 string.append(", id=").append(mId); 387 } 388 if (mIds != null) { 389 string.append(", ids=").append(mIds); 390 } 391 if (mNode != null) { 392 final String className = mNode.getClassName(); 393 if (mNode != null) { 394 string.append(", class=").append(className); 395 } 396 string.append(", id=").append(mNode.getAutofillId()); 397 } 398 if (mText != null) { 399 string.append(", text=").append(getSanitizedString(mText)); 400 } 401 if (mClientContext != null) { 402 string.append(", context=").append(mClientContext); 403 } 404 return string.append(']').toString(); 405 } 406 407 @Override describeContents()408 public int describeContents() { 409 return 0; 410 } 411 412 @Override writeToParcel(Parcel parcel, int flags)413 public void writeToParcel(Parcel parcel, int flags) { 414 parcel.writeInt(mSessionId); 415 parcel.writeInt(mType); 416 parcel.writeLong(mEventTime); 417 parcel.writeParcelable(mId, flags); 418 parcel.writeTypedList(mIds); 419 ViewNode.writeToParcel(parcel, mNode, flags); 420 parcel.writeCharSequence(mText); 421 if (mType == TYPE_SESSION_STARTED || mType == TYPE_SESSION_FINISHED) { 422 parcel.writeInt(mParentSessionId); 423 } 424 if (mType == TYPE_SESSION_STARTED || mType == TYPE_CONTEXT_UPDATED) { 425 parcel.writeParcelable(mClientContext, flags); 426 } 427 } 428 429 public static final @android.annotation.NonNull Parcelable.Creator<ContentCaptureEvent> CREATOR = 430 new Parcelable.Creator<ContentCaptureEvent>() { 431 432 @Override 433 @NonNull 434 public ContentCaptureEvent createFromParcel(Parcel parcel) { 435 final int sessionId = parcel.readInt(); 436 final int type = parcel.readInt(); 437 final long eventTime = parcel.readLong(); 438 final ContentCaptureEvent event = new ContentCaptureEvent(sessionId, type, eventTime); 439 final AutofillId id = parcel.readParcelable(null); 440 if (id != null) { 441 event.setAutofillId(id); 442 } 443 final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR); 444 if (ids != null) { 445 event.setAutofillIds(ids); 446 } 447 final ViewNode node = ViewNode.readFromParcel(parcel); 448 if (node != null) { 449 event.setViewNode(node); 450 } 451 event.setText(parcel.readCharSequence()); 452 if (type == TYPE_SESSION_STARTED || type == TYPE_SESSION_FINISHED) { 453 event.setParentSessionId(parcel.readInt()); 454 } 455 if (type == TYPE_SESSION_STARTED || type == TYPE_CONTEXT_UPDATED) { 456 event.setClientContext(parcel.readParcelable(null)); 457 } 458 return event; 459 } 460 461 @Override 462 @NonNull 463 public ContentCaptureEvent[] newArray(int size) { 464 return new ContentCaptureEvent[size]; 465 } 466 }; 467 468 /** @hide */ getTypeAsString(@ventType int type)469 public static String getTypeAsString(@EventType int type) { 470 switch (type) { 471 case TYPE_SESSION_STARTED: 472 return "SESSION_STARTED"; 473 case TYPE_SESSION_FINISHED: 474 return "SESSION_FINISHED"; 475 case TYPE_SESSION_RESUMED: 476 return "SESSION_RESUMED"; 477 case TYPE_SESSION_PAUSED: 478 return "SESSION_PAUSED"; 479 case TYPE_VIEW_APPEARED: 480 return "VIEW_APPEARED"; 481 case TYPE_VIEW_DISAPPEARED: 482 return "VIEW_DISAPPEARED"; 483 case TYPE_VIEW_TEXT_CHANGED: 484 return "VIEW_TEXT_CHANGED"; 485 case TYPE_VIEW_TREE_APPEARING: 486 return "VIEW_TREE_APPEARING"; 487 case TYPE_VIEW_TREE_APPEARED: 488 return "VIEW_TREE_APPEARED"; 489 case TYPE_CONTEXT_UPDATED: 490 return "CONTEXT_UPDATED"; 491 default: 492 return "UKNOWN_TYPE: " + type; 493 } 494 } 495 } 496