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     public static void enterPipActivityHandlesConfigChanges(TestActivity activity) {
303         if (activity.isInPictureInPictureMode()) {
304             throw new IllegalStateException("Activity must not be in PiP");
305         }
306         activity.resetOnConfigurationChangeCounter();
307         // Change the orientation
308         PictureInPictureParams params = (new PictureInPictureParams.Builder()).build();
309         activity.enterPictureInPictureMode(params);
310         activity.waitForConfigurationChange();
311     }
312 
313     public static void exitPipActivityHandlesConfigChanges(TestActivity activity) {
314         if (!activity.isInPictureInPictureMode()) {
315             throw new IllegalStateException("Activity must be in PiP");
316         }
317         activity.resetOnConfigurationChangeCounter();
318         Intent intent = new Intent(activity, activity.getClass());
319         intent.addFlags(FLAG_ACTIVITY_SINGLE_TOP);
320         activity.startActivity(intent);
321         activity.waitForConfigurationChange();
322     }
323 
324     public void setActivityOrientationActivityHandlesOrientationChanges(
325             TestActivity activity, int orientation) {
326         setActivityOrientation(activity, orientation, true);
327     }
328 
329     public void setActivityOrientationActivityDoesNotHandleOrientationChanges(
330             TestActivity activity, int orientation) {
331         setActivityOrientation(activity, orientation, false);
332     }
333 
334     private void setActivityOrientation(TestActivity activity, int orientation,
335             boolean activityHandlesOrientationChanges) {
336         // Make sure that the provided orientation is a fixed orientation
337         assertTrue(orientation == ORIENTATION_PORTRAIT || orientation == ORIENTATION_LANDSCAPE);
338         if (isCloseToSquareDisplay() && !activity.isInMultiWindowMode()) {
339             // When the display is close to square, the app config orientation may always be
340             // landscape excluding the system insets. Rotate the device away from the current
341             // orientation to change the activity/hinge orientation instead of requesting an
342             // orientation change to the specified orientation. Rotating the device won't work in
343             // multi-window mode, so handle that below.
344             // TODO(b/358463936): Checking for square display should ideally be done at the
345             // callsites of this method not within this method.
346             rotateFromCurrentOrientation(activity);
347         } else {
348             // Do nothing if the orientation already matches
349             if (activity.getResources().getConfiguration().orientation == orientation) {
350                 return;
351             }
352             if (activityHandlesOrientationChanges) {
353                 // Change the orientation
354                 changeOrientation(activity, orientation, activity.isInMultiWindowMode());
355                 // Wait for the activity to layout, which will happen after the orientation change
356                 waitForOrFail("Activity orientation must be updated",
357                         () -> activity.getResources().getConfiguration()
358                                 .orientation == orientation);
359             } else {
360                 TestActivity.resetResumeCounter();
361                 // Change the orientation
362                 changeOrientation(activity, orientation, activity.isInMultiWindowMode());
363                 // The activity will relaunch because it does not handle the orientation change
364                 assertTrue(TestActivity.waitForOnResume());
365                 assertTrue(activity.isDestroyed());
366                 // Since the last activity instance would be destroyed and recreated, check the top
367                 // resumed activity
368                 Activity resumedActivity = getTopResumedActivity();
369                 assertNotNull(resumedActivity);
370                 // Check that orientation matches
371                 assertEquals(
372                         orientation, resumedActivity.getResources().getConfiguration().orientation);
373             }
374         }
375     }
376 
377     private void changeOrientation(TestActivity activity, int requestedOrientation,
378             boolean activityIsInMultiWindowMode) {
379         if (activityIsInMultiWindowMode) {
380             // When the activity is in multi-window mode, rotating the device or requesting
381             // an orientation change may not result in the app config orientation changing.
382             // In this case, resize activity task to trigger the requested orientation.
383             resizeActivityTaskToSwitchOrientation(activity);
384         } else {
385             activity.setRequestedOrientation(requestedOrientation == ORIENTATION_PORTRAIT
386                     ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE);
387         }
388     }
389 
390     public void resizeActivityTaskToSwitchOrientation(TestActivity activity) {
391         ComponentName activityName = activity.getComponentName();
392         mWmState.computeState(activityName);
393         final Rect boundsBeforeResize = mWmState.getTaskByActivity(activityName).getBounds();
394         // To account for the case where the task was square (or close to it) before, scale the
395         // width/height larger to ensure a different resulting aspect ratio
396         final boolean isPortrait = boundsBeforeResize.width() <= boundsBeforeResize.height();
397         final double scaledHeight =
398                 isPortrait ? boundsBeforeResize.height() * 1.5 : boundsBeforeResize.height();
399         final double scaledWidth =
400                 isPortrait ? boundsBeforeResize.width() : boundsBeforeResize.width() * 1.5;
401         // Switch the height and width of the bounds for orientation change
402         final int newRight = boundsBeforeResize.left + (int) scaledHeight;
403         final int newBottom = boundsBeforeResize.top + (int) scaledWidth;
404         resizeActivityTask(activity.getComponentName(),
405                 boundsBeforeResize.left, boundsBeforeResize.top, newRight, newBottom);
406         // Check if resize applied correctly
407         mWmState.computeState(activityName);
408         waitForOrFail("Activity bounds right must be updated",
409                 () -> mWmState.getTaskByActivity(activityName).getBounds().right == newRight);
410         waitForOrFail("Activity bounds bottom must be updated",
411                 () -> mWmState.getTaskByActivity(activityName).getBounds().bottom == newBottom);
412         final Rect boundsAfterResize = mWmState.getTaskByActivity(activityName).getBounds();
413         assertEquals(scaledHeight, boundsAfterResize.width(), 1.0f);
414         assertEquals(scaledWidth, boundsAfterResize.height(), 1.0f);
415     }
416 
rotateFromCurrentOrientation(TestActivity activity)417     private void rotateFromCurrentOrientation(TestActivity activity) {
418         ComponentName activityName = activity.getComponentName();
419         mWmState.computeState(activityName);
420         final WindowManagerState.Task task = mWmState.getTaskByActivity(activityName);
421         final int displayId = mWmState.getRootTask(task.getRootTaskId()).mDisplayId;
422         final RotationSession rotationSession = createManagedRotationSession();
423         final int currentDeviceRotation = getDeviceRotation(displayId);
424         final int newDeviceRotation =
425                 currentDeviceRotation == ROTATION_0 || currentDeviceRotation == ROTATION_180 ?
426                         ROTATION_90 : ROTATION_0;
427         rotationSession.set(newDeviceRotation);
428         waitForOrFail("Activity display rotation must be updated",
429                 () -> activity.getResources().getConfiguration().windowConfiguration
430                         .getRotation() == newDeviceRotation);
431         assertEquals(newDeviceRotation, getDeviceRotation(displayId));
432     }
433 
434     /**
435      * Returns whether the display rotates to respect activity orientation, which will be false if
436      * both portrait activities and landscape activities have the same maximum bounds. If the
437      * display rotates for orientation, then the maximum portrait bounds will be a rotated version
438      * of the maximum landscape bounds.
439      */
440     // TODO(b/186631239): ActivityManagerTestBase#ignoresOrientationRequests could disable
441     // activity rotation, as a result the display area would remain in the old orientation while
442     // the activity orientation changes. We should check the existence of this request before
443     // running tests that compare orientation values.
doesDisplayRotateForOrientation(@onNull Rect portraitMaximumBounds, @NonNull Rect landscapeMaximumBounds)444     public static boolean doesDisplayRotateForOrientation(@NonNull Rect portraitMaximumBounds,
445             @NonNull Rect landscapeMaximumBounds) {
446         return !portraitMaximumBounds.equals(landscapeMaximumBounds);
447     }
448 
areExtensionAndSidecarDeviceStateEqual(int extensionDeviceState, int sidecarDeviceStatePosture)449     public static boolean areExtensionAndSidecarDeviceStateEqual(int extensionDeviceState,
450             int sidecarDeviceStatePosture) {
451         return (extensionDeviceState == FoldingFeature.STATE_FLAT
452                 && sidecarDeviceStatePosture == SidecarDeviceState.POSTURE_OPENED)
453                 || (extensionDeviceState == FoldingFeature.STATE_HALF_OPENED
454                 && sidecarDeviceStatePosture == SidecarDeviceState.POSTURE_HALF_OPENED);
455     }
456 
clearLaunchParams()457     private void clearLaunchParams() {
458         final ActivityTaskManager atm = mContext.getSystemService(ActivityTaskManager.class);
459         SystemUtil.runWithShellPermissionIdentity(() -> {
460             atm.clearLaunchParamsForPackages(List.of(mContext.getPackageName()));
461         }, Manifest.permission.MANAGE_ACTIVITY_TASKS);
462     }
463 
registerActivityLifecycleCallbacks()464     private void registerActivityLifecycleCallbacks() {
465         mApplication.registerActivityLifecycleCallbacks(
466                 new Application.ActivityLifecycleCallbacks() {
467                     @Override
468                     public void onActivityCreated(@NonNull Activity activity,
469                             @Nullable Bundle savedInstanceState) {
470                     }
471 
472                     @Override
473                     public void onActivityStarted(@NonNull Activity activity) {
474                         synchronized (sVisibleActivities) {
475                             sVisibleActivities.add(activity);
476                         }
477                     }
478 
479                     @Override
480                     public void onActivityResumed(@NonNull Activity activity) {
481                         synchronized (sResumedActivities) {
482                             sResumedActivities.add(activity);
483                         }
484                     }
485 
486                     @Override
487                     public void onActivityPaused(@NonNull Activity activity) {
488                         synchronized (sResumedActivities) {
489                             sResumedActivities.remove(activity);
490                         }
491                     }
492 
493                     @Override
494                     public void onActivityStopped(@NonNull Activity activity) {
495                         synchronized (sVisibleActivities) {
496                             sVisibleActivities.remove(activity);
497                         }
498                     }
499 
500                     @Override
501                     public void onActivitySaveInstanceState(@NonNull Activity activity,
502                             @NonNull Bundle outState) {
503                     }
504 
505                     @Override
506                     public void onActivityDestroyed(@NonNull Activity activity) {
507                     }
508         });
509     }
510 
isActivityResumed(Activity activity)511     public static boolean isActivityResumed(Activity activity) {
512         synchronized (sResumedActivities) {
513             return sResumedActivities.contains(activity);
514         }
515     }
516 
isActivityVisible(Activity activity)517     public static boolean isActivityVisible(Activity activity) {
518         synchronized (sVisibleActivities) {
519             return sVisibleActivities.contains(activity);
520         }
521     }
522 
523     @Nullable
getResumedActivityById(@onNull String activityId)524     public static TestActivityWithId getResumedActivityById(@NonNull String activityId) {
525         return getResumedActivityById(activityId, INVALID_DISPLAY);
526     }
527 
528     /**
529      * Gets the activity with specified {@code activityId} on the display with {@code displayId}.
530      */
531     @Nullable
getResumedActivityById(@onNull String activityId, int displayId)532     public static TestActivityWithId getResumedActivityById(@NonNull String activityId,
533             int displayId) {
534         synchronized (sResumedActivities) {
535             for (Activity activity : sResumedActivities) {
536                 if (activity instanceof TestActivityWithId
537                         && activityId.equals(((TestActivityWithId) activity).getId())
538                         && (displayId == INVALID_DISPLAY || displayId == activity.getDisplayId())) {
539                     return (TestActivityWithId) activity;
540                 }
541             }
542             return null;
543         }
544     }
545 
546     @Nullable
getTopResumedActivity()547     public static Activity getTopResumedActivity() {
548         synchronized (sResumedActivities) {
549             return !sResumedActivities.isEmpty() ? sResumedActivities.iterator().next() : null;
550         }
551     }
552 }
553