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.jetpack.utils.TestActivityLauncher.KEY_ACTIVITY_ID; 31 32 import static androidx.test.core.app.ApplicationProvider.getApplicationContext; 33 import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; 34 35 import static org.junit.Assert.assertEquals; 36 import static org.junit.Assert.assertFalse; 37 import static org.junit.Assert.assertNotNull; 38 import static org.junit.Assert.assertTrue; 39 import static org.junit.Assert.fail; 40 import static org.junit.Assume.assumeTrue; 41 42 import android.app.Activity; 43 import android.app.ActivityOptions; 44 import android.app.ActivityTaskManager; 45 import android.app.Application; 46 import android.app.Instrumentation; 47 import android.app.PictureInPictureParams; 48 import android.content.ComponentName; 49 import android.content.Context; 50 import android.content.Intent; 51 import android.graphics.Rect; 52 import android.os.Bundle; 53 import android.os.IBinder; 54 import android.server.wm.NestedShellPermission; 55 import android.view.WindowManager; 56 57 import androidx.annotation.NonNull; 58 import androidx.annotation.Nullable; 59 import androidx.window.extensions.layout.FoldingFeature; 60 import androidx.window.sidecar.SidecarDeviceState; 61 62 import org.junit.After; 63 import org.junit.Before; 64 65 import java.util.Collections; 66 import java.util.HashSet; 67 import java.util.Set; 68 import java.util.function.BooleanSupplier; 69 70 /** Base class for all tests in the module. */ 71 public class WindowManagerJetpackTestBase { 72 73 public static final String EXTRA_EMBED_ACTIVITY = "EmbedActivity"; 74 public static final String EXTRA_SPLIT_RATIO = "SplitRatio"; 75 76 public Instrumentation mInstrumentation; 77 public Context mContext; 78 public Application mApplication; 79 80 private static final Set<Activity> sResumedActivities = new HashSet<>(); 81 private static final Set<Activity> sVisibleActivities = new HashSet<>(); 82 83 @Before setUp()84 public void setUp() { 85 mInstrumentation = getInstrumentation(); 86 assertNotNull(mInstrumentation); 87 mContext = getApplicationContext(); 88 assertNotNull(mContext); 89 mApplication = (Application) mContext.getApplicationContext(); 90 assertNotNull(mApplication); 91 // Register activity lifecycle callbacks to know which activities are resumed 92 registerActivityLifecycleCallbacks(); 93 // Clear the previous launch bounds / windowing mode, otherwise persisted launch bounds may 94 // prepend startFullScreenActivityNewTask from launching Activities in full-screen. 95 NestedShellPermission.run(() -> 96 mContext.getSystemService(ActivityTaskManager.class).clearLaunchParamsForPackages( 97 Collections.singletonList("android.server.wm.jetpack"))); 98 } 99 100 @After tearDown()101 public void tearDown() { 102 sResumedActivities.clear(); 103 sVisibleActivities.clear(); 104 } 105 hasDeviceFeature(final String requiredFeature)106 protected boolean hasDeviceFeature(final String requiredFeature) { 107 return mContext.getPackageManager().hasSystemFeature(requiredFeature); 108 } 109 110 /** Assume this device supports rotation */ assumeSupportsRotation()111 protected void assumeSupportsRotation() { 112 assumeTrue(doesDeviceSupportRotation()); 113 } 114 115 /** 116 * Rotation support is indicated by explicitly having both landscape and portrait 117 * features or not listing either at all. 118 */ doesDeviceSupportRotation()119 protected boolean doesDeviceSupportRotation() { 120 final boolean supportsLandscape = hasDeviceFeature(FEATURE_SCREEN_LANDSCAPE); 121 final boolean supportsPortrait = hasDeviceFeature(FEATURE_SCREEN_PORTRAIT); 122 return (supportsLandscape && supportsPortrait) || (!supportsLandscape && !supportsPortrait); 123 } 124 supportsPip()125 protected boolean supportsPip() { 126 return hasDeviceFeature(FEATURE_PICTURE_IN_PICTURE); 127 } 128 startActivityNewTask(@onNull Class<T> activityClass)129 public <T extends Activity> T startActivityNewTask(@NonNull Class<T> activityClass) { 130 return startActivityNewTask(activityClass, null /* activityId */); 131 } 132 launcherForNewActivity( @onNull Class<T> activityClass, int launchDisplayId)133 public <T extends Activity> TestActivityLauncher<T> launcherForNewActivity( 134 @NonNull Class<T> activityClass, int launchDisplayId) { 135 return launcherForActivityNewTask(activityClass, null /* activityId */, 136 false /* isFullScreen */, launchDisplayId); 137 } 138 startActivityNewTask(@onNull Class<T> activityClass, @Nullable String activityId)139 public <T extends Activity> T startActivityNewTask(@NonNull Class<T> activityClass, 140 @Nullable String activityId) { 141 return launcherForActivityNewTask(activityClass, activityId, false /* isFullScreen */, 142 null /* launchDisplayId */) 143 .launch(mInstrumentation); 144 } 145 startFullScreenActivityNewTask(@onNull Class<T> activityClass)146 public <T extends Activity> T startFullScreenActivityNewTask(@NonNull Class<T> activityClass) { 147 return startFullScreenActivityNewTask(activityClass, null /* activityId */); 148 } 149 startFullScreenActivityNewTask(@onNull Class<T> activityClass, @Nullable String activityId)150 public <T extends Activity> T startFullScreenActivityNewTask(@NonNull Class<T> activityClass, 151 @Nullable String activityId) { 152 return launcherForActivityNewTask(activityClass, activityId, true/* isFullScreen */, 153 null /* launchDisplayId */) 154 .launch(mInstrumentation); 155 } 156 waitForOrFail(String message, BooleanSupplier condition)157 public static void waitForOrFail(String message, BooleanSupplier condition) { 158 Condition.waitFor(new Condition<>(message, condition) 159 .setRetryIntervalMs(500) 160 .setRetryLimit(5) 161 .setOnFailure(unusedResult -> fail("FAILED because unsatisfied: " + message))); 162 } 163 launcherForActivityNewTask( @onNull Class<T> activityClass, @Nullable String activityId, boolean isFullScreen, @Nullable Integer launchDisplayId)164 private <T extends Activity> TestActivityLauncher<T> launcherForActivityNewTask( 165 @NonNull Class<T> activityClass, @Nullable String activityId, boolean isFullScreen, 166 @Nullable Integer launchDisplayId) { 167 final int windowingMode = isFullScreen ? WINDOWING_MODE_FULLSCREEN : 168 WINDOWING_MODE_UNDEFINED; 169 final TestActivityLauncher launcher = new TestActivityLauncher<>(mContext, activityClass) 170 .addIntentFlag(FLAG_ACTIVITY_NEW_TASK) 171 .setActivityId(activityId) 172 .setWindowingMode(windowingMode); 173 if (launchDisplayId != null) { 174 launcher.setLaunchDisplayId(launchDisplayId); 175 } 176 return launcher; 177 } 178 179 /** 180 * Start an activity using a component name. Can be used for activities from a different UIDs. 181 */ startActivityNoWait(@onNull Context context, @NonNull ComponentName activityComponent, @NonNull Bundle extras)182 public static void startActivityNoWait(@NonNull Context context, 183 @NonNull ComponentName activityComponent, @NonNull Bundle extras) { 184 final Intent intent = new Intent() 185 .setClassName(activityComponent.getPackageName(), activityComponent.getClassName()) 186 .addFlags(FLAG_ACTIVITY_NEW_TASK) 187 .putExtras(extras); 188 context.startActivity(intent); 189 } 190 191 /** 192 * Start an activity using a component name on the specified display with 193 * {@link FLAG_ACTIVITY_SINGLE_TOP}. Can be used for activities from a different UIDs. 194 */ startActivityOnDisplaySingleTop(@onNull Context context, int displayId, @NonNull ComponentName activityComponent, @NonNull Bundle extras)195 public static void startActivityOnDisplaySingleTop(@NonNull Context context, 196 int displayId, @NonNull ComponentName activityComponent, @NonNull Bundle extras) { 197 final ActivityOptions options = ActivityOptions.makeBasic(); 198 options.setLaunchDisplayId(displayId); 199 200 Intent intent = new Intent() 201 .addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_SINGLE_TOP) 202 .setComponent(activityComponent) 203 .putExtras(extras); 204 context.startActivity(intent, options.toBundle()); 205 } 206 207 /** 208 * Starts an instance of {@param activityToLaunchClass} from {@param activityToLaunchFrom} 209 * and returns the activity ID from the newly launched class. 210 */ startActivityFromActivity(Activity activityToLaunchFrom, Class<T> activityToLaunchClass, String newActivityId)211 public static <T extends Activity> void startActivityFromActivity(Activity activityToLaunchFrom, 212 Class<T> activityToLaunchClass, String newActivityId) { 213 Intent intent = new Intent(activityToLaunchFrom, activityToLaunchClass); 214 intent.putExtra(KEY_ACTIVITY_ID, newActivityId); 215 activityToLaunchFrom.startActivity(intent); 216 } 217 218 /** 219 * Starts a specified activity class from {@param activityToLaunchFrom}. 220 */ startActivityFromActivity(@onNull Activity activityToLaunchFrom, @NonNull ComponentName activityToLaunchComponent, @NonNull String newActivityId, @NonNull Bundle extras)221 public static void startActivityFromActivity(@NonNull Activity activityToLaunchFrom, 222 @NonNull ComponentName activityToLaunchComponent, @NonNull String newActivityId, 223 @NonNull Bundle extras) { 224 Intent intent = new Intent(); 225 intent.setClassName(activityToLaunchComponent.getPackageName(), 226 activityToLaunchComponent.getClassName()); 227 intent.putExtra(KEY_ACTIVITY_ID, newActivityId); 228 intent.putExtras(extras); 229 activityToLaunchFrom.startActivity(intent); 230 } 231 getActivityWindowToken(Activity activity)232 public static IBinder getActivityWindowToken(Activity activity) { 233 return activity.getWindow().getAttributes().token; 234 } 235 assertHasNonNegativeDimensions(@onNull Rect rect)236 public static void assertHasNonNegativeDimensions(@NonNull Rect rect) { 237 assertFalse(rect.width() < 0 || rect.height() < 0); 238 } 239 240 public static void assertNotBothDimensionsZero(@NonNull Rect rect) { 241 assertFalse(rect.width() == 0 && rect.height() == 0); 242 } 243 244 public static Rect getActivityBounds(Activity activity) { 245 return activity.getWindowManager().getCurrentWindowMetrics().getBounds(); 246 } 247 248 public static Rect getMaximumActivityBounds(Activity activity) { 249 return activity.getWindowManager().getMaximumWindowMetrics().getBounds(); 250 } 251 252 /** 253 * Gets the width of a full-screen task. 254 */ 255 public int getTaskWidth() { 256 return mContext.getSystemService(WindowManager.class).getMaximumWindowMetrics().getBounds() 257 .width(); 258 } 259 260 public int getTaskHeight() { 261 return mContext.getSystemService(WindowManager.class).getMaximumWindowMetrics().getBounds() 262 .height(); 263 } 264 265 public static void setActivityOrientationActivityHandlesOrientationChanges( 266 TestActivity activity, int orientation) { 267 // Make sure that the provided orientation is a fixed orientation 268 assertTrue(orientation == ORIENTATION_PORTRAIT || orientation == ORIENTATION_LANDSCAPE); 269 // Do nothing if the orientation already matches 270 if (activity.getResources().getConfiguration().orientation == orientation) { 271 return; 272 } 273 activity.resetLayoutCounter(); 274 // Change the orientation 275 activity.setRequestedOrientation(orientation == ORIENTATION_PORTRAIT 276 ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE); 277 // Wait for the activity to layout, which will happen after the orientation change 278 waitForOrFail("Activity orientation must be updated", 279 () -> activity.getResources().getConfiguration().orientation == orientation); 280 } 281 282 public static void enterPipActivityHandlesConfigChanges(TestActivity activity) { 283 if (activity.isInPictureInPictureMode()) { 284 throw new IllegalStateException("Activity must not be in PiP"); 285 } 286 activity.resetLayoutCounter(); 287 // Change the orientation 288 PictureInPictureParams params = (new PictureInPictureParams.Builder()).build(); 289 activity.enterPictureInPictureMode(params); 290 // Wait for the activity to layout, which will happen after the orientation change 291 assertTrue(activity.waitForLayout()); 292 // Check that orientation matches 293 assertTrue(activity.isInPictureInPictureMode()); 294 } 295 296 public static void exitPipActivityHandlesConfigChanges(TestActivity activity) { 297 if (!activity.isInPictureInPictureMode()) { 298 throw new IllegalStateException("Activity must be in PiP"); 299 } 300 activity.resetLayoutCounter(); 301 Intent intent = new Intent(activity, activity.getClass()); 302 intent.addFlags(FLAG_ACTIVITY_SINGLE_TOP); 303 activity.startActivity(intent); 304 // Wait for the activity to layout, which will happen after the orientation change 305 assertTrue(activity.waitForLayout()); 306 // Check that orientation matches 307 assertFalse(activity.isInPictureInPictureMode()); 308 } 309 310 public static void setActivityOrientationActivityDoesNotHandleOrientationChanges( 311 TestActivity activity, int orientation) { 312 // Make sure that the provided orientation is a fixed orientation 313 assertTrue(orientation == ORIENTATION_PORTRAIT || orientation == ORIENTATION_LANDSCAPE); 314 // Do nothing if the orientation already matches 315 if (activity.getResources().getConfiguration().orientation == orientation) { 316 return; 317 } 318 TestActivity.resetResumeCounter(); 319 // Change the orientation 320 activity.setRequestedOrientation(orientation == ORIENTATION_PORTRAIT 321 ? SCREEN_ORIENTATION_PORTRAIT : SCREEN_ORIENTATION_LANDSCAPE); 322 // The activity will relaunch because it does not handle the orientation change, so wait 323 // for the activity to be resumed again 324 assertTrue(activity.waitForOnResume()); 325 // Check that orientation matches 326 assertEquals(orientation, activity.getResources().getConfiguration().orientation); 327 } 328 329 /** 330 * Returns whether the display rotates to respect activity orientation, which will be false if 331 * both portrait activities and landscape activities have the same maximum bounds. If the 332 * display rotates for orientation, then the maximum portrait bounds will be a rotated version 333 * of the maximum landscape bounds. 334 */ 335 // TODO(b/186631239): ActivityManagerTestBase#ignoresOrientationRequests could disable 336 // activity rotation, as a result the display area would remain in the old orientation while 337 // the activity orientation changes. We should check the existence of this request before 338 // running tests that compare orientation values. 339 public static boolean doesDisplayRotateForOrientation(@NonNull Rect portraitMaximumBounds, 340 @NonNull Rect landscapeMaximumBounds) { 341 return !portraitMaximumBounds.equals(landscapeMaximumBounds); 342 } 343 344 public static boolean areExtensionAndSidecarDeviceStateEqual(int extensionDeviceState, 345 int sidecarDeviceStatePosture) { 346 return (extensionDeviceState == FoldingFeature.STATE_FLAT 347 && sidecarDeviceStatePosture == SidecarDeviceState.POSTURE_OPENED) 348 || (extensionDeviceState == FoldingFeature.STATE_HALF_OPENED 349 && sidecarDeviceStatePosture == SidecarDeviceState.POSTURE_HALF_OPENED); 350 } 351 352 private void registerActivityLifecycleCallbacks() { 353 mApplication.registerActivityLifecycleCallbacks( 354 new Application.ActivityLifecycleCallbacks() { 355 @Override 356 public void onActivityCreated(@NonNull Activity activity, 357 @Nullable Bundle savedInstanceState) { 358 } 359 360 @Override 361 public void onActivityStarted(@NonNull Activity activity) { 362 synchronized (sVisibleActivities) { 363 sVisibleActivities.add(activity); 364 } 365 } 366 367 @Override 368 public void onActivityResumed(@NonNull Activity activity) { 369 synchronized (sResumedActivities) { 370 sResumedActivities.add(activity); 371 } 372 } 373 374 @Override 375 public void onActivityPaused(@NonNull Activity activity) { 376 synchronized (sResumedActivities) { 377 sResumedActivities.remove(activity); 378 } 379 } 380 381 @Override 382 public void onActivityStopped(@NonNull Activity activity) { 383 synchronized (sVisibleActivities) { 384 sVisibleActivities.remove(activity); 385 } 386 } 387 388 @Override 389 public void onActivitySaveInstanceState(@NonNull Activity activity, 390 @NonNull Bundle outState) { 391 } 392 393 @Override 394 public void onActivityDestroyed(@NonNull Activity activity) { 395 } 396 }); 397 } 398 399 public static boolean isActivityResumed(Activity activity) { 400 synchronized (sResumedActivities) { 401 return sResumedActivities.contains(activity); 402 } 403 } 404 405 public static boolean isActivityVisible(Activity activity) { 406 synchronized (sVisibleActivities) { 407 return sVisibleActivities.contains(activity); 408 } 409 } 410 411 @Nullable 412 public static TestActivityWithId getResumedActivityById(@NonNull String activityId) { 413 synchronized (sResumedActivities) { 414 for (Activity activity : sResumedActivities) { 415 if (activity instanceof TestActivityWithId 416 && activityId.equals(((TestActivityWithId) activity).getId())) { 417 return (TestActivityWithId) activity; 418 } 419 } 420 return null; 421 } 422 } 423 424 @Nullable 425 public static Activity getTopResumedActivity() { 426 synchronized (sResumedActivities) { 427 return !sResumedActivities.isEmpty() ? sResumedActivities.iterator().next() : null; 428 } 429 } 430 } 431