• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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