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 = removeUnexpectedEvents(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 unexpected 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      */
removeUnexpectedEvents( @onNull List<ContentCaptureEvent> events)317     public static List<ContentCaptureEvent> removeUnexpectedEvents(
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     /** Gets a list of events that are related to view changes only. */
getViewLevelEvents( @onNull List<ContentCaptureEvent> events)328     public static List<ContentCaptureEvent> getViewLevelEvents(
329             @NonNull List<ContentCaptureEvent> events) {
330         return Collections.unmodifiableList(events).stream().filter(
331                 e -> e.getType() == TYPE_WINDOW_BOUNDS_CHANGED
332                         || e.getType() == TYPE_VIEW_INSETS_CHANGED
333                         || e.getType() == TYPE_VIEW_TREE_APPEARING
334                         || e.getType() == TYPE_VIEW_TREE_APPEARED
335         ).collect(Collectors.toList());
336     }
337 
338     /**
339      * Asserts that a session for the given activity has events at all.
340      */
assertNoEvents(@onNull Session session, @NonNull ComponentName componentName)341     public static void assertNoEvents(@NonNull Session session,
342             @NonNull ComponentName componentName) {
343         assertRightActivity(session, session.id, componentName);
344         assertThat(session.getEvents()).isEmpty();
345     }
346 
347     /**
348      * Asserts that the events received by the service optionally contains the
349      * {@code TYPE_VIEW_DISAPPEARED} events, as they might have not been generated if the views
350      * disappeared after the activity stopped.
351      *
352      * @param events events received by the service.
353      * @param minimumSize size of events received if activity stopped before views disappeared
354      * @param expectedIds ids of views that might have disappeared.
355      *
356      * @return whether the view disappeared events were generated
357      */
358     // TODO(b/123540067, 122315042): remove this method if we could make it deterministic, and
359     // inline the assertions (or rename / change its logic)
assertViewsOptionallyDisappeared( @onNull List<ContentCaptureEvent> events, int minimumSize, @NonNull AutofillId... expectedIds)360     public static boolean assertViewsOptionallyDisappeared(
361             @NonNull List<ContentCaptureEvent> events, int minimumSize,
362             @NonNull AutofillId... expectedIds) {
363         final int actualSize = events.size();
364         final int disappearedEventIndex;
365         if (actualSize == minimumSize) {
366             // Activity stopped before TYPE_VIEW_DISAPPEARED were sent.
367             return false;
368         } else if (actualSize == minimumSize + 1) {
369             // Activity did not receive TYPE_VIEW_TREE_APPEARING and TYPE_VIEW_TREE_APPEARED.
370             disappearedEventIndex = minimumSize;
371         } else {
372             disappearedEventIndex = minimumSize + 1;
373         }
374         final ContentCaptureEvent batchDisappearEvent = events.get(disappearedEventIndex);
375 
376         if (expectedIds.length == 1) {
377             assertWithMessage("Should have just one deleted id on %s", batchDisappearEvent)
378                     .that(batchDisappearEvent.getIds()).isNull();
379             assertWithMessage("wrong deleted id on %s", batchDisappearEvent)
380                     .that(batchDisappearEvent.getId()).isEqualTo(expectedIds[0]);
381         } else {
382             assertWithMessage("Should not have individual deleted id on %s", batchDisappearEvent)
383                     .that(batchDisappearEvent.getId()).isNull();
384             final List<AutofillId> actualIds = batchDisappearEvent.getIds();
385             assertWithMessage("wrong deleteds id on %s", batchDisappearEvent)
386                     .that(actualIds).containsExactly((Object[]) expectedIds);
387         }
388         return true;
389     }
390 
391     /**
392      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event, without checking for parent
393      */
assertViewWithUnknownParentAppeared( @onNull List<ContentCaptureEvent> events, int index, @NonNull ContentCaptureSessionId expectedSessionId, @NonNull View expectedView)394     public static void assertViewWithUnknownParentAppeared(
395             @NonNull List<ContentCaptureEvent> events, int index,
396             @NonNull ContentCaptureSessionId expectedSessionId, @NonNull View expectedView) {
397         assertViewWithUnknownParentAppeared(events, index, expectedView);
398         assertSessionId(expectedSessionId, expectedView);
399     }
400 
401     /**
402      * Asserts the contents of a {@link #TYPE_VIEW_DISAPPEARED} event for a single view.
403      */
assertViewDisappeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId expectedId)404     public static void assertViewDisappeared(@NonNull List<ContentCaptureEvent> events, int index,
405             @NonNull AutofillId expectedId) {
406         final ContentCaptureEvent event = assertCommonViewDisappearedProperties(events, index);
407         assertWithMessage("wrong autofillId on event %s (index %s)", event, index)
408             .that(event.getId()).isEqualTo(expectedId);
409         assertWithMessage("event %s (index %s) should not have autofillIds", event, index)
410             .that(event.getIds()).isNull();
411     }
412 
413     /**
414      * Asserts the contents of a {@link #TYPE_VIEW_DISAPPEARED} event for multiple views.
415      */
assertViewsDisappeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId... expectedIds)416     public static void assertViewsDisappeared(@NonNull List<ContentCaptureEvent> events, int index,
417             @NonNull AutofillId... expectedIds) {
418         final ContentCaptureEvent event = assertCommonViewDisappearedProperties(events, index);
419         final List<AutofillId> ids = event.getIds();
420         assertWithMessage("no autofillIds on event %s (index %s)", event, index).that(ids)
421                 .isNotNull();
422         assertWithMessage("wrong autofillId on event %s (index %s)", event, index)
423             .that(ids).containsExactly((Object[]) expectedIds).inOrder();
424         assertWithMessage("event %s (index %s) should not have autofillId", event, index)
425             .that(event.getId()).isNull();
426     }
427 
assertCommonViewDisappearedProperties( @onNull List<ContentCaptureEvent> events, int index)428     private static ContentCaptureEvent assertCommonViewDisappearedProperties(
429             @NonNull List<ContentCaptureEvent> events, int index) {
430         final ContentCaptureEvent event = getEvent(events, index, TYPE_VIEW_DISAPPEARED);
431         assertWithMessage("event %s (index %s) should not have a ViewNode", event, index)
432                 .that(event.getViewNode()).isNull();
433         assertWithMessage("event %s (index %s) should not have text", event, index)
434                 .that(event.getText()).isNull();
435         return event;
436     }
437 
438     /**
439      * Asserts the contents of a {@link #TYPE_VIEW_APPEARED} event for a virtual node.
440      */
assertVirtualViewAppeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull ContentCaptureSession session, @NonNull AutofillId parentId, int childId, @Nullable String expectedText)441     public static void assertVirtualViewAppeared(@NonNull List<ContentCaptureEvent> events,
442             int index, @NonNull ContentCaptureSession session, @NonNull AutofillId parentId,
443             int childId, @Nullable String expectedText) {
444         final ContentCaptureEvent event = getEvent(events, index, TYPE_VIEW_APPEARED);
445         final ViewNode node = event.getViewNode();
446         assertThat(node).isNotNull();
447         final AutofillId expectedId = session.newAutofillId(parentId, childId);
448         assertWithMessage("wrong autofill id on %s (index %s)", event, index)
449             .that(node.getAutofillId()).isEqualTo(expectedId);
450         if (expectedText != null) {
451             assertWithMessage("wrong text on %s(index %s) ", event, index)
452                 .that(node.getText().toString()).isEqualTo(expectedText);
453         } else {
454             assertWithMessage("%s (index %s) should not have text", node, index)
455                 .that(node.getText()).isNull();
456         }
457     }
458 
459     /**
460      * Asserts the contents of a {@link #TYPE_VIEW_DISAPPEARED} event for a virtual node.
461      */
assertVirtualViewDisappeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId parentId, @NonNull ContentCaptureSession session, long childId)462     public static void assertVirtualViewDisappeared(@NonNull List<ContentCaptureEvent> events,
463             int index, @NonNull AutofillId parentId, @NonNull ContentCaptureSession session,
464             long childId) {
465         assertViewDisappeared(events, index, session.newAutofillId(parentId, childId));
466     }
467 
468     /**
469      * Asserts the contents of a {@link #TYPE_VIEW_DISAPPEARED} event for many virtual nodes.
470      */
assertVirtualViewsDisappeared(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId parentId, @NonNull ContentCaptureSession session, long... childrenIds)471     public static void assertVirtualViewsDisappeared(@NonNull List<ContentCaptureEvent> events,
472             int index, @NonNull AutofillId parentId, @NonNull ContentCaptureSession session,
473             long... childrenIds) {
474         final int size = childrenIds.length;
475         final AutofillId[] expectedIds = new AutofillId[size];
476         for (int i = 0; i < childrenIds.length; i++) {
477             expectedIds[i] = session.newAutofillId(parentId, childrenIds[i]);
478         }
479         assertViewsDisappeared(events, index, expectedIds);
480     }
481 
482     /**
483      * Asserts a view has the given session id.
484      */
assertSessionId(@onNull ContentCaptureSessionId expectedSessionId, @NonNull View view)485     public static void assertSessionId(@NonNull ContentCaptureSessionId expectedSessionId,
486             @NonNull View view) {
487         assertThat(expectedSessionId).isNotNull();
488         final ContentCaptureSession session = view.getContentCaptureSession();
489         assertWithMessage("no session for view %s", view).that(session).isNotNull();
490         assertWithMessage("wrong session id for for view %s", view)
491                 .that(session.getContentCaptureSessionId()).isEqualTo(expectedSessionId);
492     }
493 
494     /**
495      * Asserts the contents of a {@link #TYPE_VIEW_TEXT_CHANGED} event.
496      */
assertViewTextChanged(@onNull List<ContentCaptureEvent> events, int index, @NonNull AutofillId expectedId, @NonNull String expectedText)497     public static void assertViewTextChanged(@NonNull List<ContentCaptureEvent> events, int index,
498             @NonNull AutofillId expectedId, @NonNull String expectedText) {
499         final ContentCaptureEvent event = getEvent(events, index, TYPE_VIEW_TEXT_CHANGED);
500         assertWithMessage("Wrong id on %s (%s)", event, index).that(event.getId())
501                 .isEqualTo(expectedId);
502         assertWithMessage("Wrong text on %s (%s)", event, index).that(event.getText().toString())
503                 .isEqualTo(expectedText);
504     }
505 
506     /**
507      * Asserts the existence and contents of a {@link #TYPE_VIEW_INSETS_CHANGED} event.
508      */
assertViewInsetsChanged(@onNull List<ContentCaptureEvent> events)509     public static void assertViewInsetsChanged(@NonNull List<ContentCaptureEvent> events) {
510         boolean insetsEventFound = false;
511         for (ContentCaptureEvent event : events) {
512             if (event.getType() == TYPE_VIEW_INSETS_CHANGED) {
513                 assertWithMessage("Expected view insets to be non-null on %s", event)
514                     .that(event.getInsets()).isNotNull();
515                 insetsEventFound = true;
516             }
517         }
518 
519         if (!insetsEventFound) {
520             throw new RetryableException(
521                 String.format(
522                     "Expected at least one VIEW_INSETS_CHANGED event in the set of events %s",
523                     events));
524         }
525     }
526 
527     /**
528      * Asserts the existence and contents of a {@link #TYPE_WINDOW_BOUNDS_CHANGED} event.
529      */
assertWindowBoundsChanged(@onNull List<ContentCaptureEvent> events)530     public static void assertWindowBoundsChanged(@NonNull List<ContentCaptureEvent> events) {
531         boolean boundsEventFound = false;
532         for (ContentCaptureEvent event : events) {
533             if (event.getType() == TYPE_WINDOW_BOUNDS_CHANGED) {
534                 assertWithMessage("Expected window bounds to be non-null on %s", event)
535                         .that(event.getBounds()).isNotNull();
536                 boundsEventFound = true;
537             }
538         }
539 
540         if (!boundsEventFound) {
541             throw new RetryableException(
542                 String.format(
543                         "Expected at least one WINDOW_BOUNDS_CHANGED event in the set of events %s",
544                         events));
545         }
546     }
547 
548     /**
549      * Asserts the basic contents of a {@link #TYPE_CONTEXT_UPDATED} event.
550      */
assertContextUpdated( @onNull List<ContentCaptureEvent> events, int index)551     public static ContentCaptureEvent assertContextUpdated(
552             @NonNull List<ContentCaptureEvent> events, int index) {
553         final ContentCaptureEvent event = getEvent(events, index, TYPE_CONTEXT_UPDATED);
554         assertWithMessage("event %s (index %s) should not have a ViewNode", event, index)
555                 .that(event.getViewNode()).isNull();
556         assertWithMessage("event %s (index %s) should not have text", event, index)
557                 .that(event.getViewNode()).isNull();
558         assertWithMessage("event %s (index %s) should not have an autofillId", event, index)
559                 .that(event.getId()).isNull();
560         assertWithMessage("event %s (index %s) should not have autofillIds", event, index)
561                 .that(event.getIds()).isNull();
562         return event;
563     }
564 
565     /**
566      * Asserts the order a session was created or destroyed.
567      */
assertLifecycleOrder(int expectedOrder, @NonNull Session session, @NonNull LifecycleOrder type)568     public static void assertLifecycleOrder(int expectedOrder, @NonNull Session session,
569             @NonNull LifecycleOrder type) {
570         switch(type) {
571             case CREATION:
572                 assertWithMessage("Wrong order of creation for session %s", session)
573                     .that(session.creationOrder).isEqualTo(expectedOrder);
574                 break;
575             case DESTRUCTION:
576                 assertWithMessage("Wrong order of destruction for session %s", session)
577                     .that(session.destructionOrder).isEqualTo(expectedOrder);
578                 break;
579             default:
580                 throw new IllegalArgumentException("Invalid type: " + type);
581         }
582     }
583 
584     /**
585      * Gets the event at the given index, failing with the user-friendly message if necessary...
586      */
587     @NonNull
getEvent(@onNull List<ContentCaptureEvent> events, int index, int expectedType)588     private static ContentCaptureEvent getEvent(@NonNull List<ContentCaptureEvent> events,
589             int index, int expectedType) {
590         assertWithMessage("events is null").that(events).isNotNull();
591         final ContentCaptureEvent event = events.get(index);
592         assertWithMessage("no event at index %s (size %s): %s", index, events.size(), events)
593                 .that(event).isNotNull();
594         final int actualType = event.getType();
595         if (actualType != expectedType) {
596             throw new AssertionError(String.format(
597                     "wrong event type (expected %s, actual is %s) at index %s: %s",
598                     eventTypeAsString(expectedType), eventTypeAsString(actualType), index, event));
599         }
600         assertWithMessage("invalid time on %s (index %s)", event, index).that(event.getEventTime())
601                  .isAtLeast(MY_EPOCH);
602         return event;
603     }
604 
605     /**
606      * Gets an user-friendly description of the given event type.
607      */
608     @NonNull
eventTypeAsString(int type)609     public static String eventTypeAsString(int type) {
610         final String string;
611         switch (type) {
612             case TYPE_VIEW_APPEARED:
613                 string = "APPEAR";
614                 break;
615             case TYPE_VIEW_DISAPPEARED:
616                 string = "DISAPPEAR";
617                 break;
618             case TYPE_VIEW_TEXT_CHANGED:
619                 string = "TEXT_CHANGE";
620                 break;
621             case TYPE_VIEW_TREE_APPEARING:
622                 string = "TREE_START";
623                 break;
624             case TYPE_VIEW_TREE_APPEARED:
625                 string = "TREE_END";
626                 break;
627             case TYPE_SESSION_PAUSED:
628                 string = "PAUSED";
629                 break;
630             case TYPE_SESSION_RESUMED:
631                 string = "RESUMED";
632                 break;
633             case TYPE_WINDOW_BOUNDS_CHANGED:
634                 string = "WINDOW_BOUNDS";
635                 break;
636             default:
637                 return "UNKNOWN-" + type;
638         }
639         return String.format("%s-%d", string, type);
640     }
641 
Assertions()642     private Assertions() {
643         throw new UnsupportedOperationException("contain static methods only");
644     }
645 
646     public enum LifecycleOrder {
647         CREATION, DESTRUCTION
648     }
649 }
650