• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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_PACKAGE;
19 import static android.contentcaptureservice.cts.Helper.await;
20 import static android.contentcaptureservice.cts.Helper.componentNameFor;
21 
22 import static com.google.common.truth.Truth.assertWithMessage;
23 
24 import android.content.ComponentName;
25 import android.service.contentcapture.ActivityEvent;
26 import android.service.contentcapture.ContentCaptureService;
27 import android.util.ArrayMap;
28 import android.util.Log;
29 import android.util.Pair;
30 import android.view.contentcapture.ContentCaptureContext;
31 import android.view.contentcapture.ContentCaptureEvent;
32 import android.view.contentcapture.ContentCaptureSessionId;
33 import android.view.contentcapture.DataRemovalRequest;
34 import android.view.contentcapture.ViewNode;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import java.io.FileDescriptor;
40 import java.io.PrintWriter;
41 import java.util.ArrayList;
42 import java.util.Collections;
43 import java.util.List;
44 import java.util.Set;
45 import java.util.concurrent.CountDownLatch;
46 
47 // TODO(b/123540602): if we don't move this service to a separate package, we need to handle the
48 // onXXXX methods in a separate thread
49 // Either way, we need to make sure its methods are thread safe
50 
51 public class CtsContentCaptureService extends ContentCaptureService {
52 
53     private static final String TAG = CtsContentCaptureService.class.getSimpleName();
54 
55     public static final String SERVICE_NAME = MY_PACKAGE + "/."
56             + CtsContentCaptureService.class.getSimpleName();
57     public static final ComponentName CONTENT_CAPTURE_SERVICE_COMPONENT_NAME =
58             componentNameFor(CtsContentCaptureService.class);
59 
60     private static int sIdCounter;
61 
62     private static ServiceWatcher sServiceWatcher;
63 
64     private final int mId = ++sIdCounter;
65 
66     private static final ArrayList<Throwable> sExceptions = new ArrayList<>();
67 
68     private final CountDownLatch mConnectedLatch = new CountDownLatch(1);
69     private final CountDownLatch mDisconnectedLatch = new CountDownLatch(1);
70 
71     /**
72      * List of all sessions started - never reset.
73      */
74     private final ArrayList<ContentCaptureSessionId> mAllSessions = new ArrayList<>();
75 
76     /**
77      * Map of all sessions started but not finished yet - sessions are removed as they're finished.
78      */
79     private final ArrayMap<ContentCaptureSessionId, Session> mOpenSessions = new ArrayMap<>();
80 
81     /**
82      * Map of all sessions finished.
83      */
84     private final ArrayMap<ContentCaptureSessionId, Session> mFinishedSessions = new ArrayMap<>();
85 
86     /**
87      * Map of latches for sessions that started but haven't finished yet.
88      */
89     private final ArrayMap<ContentCaptureSessionId, CountDownLatch> mUnfinishedSessionLatches =
90             new ArrayMap<>();
91 
92     /**
93      * Counter of onCreate() / onDestroy() events.
94      */
95     private int mLifecycleEventsCounter;
96 
97     /**
98      * Counter of received {@link ActivityEvent} events.
99      */
100     private int mActivityEventsCounter;
101 
102     // NOTE: we could use the same counter for mLifecycleEventsCounter and mActivityEventsCounter,
103     // but that would make the tests flaker.
104 
105     /**
106      * Used for testing onDataRemovalRequest.
107      */
108     private DataRemovalRequest mRemovalRequest;
109 
110     /**
111      * List of activity lifecycle events received.
112      */
113     private final ArrayList<MyActivityEvent> mActivityEvents = new ArrayList<>();
114 
115     /**
116      * Optional listener for {@code onDisconnect()}.
117      */
118     @Nullable
119     private DisconnectListener mOnDisconnectListener;
120 
121     /**
122      * When set, doesn't throw exceptions when it receives an event from a session that doesn't
123      * exist.
124      */
125     private boolean mIgnoreOrphanSessionEvents;
126 
127     @NonNull
setServiceWatcher()128     public static ServiceWatcher setServiceWatcher() {
129         if (sServiceWatcher != null) {
130             throw new IllegalStateException("There Can Be Only One!");
131         }
132         sServiceWatcher = new ServiceWatcher();
133         return sServiceWatcher;
134     }
135 
resetStaticState()136     public static void resetStaticState() {
137         sExceptions.clear();
138         // TODO(b/123540602): should probably set sInstance to null as well, but first we would need
139         // to make sure each test unbinds the service.
140 
141         // TODO(b/123540602): each test should use a different service instance, but we need
142         // to provide onConnected() / onDisconnected() methods first and then change the infra so
143         // we can wait for those
144 
145         if (sServiceWatcher != null) {
146             Log.wtf(TAG, "resetStaticState(): should not have sServiceWatcher");
147             sServiceWatcher = null;
148         }
149     }
150 
151 
152     /**
153      * When set, doesn't throw exceptions when it receives an event from a session that doesn't
154      * exist.
155      */
156     // TODO: try to refactor WhitelistTest so it doesn't need this hack.
setIgnoreOrphanSessionEvents(boolean newValue)157     public void setIgnoreOrphanSessionEvents(boolean newValue) {
158         Log.d(TAG, "setIgnoreOrphanSessionEvents(): changing from " + mIgnoreOrphanSessionEvents
159                 + " to " + newValue);
160         mIgnoreOrphanSessionEvents = newValue;
161     }
162 
163     @Override
onConnected()164     public void onConnected() {
165         Log.i(TAG, "onConnected(id=" + mId + "): sServiceWatcher=" + sServiceWatcher);
166 
167         if (sServiceWatcher == null) {
168             addException("onConnected() without a watcher");
169             return;
170         }
171 
172         if (sServiceWatcher.mService != null) {
173             addException("onConnected(): already created: %s", sServiceWatcher);
174             return;
175         }
176 
177         sServiceWatcher.mService = this;
178         sServiceWatcher.mCreated.countDown();
179 
180         if (mConnectedLatch.getCount() == 0) {
181             addException("already connected: %s", mConnectedLatch);
182         }
183         mConnectedLatch.countDown();
184     }
185 
186     @Override
onDisconnected()187     public void onDisconnected() {
188         Log.i(TAG, "onDisconnected(id=" + mId + "): sServiceWatcher=" + sServiceWatcher);
189 
190         if (mDisconnectedLatch.getCount() == 0) {
191             addException("already disconnected: %s", mConnectedLatch);
192         }
193         mDisconnectedLatch.countDown();
194 
195         if (sServiceWatcher == null) {
196             addException("onDisconnected() without a watcher");
197             return;
198         }
199         if (sServiceWatcher.mService == null) {
200             addException("onDisconnected(): no service on %s", sServiceWatcher);
201             return;
202         }
203         // Notify test case as well
204         if (mOnDisconnectListener != null) {
205             final CountDownLatch latch = mOnDisconnectListener.mLatch;
206             mOnDisconnectListener = null;
207             latch.countDown();
208         }
209         sServiceWatcher.mDestroyed.countDown();
210         sServiceWatcher.mService = null;
211         sServiceWatcher = null;
212     }
213 
214     /**
215      * Waits until the system calls {@link #onConnected()}.
216      */
waitUntilConnected()217     public void waitUntilConnected() throws InterruptedException {
218         await(mConnectedLatch, "not connected");
219     }
220 
221     /**
222      * Waits until the system calls {@link #onDisconnected()}.
223      */
waitUntilDisconnected()224     public void waitUntilDisconnected() throws InterruptedException {
225         await(mDisconnectedLatch, "not disconnected");
226     }
227 
228     @Override
onCreateContentCaptureSession(ContentCaptureContext context, ContentCaptureSessionId sessionId)229     public void onCreateContentCaptureSession(ContentCaptureContext context,
230             ContentCaptureSessionId sessionId) {
231         Log.i(TAG, "onCreateContentCaptureSession(id=" + mId + ", ignoreOrpahn="
232                 + mIgnoreOrphanSessionEvents + ", ctx=" + context + ", session=" + sessionId);
233         if (mIgnoreOrphanSessionEvents) return;
234         mAllSessions.add(sessionId);
235 
236         safeRun(() -> {
237             final Session session = mOpenSessions.get(sessionId);
238             if (session != null) {
239                 throw new IllegalStateException("Already contains session for " + sessionId
240                         + ": " + session);
241             }
242             mUnfinishedSessionLatches.put(sessionId, new CountDownLatch(1));
243             mOpenSessions.put(sessionId, new Session(sessionId, context));
244         });
245     }
246 
247     @Override
onDestroyContentCaptureSession(ContentCaptureSessionId sessionId)248     public void onDestroyContentCaptureSession(ContentCaptureSessionId sessionId) {
249         Log.i(TAG, "onDestroyContentCaptureSession(id=" + mId + ", ignoreOrpahn="
250                 + mIgnoreOrphanSessionEvents + ", session=" + sessionId + ")");
251         if (mIgnoreOrphanSessionEvents) return;
252         safeRun(() -> {
253             final Session session = getExistingSession(sessionId);
254             session.finish();
255             mOpenSessions.remove(sessionId);
256             if (mFinishedSessions.containsKey(sessionId)) {
257                 throw new IllegalStateException("Already destroyed " + sessionId);
258             } else {
259                 mFinishedSessions.put(sessionId, session);
260                 final CountDownLatch latch = getUnfinishedSessionLatch(sessionId);
261                 latch.countDown();
262             }
263         });
264     }
265 
266     @Override
onContentCaptureEvent(ContentCaptureSessionId sessionId, ContentCaptureEvent event)267     public void onContentCaptureEvent(ContentCaptureSessionId sessionId,
268             ContentCaptureEvent event) {
269         Log.i(TAG, "onContentCaptureEventsRequest(id=" + mId + ", ignoreOrpahn="
270                 + mIgnoreOrphanSessionEvents + ", session=" + sessionId + "): " + event);
271         if (mIgnoreOrphanSessionEvents) return;
272         final ViewNode node = event.getViewNode();
273         if (node != null) {
274             Log.v(TAG, "onContentCaptureEvent(): parentId=" + node.getParentAutofillId());
275         }
276         safeRun(() -> {
277             final Session session = getExistingSession(sessionId);
278             session.mEvents.add(event);
279         });
280     }
281 
282     @Override
onDataRemovalRequest(DataRemovalRequest request)283     public void onDataRemovalRequest(DataRemovalRequest request) {
284         Log.i(TAG, "onDataRemovalRequest(id=" + mId + ",req=" + request + ")");
285         mRemovalRequest = request;
286     }
287 
288     @Override
onActivityEvent(ActivityEvent event)289     public void onActivityEvent(ActivityEvent event) {
290         Log.i(TAG, "onActivityEvent(): " + event);
291         mActivityEvents.add(new MyActivityEvent(event));
292     }
293 
294     /**
295      * Gets the cached DataRemovalRequest for testing.
296      */
getRemovalRequest()297     public DataRemovalRequest getRemovalRequest() {
298         return mRemovalRequest;
299     }
300 
301     /**
302      * Gets the finished session for the given session id.
303      *
304      * @throws IllegalStateException if the session didn't finish yet.
305      */
306     @NonNull
getFinishedSession(@onNull ContentCaptureSessionId sessionId)307     public Session getFinishedSession(@NonNull ContentCaptureSessionId sessionId)
308             throws InterruptedException {
309         final CountDownLatch latch = getUnfinishedSessionLatch(sessionId);
310         await(latch, "session %s not finished yet", sessionId);
311 
312         final Session session = mFinishedSessions.get(sessionId);
313         if (session == null) {
314             throwIllegalSessionStateException("No finished session for id %s", sessionId);
315         }
316         return session;
317     }
318 
319     /**
320      * Gets the finished session when only one session is expected.
321      *
322      * <p>Should be used when the test case doesn't known in advance the id of the session.
323      */
324     @NonNull
getOnlyFinishedSession()325     public Session getOnlyFinishedSession() throws InterruptedException {
326         final ArrayList<ContentCaptureSessionId> allSessions = mAllSessions;
327         assertWithMessage("Wrong number of sessions").that(allSessions).hasSize(1);
328         final ContentCaptureSessionId id = allSessions.get(0);
329         Log.d(TAG, "getOnlyFinishedSession(): id=" + id);
330         return getFinishedSession(id);
331     }
332 
333     /**
334      * Gets all sessions that have been created so far.
335      */
336     @NonNull
getAllSessionIds()337     public List<ContentCaptureSessionId> getAllSessionIds() {
338         return Collections.unmodifiableList(mAllSessions);
339     }
340 
341     /**
342      * Sets a listener to wait until the service disconnects.
343      */
344     @NonNull
setOnDisconnectListener()345     public DisconnectListener setOnDisconnectListener() {
346         if (mOnDisconnectListener != null) {
347             throw new IllegalStateException("already set");
348         }
349         mOnDisconnectListener = new DisconnectListener();
350         return mOnDisconnectListener;
351     }
352 
353     @Override
dump(FileDescriptor fd, PrintWriter pw, String[] args)354     protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) {
355         super.dump(fd, pw, args);
356 
357         pw.print("sServiceWatcher: "); pw.println(sServiceWatcher);
358         pw.print("sExceptions: "); pw.println(sExceptions);
359         pw.print("sIdCounter: "); pw.println(sIdCounter);
360         pw.print("mId: "); pw.println(mId);
361         pw.print("mConnectedLatch: "); pw.println(mConnectedLatch);
362         pw.print("mDisconnectedLatch: "); pw.println(mDisconnectedLatch);
363         pw.print("mAllSessions: "); pw.println(mAllSessions);
364         pw.print("mOpenSessions: "); pw.println(mOpenSessions);
365         pw.print("mFinishedSessions: "); pw.println(mFinishedSessions);
366         pw.print("mUnfinishedSessionLatches: "); pw.println(mUnfinishedSessionLatches);
367         pw.print("mLifecycleEventsCounter: "); pw.println(mLifecycleEventsCounter);
368         pw.print("mActivityEventsCounter: "); pw.println(mActivityEventsCounter);
369         pw.print("mActivityLifecycleEvents: "); pw.println(mActivityEvents);
370         pw.print("mIgnoreOrphanSessionEvents: "); pw.println(mIgnoreOrphanSessionEvents);
371     }
372 
373     @NonNull
getUnfinishedSessionLatch(final ContentCaptureSessionId sessionId)374     private CountDownLatch getUnfinishedSessionLatch(final ContentCaptureSessionId sessionId) {
375         final CountDownLatch latch = mUnfinishedSessionLatches.get(sessionId);
376         if (latch == null) {
377             throwIllegalSessionStateException("no latch for %s", sessionId);
378         }
379         return latch;
380     }
381 
382     /**
383      * Gets the exceptions that were thrown while the service handlded requests.
384      */
getExceptions()385     public static List<Throwable> getExceptions() throws Exception {
386         return Collections.unmodifiableList(sExceptions);
387     }
388 
throwIllegalSessionStateException(@onNull String fmt, @Nullable Object...args)389     private void throwIllegalSessionStateException(@NonNull String fmt, @Nullable Object...args) {
390         throw new IllegalStateException(String.format(fmt, args)
391                 + ".\nID=" + mId
392                 + ".\nAll=" + mAllSessions
393                 + ".\nOpen=" + mOpenSessions
394                 + ".\nLatches=" + mUnfinishedSessionLatches
395                 + ".\nFinished=" + mFinishedSessions
396                 + ".\nLifecycles=" + mActivityEvents
397                 + ".\nIgnoringOrphan=" + mIgnoreOrphanSessionEvents);
398     }
399 
getExistingSession(@onNull ContentCaptureSessionId sessionId)400     private Session getExistingSession(@NonNull ContentCaptureSessionId sessionId) {
401         final Session session = mOpenSessions.get(sessionId);
402         if (session == null) {
403             throwIllegalSessionStateException("No open session with id %s", sessionId);
404         }
405         if (session.finished) {
406             throw new IllegalStateException("session already finished: " + session);
407         }
408 
409         return session;
410     }
411 
safeRun(@onNull Runnable r)412     private void safeRun(@NonNull Runnable r) {
413         try {
414             r.run();
415         } catch (Throwable t) {
416             Log.e(TAG, "Exception handling service callback: " + t);
417             sExceptions.add(t);
418         }
419     }
420 
addException(@onNull String fmt, @Nullable Object...args)421     private static void addException(@NonNull String fmt, @Nullable Object...args) {
422         final String msg = String.format(fmt, args);
423         Log.e(TAG, msg);
424         sExceptions.add(new IllegalStateException(msg));
425     }
426 
427     public final class Session {
428         public final ContentCaptureSessionId id;
429         public final ContentCaptureContext context;
430         public final int creationOrder;
431         private final List<ContentCaptureEvent> mEvents = new ArrayList<>();
432         public boolean finished;
433         public int destructionOrder;
434 
Session(ContentCaptureSessionId id, ContentCaptureContext context)435         private Session(ContentCaptureSessionId id, ContentCaptureContext context) {
436             this.id = id;
437             this.context = context;
438             creationOrder = ++mLifecycleEventsCounter;
439             Log.d(TAG, "create(" + id  + "): order=" + creationOrder);
440         }
441 
finish()442         private void finish() {
443             finished = true;
444             destructionOrder = ++mLifecycleEventsCounter;
445             Log.d(TAG, "finish(" + id  + "): order=" + destructionOrder);
446         }
447 
448         // TODO(b/123540602): currently we're only interested on all events, but eventually we
449         // should track individual requests as well to make sure they're probably batch (it will
450         // require adding a Settings to tune the buffer parameters.
getEvents()451         public List<ContentCaptureEvent> getEvents() {
452             return Collections.unmodifiableList(mEvents);
453         }
454 
455         @Override
toString()456         public String toString() {
457             return "[id=" + id + ", context=" + context + ", events=" + mEvents.size()
458                     + ", finished=" + finished + "]";
459         }
460     }
461 
462     private final class MyActivityEvent {
463         public final int order;
464         public final ActivityEvent event;
465 
MyActivityEvent(ActivityEvent event)466         private MyActivityEvent(ActivityEvent event) {
467             order = ++mActivityEventsCounter;
468             this.event = event;
469         }
470 
471         @Override
toString()472         public String toString() {
473             return order + "-" + event;
474         }
475     }
476 
477     public static final class ServiceWatcher {
478 
479         private final CountDownLatch mCreated = new CountDownLatch(1);
480         private final CountDownLatch mDestroyed = new CountDownLatch(1);
481         private Pair<Set<String>, Set<ComponentName>> mWhitelist;
482 
483         private CtsContentCaptureService mService;
484 
485         @NonNull
waitOnCreate()486         public CtsContentCaptureService waitOnCreate() throws InterruptedException {
487             await(mCreated, "not created");
488 
489             if (mService == null) {
490                 throw new IllegalStateException("not created");
491             }
492 
493             if (mWhitelist != null) {
494                 Log.d(TAG, "Whitelisting after created: " + mWhitelist);
495                 mService.setContentCaptureWhitelist(mWhitelist.first, mWhitelist.second);
496             }
497 
498             return mService;
499         }
500 
waitOnDestroy()501         public void waitOnDestroy() throws InterruptedException {
502             await(mDestroyed, "not destroyed");
503         }
504 
505         /**
506          * Whitelist stuff when the service connects.
507          */
whitelist(@ullable Pair<Set<String>, Set<ComponentName>> whitelist)508         public void whitelist(@Nullable Pair<Set<String>, Set<ComponentName>> whitelist) {
509             mWhitelist = whitelist;
510         }
511 
512         @Override
toString()513         public String toString() {
514             return "mService: " + mService + " created: " + (mCreated.getCount() == 0)
515                     + " destroyed: " + (mDestroyed.getCount() == 0)
516                     + " whitelist: " + mWhitelist;
517         }
518     }
519 
520     /**
521      * Listener used to block until the service is disconnected.
522      */
523     public class DisconnectListener {
524         private final CountDownLatch mLatch = new CountDownLatch(1);
525 
526         /**
527          * Wait or die!
528          */
waitForOnDisconnected()529         public void waitForOnDisconnected() {
530             try {
531                 await(mLatch, "not disconnected");
532             } catch (Exception e) {
533                 addException("DisconnectListener: onDisconnected() not called: " + e);
534             }
535         }
536     }
537 
538     // TODO: make logic below more generic so it can be used for other events (and possibly move
539     // it to another helper class)
540 
541     @NonNull
assertThat()542     public EventsAssertor assertThat() {
543         return new EventsAssertor(mActivityEvents);
544     }
545 
546     public static final class EventsAssertor {
547         private final List<MyActivityEvent> mEvents;
548         private int mNextEvent = 0;
549 
EventsAssertor(ArrayList<MyActivityEvent> events)550         private EventsAssertor(ArrayList<MyActivityEvent> events) {
551             mEvents = Collections.unmodifiableList(events);
552             Log.v(TAG, "EventsAssertor: " + mEvents);
553         }
554 
555         @NonNull
activityResumed(@onNull ComponentName expectedActivity)556         public EventsAssertor activityResumed(@NonNull ComponentName expectedActivity) {
557             assertNextEvent((event) -> assertActivityEvent(event, expectedActivity,
558                     ActivityEvent.TYPE_ACTIVITY_RESUMED), "no ACTIVITY_RESUMED event for %s",
559                     expectedActivity);
560             return this;
561         }
562 
563         @NonNull
activityPaused(@onNull ComponentName expectedActivity)564         public EventsAssertor activityPaused(@NonNull ComponentName expectedActivity) {
565             assertNextEvent((event) -> assertActivityEvent(event, expectedActivity,
566                     ActivityEvent.TYPE_ACTIVITY_PAUSED), "no ACTIVITY_PAUSED event for %s",
567                     expectedActivity);
568             return this;
569         }
570 
571         @NonNull
activityStopped(@onNull ComponentName expectedActivity)572         public EventsAssertor activityStopped(@NonNull ComponentName expectedActivity) {
573             assertNextEvent((event) -> assertActivityEvent(event, expectedActivity,
574                     ActivityEvent.TYPE_ACTIVITY_STOPPED), "no ACTIVITY_STOPPED event for %s",
575                     expectedActivity);
576             return this;
577         }
578 
579         @NonNull
activityDestroyed(@onNull ComponentName expectedActivity)580         public EventsAssertor activityDestroyed(@NonNull ComponentName expectedActivity) {
581             assertNextEvent((event) -> assertActivityEvent(event, expectedActivity,
582                     ActivityEvent.TYPE_ACTIVITY_DESTROYED), "no ACTIVITY_DESTROYED event for %s",
583                     expectedActivity);
584             return this;
585         }
586 
assertNextEvent(@onNull EventAssertion assertion, @NonNull String errorFormat, @Nullable Object... errorArgs)587         private void assertNextEvent(@NonNull EventAssertion assertion, @NonNull String errorFormat,
588                 @Nullable Object... errorArgs) {
589             if (mNextEvent >= mEvents.size()) {
590                 throw new AssertionError("Reached the end of the events: "
591                         + String.format(errorFormat, errorArgs) + "\n. Events("
592                         + mEvents.size() + "): " + mEvents);
593             }
594             do {
595                 final int index = mNextEvent++;
596                 final MyActivityEvent event = mEvents.get(index);
597                 final String error = assertion.getErrorMessage(event);
598                 if (error == null) return;
599                 Log.w(TAG, "assertNextEvent(): ignoring event #" + index + "(" + event + "): "
600                         + error);
601             } while (mNextEvent < mEvents.size());
602             throw new AssertionError(String.format(errorFormat, errorArgs) + "\n. Events("
603                     + mEvents.size() + "): " + mEvents);
604         }
605     }
606 
607     @Nullable
assertActivityEvent(@onNull MyActivityEvent myEvent, @NonNull ComponentName expectedActivity, int expectedType)608     public static String assertActivityEvent(@NonNull MyActivityEvent myEvent,
609             @NonNull ComponentName expectedActivity, int expectedType) {
610         if (myEvent == null) {
611             return "myEvent is null";
612         }
613         final ActivityEvent event = myEvent.event;
614         if (event == null) {
615             return "event is null";
616         }
617         final int actualType = event.getEventType();
618         if (actualType != expectedType) {
619             return String.format("wrong event type for %s: expected %s, got %s", event,
620                     expectedType, actualType);
621         }
622         final ComponentName actualActivity = event.getComponentName();
623         if (!expectedActivity.equals(actualActivity)) {
624             return String.format("wrong activity for %s: expected %s, got %s", event,
625                     expectedActivity, actualActivity);
626         }
627         return null;
628     }
629 
630     private interface EventAssertion {
631         @Nullable
getErrorMessage(@onNull MyActivityEvent event)632         String getErrorMessage(@NonNull MyActivityEvent event);
633     }
634 }
635