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