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