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