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