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