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