• 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_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