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