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.service.contentcapture.ActivityEvent; 26 import android.service.contentcapture.ContentCaptureService; 27 import android.util.ArrayMap; 28 import android.util.ArraySet; 29 import android.util.Log; 30 import android.util.Pair; 31 import android.view.contentcapture.ContentCaptureContext; 32 import android.view.contentcapture.ContentCaptureEvent; 33 import android.view.contentcapture.ContentCaptureSessionId; 34 import android.view.contentcapture.DataRemovalRequest; 35 import android.view.contentcapture.ViewNode; 36 37 import androidx.annotation.NonNull; 38 import androidx.annotation.Nullable; 39 40 import java.io.FileDescriptor; 41 import java.io.PrintWriter; 42 import java.util.ArrayList; 43 import java.util.Collections; 44 import java.util.List; 45 import java.util.Set; 46 import java.util.concurrent.CountDownLatch; 47 48 // TODO(b/123540602): if we don't move this service to a separate package, we need to handle the 49 // onXXXX methods in a separate thread 50 // Either way, we need to make sure its methods are thread safe 51 52 public class CtsContentCaptureService extends ContentCaptureService { 53 54 private static final String TAG = CtsContentCaptureService.class.getSimpleName(); 55 56 public static final String SERVICE_NAME = MY_PACKAGE + "/." 57 + CtsContentCaptureService.class.getSimpleName(); 58 public static final ComponentName CONTENT_CAPTURE_SERVICE_COMPONENT_NAME = 59 componentNameFor(CtsContentCaptureService.class); 60 61 private static int sIdCounter; 62 63 private static ServiceWatcher sServiceWatcher; 64 65 private final int mId = ++sIdCounter; 66 67 private static final ArrayList<Throwable> sExceptions = new ArrayList<>(); 68 69 private final CountDownLatch mConnectedLatch = new CountDownLatch(1); 70 private final CountDownLatch mDisconnectedLatch = new CountDownLatch(1); 71 72 /** 73 * List of all sessions started - never reset. 74 */ 75 private final ArrayList<ContentCaptureSessionId> mAllSessions = new ArrayList<>(); 76 77 /** 78 * Map of all sessions started but not finished yet - sessions are removed as they're finished. 79 */ 80 private final ArrayMap<ContentCaptureSessionId, Session> mOpenSessions = new ArrayMap<>(); 81 82 /** 83 * Map of all sessions finished. 84 */ 85 private final ArrayMap<ContentCaptureSessionId, Session> mFinishedSessions = new ArrayMap<>(); 86 87 /** 88 * Map of latches for sessions that started but haven't finished yet. 89 */ 90 private final ArrayMap<ContentCaptureSessionId, CountDownLatch> mUnfinishedSessionLatches = 91 new ArrayMap<>(); 92 93 /** 94 * Counter of onCreate() / onDestroy() events. 95 */ 96 private int mLifecycleEventsCounter; 97 98 /** 99 * Counter of received {@link ActivityEvent} events. 100 */ 101 private int mActivityEventsCounter; 102 103 // NOTE: we could use the same counter for mLifecycleEventsCounter and mActivityEventsCounter, 104 // but that would make the tests flaker. 105 106 /** 107 * Used for testing onUserDataRemovalRequest. 108 */ 109 private DataRemovalRequest mRemovalRequest; 110 111 /** 112 * List of activity lifecycle events received. 113 */ 114 private final ArrayList<MyActivityEvent> mActivityEvents = new ArrayList<>(); 115 116 /** 117 * Optional listener for {@code onDisconnect()}. 118 */ 119 @Nullable 120 private DisconnectListener mOnDisconnectListener; 121 122 /** 123 * When set, doesn't throw exceptions when it receives an event from a session that doesn't 124 * exist. 125 */ 126 private boolean mIgnoreOrphanSessionEvents; 127 128 @NonNull setServiceWatcher()129 public static ServiceWatcher setServiceWatcher() { 130 if (sServiceWatcher != null) { 131 throw new IllegalStateException("There Can Be Only One!"); 132 } 133 sServiceWatcher = new ServiceWatcher(); 134 return sServiceWatcher; 135 } 136 resetStaticState()137 public static void resetStaticState() { 138 sExceptions.clear(); 139 // TODO(b/123540602): should probably set sInstance to null as well, but first we would need 140 // to make sure each test unbinds the service. 141 142 // TODO(b/123540602): each test should use a different service instance, but we need 143 // to provide onConnected() / onDisconnected() methods first and then change the infra so 144 // we can wait for those 145 146 if (sServiceWatcher != null) { 147 Log.wtf(TAG, "resetStaticState(): should not have sServiceWatcher"); 148 sServiceWatcher = null; 149 } 150 } 151 152 153 /** 154 * When set, doesn't throw exceptions when it receives an event from a session that doesn't 155 * exist. 156 */ 157 // TODO: try to refactor WhitelistTest so it doesn't need this hack. setIgnoreOrphanSessionEvents(boolean newValue)158 public void setIgnoreOrphanSessionEvents(boolean newValue) { 159 Log.d(TAG, "setIgnoreOrphanSessionEvents(): changing from " + mIgnoreOrphanSessionEvents 160 + " to " + newValue); 161 mIgnoreOrphanSessionEvents = newValue; 162 } 163 164 @Override onConnected()165 public void onConnected() { 166 Log.i(TAG, "onConnected(id=" + mId + "): sServiceWatcher=" + sServiceWatcher); 167 168 if (sServiceWatcher == null) { 169 addException("onConnected() without a watcher"); 170 return; 171 } 172 173 if (sServiceWatcher.mService != null) { 174 addException("onConnected(): already created: %s", sServiceWatcher); 175 return; 176 } 177 178 sServiceWatcher.mService = this; 179 sServiceWatcher.mCreated.countDown(); 180 181 if (mConnectedLatch.getCount() == 0) { 182 addException("already connected: %s", mConnectedLatch); 183 } 184 mConnectedLatch.countDown(); 185 } 186 187 @Override onDisconnected()188 public void onDisconnected() { 189 Log.i(TAG, "onDisconnected(id=" + mId + "): sServiceWatcher=" + sServiceWatcher); 190 191 if (mDisconnectedLatch.getCount() == 0) { 192 addException("already disconnected: %s", mConnectedLatch); 193 } 194 mDisconnectedLatch.countDown(); 195 196 if (sServiceWatcher == null) { 197 addException("onDisconnected() without a watcher"); 198 return; 199 } 200 if (sServiceWatcher.mService == null) { 201 addException("onDisconnected(): no service on %s", sServiceWatcher); 202 return; 203 } 204 // Notify test case as well 205 if (mOnDisconnectListener != null) { 206 final CountDownLatch latch = mOnDisconnectListener.mLatch; 207 mOnDisconnectListener = null; 208 latch.countDown(); 209 } 210 sServiceWatcher.mDestroyed.countDown(); 211 sServiceWatcher.mService = null; 212 sServiceWatcher = null; 213 } 214 215 /** 216 * Waits until the system calls {@link #onConnected()}. 217 */ waitUntilConnected()218 public void waitUntilConnected() throws InterruptedException { 219 await(mConnectedLatch, "not connected"); 220 } 221 222 /** 223 * Waits until the system calls {@link #onDisconnected()}. 224 */ waitUntilDisconnected()225 public void waitUntilDisconnected() throws InterruptedException { 226 await(mDisconnectedLatch, "not disconnected"); 227 } 228 229 @Override onCreateContentCaptureSession(ContentCaptureContext context, ContentCaptureSessionId sessionId)230 public void onCreateContentCaptureSession(ContentCaptureContext context, 231 ContentCaptureSessionId sessionId) { 232 Log.i(TAG, "onCreateContentCaptureSession(id=" + mId + ", ignoreOrpahn=" 233 + mIgnoreOrphanSessionEvents + ", ctx=" + context + ", session=" + sessionId); 234 if (mIgnoreOrphanSessionEvents) return; 235 mAllSessions.add(sessionId); 236 237 safeRun(() -> { 238 final Session session = mOpenSessions.get(sessionId); 239 if (session != null) { 240 throw new IllegalStateException("Already contains session for " + sessionId 241 + ": " + session); 242 } 243 mUnfinishedSessionLatches.put(sessionId, new CountDownLatch(1)); 244 mOpenSessions.put(sessionId, new Session(sessionId, context)); 245 }); 246 } 247 248 @Override onDestroyContentCaptureSession(ContentCaptureSessionId sessionId)249 public void onDestroyContentCaptureSession(ContentCaptureSessionId sessionId) { 250 Log.i(TAG, "onDestroyContentCaptureSession(id=" + mId + ", ignoreOrpahn=" 251 + mIgnoreOrphanSessionEvents + ", session=" + sessionId + ")"); 252 if (mIgnoreOrphanSessionEvents) return; 253 safeRun(() -> { 254 final Session session = getExistingSession(sessionId); 255 session.finish(); 256 mOpenSessions.remove(sessionId); 257 if (mFinishedSessions.containsKey(sessionId)) { 258 throw new IllegalStateException("Already destroyed " + sessionId); 259 } else { 260 mFinishedSessions.put(sessionId, session); 261 final CountDownLatch latch = getUnfinishedSessionLatch(sessionId); 262 latch.countDown(); 263 } 264 }); 265 } 266 267 @Override onContentCaptureEvent(ContentCaptureSessionId sessionId, ContentCaptureEvent event)268 public void onContentCaptureEvent(ContentCaptureSessionId sessionId, 269 ContentCaptureEvent event) { 270 Log.i(TAG, "onContentCaptureEventsRequest(id=" + mId + ", ignoreOrpahn=" 271 + mIgnoreOrphanSessionEvents + ", session=" + sessionId + "): " + event); 272 if (mIgnoreOrphanSessionEvents) return; 273 final ViewNode node = event.getViewNode(); 274 if (node != null) { 275 Log.v(TAG, "onContentCaptureEvent(): parentId=" + node.getParentAutofillId()); 276 } 277 safeRun(() -> { 278 final Session session = getExistingSession(sessionId); 279 session.mEvents.add(event); 280 }); 281 } 282 283 @Override onDataRemovalRequest(DataRemovalRequest request)284 public void onDataRemovalRequest(DataRemovalRequest request) { 285 Log.i(TAG, "onUserDataRemovalRequest(id=" + mId + ",req=" + request + ")"); 286 mRemovalRequest = request; 287 } 288 289 @Override onActivityEvent(ActivityEvent event)290 public void onActivityEvent(ActivityEvent event) { 291 Log.i(TAG, "onActivityEvent(): " + event); 292 mActivityEvents.add(new MyActivityEvent(event)); 293 } 294 295 /** 296 * Gets the cached UserDataRemovalRequest for testing. 297 */ getRemovalRequest()298 public DataRemovalRequest getRemovalRequest() { 299 return mRemovalRequest; 300 } 301 302 /** 303 * Gets the finished session for the given session id. 304 * 305 * @throws IllegalStateException if the session didn't finish yet. 306 */ 307 @NonNull getFinishedSession(@onNull ContentCaptureSessionId sessionId)308 public Session getFinishedSession(@NonNull ContentCaptureSessionId sessionId) 309 throws InterruptedException { 310 final CountDownLatch latch = getUnfinishedSessionLatch(sessionId); 311 await(latch, "session %s not finished yet", sessionId); 312 313 final Session session = mFinishedSessions.get(sessionId); 314 if (session == null) { 315 throwIllegalSessionStateException("No finished session for id %s", sessionId); 316 } 317 return session; 318 } 319 320 /** 321 * Gets the finished session when only one session is expected. 322 * 323 * <p>Should be used when the test case doesn't known in advance the id of the session. 324 */ 325 @NonNull getOnlyFinishedSession()326 public Session getOnlyFinishedSession() throws InterruptedException { 327 final ArrayList<ContentCaptureSessionId> allSessions = mAllSessions; 328 assertWithMessage("Wrong number of sessions").that(allSessions).hasSize(1); 329 final ContentCaptureSessionId id = allSessions.get(0); 330 Log.d(TAG, "getOnlyFinishedSession(): id=" + id); 331 return getFinishedSession(id); 332 } 333 334 /** 335 * Gets all sessions that have been created so far. 336 */ 337 @NonNull getAllSessionIds()338 public List<ContentCaptureSessionId> getAllSessionIds() { 339 return Collections.unmodifiableList(mAllSessions); 340 } 341 342 /** 343 * Sets a listener to wait until the service disconnects. 344 */ 345 @NonNull setOnDisconnectListener()346 public DisconnectListener setOnDisconnectListener() { 347 if (mOnDisconnectListener != null) { 348 throw new IllegalStateException("already set"); 349 } 350 mOnDisconnectListener = new DisconnectListener(); 351 return mOnDisconnectListener; 352 } 353 354 @Override dump(FileDescriptor fd, PrintWriter pw, String[] args)355 protected void dump(FileDescriptor fd, PrintWriter pw, String[] args) { 356 super.dump(fd, pw, args); 357 358 pw.print("sServiceWatcher: "); pw.println(sServiceWatcher); 359 pw.print("sExceptions: "); pw.println(sExceptions); 360 pw.print("sIdCounter: "); pw.println(sIdCounter); 361 pw.print("mId: "); pw.println(mId); 362 pw.print("mConnectedLatch: "); pw.println(mConnectedLatch); 363 pw.print("mDisconnectedLatch: "); pw.println(mDisconnectedLatch); 364 pw.print("mAllSessions: "); pw.println(mAllSessions); 365 pw.print("mOpenSessions: "); pw.println(mOpenSessions); 366 pw.print("mFinishedSessions: "); pw.println(mFinishedSessions); 367 pw.print("mUnfinishedSessionLatches: "); pw.println(mUnfinishedSessionLatches); 368 pw.print("mLifecycleEventsCounter: "); pw.println(mLifecycleEventsCounter); 369 pw.print("mActivityEventsCounter: "); pw.println(mActivityEventsCounter); 370 pw.print("mActivityLifecycleEvents: "); pw.println(mActivityEvents); 371 pw.print("mIgnoreOrphanSessionEvents: "); pw.println(mIgnoreOrphanSessionEvents); 372 } 373 374 @NonNull getUnfinishedSessionLatch(final ContentCaptureSessionId sessionId)375 private CountDownLatch getUnfinishedSessionLatch(final ContentCaptureSessionId sessionId) { 376 final CountDownLatch latch = mUnfinishedSessionLatches.get(sessionId); 377 if (latch == null) { 378 throwIllegalSessionStateException("no latch for %s", sessionId); 379 } 380 return latch; 381 } 382 383 /** 384 * Gets the exceptions that were thrown while the service handlded requests. 385 */ getExceptions()386 public static List<Throwable> getExceptions() throws Exception { 387 return Collections.unmodifiableList(sExceptions); 388 } 389 throwIllegalSessionStateException(@onNull String fmt, @Nullable Object...args)390 private void throwIllegalSessionStateException(@NonNull String fmt, @Nullable Object...args) { 391 throw new IllegalStateException(String.format(fmt, args) 392 + ".\nID=" + mId 393 + ".\nAll=" + mAllSessions 394 + ".\nOpen=" + mOpenSessions 395 + ".\nLatches=" + mUnfinishedSessionLatches 396 + ".\nFinished=" + mFinishedSessions 397 + ".\nLifecycles=" + mActivityEvents 398 + ".\nIgnoringOrphan=" + mIgnoreOrphanSessionEvents); 399 } 400 getExistingSession(@onNull ContentCaptureSessionId sessionId)401 private Session getExistingSession(@NonNull ContentCaptureSessionId sessionId) { 402 final Session session = mOpenSessions.get(sessionId); 403 if (session == null) { 404 throwIllegalSessionStateException("No open session with id %s", sessionId); 405 } 406 if (session.finished) { 407 throw new IllegalStateException("session already finished: " + session); 408 } 409 410 return session; 411 } 412 safeRun(@onNull Runnable r)413 private void safeRun(@NonNull Runnable r) { 414 try { 415 r.run(); 416 } catch (Throwable t) { 417 Log.e(TAG, "Exception handling service callback: " + t); 418 sExceptions.add(t); 419 } 420 } 421 addException(@onNull String fmt, @Nullable Object...args)422 private static void addException(@NonNull String fmt, @Nullable Object...args) { 423 final String msg = String.format(fmt, args); 424 Log.e(TAG, msg); 425 sExceptions.add(new IllegalStateException(msg)); 426 } 427 428 public final class Session { 429 public final ContentCaptureSessionId id; 430 public final ContentCaptureContext context; 431 public final int creationOrder; 432 private final List<ContentCaptureEvent> mEvents = new ArrayList<>(); 433 public boolean finished; 434 public int destructionOrder; 435 Session(ContentCaptureSessionId id, ContentCaptureContext context)436 private Session(ContentCaptureSessionId id, ContentCaptureContext context) { 437 this.id = id; 438 this.context = context; 439 creationOrder = ++mLifecycleEventsCounter; 440 Log.d(TAG, "create(" + id + "): order=" + creationOrder); 441 } 442 finish()443 private void finish() { 444 finished = true; 445 destructionOrder = ++mLifecycleEventsCounter; 446 Log.d(TAG, "finish(" + id + "): order=" + destructionOrder); 447 } 448 449 // TODO(b/123540602): currently we're only interested on all events, but eventually we 450 // should track individual requests as well to make sure they're probably batch (it will 451 // require adding a Settings to tune the buffer parameters. getEvents()452 public List<ContentCaptureEvent> getEvents() { 453 return Collections.unmodifiableList(mEvents); 454 } 455 456 @Override toString()457 public String toString() { 458 return "[id=" + id + ", context=" + context + ", events=" + mEvents.size() 459 + ", finished=" + finished + "]"; 460 } 461 } 462 463 private final class MyActivityEvent { 464 public final int order; 465 public final ActivityEvent event; 466 MyActivityEvent(ActivityEvent event)467 private MyActivityEvent(ActivityEvent event) { 468 order = ++mActivityEventsCounter; 469 this.event = event; 470 } 471 472 @Override toString()473 public String toString() { 474 return order + "-" + event; 475 } 476 } 477 478 public static final class ServiceWatcher { 479 480 private final CountDownLatch mCreated = new CountDownLatch(1); 481 private final CountDownLatch mDestroyed = new CountDownLatch(1); 482 private Pair<Set<String>, Set<ComponentName>> mWhitelist; 483 484 private CtsContentCaptureService mService; 485 486 @NonNull waitOnCreate()487 public CtsContentCaptureService waitOnCreate() throws InterruptedException { 488 await(mCreated, "not created"); 489 490 if (mService == null) { 491 throw new IllegalStateException("not created"); 492 } 493 494 if (mWhitelist != null) { 495 Log.d(TAG, "Whitelisting after created: " + mWhitelist); 496 mService.setContentCaptureWhitelist(mWhitelist.first, mWhitelist.second); 497 } 498 499 return mService; 500 } 501 waitOnDestroy()502 public void waitOnDestroy() throws InterruptedException { 503 await(mDestroyed, "not destroyed"); 504 } 505 506 /** 507 * Whitelists stuff when the service connects. 508 */ whitelist(@ullable Pair<Set<String>, Set<ComponentName>> whitelist)509 public void whitelist(@Nullable Pair<Set<String>, Set<ComponentName>> whitelist) { 510 mWhitelist = whitelist; 511 } 512 513 /** 514 * Whitelists just this package. 515 */ whitelistSelf()516 public void whitelistSelf() { 517 final ArraySet<String> pkgs = new ArraySet<>(1); 518 pkgs.add(MY_PACKAGE); 519 whitelist(new Pair<>(pkgs, null)); 520 } 521 522 @Override toString()523 public String toString() { 524 return "mService: " + mService + " created: " + (mCreated.getCount() == 0) 525 + " destroyed: " + (mDestroyed.getCount() == 0) 526 + " whitelist: " + mWhitelist; 527 } 528 } 529 530 /** 531 * Listener used to block until the service is disconnected. 532 */ 533 public class DisconnectListener { 534 private final CountDownLatch mLatch = new CountDownLatch(1); 535 536 /** 537 * Wait or die! 538 */ waitForOnDisconnected()539 public void waitForOnDisconnected() { 540 try { 541 await(mLatch, "not disconnected"); 542 } catch (Exception e) { 543 addException("DisconnectListener: onDisconnected() not called: " + e); 544 } 545 } 546 } 547 548 // TODO: make logic below more generic so it can be used for other events (and possibly move 549 // it to another helper class) 550 551 @NonNull assertThat()552 public EventsAssertor assertThat() { 553 return new EventsAssertor(mActivityEvents); 554 } 555 556 public static final class EventsAssertor { 557 private final List<MyActivityEvent> mEvents; 558 private int mNextEvent = 0; 559 EventsAssertor(ArrayList<MyActivityEvent> events)560 private EventsAssertor(ArrayList<MyActivityEvent> events) { 561 mEvents = Collections.unmodifiableList(events); 562 Log.v(TAG, "EventsAssertor: " + mEvents); 563 } 564 565 @NonNull activityResumed(@onNull ComponentName expectedActivity)566 public EventsAssertor activityResumed(@NonNull ComponentName expectedActivity) { 567 assertNextEvent((event) -> assertActivityEvent(event, expectedActivity, 568 ActivityEvent.TYPE_ACTIVITY_RESUMED), "no ACTIVITY_RESUMED event for %s", 569 expectedActivity); 570 return this; 571 } 572 573 @NonNull activityPaused(@onNull ComponentName expectedActivity)574 public EventsAssertor activityPaused(@NonNull ComponentName expectedActivity) { 575 assertNextEvent((event) -> assertActivityEvent(event, expectedActivity, 576 ActivityEvent.TYPE_ACTIVITY_PAUSED), "no ACTIVITY_PAUSED event for %s", 577 expectedActivity); 578 return this; 579 } 580 581 @NonNull activityStopped(@onNull ComponentName expectedActivity)582 public EventsAssertor activityStopped(@NonNull ComponentName expectedActivity) { 583 assertNextEvent((event) -> assertActivityEvent(event, expectedActivity, 584 ActivityEvent.TYPE_ACTIVITY_STOPPED), "no ACTIVITY_STOPPED event for %s", 585 expectedActivity); 586 return this; 587 } 588 589 @NonNull activityDestroyed(@onNull ComponentName expectedActivity)590 public EventsAssertor activityDestroyed(@NonNull ComponentName expectedActivity) { 591 assertNextEvent((event) -> assertActivityEvent(event, expectedActivity, 592 ActivityEvent.TYPE_ACTIVITY_DESTROYED), "no ACTIVITY_DESTROYED event for %s", 593 expectedActivity); 594 return this; 595 } 596 assertNextEvent(@onNull EventAssertion assertion, @NonNull String errorFormat, @Nullable Object... errorArgs)597 private void assertNextEvent(@NonNull EventAssertion assertion, @NonNull String errorFormat, 598 @Nullable Object... errorArgs) { 599 if (mNextEvent >= mEvents.size()) { 600 throw new AssertionError("Reached the end of the events: " 601 + String.format(errorFormat, errorArgs) + "\n. Events(" 602 + mEvents.size() + "): " + mEvents); 603 } 604 do { 605 final int index = mNextEvent++; 606 final MyActivityEvent event = mEvents.get(index); 607 final String error = assertion.getErrorMessage(event); 608 if (error == null) return; 609 Log.w(TAG, "assertNextEvent(): ignoring event #" + index + "(" + event + "): " 610 + error); 611 } while (mNextEvent < mEvents.size()); 612 throw new AssertionError(String.format(errorFormat, errorArgs) + "\n. Events(" 613 + mEvents.size() + "): " + mEvents); 614 } 615 } 616 617 @Nullable assertActivityEvent(@onNull MyActivityEvent myEvent, @NonNull ComponentName expectedActivity, int expectedType)618 public static String assertActivityEvent(@NonNull MyActivityEvent myEvent, 619 @NonNull ComponentName expectedActivity, int expectedType) { 620 if (myEvent == null) { 621 return "myEvent is null"; 622 } 623 final ActivityEvent event = myEvent.event; 624 if (event == null) { 625 return "event is null"; 626 } 627 final int actualType = event.getEventType(); 628 if (actualType != expectedType) { 629 return String.format("wrong event type for %s: expected %s, got %s", event, 630 expectedType, actualType); 631 } 632 final ComponentName actualActivity = event.getComponentName(); 633 if (!expectedActivity.equals(actualActivity)) { 634 return String.format("wrong activity for %s: expected %s, got %s", event, 635 expectedActivity, actualActivity); 636 } 637 return null; 638 } 639 640 private interface EventAssertion { 641 @Nullable getErrorMessage(@onNull MyActivityEvent event)642 String getErrorMessage(@NonNull MyActivityEvent event); 643 } 644 } 645