• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 
17 package android.server.wm.jetpack.utils;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
21 import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK;
22 import static android.content.Intent.FLAG_ACTIVITY_SINGLE_TOP;
23 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
24 import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
25 import static android.content.pm.PackageManager.FEATURE_PICTURE_IN_PICTURE;
26 import static android.content.pm.PackageManager.FEATURE_SCREEN_LANDSCAPE;
27 import static android.content.pm.PackageManager.FEATURE_SCREEN_PORTRAIT;
28 import static android.content.res.Configuration.ORIENTATION_LANDSCAPE;
29 import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
30 import static android.server.wm.WindowManagerState.STATE_RESUMED;
31 import static android.server.wm.jetpack.utils.TestActivityLauncher.KEY_ACTIVITY_ID;
32 import static android.view.Display.INVALID_DISPLAY;
33 import static android.view.Surface.ROTATION_0;
34 import static android.view.Surface.ROTATION_180;
35 import static android.view.Surface.ROTATION_90;
36 
37 import static androidx.test.core.app.ApplicationProvider.getApplicationContext;
38 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
39 
40 import static org.junit.Assert.assertEquals;
41 import static org.junit.Assert.assertFalse;
42 import static org.junit.Assert.assertNotNull;
43 import static org.junit.Assert.assertTrue;
44 import static org.junit.Assert.fail;
45 import static org.junit.Assume.assumeTrue;
46 
47 import android.Manifest;
48 import android.app.Activity;
49 import android.app.ActivityOptions;
50 import android.app.ActivityTaskManager;
51 import android.app.Application;
52 import android.app.Instrumentation;
53 import android.app.PictureInPictureParams;
54 import android.content.ComponentName;
55 import android.content.Context;
56 import android.content.Intent;
57 import android.graphics.Rect;
58 import android.os.Bundle;
59 import android.os.IBinder;
60 import android.server.wm.ActivityManagerTestBase;
61 import android.server.wm.RotationSession;
62 import android.server.wm.WindowManagerState;
63 
64 import androidx.annotation.NonNull;
65 import androidx.annotation.Nullable;
66 import androidx.window.extensions.layout.FoldingFeature;
67 import androidx.window.sidecar.SidecarDeviceState;
68 
69 import com.android.compatibility.common.util.SystemUtil;
70 
71 import org.junit.After;
72 import org.junit.Before;
73 
74 import java.util.HashSet;
75 import java.util.List;
76 import java.util.Set;
77 import java.util.function.BooleanSupplier;
78 
79 /** Base class for all tests in the module. */
80 public class WindowManagerJetpackTestBase extends ActivityManagerTestBase {
81 
82     public static final String EXTRA_EMBED_ACTIVITY = "EmbedActivity";
83     public static final String EXTRA_SPLIT_RATIO = "SplitRatio";
84 
85     public Instrumentation mInstrumentation;
86     public Context mContext;
87     public Application mApplication;
88 
89     private static final Set<Activity> sResumedActivities = new HashSet<>();
90     private static final Set<Activity> sVisibleActivities = new HashSet<>();
91 
92     @Before
setUp()93     public void setUp() throws Exception {
94         super.setUp();
95         mInstrumentation = getInstrumentation();
96         assertNotNull(mInstrumentation);
97         mContext = getApplicationContext();
98         assertNotNull(mContext);
99         mApplication = (Application) mContext.getApplicationContext();
100         assertNotNull(mApplication);
101         clearLaunchParams();
102         // Register activity lifecycle callbacks to know which activities are resumed
103         registerActivityLifecycleCallbacks();
104     }
105 
106     @After
tearDown()107     public void tearDown() throws Throwable {
108         sResumedActivities.clear();
109         sVisibleActivities.clear();
110     }
111 
hasDeviceFeature(final String requiredFeature)112     protected boolean hasDeviceFeature(final String requiredFeature) {
113         return mContext.getPackageManager().hasSystemFeature(requiredFeature);
114     }
115 
116     /** Assume this device supports rotation */
assumeSupportsRotation()117     protected void assumeSupportsRotation() {
118         assumeTrue(doesDeviceSupportRotation());
119     }
120 
121     /**
122      * Rotation support is indicated by explicitly having both landscape and portrait
123      * features or not listing either at all.
124      */
doesDeviceSupportRotation()125     protected boolean doesDeviceSupportRotation() {
126         final boolean supportsLandscape = hasDeviceFeature(FEATURE_SCREEN_LANDSCAPE);
127         final boolean supportsPortrait = hasDeviceFeature(FEATURE_SCREEN_PORTRAIT);
128         return (supportsLandscape && supportsPortrait) || (!supportsLandscape && !supportsPortrait);
129     }
130 
supportsPip()131     protected boolean supportsPip() {
132         return hasDeviceFeature(FEATURE_PICTURE_IN_PICTURE);
133     }
134 
startActivityNewTask(@onNull Class<T> activityClass)135     public <T extends Activity> T startActivityNewTask(@NonNull Class<T> activityClass) {
136         return startActivityNewTask(activityClass, null /* activityId */);
137     }
138 
launcherForNewActivity( @onNull Class<T> activityClass, int launchDisplayId)139     public <T extends Activity> TestActivityLauncher<T> launcherForNewActivity(
140             @NonNull Class<T> activityClass, int launchDisplayId) {
141         return launcherForActivityNewTask(activityClass, null /* activityId */,
142                 false /* isFullScreen */, launchDisplayId);
143     }
144 
145     /**
146      * Starts an {@link Activity} with {@code activityId}, of which the windowing mode and launched
147      * display follows system default.
148      */
startActivityNewTask(@onNull Class<T> activityClass, @Nullable String activityId)149     public <T extends Activity> T startActivityNewTask(@NonNull Class<T> activityClass,
150             @Nullable String activityId) {
151         return startActivityNewTask(activityClass, activityId, null /* displayId */);
152     }
153 
154     /**
155      * Starts an {@link Activity} on given {@code displayId}, of which the windowing mode follows
156      * system default.
157      */
startActivityNewTask(@onNull Class<T> activityClass, @Nullable String activityId, @Nullable Integer displayId)158     public <T extends Activity> T startActivityNewTask(@NonNull Class<T> activityClass,
159             @Nullable String activityId, @Nullable Integer displayId) {
160         return startActivityNewTaskInternal(activityClass, activityId, false /* isFullscreen */,
161                 displayId);
162     }
163 
startFullScreenActivityNewTask(@onNull Class<T> activityClass)164     public <T extends Activity> T startFullScreenActivityNewTask(@NonNull Class<T> activityClass) {
165         return startFullScreenActivityNewTask(activityClass, null /* activityId */);
166     }
167 
startFullScreenActivityNewTask(@onNull Class<T> activityClass, @Nullable String activityId)168     public <T extends  Activity> T startFullScreenActivityNewTask(@NonNull Class<T> activityClass,
169             @Nullable String activityId) {
170         return startFullScreenActivityNewTask(activityClass, activityId,
171                 null /* displayId */);
172     }
173 
174     /**
175      * Starts a fullscreen {@link Activity} on given {@code displayId}.
176      */
startFullScreenActivityNewTask(@onNull Class<T> activityClass, @Nullable String activityId, @Nullable Integer displayId)177     public <T extends Activity> T startFullScreenActivityNewTask(@NonNull Class<T> activityClass,
178             @Nullable String activityId, @Nullable Integer displayId) {
179         return startActivityNewTaskInternal(activityClass, activityId, true /* isFullscreen */,
180                 displayId);
181     }
182 
waitForOrFail(String message, BooleanSupplier condition)183     public static void waitForOrFail(String message, BooleanSupplier condition) {
184         Condition.waitFor(new Condition<>(message, condition)
185                 .setRetryIntervalMs(500)
186                 .setRetryLimit(5)
187                 .setOnFailure(unusedResult -> fail("FAILED because unsatisfied: " + message)));
188     }
189 
190     /**
191      * Starts an activity to front and returns the activity instance.
192      *
193      * @param activityClass the activity class to launch
194      * @param activityId the Activity ID to identify the activity
195      * @param isFullScreen {@code true} to launch in fullscreen, or {@code false} to follow the
196      *                      system default windowing mode
197      * @param displayId the display to launch the activity, or {@code null} to follow system default
198      * @return the launch activity instance
199      * @param <T> A activity type
200      */
startActivityNewTaskInternal(@onNull Class<T> activityClass, @Nullable String activityId, boolean isFullScreen, @Nullable Integer displayId)201     private  <T extends Activity> T startActivityNewTaskInternal(@NonNull Class<T> activityClass,
202             @Nullable String activityId, boolean isFullScreen, @Nullable Integer displayId) {
203         final T activity = launcherForActivityNewTask(activityClass, activityId, isFullScreen,
204                 displayId)
205                 .launch(mInstrumentation);
206         if (displayId != null) {
207             waitAndAssertActivityStateOnDisplay(activity.getComponentName(), STATE_RESUMED,
208                     displayId, "Activity must be launched on display#" + displayId);
209         }
210         return activity;
211     }
212 
launcherForActivityNewTask( @onNull Class<T> activityClass, @Nullable String activityId, boolean isFullScreen, @Nullable Integer launchDisplayId)213     private <T extends Activity> TestActivityLauncher<T> launcherForActivityNewTask(
214             @NonNull Class<T> activityClass, @Nullable String activityId, boolean isFullScreen,
215             @Nullable Integer launchDisplayId) {
216         final int windowingMode = isFullScreen ? WINDOWING_MODE_FULLSCREEN :
217                 WINDOWING_MODE_UNDEFINED;
218         final TestActivityLauncher<T> launcher =
219                 new TestActivityLauncher<>(mContext, activityClass)
220                         .addIntentFlag(FLAG_ACTIVITY_NEW_TASK)
221                         .setActivityId(activityId)
222                         .setWindowingMode(windowingMode);
223         if (launchDisplayId != null) {
224             launcher.setLaunchDisplayId(launchDisplayId);
225         }
226         return launcher;
227     }
228 
229     /**
230      * Start an activity using a component name. Can be used for activities from a different UIDs.
231      */
startActivityNoWait(@onNull Context context, @NonNull ComponentName activityComponent, @NonNull Bundle extras)232     public static void startActivityNoWait(@NonNull Context context,
233             @NonNull ComponentName activityComponent, @NonNull Bundle extras) {
234         final Intent intent = new Intent()
235                 .setClassName(activityComponent.getPackageName(), activityComponent.getClassName())
236                 .addFlags(FLAG_ACTIVITY_NEW_TASK)
237                 .putExtras(extras);
238         context.startActivity(intent);
239     }
240 
241     /**
242      * Start an activity using a component name on the specified display with
243      * {@link FLAG_ACTIVITY_SINGLE_TOP}. Can be used for activities from a different UIDs.
244      */
startActivityOnDisplaySingleTop(@onNull Context context, int displayId, @NonNull ComponentName activityComponent, @NonNull Bundle extras)245     public static void startActivityOnDisplaySingleTop(@NonNull Context context,
246             int displayId, @NonNull ComponentName activityComponent, @NonNull Bundle extras) {
247         final ActivityOptions options = ActivityOptions.makeBasic();
248         options.setLaunchDisplayId(displayId);
249 
250         Intent intent = new Intent()
251                 .addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_SINGLE_TOP)
252                 .setComponent(activityComponent)
253                 .putExtras(extras);
254         context.startActivity(intent, options.toBundle());
255     }
256 
257     /**
258      * Starts an instance of {@param activityToLaunchClass} from {@param activityToLaunchFrom}
259      * and returns the activity ID from the newly launched class.
260      */
startActivityFromActivity(Activity activityToLaunchFrom, Class<T> activityToLaunchClass, String newActivityId)261     public static <T extends Activity> void startActivityFromActivity(Activity activityToLaunchFrom,
262             Class<T> activityToLaunchClass, String newActivityId) {
263         Intent intent = new Intent(activityToLaunchFrom, activityToLaunchClass);
264         intent.putExtra(KEY_ACTIVITY_ID, newActivityId);
265         activityToLaunchFrom.startActivity(intent);
266     }
267 
268     /**
269      * Starts a specified activity class from {@param activityToLaunchFrom}.
270      */
startActivityFromActivity(@onNull Activity activityToLaunchFrom, @NonNull ComponentName activityToLaunchComponent, @NonNull String newActivityId, @NonNull Bundle extras)271     public static void startActivityFromActivity(@NonNull Activity activityToLaunchFrom,
272             @NonNull ComponentName activityToLaunchComponent, @NonNull String newActivityId,
273             @NonNull Bundle extras) {
274         Intent intent = new Intent();
275         intent.setClassName(activityToLaunchComponent.getPackageName(),
276                 activityToLaunchComponent.getClassName());
277         intent.putExtra(KEY_ACTIVITY_ID, newActivityId);
278         intent.putExtras(extras);
279         activityToLaunchFrom.startActivity(intent);
280     }
281 
getActivityWindowToken(Activity activity)282     public static IBinder getActivityWindowToken(Activity activity) {
283         return activity.getWindow().getAttributes().token;
284     }
285 
assertHasNonNegativeDimensions(@onNull Rect rect)286     public static void assertHasNonNegativeDimensions(@NonNull Rect rect) {
287         assertFalse(rect.width() < 0 || rect.height() < 0);
288     }
289 
290     public static void assertNotBothDimensionsZero(@NonNull Rect rect) {
291         assertFalse(rect.width() == 0 && rect.height() == 0);
292     }
293 
294     public static Rect getActivityBounds(Activity activity) {
295         return activity.getWindowManager().getCurrentWindowMetrics().getBounds();
296     }
297 
298     public static Rect getMaximumActivityBounds(Activity activity) {
299         return activity.getWindowManager().getMaximumWindowMetrics().getBounds();
300     }
301 
302     /**
303      * Move an {@link Activity} into PiP windowing mode. Returns {@code true} if the {@link
304      * Activity} entered PiP within the timeout, {@code false} otherwise.
305      *
306      * @param activity to be moved into PiP
307      * @return {@code true} if the activity successfully entered PiP.
308      */
309     public static boolean enterPipActivityHandlesConfigChanges(TestActivity activity) {
310         if (activity.isInPictureInPictureMode()) {
311             throw new IllegalStateException("Activity must not be in PiP");
312         }
313         activity.resetOnConfigurationChangeCounter();
314         // Change the orientation
315         PictureInPictureParams params = (new PictureInPictureParams.Builder()).build();
316         activity.enterPictureInPictureMode(params);
317         return activity.waitForConfigurationChange();
318     }
319 
320     /**
321      * Move an {@link Activity} out of PiP windowing mode. Returns {@code true} if the {@link
322      * Activity} exited PiP within the timeout, {@code false} otherwise.
323      *
324      * @param activity to be moved out of PiP
325      * @return {@code true} if the activity successfully exited PiP.
326      */
327     public static boolean exitPipActivityHandlesConfigChanges(TestActivity activity) {
328         if (!activity.isInPictureInPictureMode()) {
329             throw new IllegalStateException("Activity must be in PiP");
330         }
331         activity.resetOnConfigurationChangeCounter();
332         Intent intent = new Intent(activity, activity.getClass());
333         intent.addFlags(FLAG_ACTIVITY_SINGLE_TOP);
334         activity.startActivity(intent);
335         return activity.waitForConfigurationChange();
336     }
337 
338     public void setActivityOrientationActivityHandlesOrientationChanges(
339             TestActivity activity, int orientation) {
340         setActivityOrientation(activity, orientation, true);
341     }
342 
343     public void setActivityOrientationActivityDoesNotHandleOrientationChanges(
344             TestActivity activity, int orientation) {
345         setActivityOrientation(activity, orientation, false);
346     }
347 
348     private void setActivityOrientation(TestActivity activity, int orientation,
349             boolean activityHandlesOrientationChanges) {
350         // Make sure that the provided orientation is a fixed orientation
351         assertTrue(orientation == ORIENTATION_PORTRAIT || orientation == ORIENTATION_LANDSCAPE);
352         if (isCloseToSquareDisplay() && !activity.isInMultiWindowMode()) {
353             // When the display is close to square, the app config orientation may always be
354             // landscape excluding the system insets. Rotate the device away from the current
355             // orientation to change the activity/hinge orientation instead of requesting an
356             // orientation change to the specified orientation. Rotating the device won't work in
357             // multi-window mode, so handle that below.
358             // TODO(b/358463936): Checking for square display should ideally be done at the
359             // callsites of this method not within this method.
360             rotateFromCurrentOrientation(activity);
361         } else {
362             // Do nothing if the orientation already matches
363             if (activity.getResources().getConfiguration().orientation == orientation) {
364                 return;
365             }
366             if (activityHandlesOrientationChanges) {
367                 // Change the orientation
368                 changeOrientation(activity, orientation, activity.isInMultiWindowMode());
369                 // Wait for the activity to layout, which will happen after the orientation change
370                 waitForOrFail("Activity orientation must be updated",
371                         () -> activity.getResources().getConfiguration()
372                                 .orientation == orientation);
373             } else {
374                 TestActivity.resetResumeCounter();
375                 // Change the orientation
376                 changeOrientation(activity, orientation, activity.isInMultiWindowMode());
377                 // The activity will relaunch because it does not handle the orientation change
378                 assertTrue(TestActivity.waitForOnResume());
379                 assertTrue(activity.isDestroyed());
380                 // Since the last activity instance would be destroyed and recreated, check the top
381                 // resumed activity
382                 Activity resumedActivity = getTopResumedActivity();
383                 assertNotNull(resumedActivity);
384                 // Check that orientation matches
385                 assertEquals(
386                         orientation, resumedActivity.getResources().getConfiguration().orientation);
387             }
388         }
389     }
390 
391     private void changeOrientation(TestActivity activity, int requestedOrientation,
392             boolean activityIsInMultiWindowMode) {
393         if (activityIsInMultiWindowMode) {
394             // When the activity is in multi-window mode, rotating the device or requesting
395             // an orientation change may not result in the app config orientation changing.
396             // In this case, resize activity task to trigger the requested orientation.
397             resizeActivityTaskToSwitchOrientation(activity);
398         } else {
399             activity.setRequestedOrientation(requestedOrientation == ORIENTATION_PORTRAIT
400                     ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE);
401         }
402     }
403 
404     public void resizeActivityTaskToSwitchOrientation(TestActivity activity) {
405         ComponentName activityName = activity.getComponentName();
406         mWmState.computeState(activityName);
407         final Rect boundsBeforeResize = mWmState.getTaskByActivity(activityName).getBounds();
408         // To account for the case where the task was square (or close to it) before, scale the
409         // width/height larger to ensure a different resulting aspect ratio
410         final boolean isPortrait = boundsBeforeResize.width() <= boundsBeforeResize.height();
411         final double scaledHeight =
412                 isPortrait ? boundsBeforeResize.height() * 1.5 : boundsBeforeResize.height();
413         final double scaledWidth =
414                 isPortrait ? boundsBeforeResize.width() : boundsBeforeResize.width() * 1.5;
415         // Switch the height and width of the bounds for orientation change
416         final int newRight = boundsBeforeResize.left + (int) scaledHeight;
417         final int newBottom = boundsBeforeResize.top + (int) scaledWidth;
418         resizeActivityTask(activity.getComponentName(),
419                 boundsBeforeResize.left, boundsBeforeResize.top, newRight, newBottom);
420         // Check if resize applied correctly
421         mWmState.computeState(activityName);
422         waitForOrFail("Activity bounds right must be updated",
423                 () -> mWmState.getTaskByActivity(activityName).getBounds().right == newRight);
424         waitForOrFail("Activity bounds bottom must be updated",
425                 () -> mWmState.getTaskByActivity(activityName).getBounds().bottom == newBottom);
426         final Rect boundsAfterResize = mWmState.getTaskByActivity(activityName).getBounds();
427         assertEquals(scaledHeight, boundsAfterResize.width(), 1.0f);
428         assertEquals(scaledWidth, boundsAfterResize.height(), 1.0f);
429     }
430 
rotateFromCurrentOrientation(TestActivity activity)431     private void rotateFromCurrentOrientation(TestActivity activity) {
432         ComponentName activityName = activity.getComponentName();
433         mWmState.computeState(activityName);
434         final WindowManagerState.Task task = mWmState.getTaskByActivity(activityName);
435         final int displayId = mWmState.getRootTask(task.getRootTaskId()).mDisplayId;
436         final RotationSession rotationSession = createManagedRotationSession();
437         final int currentDeviceRotation = getDeviceRotation(displayId);
438         final int newDeviceRotation =
439                 currentDeviceRotation == ROTATION_0 || currentDeviceRotation == ROTATION_180 ?
440                         ROTATION_90 : ROTATION_0;
441         rotationSession.set(newDeviceRotation);
442         waitForOrFail("Activity display rotation must be updated",
443                 () -> activity.getResources().getConfiguration().windowConfiguration
444                         .getRotation() == newDeviceRotation);
445         assertEquals(newDeviceRotation, getDeviceRotation(displayId));
446     }
447 
448     /**
449      * Returns whether the display rotates to respect activity orientation, which will be false if
450      * both portrait activities and landscape activities have the same maximum bounds. If the
451      * display rotates for orientation, then the maximum portrait bounds will be a rotated version
452      * of the maximum landscape bounds.
453      */
454     // TODO(b/186631239): ActivityManagerTestBase#ignoresOrientationRequests could disable
455     // activity rotation, as a result the display area would remain in the old orientation while
456     // the activity orientation changes. We should check the existence of this request before
457     // running tests that compare orientation values.
doesDisplayRotateForOrientation(@onNull Rect portraitMaximumBounds, @NonNull Rect landscapeMaximumBounds)458     public static boolean doesDisplayRotateForOrientation(@NonNull Rect portraitMaximumBounds,
459             @NonNull Rect landscapeMaximumBounds) {
460         return !portraitMaximumBounds.equals(landscapeMaximumBounds);
461     }
462 
areExtensionAndSidecarDeviceStateEqual(int extensionDeviceState, int sidecarDeviceStatePosture)463     public static boolean areExtensionAndSidecarDeviceStateEqual(int extensionDeviceState,
464             int sidecarDeviceStatePosture) {
465         return (extensionDeviceState == FoldingFeature.STATE_FLAT
466                 && sidecarDeviceStatePosture == SidecarDeviceState.POSTURE_OPENED)
467                 || (extensionDeviceState == FoldingFeature.STATE_HALF_OPENED
468                 && sidecarDeviceStatePosture == SidecarDeviceState.POSTURE_HALF_OPENED);
469     }
470 
clearLaunchParams()471     private void clearLaunchParams() {
472         final ActivityTaskManager atm = mContext.getSystemService(ActivityTaskManager.class);
473         SystemUtil.runWithShellPermissionIdentity(() -> {
474             atm.clearLaunchParamsForPackages(List.of(mContext.getPackageName()));
475         }, Manifest.permission.MANAGE_ACTIVITY_TASKS);
476     }
477 
registerActivityLifecycleCallbacks()478     private void registerActivityLifecycleCallbacks() {
479         mApplication.registerActivityLifecycleCallbacks(
480                 new Application.ActivityLifecycleCallbacks() {
481                     @Override
482                     public void onActivityCreated(@NonNull Activity activity,
483                             @Nullable Bundle savedInstanceState) {
484                     }
485 
486                     @Override
487                     public void onActivityStarted(@NonNull Activity activity) {
488                         synchronized (sVisibleActivities) {
489                             sVisibleActivities.add(activity);
490                         }
491                     }
492 
493                     @Override
494                     public void onActivityResumed(@NonNull Activity activity) {
495                         synchronized (sResumedActivities) {
496                             sResumedActivities.add(activity);
497                         }
498                     }
499 
500                     @Override
501                     public void onActivityPaused(@NonNull Activity activity) {
502                         synchronized (sResumedActivities) {
503                             sResumedActivities.remove(activity);
504                         }
505                     }
506 
507                     @Override
508                     public void onActivityStopped(@NonNull Activity activity) {
509                         synchronized (sVisibleActivities) {
510                             sVisibleActivities.remove(activity);
511                         }
512                     }
513 
514                     @Override
515                     public void onActivitySaveInstanceState(@NonNull Activity activity,
516                             @NonNull Bundle outState) {
517                     }
518 
519                     @Override
520                     public void onActivityDestroyed(@NonNull Activity activity) {
521                     }
522         });
523     }
524 
isActivityResumed(Activity activity)525     public static boolean isActivityResumed(Activity activity) {
526         synchronized (sResumedActivities) {
527             return sResumedActivities.contains(activity);
528         }
529     }
530 
isActivityVisible(Activity activity)531     public static boolean isActivityVisible(Activity activity) {
532         synchronized (sVisibleActivities) {
533             return sVisibleActivities.contains(activity);
534         }
535     }
536 
537     @Nullable
getResumedActivityById(@onNull String activityId)538     public static TestActivityWithId getResumedActivityById(@NonNull String activityId) {
539         return getResumedActivityById(activityId, INVALID_DISPLAY);
540     }
541 
542     /**
543      * Gets the activity with specified {@code activityId} on the display with {@code displayId}.
544      */
545     @Nullable
getResumedActivityById(@onNull String activityId, int displayId)546     public static TestActivityWithId getResumedActivityById(@NonNull String activityId,
547             int displayId) {
548         synchronized (sResumedActivities) {
549             for (Activity activity : sResumedActivities) {
550                 if (activity instanceof TestActivityWithId
551                         && activityId.equals(((TestActivityWithId) activity).getId())
552                         && (displayId == INVALID_DISPLAY || displayId == activity.getDisplayId())) {
553                     return (TestActivityWithId) activity;
554                 }
555             }
556             return null;
557         }
558     }
559 
560     @Nullable
getTopResumedActivity()561     public static Activity getTopResumedActivity() {
562         synchronized (sResumedActivities) {
563             return !sResumedActivities.isEmpty() ? sResumedActivities.iterator().next() : null;
564         }
565     }
566 }
567