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.contentcaptureservice.cts;
17 
18 import static android.contentcaptureservice.cts.Helper.MY_EPOCH;
19 import static android.contentcaptureservice.cts.Helper.TAG;
20 import static android.view.contentcapture.ContentCaptureEvent.TYPE_CONTEXT_UPDATED;
21 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_PAUSED;
22 import static android.view.contentcapture.ContentCaptureEvent.TYPE_SESSION_RESUMED;
23 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_APPEARED;
24 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_DISAPPEARED;
25 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_INSETS_CHANGED;
26 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TEXT_CHANGED;
27 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARED;
28 import static android.view.contentcapture.ContentCaptureEvent.TYPE_VIEW_TREE_APPEARING;
29 import static android.view.contentcapture.ContentCaptureEvent.TYPE_WINDOW_BOUNDS_CHANGED;
30 
31 import static com.google.common.truth.Truth.assertThat;
32 import static com.google.common.truth.Truth.assertWithMessage;
33 
34 import android.app.assist.ActivityId;
35 import android.content.ComponentName;
36 import android.content.LocusId;
37 import android.contentcaptureservice.cts.CtsContentCaptureService.Session;
38 import android.util.Log;
39 import android.view.Display;
40 import android.view.View;
41 import android.view.autofill.AutofillId;
42 import android.view.contentcapture.ContentCaptureEvent;
43 import android.view.contentcapture.ContentCaptureSession;
44 import android.view.contentcapture.ContentCaptureSessionId;
45 import android.view.contentcapture.ViewNode;
46 
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 
50 import com.android.compatibility.common.util.RetryableException;
51 
52 import java.util.Collections;
53 import java.util.List;
54 import java.util.stream.Collectors;
55 
56 /**
57  * Helper for common assertions.
58  */
59 final class Assertions {
60 
61     /**
62      * Asserts a session belongs to the right activity.
63      */
assertRightActivity(@onNull Session session, @NonNull ContentCaptureSessionId expectedSessionId, @NonNull AbstractContentCaptureActivity activity)64     public static void assertRightActivity(@NonNull Session session,
65             @NonNull ContentCaptureSessionId expectedSessionId,
66             @NonNull AbstractContentCaptureActivity activity) {
67         assertRightActivity(session, expectedSessionId, activity.getComponentName());
68     }
69 
70     /**
71      * Asserts a session belongs to the right activity.
72      */
assertRightActivity(@onNull Session session, @NonNull ContentCaptureSessionId expectedSessionId, @NonNull ComponentName componentName)73     public static void assertRightActivity(@NonNull Session session,
74             @NonNull ContentCaptureSessionId expectedSessionId,
75             @NonNull ComponentName componentName) {
76         assertWithMessage("wrong activity for %s", session)
77                 .that(session.context.getActivityComponent()).isEqualTo(componentName);
78         // TODO(b/123540602): merge both or replace check above by:
79         //  assertMainSessionContext(session, activity);
80         assertThat(session.id).isEqualTo(expectedSessionId);
81     }
82 
83     /**
84      * Asserts the context of a main session.
85      */
assertMainSessionContext(@onNull Session session, @NonNull AbstractContentCaptureActivity activity)86     public static void assertMainSessionContext(@NonNull Session session,
87             @NonNull AbstractContentCaptureActivity activity) {
88         assertMainSessionContext(session, activity, /* expectedFlags= */ 0);
89     }
90 
91     /**
92      * Asserts the context of a main session.
93      */
assertMainSessionContext(@onNull Session session, @NonNull AbstractContentCaptureActivity activity, int expectedFlags)94     public static void assertMainSessionContext(@NonNull Session session,
95             @NonNull AbstractContentCaptureActivity activity, int expectedFlags) {
96         assertWithMessage("no context on %s", session).that(session.context).isNotNull();
97         assertWithMessage("wrong activity for %s", session)
98                 .that(session.context.getActivityComponent())
99                 .isEqualTo(activity.getComponentName());
100         assertWithMessage("context for session %s should have displayId", session)
101                 .that(session.context.getDisplayId()).isNotEqualTo(Display.INVALID_DISPLAY);
102         assertWithMessage("wrong task id for session %s", session)
103                 .that(session.context.getTaskId()).isEqualTo(activity.getRealTaskId());
104         assertWithMessage("wrong flags on context for session %s", session)
105                 .that(session.context.getFlags()).isEqualTo(expectedFlags);
106         assertWithMessage("context for session %s should not have ID", session)
107                 .that(session.context.getLocusId()).isNull();
108         assertWithMessage("context for session %s should not have extras", session)
109                 .that(session.context.getExtras()).isNull();
110         final ActivityId activityId = session.context.getActivityId();
111         assertWithMessage("context for session %s should have ActivityIds", session)
112                 .that(activityId).isNotNull();
113         assertWithMessage("wrong task id for session %s", session)
114                 .that(activityId.getTaskId()).isEqualTo(activity.getRealTaskId());
115         assertWithMessage("context for session %s should have ActivityId", session)
116                 .that(activityId.getToken()).isNotNull();
117         assertWithMessage("context for session %s should have windowToken", session)
118                 .that(session.context.getWindowToken()).isNotNull();
119     }
120 
121     /**
122      * Asserts the invariants of a child session.
123      */
assertChildSessionContext(@onNull Session session)124     public static void assertChildSessionContext(@NonNull Session session) {
125         assertWithMessage("no context on %s", session).that(session.context).isNotNull();
126         assertWithMessage("context for session %s should not have component", session)
127                 .that(session.context.getActivityComponent()).isNull();
128         assertWithMessage("context for session %s should not have displayId", session)
129                 .that(session.context.getDisplayId()).isEqualTo(Display.INVALID_DISPLAY);
130         assertWithMessage("context for session %s should not have taskId", session)
131                 .that(session.context.getTaskId()).isEqualTo(0);
132         assertWithMessage("context for session %s should not have flags", session)
133                 .that(session.context.getFlags()).isEqualTo(0);
134         final ActivityId activityId = session.context.getActivityId();
135         assertWithMessage("context for session %s should not have ActivityIds", session)
136                 .that(activityId).isNull();
137         assertWithMessage("context for session %s should not have windowToken", session)
138                 .that(session.context.getWindowToken()).isNull();
139     }
140 
141     /**
142      * Asserts the invariants of a child session.
143      */
assertChildSessionContext(@onNull Session session, @NonNull String expectedId)144     public static void assertChildSessionContext(@NonNull Session session,
145             @NonNull String expectedId) {
146         assertChildSessionContext(session);
147         assertThat(session.context.getLocusId()).isEqualTo(new LocusId(expectedId));
148     }
149 
150     /**
151      * Asserts a session belongs to the right parent
152      */
assertRightRelationship(@onNull Session parent, @NonNull Session child)153     public static void assertRightRelationship(@NonNull Session parent, @NonNull Session child) {
154         final ContentCaptureSessionId expectedParentId = parent.id;
155         assertWithMessage("No id on parent session %s", parent).that(expectedParentId).isNotNull();
156         assertWithMessage("No context on child session %s", child).that(child.context).isNotNull();
157         final ContentCaptureSessionId actualParentId = child.context.getParentSessionId();
158         assertWithMessage("No parent id on context %s of child session %s", child.context, child)
159                 .that(actualParentId).isNotNull();
160         assertWithMessage("id of parent session doesn't match child").that(actualParentId)
161                 .isEqualTo(expectedParentId);
162     }
163 
164     /**
165      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event, without checking for parent id.
166      */
assertViewWithUnknownParentAppeared( @onNull List<ContentCaptureEvent> events, int index, @NonNull View expectedView)167     public static ViewNode assertViewWithUnknownParentAppeared(
168             @NonNull List<ContentCaptureEvent> events, int index, @NonNull View expectedView) {
169         return assertViewWithUnknownParentAppeared(events, index, expectedView,
170                 /* expectedText= */ null);
171     }
172 
173     /**
174      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event for a decor view.
175      *
176      * <P>The decor view is typically internal, so there isn't much we can assert, other than its
177      * autofill id.
178      */
assertDecorViewAppeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull View expectedDecorView)179     public static void assertDecorViewAppeared(@NonNull List<ContentCaptureEvent> events,
180             int index, @NonNull View expectedDecorView) {
181         final ContentCaptureEvent event = assertViewAppeared(events, index);
182         assertWithMessage("wrong autofill id on %s (%s)", event, index)
183                 .that(event.getViewNode().getAutofillId())
184                 .isEqualTo(expectedDecorView.getAutofillId());
185     }
186 
187     /**
188      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event, without checking for parent id.
189      */
assertViewWithUnknownParentAppeared( @onNull List<ContentCaptureEvent> events, int index, @NonNull View expectedView, @Nullable String expectedText)190     public static ViewNode assertViewWithUnknownParentAppeared(
191             @NonNull List<ContentCaptureEvent> events, int index, @NonNull View expectedView,
192             @Nullable String expectedText) {
193         final ContentCaptureEvent event = assertViewAppeared(events, index);
194         final ViewNode node = event.getViewNode();
195 
196         assertWithMessage("wrong class on %s (%s)", event, index).that(node.getClassName())
197                 .isEqualTo(expectedView.getClass().getName());
198         assertWithMessage("wrong autofill id on %s (%s)", event, index).that(node.getAutofillId())
199                 .isEqualTo(expectedView.getAutofillId());
200 
201         if (expectedText != null) {
202             assertWithMessage("wrong text on %s (%s)", event, index).that(node.getText().toString())
203                     .isEqualTo(expectedText);
204         }
205         // TODO(b/123540602): test more fields, like resource id
206         return node;
207     }
208 
209     /**
210      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event, without checking for parent id.
211      */
assertViewAppeared(@onNull List<ContentCaptureEvent> events, int index)212     public static ContentCaptureEvent assertViewAppeared(@NonNull List<ContentCaptureEvent> events,
213             int index) {
214         final ContentCaptureEvent event = getEvent(events, index, TYPE_VIEW_APPEARED);
215         final ViewNode node = event.getViewNode();
216         assertThat(node).isNotNull();
217         return event;
218     }
219 
220     /**
221      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event.
222      */
assertViewAppeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull View expectedView, @Nullable AutofillId expectedParentId, @Nullable String expectedText)223     public static void assertViewAppeared(@NonNull List<ContentCaptureEvent> events, int index,
224             @NonNull View expectedView, @Nullable AutofillId expectedParentId,
225             @Nullable String expectedText) {
226         final ViewNode node = assertViewWithUnknownParentAppeared(events, index, expectedView,
227                 expectedText);
228         assertWithMessage("wrong parent autofill id on %s (%s)", events.get(index), index)
229             .that(node.getParentAutofillId()).isEqualTo(expectedParentId);
230     }
231 
232     /**
233      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event.
234      */
assertViewAppeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull View expectedView, @Nullable AutofillId expectedParentId)235     public static void assertViewAppeared(@NonNull List<ContentCaptureEvent> events, int index,
236             @NonNull View expectedView, @Nullable AutofillId expectedParentId) {
237         assertViewAppeared(events, index, expectedView, expectedParentId, /* expectedText= */ null);
238     }
239 
240     /**
241      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event.
242      */
assertViewAppeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull ContentCaptureSessionId expectedSessionId, @NonNull View expectedView, @Nullable AutofillId expectedParentId)243     public static void assertViewAppeared(@NonNull List<ContentCaptureEvent> events, int index,
244             @NonNull ContentCaptureSessionId expectedSessionId,
245             @NonNull View expectedView, @Nullable AutofillId expectedParentId) {
246         assertViewAppeared(events, index, expectedView, expectedParentId);
247         assertSessionId(expectedSessionId, expectedView);
248     }
249 
250     /**
251      * Asserts the contents of a {@link #TYPE_VIEW_TREE_APPEARING} event.
252      */
assertViewTreeStarted(@onNull List<ContentCaptureEvent> events, int index)253     public static void assertViewTreeStarted(@NonNull List<ContentCaptureEvent> events,
254             int index) {
255         assertSessionLevelEvent(events, index, TYPE_VIEW_TREE_APPEARING);
256     }
257 
258     /**
259      * Asserts the contents of a {@link #TYPE_VIEW_TREE_APPEARED} event.
260      */
assertViewTreeFinished(@onNull List<ContentCaptureEvent> events, int index)261     public static void assertViewTreeFinished(@NonNull List<ContentCaptureEvent> events,
262             int index) {
263         assertSessionLevelEvent(events, index, TYPE_VIEW_TREE_APPEARED);
264     }
265 
assertSessionLevelEvent(@onNull List<ContentCaptureEvent> events, int index, int expectedType)266     private static void assertSessionLevelEvent(@NonNull List<ContentCaptureEvent> events,
267             int index, int expectedType) {
268         final ContentCaptureEvent event = getEvent(events, index, expectedType);
269         assertWithMessage("event %s (index %s) should not have a ViewNode", event, index)
270                 .that(event.getViewNode()).isNull();
271         assertWithMessage("event %s (index %s) should not have text", event, index)
272                 .that(event.getText()).isNull();
273         assertWithMessage("event %s (index %s) should not have an autofillId", event, index)
274                 .that(event.getId()).isNull();
275         assertWithMessage("event %s (index %s) should not have autofillIds", event, index)
276                 .that(event.getIds()).isNull();
277     }
278 
279     /**
280      * Asserts the contents of a {@link #TYPE_SESSION_RESUMED} event.
281      */
assertSessionResumed(@onNull List<ContentCaptureEvent> events, int index)282     public static void assertSessionResumed(@NonNull List<ContentCaptureEvent> events,
283             int index) {
284         assertSessionLevelEvent(events, index, TYPE_SESSION_RESUMED);
285     }
286 
287     /**
288      * Asserts the contents of a {@link #TYPE_SESSION_PAUSED} event.
289      */
assertSessionPaused(@onNull List<ContentCaptureEvent> events, int index)290     public static void assertSessionPaused(@NonNull List<ContentCaptureEvent> events,
291             int index) {
292         assertSessionLevelEvent(events, index, TYPE_SESSION_PAUSED);
293     }
294 
295     /**
296      * Asserts that a session for the given activity has no view-level events, just
297      * {@link #TYPE_SESSION_RESUMED} and {@link #TYPE_SESSION_PAUSED}.
298      */
assertNoViewLevelEvents(@onNull Session session, @NonNull AbstractContentCaptureActivity activity)299     public static void assertNoViewLevelEvents(@NonNull Session session,
300             @NonNull AbstractContentCaptureActivity activity) {
301         assertRightActivity(session, session.id, activity);
302         final List<ContentCaptureEvent> events = removeBoundsAndInsetsEvents(session.getEvents());
303         Log.v(TAG, "events on " + activity + ": " + events);
304         assertThat(events).hasSize(2);
305         assertSessionResumed(events, 0);
306         assertSessionPaused(events, 1);
307     }
308 
309     /**
310      * This method is used to remove Bounds and Insets changed events if the test should only
311      * contain session level events.
312      * In special case, there are some events, such as {@link #TYPE_WINDOW_BOUNDS_CHANGED}
313      * and {@link #TYPE_VIEW_INSETS_CHANGED}, will be accidentally generated by
314      * the system. Because these events were not expected in the test, remove
315      * them if needed.
316      */
removeBoundsAndInsetsEvents( @onNull List<ContentCaptureEvent> events)317     public static List<ContentCaptureEvent> removeBoundsAndInsetsEvents(
318             @NonNull List<ContentCaptureEvent> events) {
319         return Collections.unmodifiableList(events).stream().filter(
320                 e -> e.getType() != TYPE_WINDOW_BOUNDS_CHANGED
321                         && e.getType() != TYPE_VIEW_INSETS_CHANGED
322                         && e.getType() != TYPE_VIEW_TREE_APPEARING
323                         && e.getType() != TYPE_VIEW_TREE_APPEARED
324         ).collect(Collectors.toList());
325     }
326 
327     /**
328      * Asserts that a session for the given activity has events at all.
329      */
assertNoEvents(@onNull Session session, @NonNull ComponentName componentName)330     public static void assertNoEvents(@NonNull Session session,
331             @NonNull ComponentName componentName) {
332         assertRightActivity(session, session.id, componentName);
333         assertThat(session.getEvents()).isEmpty();
334     }
335 
336     /**
337      * Asserts that the events received by the service optionally contains the
338      * {@code TYPE_VIEW_DISAPPEARED} events, as they might have not been generated if the views
339      * disappeared after the activity stopped.
340      *
341      * @param events events received by the service.
342      * @param minimumSize size of events received if activity stopped before views disappeared
343      * @param expectedIds ids of views that might have disappeared.
344      *
345      * @return whether the view disappeared events were generated
346      */
347     // TODO(b/123540067, 122315042): remove this method if we could make it deterministic, and
348     // inline the assertions (or rename / change its logic)
assertViewsOptionallyDisappeared( @onNull List<ContentCaptureEvent> events, int minimumSize, @NonNull AutofillId... expectedIds)349     public static boolean assertViewsOptionallyDisappeared(
350             @NonNull List<ContentCaptureEvent> events, int minimumSize,
351             @NonNull AutofillId... expectedIds) {
352         final int actualSize = events.size();
353         final int disappearedEventIndex;
354         if (actualSize == minimumSize) {
355             // Activity stopped before TYPE_VIEW_DISAPPEARED were sent.
356             return false;
357         } else if (actualSize == minimumSize + 1) {
358             // Activity did not receive TYPE_VIEW_TREE_APPEARING and TYPE_VIEW_TREE_APPEARED.
359             disappearedEventIndex = minimumSize;
360         } else {
361             disappearedEventIndex = minimumSize + 1;
362         }
363         final ContentCaptureEvent batchDisappearEvent = events.get(disappearedEventIndex);
364 
365         if (expectedIds.length == 1) {
366             assertWithMessage("Should have just one deleted id on %s", batchDisappearEvent)
367                     .that(batchDisappearEvent.getIds()).isNull();
368             assertWithMessage("wrong deleted id on %s", batchDisappearEvent)
369                     .that(batchDisappearEvent.getId()).isEqualTo(expectedIds[0]);
370         } else {
371             assertWithMessage("Should not have individual deleted id on %s", batchDisappearEvent)
372                     .that(batchDisappearEvent.getId()).isNull();
373             final List<AutofillId> actualIds = batchDisappearEvent.getIds();
374             assertWithMessage("wrong deleteds id on %s", batchDisappearEvent)
375                     .that(actualIds).containsExactly((Object[]) expectedIds);
376         }
377         return true;
378     }
379 
380     /**
381      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event, without checking for parent
382      */
assertViewWithUnknownParentAppeared( @onNull List<ContentCaptureEvent> events, int index, @NonNull ContentCaptureSessionId expectedSessionId, @NonNull View expectedView)383     public static void assertViewWithUnknownParentAppeared(
384             @NonNull List<ContentCaptureEvent> events, int index,
385             @NonNull ContentCaptureSessionId expectedSessionId, @NonNull View expectedView) {
386         assertViewWithUnknownParentAppeared(events, index, expectedView);
387         assertSessionId(expectedSessionId, expectedView);
388     }
389 
390     /**
391      * Asserts the contents of a {@link #TYPE_VIEW_DISAPPEARED} event for a single view.
392      */
assertViewDisappeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId expectedId)393     public static void assertViewDisappeared(@NonNull List<ContentCaptureEvent> events, int index,
394             @NonNull AutofillId expectedId) {
395         final ContentCaptureEvent event = assertCommonViewDisappearedProperties(events, index);
396         assertWithMessage("wrong autofillId on event %s (index %s)", event, index)
397             .that(event.getId()).isEqualTo(expectedId);
398         assertWithMessage("event %s (index %s) should not have autofillIds", event, index)
399             .that(event.getIds()).isNull();
400     }
401 
402     /**
403      * Asserts the contents of a {@link #TYPE_VIEW_DISAPPEARED} event for multiple views.
404      */
assertViewsDisappeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId... expectedIds)405     public static void assertViewsDisappeared(@NonNull List<ContentCaptureEvent> events, int index,
406             @NonNull AutofillId... expectedIds) {
407         final ContentCaptureEvent event = assertCommonViewDisappearedProperties(events, index);
408         final List<AutofillId> ids = event.getIds();
409         assertWithMessage("no autofillIds on event %s (index %s)", event, index).that(ids)
410                 .isNotNull();
411         assertWithMessage("wrong autofillId on event %s (index %s)", event, index)
412             .that(ids).containsExactly((Object[]) expectedIds).inOrder();
413         assertWithMessage("event %s (index %s) should not have autofillId", event, index)
414             .that(event.getId()).isNull();
415     }
416 
assertCommonViewDisappearedProperties( @onNull List<ContentCaptureEvent> events, int index)417     private static ContentCaptureEvent assertCommonViewDisappearedProperties(
418             @NonNull List<ContentCaptureEvent> events, int index) {
419         final ContentCaptureEvent event = getEvent(events, index, TYPE_VIEW_DISAPPEARED);
420         assertWithMessage("event %s (index %s) should not have a ViewNode", event, index)
421                 .that(event.getViewNode()).isNull();
422         assertWithMessage("event %s (index %s) should not have text", event, index)
423                 .that(event.getText()).isNull();
424         return event;
425     }
426 
427     /**
428      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event for a virtual node.
429      */
assertVirtualViewAppeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull ContentCaptureSession session, @NonNull AutofillId parentId, int childId, @Nullable String expectedText)430     public static void assertVirtualViewAppeared(@NonNull List<ContentCaptureEvent> events,
431             int index, @NonNull ContentCaptureSession session, @NonNull AutofillId parentId,
432             int childId, @Nullable String expectedText) {
433         final ContentCaptureEvent event = getEvent(events, index, TYPE_VIEW_APPEARED);
434         final ViewNode node = event.getViewNode();
435         assertThat(node).isNotNull();
436         final AutofillId expectedId = session.newAutofillId(parentId, childId);
437         assertWithMessage("wrong autofill id on %s (index %s)", event, index)
438             .that(node.getAutofillId()).isEqualTo(expectedId);
439         if (expectedText != null) {
440             assertWithMessage("wrong text on %s(index %s) ", event, index)
441                 .that(node.getText().toString()).isEqualTo(expectedText);
442         } else {
443             assertWithMessage("%s (index %s) should not have text", node, index)
444                 .that(node.getText()).isNull();
445         }
446     }
447 
448     /**
449      * Asserts the contents of a {@link #TYPE_VIEW_DISAPPEARED} event for a virtual node.
450      */
assertVirtualViewDisappeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId parentId, @NonNull ContentCaptureSession session, long childId)451     public static void assertVirtualViewDisappeared(@NonNull List<ContentCaptureEvent> events,
452             int index, @NonNull AutofillId parentId, @NonNull ContentCaptureSession session,
453             long childId) {
454         assertViewDisappeared(events, index, session.newAutofillId(parentId, childId));
455     }
456 
457     /**
458      * Asserts the contents of a {@link #TYPE_VIEW_DISAPPEARED} event for many virtual nodes.
459      */
assertVirtualViewsDisappeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId parentId, @NonNull ContentCaptureSession session, long... childrenIds)460     public static void assertVirtualViewsDisappeared(@NonNull List<ContentCaptureEvent> events,
461             int index, @NonNull AutofillId parentId, @NonNull ContentCaptureSession session,
462             long... childrenIds) {
463         final int size = childrenIds.length;
464         final AutofillId[] expectedIds = new AutofillId[size];
465         for (int i = 0; i < childrenIds.length; i++) {
466             expectedIds[i] = session.newAutofillId(parentId, childrenIds[i]);
467         }
468         assertViewsDisappeared(events, index, expectedIds);
469     }
470 
471     /**
472      * Asserts a view has the given session id.
473      */
assertSessionId(@onNull ContentCaptureSessionId expectedSessionId, @NonNull View view)474     public static void assertSessionId(@NonNull ContentCaptureSessionId expectedSessionId,
475             @NonNull View view) {
476         assertThat(expectedSessionId).isNotNull();
477         final ContentCaptureSession session = view.getContentCaptureSession();
478         assertWithMessage("no session for view %s", view).that(session).isNotNull();
479         assertWithMessage("wrong session id for for view %s", view)
480                 .that(session.getContentCaptureSessionId()).isEqualTo(expectedSessionId);
481     }
482 
483     /**
484      * Asserts the contents of a {@link #TYPE_VIEW_TEXT_CHANGED} event.
485      */
assertViewTextChanged(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId expectedId, @NonNull String expectedText)486     public static void assertViewTextChanged(@NonNull List<ContentCaptureEvent> events, int index,
487             @NonNull AutofillId expectedId, @NonNull String expectedText) {
488         final ContentCaptureEvent event = getEvent(events, index, TYPE_VIEW_TEXT_CHANGED);
489         assertWithMessage("Wrong id on %s (%s)", event, index).that(event.getId())
490                 .isEqualTo(expectedId);
491         assertWithMessage("Wrong text on %s (%s)", event, index).that(event.getText().toString())
492                 .isEqualTo(expectedText);
493     }
494 
495     /**
496      * Asserts the existence and contents of a {@link #TYPE_VIEW_INSETS_CHANGED} event.
497      */
assertViewInsetsChanged(@onNull List<ContentCaptureEvent> events)498     public static void assertViewInsetsChanged(@NonNull List<ContentCaptureEvent> events) {
499         boolean insetsEventFound = false;
500         for (ContentCaptureEvent event : events) {
501             if (event.getType() == TYPE_VIEW_INSETS_CHANGED) {
502                 assertWithMessage("Expected view insets to be non-null on %s", event)
503                     .that(event.getInsets()).isNotNull();
504                 insetsEventFound = true;
505             }
506         }
507 
508         if (!insetsEventFound) {
509             throw new RetryableException(
510                 String.format(
511                     "Expected at least one VIEW_INSETS_CHANGED event in the set of events %s",
512                     events));
513         }
514     }
515 
516     /**
517      * Asserts the existence and contents of a {@link #TYPE_WINDOW_BOUNDS_CHANGED} event.
518      */
assertWindowBoundsChanged(@onNull List<ContentCaptureEvent> events)519     public static void assertWindowBoundsChanged(@NonNull List<ContentCaptureEvent> events) {
520         boolean boundsEventFound = false;
521         for (ContentCaptureEvent event : events) {
522             if (event.getType() == TYPE_WINDOW_BOUNDS_CHANGED) {
523                 assertWithMessage("Expected window bounds to be non-null on %s", event)
524                         .that(event.getBounds()).isNotNull();
525                 boundsEventFound = true;
526             }
527         }
528 
529         if (!boundsEventFound) {
530             throw new RetryableException(
531                 String.format(
532                         "Expected at least one WINDOW_BOUNDS_CHANGED event in the set of events %s",
533                         events));
534         }
535     }
536 
537     /**
538      * Asserts the basic contents of a {@link #TYPE_CONTEXT_UPDATED} event.
539      */
assertContextUpdated( @onNull List<ContentCaptureEvent> events, int index)540     public static ContentCaptureEvent assertContextUpdated(
541             @NonNull List<ContentCaptureEvent> events, int index) {
542         final ContentCaptureEvent event = getEvent(events, index, TYPE_CONTEXT_UPDATED);
543         assertWithMessage("event %s (index %s) should not have a ViewNode", event, index)
544                 .that(event.getViewNode()).isNull();
545         assertWithMessage("event %s (index %s) should not have text", event, index)
546                 .that(event.getViewNode()).isNull();
547         assertWithMessage("event %s (index %s) should not have an autofillId", event, index)
548                 .that(event.getId()).isNull();
549         assertWithMessage("event %s (index %s) should not have autofillIds", event, index)
550                 .that(event.getIds()).isNull();
551         return event;
552     }
553 
554     /**
555      * Asserts the order a session was created or destroyed.
556      */
assertLifecycleOrder(int expectedOrder, @NonNull Session session, @NonNull LifecycleOrder type)557     public static void assertLifecycleOrder(int expectedOrder, @NonNull Session session,
558             @NonNull LifecycleOrder type) {
559         switch(type) {
560             case CREATION:
561                 assertWithMessage("Wrong order of creation for session %s", session)
562                     .that(session.creationOrder).isEqualTo(expectedOrder);
563                 break;
564             case DESTRUCTION:
565                 assertWithMessage("Wrong order of destruction for session %s", session)
566                     .that(session.destructionOrder).isEqualTo(expectedOrder);
567                 break;
568             default:
569                 throw new IllegalArgumentException("Invalid type: " + type);
570         }
571     }
572 
573     /**
574      * Gets the event at the given index, failing with the user-friendly message if necessary...
575      */
576     @NonNull
getEvent(@onNull List<ContentCaptureEvent> events, int index, int expectedType)577     private static ContentCaptureEvent getEvent(@NonNull List<ContentCaptureEvent> events,
578             int index, int expectedType) {
579         assertWithMessage("events is null").that(events).isNotNull();
580         final ContentCaptureEvent event = events.get(index);
581         assertWithMessage("no event at index %s (size %s): %s", index, events.size(), events)
582                 .that(event).isNotNull();
583         final int actualType = event.getType();
584         if (actualType != expectedType) {
585             throw new AssertionError(String.format(
586                     "wrong event type (expected %s, actual is %s) at index %s: %s",
587                     eventTypeAsString(expectedType), eventTypeAsString(actualType), index, event));
588         }
589         assertWithMessage("invalid time on %s (index %s)", event, index).that(event.getEventTime())
590                  .isAtLeast(MY_EPOCH);
591         return event;
592     }
593 
594     /**
595      * Gets an user-friendly description of the given event type.
596      */
597     @NonNull
eventTypeAsString(int type)598     public static String eventTypeAsString(int type) {
599         final String string;
600         switch (type) {
601             case TYPE_VIEW_APPEARED:
602                 string = "APPEAR";
603                 break;
604             case TYPE_VIEW_DISAPPEARED:
605                 string = "DISAPPEAR";
606                 break;
607             case TYPE_VIEW_TEXT_CHANGED:
608                 string = "TEXT_CHANGE";
609                 break;
610             case TYPE_VIEW_TREE_APPEARING:
611                 string = "TREE_START";
612                 break;
613             case TYPE_VIEW_TREE_APPEARED:
614                 string = "TREE_END";
615                 break;
616             case TYPE_SESSION_PAUSED:
617                 string = "PAUSED";
618                 break;
619             case TYPE_SESSION_RESUMED:
620                 string = "RESUMED";
621                 break;
622             case TYPE_WINDOW_BOUNDS_CHANGED:
623                 string = "WINDOW_BOUNDS";
624                 break;
625             default:
626                 return "UNKNOWN-" + type;
627         }
628         return String.format("%s-%d", string, type);
629     }
630 
Assertions()631     private Assertions() {
632         throw new UnsupportedOperationException("contain static methods only");
633     }
634 
635     public enum LifecycleOrder {
636         CREATION, DESTRUCTION
637     }
638 }
639