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