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