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