1 /*
2  * Copyright (C) 2020 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;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
23 import static android.server.wm.WindowManagerState.STATE_PAUSED;
24 import static android.server.wm.WindowMetricsTestHelper.assertBoundsMatchDisplay;
25 import static android.server.wm.WindowMetricsTestHelper.getBoundsExcludingNavigationBarAndCutout;
26 import static android.server.wm.WindowMetricsTestHelper.maxWindowBoundsSandboxed;
27 import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
28 
29 import static org.junit.Assert.assertEquals;
30 import static org.junit.Assert.assertTrue;
31 import static org.junit.Assert.fail;
32 import static org.junit.Assume.assumeTrue;
33 
34 import android.app.Activity;
35 import android.app.PictureInPictureParams;
36 import android.content.ComponentName;
37 import android.graphics.Point;
38 import android.graphics.Rect;
39 import android.os.Bundle;
40 import android.platform.test.annotations.Presubmit;
41 import android.server.wm.WindowMetricsTestHelper.OnLayoutChangeListener;
42 import android.view.Display;
43 import android.view.WindowManager;
44 import android.view.WindowMetrics;
45 
46 import androidx.test.filters.FlakyTest;
47 
48 import org.junit.Test;
49 
50 /**
51  * Tests that verify the behavior of {@link WindowMetrics} APIs on {@link Activity activities}.
52  *
53  * Build/Install/Run:
54  *     atest CtsWindowManagerDeviceTestCases:WindowMetricsActivityTests
55  */
56 @Presubmit
57 public class WindowMetricsActivityTests extends WindowManagerTestBase {
58     private static final Rect WINDOW_BOUNDS = new Rect(100, 100, 400, 400);
59     private static final Rect RESIZED_WINDOW_BOUNDS = new Rect(100, 100, 900, 900);
60     private static final int MOVE_OFFSET = 100;
61 
62     @Test
testMetricsMatchesLayoutOnActivityOnCreate()63     public void testMetricsMatchesLayoutOnActivityOnCreate() {
64         final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
65                 WINDOWING_MODE_FULLSCREEN);
66         final OnLayoutChangeListener listener = activity.mListener;
67 
68         listener.waitForLayout();
69 
70         WindowMetricsTestHelper.assertMetricsMatchesLayout(activity.mOnCreateCurrentMetrics,
71                 activity.mOnCreateMaximumMetrics, listener.getLayoutBounds(),
72                 listener.getLayoutInsets());
73     }
74 
75     @Test
testMetricsMatchesDisplayAreaOnActivity()76     public void testMetricsMatchesDisplayAreaOnActivity() {
77         final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
78                 WINDOWING_MODE_FULLSCREEN);
79 
80         assertMetricsValidity(activity);
81     }
82 
83     @Test
84     @FlakyTest(bugId = 188207199)
testMetricsMatchesActivityBoundsOnNonresizableActivity()85     public void testMetricsMatchesActivityBoundsOnNonresizableActivity() {
86         assumeTrue("Skipping test: no rotation support", supportsRotation());
87 
88         final MinAspectRatioActivity activity = startActivityInWindowingMode(
89                 MinAspectRatioActivity.class, WINDOWING_MODE_FULLSCREEN);
90         mWmState.computeState(activity.getComponentName());
91 
92         assertMetricsValidityForNonresizableActivity(activity);
93     }
94 
95     @Test
testMetricsMatchesLayoutOnPipActivity()96     public void testMetricsMatchesLayoutOnPipActivity() {
97         assumeTrue(supportsPip());
98 
99         final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
100                 WINDOWING_MODE_FULLSCREEN);
101 
102         assertMetricsMatchesLayout(activity);
103 
104         activity.enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
105         waitForEnterPipAnimationComplete(activity.getComponentName());
106 
107         assertMetricsMatchesLayout(activity);
108     }
109 
110     @Test
testMetricsMatchesDisplayAreaOnPipActivity()111     public void testMetricsMatchesDisplayAreaOnPipActivity() {
112         assumeTrue(supportsPip());
113 
114         final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
115                 WINDOWING_MODE_FULLSCREEN);
116 
117         assertMetricsValidity(activity);
118 
119         activity.enterPictureInPictureMode(new PictureInPictureParams.Builder().build());
120         waitForEnterPipAnimationComplete(activity.getComponentName());
121 
122         assertMetricsValidity(activity);
123     }
124 
125     /**
126      * Waits until the picture-in-picture animation has finished.
127      */
waitForEnterPipAnimationComplete(ComponentName activityName)128     private void waitForEnterPipAnimationComplete(ComponentName activityName) {
129         waitForEnterPip(activityName);
130         mWmState.waitForWithAmState(wmState -> {
131             WindowManagerState.Task task = wmState.getTaskByActivity(activityName);
132             if (task == null) {
133                 return false;
134             }
135             WindowManagerState.Activity activity = task.getActivity(activityName);
136             return activity.getWindowingMode() == WINDOWING_MODE_PINNED
137                     && activity.getState().equals(STATE_PAUSED);
138         }, "checking activity windowing mode");
139     }
140 
141     /**
142      * Waits until the given activity has entered picture-in-picture mode (allowing for the
143      * subsequent animation to start).
144      */
waitForEnterPip(ComponentName activityName)145     private void waitForEnterPip(ComponentName activityName) {
146         mWmState.waitForWithAmState(wmState -> {
147             WindowManagerState.Task task = wmState.getTaskByActivity(activityName);
148             return task != null && task.getWindowingMode() == WINDOWING_MODE_PINNED;
149         }, "checking task windowing mode");
150     }
151 
152     @Test
testMetricsMatchesLayoutOnSplitActivity()153     public void testMetricsMatchesLayoutOnSplitActivity() {
154         assumeTrue(supportsSplitScreenMultiWindow());
155 
156         final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
157                 WINDOWING_MODE_FULLSCREEN);
158 
159         assertMetricsMatchesLayout(activity);
160 
161         mWmState.computeState(activity.getComponentName());
162         putActivityInPrimarySplit(activity.getComponentName());
163 
164         mWmState.computeState(activity.getComponentName());
165         assertTrue(mWmState.getActivity(activity.getComponentName()).getWindowingMode()
166                 == WINDOWING_MODE_MULTI_WINDOW);
167 
168         assertMetricsMatchesLayout(activity);
169     }
170 
171     @Test
testMetricsMatchesDisplayAreaOnSplitActivity()172     public void testMetricsMatchesDisplayAreaOnSplitActivity() {
173         assumeTrue(supportsSplitScreenMultiWindow());
174 
175         final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
176                 WINDOWING_MODE_FULLSCREEN);
177 
178         assertMetricsValidity(activity);
179 
180         mWmState.computeState(activity.getComponentName());
181         putActivityInPrimarySplit(activity.getComponentName());
182 
183         mWmState.computeState(activity.getComponentName());
184         assertTrue(mWmState.getActivity(activity.getComponentName()).getWindowingMode()
185                 == WINDOWING_MODE_MULTI_WINDOW);
186 
187         assertMetricsValidity(activity);
188     }
189 
190     @Test
testMetricsMatchesLayoutOnFreeformActivity()191     public void testMetricsMatchesLayoutOnFreeformActivity() {
192         assumeTrue(supportsFreeform());
193 
194         final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
195                 WINDOWING_MODE_FULLSCREEN);
196 
197         assertMetricsMatchesLayout(activity);
198 
199         launchActivity(new ComponentName(mTargetContext, MetricsActivity.class),
200                 WINDOWING_MODE_FREEFORM);
201 
202         // Resize the freeform activity.
203         resizeActivityTask(activity.getComponentName(), WINDOW_BOUNDS.left, WINDOW_BOUNDS.top,
204                 WINDOW_BOUNDS.right, WINDOW_BOUNDS.bottom);
205         mWmState.computeState(activity.getComponentName());
206 
207         assertMetricsMatchesLayout(activity);
208 
209         // Resize again.
210         resizeActivityTask(activity.getComponentName(), RESIZED_WINDOW_BOUNDS.left,
211                 RESIZED_WINDOW_BOUNDS.top, RESIZED_WINDOW_BOUNDS.right,
212                 RESIZED_WINDOW_BOUNDS.bottom);
213         mWmState.computeState(activity.getComponentName());
214 
215         assertMetricsMatchesLayout(activity);
216 
217         // Move the activity.
218         resizeActivityTask(activity.getComponentName(), MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.left,
219                 MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.top, MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.right,
220                 MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.bottom);
221         mWmState.computeState(activity.getComponentName());
222 
223         assertMetricsMatchesLayout(activity);
224     }
225 
226     @Test
testMetricsMatchesDisplayAreaOnFreeformActivity()227     public void testMetricsMatchesDisplayAreaOnFreeformActivity() {
228         assumeTrue(supportsFreeform());
229 
230         final MetricsActivity activity = startActivityInWindowingMode(MetricsActivity.class,
231                 WINDOWING_MODE_FULLSCREEN);
232 
233         assertMetricsValidity(activity);
234 
235         launchActivity(new ComponentName(mTargetContext, MetricsActivity.class),
236                 WINDOWING_MODE_FREEFORM);
237 
238         // Resize the freeform activity.
239         resizeActivityTask(activity.getComponentName(), WINDOW_BOUNDS.left, WINDOW_BOUNDS.top,
240                 WINDOW_BOUNDS.right, WINDOW_BOUNDS.bottom);
241 
242         assertMetricsValidity(activity);
243 
244         // Resize again.
245         resizeActivityTask(activity.getComponentName(), RESIZED_WINDOW_BOUNDS.left,
246                 RESIZED_WINDOW_BOUNDS.top, RESIZED_WINDOW_BOUNDS.right,
247                 RESIZED_WINDOW_BOUNDS.bottom);
248 
249         assertMetricsValidity(activity);
250 
251         // Move the activity.
252         resizeActivityTask(activity.getComponentName(), MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.left,
253                 MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.top, MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.right,
254                 MOVE_OFFSET + RESIZED_WINDOW_BOUNDS.bottom);
255 
256         assertMetricsValidity(activity);
257     }
258 
assertMetricsMatchesLayout(MetricsActivity activity)259     private static void assertMetricsMatchesLayout(MetricsActivity activity) {
260         final OnLayoutChangeListener listener = activity.mListener;
261         listener.waitForLayout();
262 
263         final WindowMetrics currentMetrics = activity.getWindowManager().getCurrentWindowMetrics();
264         final WindowMetrics maxMetrics = activity.getWindowManager().getMaximumWindowMetrics();
265 
266         Condition.waitFor(new Condition<>("WindowMetrics must match layout metrics",
267                 () -> currentMetrics.getBounds().equals(listener.getLayoutBounds()))
268                 .setRetryIntervalMs(500).setRetryLimit(10)
269                 .setOnFailure(unused -> fail("WindowMetrics must match layout metrics. Layout"
270                         + "bounds is" + listener.getLayoutBounds() + ", while current window"
271                         + "metrics is " + currentMetrics.getBounds())));
272 
273         final boolean isFreeForm = activity.getResources().getConfiguration().windowConfiguration
274                 .getWindowingMode() == WINDOWING_MODE_FREEFORM;
275         WindowMetricsTestHelper.assertMetricsMatchesLayout(currentMetrics, maxMetrics,
276                 listener.getLayoutBounds(), listener.getLayoutInsets(), isFreeForm);
277     }
278 
279     /**
280      * Verifies two scenarios for an {@link Activity}. If the activity is freeform, then the bounds
281      * should not include insets for navigation bar and cutout area.
282      * <ul>
283      *     <li>{@link WindowManager#getCurrentWindowMetrics()} matches
284      *     {@link Display#getSize(Point)}</li>
285      *     <li>{@link WindowManager#getMaximumWindowMetrics()} and {@link Display#getSize(Point)}
286      *     either matches DisplayArea bounds which the {@link Activity} is attached to, or matches
287      *     {@link WindowManager#getCurrentWindowMetrics()} if sandboxing is applied.</li>
288      * </ul>
289      */
assertMetricsValidity(Activity activity)290     private void assertMetricsValidity(Activity activity) {
291         mWmState.computeState(activity.getComponentName());
292         WindowMetricsTestHelper.assertMetricsValidity(activity,
293                 getTaskDisplayAreaBounds(activity.getComponentName()));
294     }
295 
296     /**
297      * Verifies two scenarios for a non-resizable {@link Activity}. Similar to
298      * {@link #assertMetricsValidity(Activity)}, verifies the values of window metrics against
299      * Display size. {@link WindowManager#getMaximumWindowMetrics()} must match activity bounds
300      * if the activity is sandboxed.
301      *
302      * App bounds calculation of a nonresizable activity depends on the orientation of the display
303      * and the app, and the display and activity bounds. Directly compare maximum WindowMetrics
304      * against the value used for sandboxing, since other ways of accessing activity bounds may
305      * have different insets applied.
306      *
307      * @param activity the activity under test
308      */
assertMetricsValidityForNonresizableActivity(Activity activity)309     private void assertMetricsValidityForNonresizableActivity(Activity activity) {
310         ComponentName activityName = activity.getComponentName();
311         mWmState.computeState(activityName);
312         WindowManagerState.Activity activityContainer = mWmState.getActivity(activityName);
313         final boolean boundsShouldIncludeInsets =
314                 activity.getResources().getConfiguration().windowConfiguration
315                         .getWindowingMode() == WINDOWING_MODE_FREEFORM
316                         || activityContainer.inSizeCompatMode();
317         final WindowManager windowManager = activity.getWindowManager();
318         final WindowMetrics currentMetrics = windowManager.getCurrentWindowMetrics();
319         final Rect currentBounds = boundsShouldIncludeInsets ? currentMetrics.getBounds()
320                 : getBoundsExcludingNavigationBarAndCutout(currentMetrics);
321         final Rect maxBounds = windowManager.getMaximumWindowMetrics().getBounds();
322         final Display display = activity.getDisplay();
323 
324         assertBoundsMatchDisplay(maxBounds, currentBounds, display);
325 
326         // Max window bounds should match either DisplayArea bounds, or activity bounds if it is
327         // sandboxed.
328         final Rect displayAreaBounds = getTaskDisplayAreaBounds(activityName);
329         final Rect activityBounds =
330                 activityContainer.mFullConfiguration.windowConfiguration.getBounds();
331         if (maxWindowBoundsSandboxed(displayAreaBounds, maxBounds)) {
332             // Max window bounds are sandboxed, so max window bounds should match activity bounds.
333             assertEquals("Maximum window metrics of a non-resizable activity matches "
334                     + "activity bounds, when sandboxed", activityBounds, maxBounds);
335         } else {
336             // Max window bounds are not sandboxed, so max window bounds should match display area
337             // bounds.
338             assertEquals("Display area bounds must match max window size", displayAreaBounds,
339                     maxBounds);
340         }
341     }
342 
getTaskDisplayAreaBounds(ComponentName activityName)343     private Rect getTaskDisplayAreaBounds(ComponentName activityName) {
344         WindowManagerState.DisplayArea tda = mWmState.getTaskDisplayArea(activityName);
345         return tda.mFullConfiguration.windowConfiguration.getBounds();
346     }
347 
348     public static class MetricsActivity extends FocusableActivity {
349         private WindowMetrics mOnCreateMaximumMetrics;
350         private WindowMetrics mOnCreateCurrentMetrics;
351         private final OnLayoutChangeListener mListener = new OnLayoutChangeListener();
352 
353         @Override
onCreate(Bundle savedInstanceState)354         protected void onCreate(Bundle savedInstanceState) {
355             super.onCreate(savedInstanceState);
356             mOnCreateCurrentMetrics = getWindowManager().getCurrentWindowMetrics();
357             mOnCreateMaximumMetrics = getWindowManager().getMaximumWindowMetrics();
358             getWindow().getDecorView().addOnLayoutChangeListener(mListener);
359 
360             // Always extend the cutout areas because layout doesn't get the waterfall cutout.
361             final WindowManager.LayoutParams attrs = getWindow().getAttributes();
362             attrs.layoutInDisplayCutoutMode = LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
363             getWindow().setAttributes(attrs);
364         }
365 
366         @Override
onDestroy()367         protected void onDestroy() {
368             super.onDestroy();
369             getWindow().getDecorView().removeOnLayoutChangeListener(mListener);
370         }
371     }
372 
373     public static class MinAspectRatioActivity extends MetricsActivity {
374     }
375 }
376