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