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