Home | History | Annotate | Download | only in cts
      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
    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 
    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.
    157     public void setIgnoreOrphanSessionEvents(boolean newValue) {
    158         Log.d(TAG, "setIgnoreOrphanSessionEvents(): changing from " + mIgnoreOrphanSessionEvents
    159                 + " to " + newValue);
    160         mIgnoreOrphanSessionEvents = newValue;
    161     }
    162 
    163     @Override
    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
    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      */
    217     public void waitUntilConnected() throws InterruptedException {
    218         await(mConnectedLatch, "not connected");
    219     }
    220 
    221     /**
    222      * Waits until the system calls {@link #onDisconnected()}.
    223      */
    224     public void waitUntilDisconnected() throws InterruptedException {
    225         await(mDisconnectedLatch, "not disconnected");
    226     }
    227 
    228     @Override
    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
    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
    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
    283     public void onDataRemovalRequest(DataRemovalRequest request) {
    284         Log.i(TAG, "onDataRemovalRequest(id=" + mId + ",req=" + request + ")");
    285         mRemovalRequest = request;
    286     }
    287 
    288     @Override
    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      */
    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
    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
    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
    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
    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
    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
    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      */
    385     public static List<Throwable> getExceptions() throws Exception {
    386         return Collections.unmodifiableList(sExceptions);
    387     }
    388 
    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 
    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 
    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 
    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 
    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 
    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.
    451         public List<ContentCaptureEvent> getEvents() {
    452             return Collections.unmodifiableList(mEvents);
    453         }
    454 
    455         @Override
    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 
    466         private MyActivityEvent(ActivityEvent event) {
    467             order = ++mActivityEventsCounter;
    468             this.event = event;
    469         }
    470 
    471         @Override
    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
    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 
    501         public void waitOnDestroy() throws InterruptedException {
    502             await(mDestroyed, "not destroyed");
    503         }
    504 
    505         /**
    506          * Whitelist stuff when the service connects.
    507          */
    508         public void whitelist(@Nullable Pair<Set<String>, Set<ComponentName>> whitelist) {
    509             mWhitelist = whitelist;
    510         }
    511 
    512         @Override
    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          */
    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
    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 
    550         private EventsAssertor(ArrayList<MyActivityEvent> events) {
    551             mEvents = Collections.unmodifiableList(events);
    552             Log.v(TAG, "EventsAssertor: " + mEvents);
    553         }
    554 
    555         @NonNull
    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
    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
    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
    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 
    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
    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
    632         String getErrorMessage(@NonNull MyActivityEvent event);
    633     }
    634 }
    635