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