1 /*
2  * Copyright (C) 2016 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.cts;
18 
19 import static android.server.cts.ActivityAndWindowManagersState.DEFAULT_DISPLAY_ID;
20 import static android.server.cts.ActivityManagerState.STATE_STOPPED;
21 
22 import android.server.cts.ActivityManagerState.Activity;
23 import android.server.cts.ActivityManagerState.ActivityStack;
24 import android.server.cts.ActivityManagerState.ActivityTask;
25 
26 import java.awt.Rectangle;
27 import java.lang.Exception;
28 import java.lang.String;
29 import java.util.ArrayList;
30 import java.util.List;
31 import java.util.regex.Matcher;
32 import java.util.regex.Pattern;
33 
34 /**
35  * Build: mmma -j32 cts/hostsidetests/services
36  * Run: cts/hostsidetests/services/activityandwindowmanager/util/run-test CtsServicesHostTestCases android.server.cts.ActivityManagerPinnedStackTests
37  */
38 public class ActivityManagerPinnedStackTests extends ActivityManagerTestBase {
39     private static final String TEST_ACTIVITY = "TestActivity";
40     private static final String TEST_ACTIVITY_WITH_SAME_AFFINITY = "TestActivityWithSameAffinity";
41     private static final String TRANSLUCENT_TEST_ACTIVITY = "TranslucentTestActivity";
42     private static final String NON_RESIZEABLE_ACTIVITY = "NonResizeableActivity";
43     private static final String RESUME_WHILE_PAUSING_ACTIVITY = "ResumeWhilePausingActivity";
44     private static final String PIP_ACTIVITY = "PipActivity";
45     private static final String PIP_ACTIVITY2 = "PipActivity2";
46     private static final String PIP_ACTIVITY_WITH_SAME_AFFINITY = "PipActivityWithSameAffinity";
47     private static final String ALWAYS_FOCUSABLE_PIP_ACTIVITY = "AlwaysFocusablePipActivity";
48     private static final String LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY =
49             "LaunchIntoPinnedStackPipActivity";
50     private static final String LAUNCH_ENTER_PIP_ACTIVITY = "LaunchEnterPipActivity";
51     private static final String PIP_ON_STOP_ACTIVITY = "PipOnStopActivity";
52 
53     private static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation";
54     private static final String EXTRA_ENTER_PIP = "enter_pip";
55     private static final String EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR =
56             "enter_pip_aspect_ratio_numerator";
57     private static final String EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR =
58             "enter_pip_aspect_ratio_denominator";
59     private static final String EXTRA_SET_ASPECT_RATIO_NUMERATOR = "set_aspect_ratio_numerator";
60     private static final String EXTRA_SET_ASPECT_RATIO_DENOMINATOR = "set_aspect_ratio_denominator";
61     private static final String EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR =
62             "set_aspect_ratio_with_delay_numerator";
63     private static final String EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR =
64             "set_aspect_ratio_with_delay_denominator";
65     private static final String EXTRA_ENTER_PIP_ON_PAUSE = "enter_pip_on_pause";
66     private static final String EXTRA_TAP_TO_FINISH = "tap_to_finish";
67     private static final String EXTRA_START_ACTIVITY = "start_activity";
68     private static final String EXTRA_FINISH_SELF_ON_RESUME = "finish_self_on_resume";
69     private static final String EXTRA_REENTER_PIP_ON_EXIT = "reenter_pip_on_exit";
70     private static final String EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP = "assert_no_on_stop_before_pip";
71     private static final String EXTRA_ON_PAUSE_DELAY = "on_pause_delay";
72 
73     private static final String PIP_ACTIVITY_ACTION_ENTER_PIP =
74             "android.server.cts.PipActivity.enter_pip";
75     private static final String PIP_ACTIVITY_ACTION_MOVE_TO_BACK =
76             "android.server.cts.PipActivity.move_to_back";
77     private static final String PIP_ACTIVITY_ACTION_EXPAND_PIP =
78             "android.server.cts.PipActivity.expand_pip";
79     private static final String PIP_ACTIVITY_ACTION_SET_REQUESTED_ORIENTATION =
80             "android.server.cts.PipActivity.set_requested_orientation";
81     private static final String PIP_ACTIVITY_ACTION_FINISH =
82             "android.server.cts.PipActivity.finish";
83     private static final String TEST_ACTIVITY_ACTION_FINISH =
84             "android.server.cts.TestActivity.finish_self";
85 
86     private static final int APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE = 67;
87     private static final int APP_OPS_MODE_ALLOWED = 0;
88     private static final int APP_OPS_MODE_IGNORED = 1;
89     private static final int APP_OPS_MODE_ERRORED = 2;
90 
91     private static final int ROTATION_0 = 0;
92     private static final int ROTATION_90 = 1;
93     private static final int ROTATION_180 = 2;
94     private static final int ROTATION_270 = 3;
95 
96     // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
97     private static final int ORIENTATION_LANDSCAPE = 0;
98     // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
99     private static final int ORIENTATION_PORTRAIT = 1;
100 
101     private static final float FLOAT_COMPARE_EPSILON = 0.005f;
102 
103     // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio
104     private static final int MIN_ASPECT_RATIO_NUMERATOR = 100;
105     private static final int MIN_ASPECT_RATIO_DENOMINATOR = 239;
106     private static final int BELOW_MIN_ASPECT_RATIO_DENOMINATOR = MIN_ASPECT_RATIO_DENOMINATOR + 1;
107     // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio
108     private static final int MAX_ASPECT_RATIO_NUMERATOR = 239;
109     private static final int MAX_ASPECT_RATIO_DENOMINATOR = 100;
110     private static final int ABOVE_MAX_ASPECT_RATIO_NUMERATOR = MAX_ASPECT_RATIO_NUMERATOR + 1;
111 
testMinimumDeviceSize()112     public void testMinimumDeviceSize() throws Exception {
113         if (!supportsPip()) return;
114 
115         mAmWmState.assertDeviceDefaultDisplaySize(mDevice,
116                 "Devices supporting picture-in-picture must be larger than the default minimum"
117                         + " task size");
118     }
119 
testEnterPictureInPictureMode()120     public void testEnterPictureInPictureMode() throws Exception {
121         pinnedStackTester(getAmStartCmd(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"), PIP_ACTIVITY,
122                 false /* moveTopToPinnedStack */, false /* isFocusable */);
123     }
124 
testMoveTopActivityToPinnedStack()125     public void testMoveTopActivityToPinnedStack() throws Exception {
126         pinnedStackTester(getAmStartCmd(PIP_ACTIVITY), PIP_ACTIVITY,
127                 true /* moveTopToPinnedStack */, false /* isFocusable */);
128     }
129 
testAlwaysFocusablePipActivity()130     public void testAlwaysFocusablePipActivity() throws Exception {
131         pinnedStackTester(getAmStartCmd(ALWAYS_FOCUSABLE_PIP_ACTIVITY),
132                 ALWAYS_FOCUSABLE_PIP_ACTIVITY, false /* moveTopToPinnedStack */,
133                 true /* isFocusable */);
134     }
135 
testLaunchIntoPinnedStack()136     public void testLaunchIntoPinnedStack() throws Exception {
137         pinnedStackTester(getAmStartCmd(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY),
138                 ALWAYS_FOCUSABLE_PIP_ACTIVITY, false /* moveTopToPinnedStack */,
139                 true /* isFocusable */);
140     }
141 
testNonTappablePipActivity()142     public void testNonTappablePipActivity() throws Exception {
143         if (!supportsPip()) return;
144 
145         // Launch the tap-to-finish activity at a specific place
146         launchActivity(PIP_ACTIVITY,
147                 EXTRA_ENTER_PIP, "true",
148                 EXTRA_TAP_TO_FINISH, "true");
149         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
150         assertPinnedStackExists();
151 
152         // Tap the screen at a known location in the pinned stack bounds, and ensure that it is
153         // not passed down to the top task
154         tapToFinishPip();
155         mAmWmState.computeState(mDevice, new String[] {PIP_ACTIVITY},
156                 false /* compareTaskAndStackBounds */);
157         mAmWmState.assertVisibility(PIP_ACTIVITY, true);
158     }
159 
testPinnedStackDefaultBounds()160     public void testPinnedStackDefaultBounds() throws Exception {
161         if (!supportsPip()) return;
162 
163         // Launch a PIP activity
164         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
165 
166         setDeviceRotation(ROTATION_0);
167         WindowManagerState wmState = mAmWmState.getWmState();
168         wmState.computeState(mDevice);
169         Rectangle defaultPipBounds = wmState.getDefaultPinnedStackBounds();
170         Rectangle stableBounds = wmState.getStableBounds();
171         assertTrue(defaultPipBounds.width > 0 && defaultPipBounds.height > 0);
172         assertTrue(stableBounds.contains(defaultPipBounds));
173 
174         setDeviceRotation(ROTATION_90);
175         wmState = mAmWmState.getWmState();
176         wmState.computeState(mDevice);
177         defaultPipBounds = wmState.getDefaultPinnedStackBounds();
178         stableBounds = wmState.getStableBounds();
179         assertTrue(defaultPipBounds.width > 0 && defaultPipBounds.height > 0);
180         assertTrue(stableBounds.contains(defaultPipBounds));
181         setDeviceRotation(ROTATION_0);
182     }
183 
testPinnedStackMovementBounds()184     public void testPinnedStackMovementBounds() throws Exception {
185         if (!supportsPip()) return;
186 
187         // Launch a PIP activity
188         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
189 
190         setDeviceRotation(ROTATION_0);
191         WindowManagerState wmState = mAmWmState.getWmState();
192         wmState.computeState(mDevice);
193         Rectangle pipMovementBounds = wmState.getPinnedStackMomentBounds();
194         Rectangle stableBounds = wmState.getStableBounds();
195         assertTrue(pipMovementBounds.width > 0 && pipMovementBounds.height > 0);
196         assertTrue(stableBounds.contains(pipMovementBounds));
197 
198         setDeviceRotation(ROTATION_90);
199         wmState = mAmWmState.getWmState();
200         wmState.computeState(mDevice);
201         pipMovementBounds = wmState.getPinnedStackMomentBounds();
202         stableBounds = wmState.getStableBounds();
203         assertTrue(pipMovementBounds.width > 0 && pipMovementBounds.height > 0);
204         assertTrue(stableBounds.contains(pipMovementBounds));
205         setDeviceRotation(ROTATION_0);
206     }
207 
testPinnedStackOutOfBoundsInsetsNonNegative()208     public void testPinnedStackOutOfBoundsInsetsNonNegative() throws Exception {
209         if (!supportsPip()) return;
210 
211         final WindowManagerState wmState = mAmWmState.getWmState();
212 
213         // Launch an activity into the pinned stack
214         launchActivity(PIP_ACTIVITY,
215                 EXTRA_ENTER_PIP, "true",
216                 EXTRA_TAP_TO_FINISH, "true");
217         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
218 
219         // Get the display dimensions
220         WindowManagerState.WindowState windowState = getWindowState(PIP_ACTIVITY);
221         WindowManagerState.Display display = wmState.getDisplay(windowState.getDisplayId());
222         Rectangle displayRect = display.getDisplayRect();
223 
224         // Move the pinned stack offscreen
225         String moveStackOffscreenCommand = String.format("am stack resize 4 %d %d %d %d",
226                 displayRect.width - 200, 0, displayRect.width + 200, 500);
227         executeShellCommand(moveStackOffscreenCommand);
228 
229         // Ensure that the surface insets are not negative
230         windowState = getWindowState(PIP_ACTIVITY);
231         Rectangle contentInsets = windowState.getContentInsets();
232         assertTrue(contentInsets.x >= 0 && contentInsets.y >= 0 && contentInsets.width >= 0 &&
233                 contentInsets.height >= 0);
234     }
235 
testPinnedStackInBoundsAfterRotation()236     public void testPinnedStackInBoundsAfterRotation() throws Exception {
237         if (!supportsPip()) return;
238 
239         // Launch an activity into the pinned stack
240         launchActivity(PIP_ACTIVITY,
241                 EXTRA_ENTER_PIP, "true",
242                 EXTRA_TAP_TO_FINISH, "true");
243         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
244 
245         // Ensure that the PIP stack is fully visible in each orientation
246         setDeviceRotation(ROTATION_0);
247         assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
248         setDeviceRotation(ROTATION_90);
249         assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
250         setDeviceRotation(ROTATION_180);
251         assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
252         setDeviceRotation(ROTATION_270);
253         assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
254         setDeviceRotation(ROTATION_0);
255     }
256 
testEnterPipToOtherOrientation()257     public void testEnterPipToOtherOrientation() throws Exception {
258         if (!supportsPip()) return;
259 
260         // Launch a portrait only app on the fullscreen stack
261         launchActivity(TEST_ACTIVITY,
262                 EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT));
263         // Launch the PiP activity fixed as landscape
264         launchActivity(PIP_ACTIVITY,
265                 EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_LANDSCAPE));
266         // Enter PiP, and assert that the PiP is within bounds now that the device is back in
267         // portrait
268         executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_ENTER_PIP);
269         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
270         assertPinnedStackExists();
271         assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
272     }
273 
testEnterPipAspectRatioMin()274     public void testEnterPipAspectRatioMin() throws Exception {
275         testEnterPipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR);
276     }
277 
testEnterPipAspectRatioMax()278     public void testEnterPipAspectRatioMax() throws Exception {
279         testEnterPipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
280     }
281 
testEnterPipAspectRatio(int num, int denom)282     private void testEnterPipAspectRatio(int num, int denom) throws Exception {
283         if (!supportsPip()) return;
284 
285         launchActivity(PIP_ACTIVITY,
286                 EXTRA_ENTER_PIP, "true",
287                 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
288                 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
289         assertPinnedStackExists();
290 
291         // Assert that we have entered PIP and that the aspect ratio is correct
292         Rectangle pinnedStackBounds =
293                 mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds();
294         assertTrue(floatEquals((float) pinnedStackBounds.width / pinnedStackBounds.height,
295                 (float) num / denom));
296     }
297 
testResizePipAspectRatioMin()298     public void testResizePipAspectRatioMin() throws Exception {
299         testResizePipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR);
300     }
301 
testResizePipAspectRatioMax()302     public void testResizePipAspectRatioMax() throws Exception {
303         testResizePipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
304     }
305 
testResizePipAspectRatio(int num, int denom)306     private void testResizePipAspectRatio(int num, int denom) throws Exception {
307         if (!supportsPip()) return;
308 
309         launchActivity(PIP_ACTIVITY,
310                 EXTRA_ENTER_PIP, "true",
311                 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
312                 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
313         assertPinnedStackExists();
314 
315         // Hacky, but we need to wait for the enterPictureInPicture animation to complete and
316         // the resize to be called before we can check the pinned stack bounds
317         final boolean[] result = new boolean[1];
318         mAmWmState.waitForWithAmState(mDevice, (state) -> {
319             Rectangle pinnedStackBounds = state.getStackById(PINNED_STACK_ID).getBounds();
320             boolean isValidAspectRatio = floatEquals(
321                     (float) pinnedStackBounds.width / pinnedStackBounds.height,
322                     (float) num / denom);
323             result[0] = isValidAspectRatio;
324             return isValidAspectRatio;
325         }, "Waiting for pinned stack to be resized");
326         assertTrue(result[0]);
327     }
328 
testEnterPipExtremeAspectRatioMin()329     public void testEnterPipExtremeAspectRatioMin() throws Exception {
330         testEnterPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR,
331                 BELOW_MIN_ASPECT_RATIO_DENOMINATOR);
332     }
333 
testEnterPipExtremeAspectRatioMax()334     public void testEnterPipExtremeAspectRatioMax() throws Exception {
335         testEnterPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR,
336                 MAX_ASPECT_RATIO_DENOMINATOR);
337     }
338 
testEnterPipExtremeAspectRatio(int num, int denom)339     private void testEnterPipExtremeAspectRatio(int num, int denom) throws Exception {
340         if (!supportsPip()) return;
341 
342         // Assert that we could not create a pinned stack with an extreme aspect ratio
343         launchActivity(PIP_ACTIVITY,
344                 EXTRA_ENTER_PIP, "true",
345                 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
346                 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
347         assertPinnedStackDoesNotExist();
348     }
349 
testSetPipExtremeAspectRatioMin()350     public void testSetPipExtremeAspectRatioMin() throws Exception {
351         testSetPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR,
352                 BELOW_MIN_ASPECT_RATIO_DENOMINATOR);
353     }
354 
testSetPipExtremeAspectRatioMax()355     public void testSetPipExtremeAspectRatioMax() throws Exception {
356         testSetPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR,
357                 MAX_ASPECT_RATIO_DENOMINATOR);
358     }
359 
testSetPipExtremeAspectRatio(int num, int denom)360     private void testSetPipExtremeAspectRatio(int num, int denom) throws Exception {
361         if (!supportsPip()) return;
362 
363         // Try to resize the a normal pinned stack to an extreme aspect ratio and ensure that
364         // fails (the aspect ratio remains the same)
365         launchActivity(PIP_ACTIVITY,
366                 EXTRA_ENTER_PIP, "true",
367                 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR,
368                         Integer.toString(MAX_ASPECT_RATIO_NUMERATOR),
369                 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR,
370                         Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR),
371                 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
372                 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
373         assertPinnedStackExists();
374         Rectangle pinnedStackBounds =
375                 mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds();
376         assertTrue(floatEquals((float) pinnedStackBounds.width / pinnedStackBounds.height,
377                 (float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR));
378     }
379 
testDisallowPipLaunchFromStoppedActivity()380     public void testDisallowPipLaunchFromStoppedActivity() throws Exception {
381         if (!supportsPip()) return;
382 
383         // Launch the bottom pip activity
384         launchActivity(PIP_ON_STOP_ACTIVITY);
385         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
386 
387         // Wait for the bottom pip activity to be stopped
388         mAmWmState.waitForActivityState(mDevice, PIP_ON_STOP_ACTIVITY, STATE_STOPPED);
389 
390         // Assert that there is no pinned stack (that enterPictureInPicture() failed)
391         assertPinnedStackDoesNotExist();
392     }
393 
testAutoEnterPictureInPicture()394     public void testAutoEnterPictureInPicture() throws Exception {
395         if (!supportsPip()) return;
396 
397         // Launch a test activity so that we're not over home
398         launchActivity(TEST_ACTIVITY);
399 
400         // Launch the PIP activity on pause
401         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
402         assertPinnedStackDoesNotExist();
403 
404         // Go home and ensure that there is a pinned stack
405         launchHomeActivity();
406         assertPinnedStackExists();
407     }
408 
testAutoEnterPictureInPictureLaunchActivity()409     public void testAutoEnterPictureInPictureLaunchActivity() throws Exception {
410         if (!supportsPip()) return;
411 
412         // Launch a test activity so that we're not over home
413         launchActivity(TEST_ACTIVITY);
414 
415         // Launch the PIP activity on pause, and have it start another activity on
416         // top of itself.  Wait for the new activity to be visible and ensure that the pinned stack
417         // was not created in the process
418         launchActivity(PIP_ACTIVITY,
419                 EXTRA_ENTER_PIP_ON_PAUSE, "true",
420                 EXTRA_START_ACTIVITY, getActivityComponentName(NON_RESIZEABLE_ACTIVITY));
421         mAmWmState.computeState(mDevice, new String[] {NON_RESIZEABLE_ACTIVITY},
422                 false /* compareTaskAndStackBounds */);
423         assertPinnedStackDoesNotExist();
424 
425         // Go home while the pip activity is open and ensure the previous activity is not PIPed
426         launchHomeActivity();
427         assertPinnedStackDoesNotExist();
428     }
429 
testAutoEnterPictureInPictureFinish()430     public void testAutoEnterPictureInPictureFinish() throws Exception {
431         if (!supportsPip()) return;
432 
433         // Launch a test activity so that we're not over home
434         launchActivity(TEST_ACTIVITY);
435 
436         // Launch the PIP activity on pause, and set it to finish itself after
437         // some period.  Wait for the previous activity to be visible, and ensure that the pinned
438         // stack was not created in the process
439         launchActivity(PIP_ACTIVITY,
440                 EXTRA_ENTER_PIP_ON_PAUSE, "true",
441                 EXTRA_FINISH_SELF_ON_RESUME, "true");
442         assertPinnedStackDoesNotExist();
443     }
444 
testAutoEnterPictureInPictureAspectRatio()445     public void testAutoEnterPictureInPictureAspectRatio() throws Exception {
446         if (!supportsPip()) return;
447 
448         // Launch the PIP activity on pause, and set the aspect ratio
449         launchActivity(PIP_ACTIVITY,
450                 EXTRA_ENTER_PIP_ON_PAUSE, "true",
451                 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(MAX_ASPECT_RATIO_NUMERATOR),
452                 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR));
453 
454         // Go home while the pip activity is open to trigger auto-PIP
455         launchHomeActivity();
456         assertPinnedStackExists();
457 
458         // Hacky, but we need to wait for the auto-enter picture-in-picture animation to complete
459         // and before we can check the pinned stack bounds
460         final boolean[] result = new boolean[1];
461         mAmWmState.waitForWithAmState(mDevice, (state) -> {
462             Rectangle pinnedStackBounds = state.getStackById(PINNED_STACK_ID).getBounds();
463             boolean isValidAspectRatio = floatEquals(
464                     (float) pinnedStackBounds.width / pinnedStackBounds.height,
465                     (float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR);
466             result[0] = isValidAspectRatio;
467             return isValidAspectRatio;
468         }, "Waiting for pinned stack to be resized");
469         assertTrue(result[0]);
470     }
471 
testAutoEnterPictureInPictureOverPip()472     public void testAutoEnterPictureInPictureOverPip() throws Exception {
473         if (!supportsPip()) return;
474 
475         // Launch another PIP activity
476         launchActivity(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY);
477         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
478         assertPinnedStackExists();
479 
480         // Launch the PIP activity on pause
481         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
482 
483         // Go home while the PIP activity is open to trigger auto-enter PIP
484         launchHomeActivity();
485         assertPinnedStackExists();
486 
487         // Ensure that auto-enter pip failed and that the resumed activity in the pinned stack is
488         // still the first activity
489         final ActivityStack pinnedStack = mAmWmState.getAmState().getStackById(PINNED_STACK_ID);
490         assertTrue(pinnedStack.getTasks().size() == 1);
491         assertTrue(pinnedStack.getTasks().get(0).mRealActivity.equals(getActivityComponentName(
492                 ALWAYS_FOCUSABLE_PIP_ACTIVITY)));
493     }
494 
testDisallowMultipleTasksInPinnedStack()495     public void testDisallowMultipleTasksInPinnedStack() throws Exception {
496         if (!supportsPip()) return;
497 
498         // Launch a test activity so that we have multiple fullscreen tasks
499         launchActivity(TEST_ACTIVITY);
500 
501         // Launch first PIP activity
502         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
503 
504         // Launch second PIP activity
505         launchActivity(PIP_ACTIVITY2, EXTRA_ENTER_PIP, "true");
506 
507         final ActivityStack pinnedStack = mAmWmState.getAmState().getStackById(PINNED_STACK_ID);
508         assertEquals(1, pinnedStack.getTasks().size());
509 
510         assertTrue(pinnedStack.getTasks().get(0).mRealActivity.equals(getActivityComponentName(
511                 PIP_ACTIVITY2)));
512 
513         final ActivityStack fullScreenStack = mAmWmState.getAmState().getStackById(
514                 FULLSCREEN_WORKSPACE_STACK_ID);
515         assertTrue(fullScreenStack.getBottomTask().mRealActivity.equals(getActivityComponentName(
516                 PIP_ACTIVITY)));
517     }
518 
testPipUnPipOverHome()519     public void testPipUnPipOverHome() throws Exception {
520         if (!supportsPip()) return;
521 
522         // Go home
523         launchHomeActivity();
524         // Launch an auto pip activity
525         launchActivity(PIP_ACTIVITY,
526                 EXTRA_ENTER_PIP, "true",
527                 EXTRA_REENTER_PIP_ON_EXIT, "true");
528         assertPinnedStackExists();
529 
530         // Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip
531         launchActivity(PIP_ACTIVITY);
532         mAmWmState.waitForWithAmState(mDevice, (amState) -> {
533             return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == FULLSCREEN_WORKSPACE_STACK_ID;
534         }, "Waiting for PIP to exit to fullscreen");
535         mAmWmState.waitForWithAmState(mDevice, (amState) -> {
536             return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == PINNED_STACK_ID;
537         }, "Waiting to re-enter PIP");
538         mAmWmState.assertFocusedStack("Expected home stack focused", HOME_STACK_ID);
539     }
540 
testPipUnPipOverApp()541     public void testPipUnPipOverApp() throws Exception {
542         if (!supportsPip()) return;
543 
544         // Launch a test activity so that we're not over home
545         launchActivity(TEST_ACTIVITY);
546 
547         // Launch an auto pip activity
548         launchActivity(PIP_ACTIVITY,
549                 EXTRA_ENTER_PIP, "true",
550                 EXTRA_REENTER_PIP_ON_EXIT, "true");
551         assertPinnedStackExists();
552 
553         // Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip
554         launchActivity(PIP_ACTIVITY);
555         mAmWmState.waitForWithAmState(mDevice, (amState) -> {
556             return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == FULLSCREEN_WORKSPACE_STACK_ID;
557         }, "Waiting for PIP to exit to fullscreen");
558         mAmWmState.waitForWithAmState(mDevice, (amState) -> {
559             return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == PINNED_STACK_ID;
560         }, "Waiting to re-enter PIP");
561         mAmWmState.assertFocusedStack("Expected fullscreen stack focused",
562                 FULLSCREEN_WORKSPACE_STACK_ID);
563     }
564 
testRemovePipWithNoFullscreenStack()565     public void testRemovePipWithNoFullscreenStack() throws Exception {
566         if (!supportsPip()) return;
567 
568         // Start with a clean slate, remove all the stacks but home
569         removeStacks(ALL_STACK_IDS_BUT_HOME);
570 
571         // Launch a pip activity
572         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
573         assertPinnedStackExists();
574 
575         // Remove the stack and ensure that the task is now in the fullscreen stack (when no
576         // fullscreen stack existed before)
577         removeStacks(PINNED_STACK_ID);
578         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID,
579                 true /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
580     }
581 
testRemovePipWithVisibleFullscreenStack()582     public void testRemovePipWithVisibleFullscreenStack() throws Exception {
583         if (!supportsPip()) return;
584 
585         // Launch a fullscreen activity, and a pip activity over that
586         launchActivity(TEST_ACTIVITY);
587         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
588         assertPinnedStackExists();
589 
590         // Remove the stack and ensure that the task is placed in the fullscreen stack, behind the
591         // top fullscreen activity
592         removeStacks(PINNED_STACK_ID);
593         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID,
594                 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
595     }
596 
testRemovePipWithHiddenFullscreenStack()597     public void testRemovePipWithHiddenFullscreenStack() throws Exception {
598         if (!supportsPip()) return;
599 
600         // Launch a fullscreen activity, return home and while the fullscreen stack is hidden,
601         // launch a pip activity over home
602         launchActivity(TEST_ACTIVITY);
603         launchHomeActivity();
604         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
605         assertPinnedStackExists();
606 
607         // Remove the stack and ensure that the task is placed on top of the hidden fullscreen
608         // stack, but that the home stack is still focused
609         removeStacks(PINNED_STACK_ID);
610         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID,
611                 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
612     }
613 
testMovePipToBackWithNoFullscreenStack()614     public void testMovePipToBackWithNoFullscreenStack() throws Exception {
615         if (!supportsPip()) return;
616 
617         // Start with a clean slate, remove all the stacks but home
618         removeStacks(ALL_STACK_IDS_BUT_HOME);
619 
620         // Launch a pip activity
621         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
622         assertPinnedStackExists();
623 
624         // Remove the stack and ensure that the task is now in the fullscreen stack (when no
625         // fullscreen stack existed before)
626         executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK);
627         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID,
628                 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
629     }
630 
testMovePipToBackWithVisibleFullscreenStack()631     public void testMovePipToBackWithVisibleFullscreenStack() throws Exception {
632         if (!supportsPip()) return;
633 
634         // Launch a fullscreen activity, and a pip activity over that
635         launchActivity(TEST_ACTIVITY);
636         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
637         assertPinnedStackExists();
638 
639         // Remove the stack and ensure that the task is placed in the fullscreen stack, behind the
640         // top fullscreen activity
641         executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK);
642         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID,
643                 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
644     }
645 
testMovePipToBackWithHiddenFullscreenStack()646     public void testMovePipToBackWithHiddenFullscreenStack() throws Exception {
647         if (!supportsPip()) return;
648 
649         // Launch a fullscreen activity, return home and while the fullscreen stack is hidden,
650         // launch a pip activity over home
651         launchActivity(TEST_ACTIVITY);
652         launchHomeActivity();
653         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
654         assertPinnedStackExists();
655 
656         // Remove the stack and ensure that the task is placed on top of the hidden fullscreen
657         // stack, but that the home stack is still focused
658         executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK);
659         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID,
660                 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
661     }
662 
testPinnedStackAlwaysOnTop()663     public void testPinnedStackAlwaysOnTop() throws Exception {
664         if (!supportsPip()) return;
665 
666         // Launch activity into pinned stack and assert it's on top.
667         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
668         assertPinnedStackExists();
669         assertPinnedStackIsOnTop();
670 
671         // Launch another activity in fullscreen stack and check that pinned stack is still on top.
672         launchActivity(TEST_ACTIVITY);
673         assertPinnedStackExists();
674         assertPinnedStackIsOnTop();
675 
676         // Launch home and check that pinned stack is still on top.
677         launchHomeActivity();
678         assertPinnedStackExists();
679         assertPinnedStackIsOnTop();
680     }
681 
testAppOpsDenyPipOnPause()682     public void testAppOpsDenyPipOnPause() throws Exception {
683         if (!supportsPip()) return;
684 
685         // Disable enter-pip and try to enter pip
686         setAppOpsOpToMode(ActivityManagerTestBase.componentName,
687                 APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE, APP_OPS_MODE_IGNORED);
688 
689         // Launch the PIP activity on pause
690         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
691         assertPinnedStackDoesNotExist();
692 
693         // Go home and ensure that there is no pinned stack
694         launchHomeActivity();
695         assertPinnedStackDoesNotExist();
696 
697         // Re-enable enter-pip-on-hide
698         setAppOpsOpToMode(ActivityManagerTestBase.componentName,
699                 APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE, APP_OPS_MODE_ALLOWED);
700     }
701 
testEnterPipFromTaskWithMultipleActivities()702     public void testEnterPipFromTaskWithMultipleActivities() throws Exception {
703         if (!supportsPip()) return;
704 
705         // Try to enter picture-in-picture from an activity that has more than one activity in the
706         // task and ensure that it works
707         launchActivity(LAUNCH_ENTER_PIP_ACTIVITY);
708         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
709         assertPinnedStackExists();
710     }
711 
testEnterPipWithResumeWhilePausingActivityNoStop()712     public void testEnterPipWithResumeWhilePausingActivityNoStop() throws Exception {
713         if (!supportsPip()) return;
714 
715         /*
716          * Launch the resumeWhilePausing activity and ensure that the PiP activity did not get
717          * stopped and actually went into the pinned stack.
718          *
719          * Note that this is a workaround because to trigger the path that we want to happen in
720          * activity manager, we need to add the leaving activity to the stopping state, which only
721          * happens when a hidden stack is brought forward. Normally, this happens when you go home,
722          * but since we can't launch into the home stack directly, we have a workaround.
723          *
724          * 1) Launch an activity in a new dynamic stack
725          * 2) Resize the dynamic stack to non-fullscreen bounds
726          * 3) Start the PiP activity that will enter picture-in-picture when paused in the
727          *    fullscreen stack
728          * 4) Bring the activity in the dynamic stack forward to trigger PiP
729          */
730         int stackId = launchActivityInNewDynamicStack(RESUME_WHILE_PAUSING_ACTIVITY);
731         resizeStack(stackId, 0, 0, 500, 500);
732         // Launch an activity that will enter PiP when it is paused with a delay that is long enough
733         // for the next resumeWhilePausing activity to finish resuming, but slow enough to not
734         // trigger the current system pause timeout (currently 500ms)
735         launchActivityInStack(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID,
736                 EXTRA_ENTER_PIP_ON_PAUSE, "true",
737                 EXTRA_ON_PAUSE_DELAY, "350",
738                 EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true");
739         launchActivity(RESUME_WHILE_PAUSING_ACTIVITY);
740         assertPinnedStackExists();
741     }
742 
testDisallowEnterPipActivityLocked()743     public void testDisallowEnterPipActivityLocked() throws Exception {
744         if (!supportsPip()) return;
745 
746         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
747         ActivityTask task =
748                 mAmWmState.getAmState().getStackById(FULLSCREEN_WORKSPACE_STACK_ID).getTopTask();
749 
750         // Lock the task and ensure that we can't enter picture-in-picture both explicitly and
751         // when paused
752         executeShellCommand("am task lock " + task.mTaskId);
753         executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_ENTER_PIP);
754         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
755         assertPinnedStackDoesNotExist();
756         launchHomeActivity();
757         assertPinnedStackDoesNotExist();
758         executeShellCommand("am task lock stop");
759     }
760 
testConfigurationChangeOrderDuringTransition()761     public void testConfigurationChangeOrderDuringTransition() throws Exception {
762         if (!supportsPip()) return;
763 
764         // Launch a PiP activity and ensure configuration change only happened once, and that the
765         // configuration change happened after the picture-in-picture and multi-window callbacks
766         launchActivity(PIP_ACTIVITY);
767         String logSeparator = clearLogcat();
768         executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_ENTER_PIP);
769         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
770         assertPinnedStackExists();
771         assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, logSeparator);
772 
773         // Trigger it to go back to fullscreen and ensure that only triggered one configuration
774         // change as well
775         logSeparator = clearLogcat();
776         launchActivity(PIP_ACTIVITY);
777         assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, logSeparator);
778     }
779 
testStopBeforeMultiWindowCallbacksOnDismiss()780     public void testStopBeforeMultiWindowCallbacksOnDismiss() throws Exception {
781         if (!supportsPip()) return;
782 
783         // Launch a PiP activity
784         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
785         assertPinnedStackExists();
786 
787         // Dismiss it
788         String logSeparator = clearLogcat();
789         removeStacks(PINNED_STACK_ID);
790         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
791 
792         // Confirm that we get stop before the multi-window and picture-in-picture mode change
793         // callbacks
794         final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY,
795                 logSeparator);
796         if (lifecycleCounts.mStopCount != 1) {
797             fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mStopCount
798                     + " onStop() calls, expecting 1");
799         } else if (lifecycleCounts.mPictureInPictureModeChangedCount != 1) {
800             fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mPictureInPictureModeChangedCount
801                     + " onPictureInPictureModeChanged() calls, expecting 1");
802         } else if (lifecycleCounts.mMultiWindowModeChangedCount != 1) {
803             fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mMultiWindowModeChangedCount
804                     + " onMultiWindowModeChanged() calls, expecting 1");
805         } else {
806             int lastStopLine = lifecycleCounts.mLastStopLineIndex;
807             int lastPipLine = lifecycleCounts.mLastPictureInPictureModeChangedLineIndex;
808             int lastMwLine = lifecycleCounts.mLastMultiWindowModeChangedLineIndex;
809             if (!(lastStopLine < lastPipLine && lastPipLine < lastMwLine)) {
810                 fail(PIP_ACTIVITY + " has received callbacks in unexpected order.  Expected:"
811                         + " stop < pip < mw, but got line indices: " + lastStopLine + ", "
812                         + lastPipLine + ", " + lastMwLine + " respectively");
813             }
814         }
815     }
816 
testPreventSetAspectRatioWhileExpanding()817     public void testPreventSetAspectRatioWhileExpanding() throws Exception {
818         if (!supportsPip()) return;
819 
820         // Launch the PiP activity
821         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
822 
823         // Trigger it to go back to fullscreen and try to set the aspect ratio, and ensure that the
824         // call to set the aspect ratio did not prevent the PiP from returning to fullscreen
825         executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_EXPAND_PIP
826                 + " -e " + EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR + " 123456789"
827                 + " -e " + EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR + " 100000000");
828         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
829         assertPinnedStackDoesNotExist();
830     }
831 
testSetRequestedOrientationWhilePinned()832     public void testSetRequestedOrientationWhilePinned() throws Exception {
833         if (!supportsPip()) return;
834 
835         // Launch the PiP activity fixed as portrait, and enter picture-in-picture
836         launchActivity(PIP_ACTIVITY,
837                 EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT),
838                 EXTRA_ENTER_PIP, "true");
839         assertPinnedStackExists();
840 
841         // Request that the orientation is set to landscape
842         executeShellCommand("am broadcast -a "
843                 + PIP_ACTIVITY_ACTION_SET_REQUESTED_ORIENTATION + " -e "
844                 + EXTRA_FIXED_ORIENTATION + " " + String.valueOf(ORIENTATION_LANDSCAPE));
845 
846         // Launch the activity back into fullscreen and ensure that it is now in landscape
847         launchActivity(PIP_ACTIVITY);
848         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
849         assertPinnedStackDoesNotExist();
850         assertTrue(mAmWmState.getWmState().getLastOrientation() == ORIENTATION_LANDSCAPE);
851     }
852 
testWindowButtonEntersPip()853     public void testWindowButtonEntersPip() throws Exception {
854         if (!supportsPip()) return;
855 
856         // Launch the PiP activity trigger the window button, ensure that we have entered PiP
857         launchActivity(PIP_ACTIVITY);
858         pressWindowButton();
859         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
860         assertPinnedStackExists();
861     }
862 
testFinishPipActivityWithTaskOverlay()863     public void testFinishPipActivityWithTaskOverlay() throws Exception {
864         if (!supportsPip()) return;
865 
866         // Launch PiP activity
867         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
868         assertPinnedStackExists();
869         int taskId = mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getTopTask().mTaskId;
870 
871         // Ensure that we don't any any other overlays as a result of launching into PIP
872         launchHomeActivity();
873 
874         // Launch task overlay activity into PiP activity task
875         launchActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId, PINNED_STACK_ID);
876 
877         // Finish the PiP activity and ensure that there is no pinned stack
878         executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_FINISH);
879         mAmWmState.waitForWithAmState(mDevice, (amState) -> {
880             ActivityStack stack = amState.getStackById(PINNED_STACK_ID);
881             return stack == null;
882         }, "Waiting for pinned stack to be removed...");
883         assertPinnedStackDoesNotExist();
884     }
885 
testNoResumeAfterTaskOverlayFinishes()886     public void testNoResumeAfterTaskOverlayFinishes() throws Exception {
887         if (!supportsPip()) return;
888 
889         // Launch PiP activity
890         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
891         assertPinnedStackExists();
892         int taskId = mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getTopTask().mTaskId;
893 
894         // Launch task overlay activity into PiP activity task
895         launchActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId, PINNED_STACK_ID);
896 
897         // Finish the task overlay activity while animating and ensure that the PiP activity never
898         // got resumed
899         String logSeparator = clearLogcat();
900         executeShellCommand("am stack resize-animated 4 20 20 500 500");
901         executeShellCommand("am broadcast -a " + TEST_ACTIVITY_ACTION_FINISH);
902         mAmWmState.waitFor(mDevice, (amState, wmState) -> !amState.containsActivity(
903                 TRANSLUCENT_TEST_ACTIVITY), "Waiting for test activity to finish...");
904         final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY,
905                 logSeparator);
906         assertTrue(lifecycleCounts.mResumeCount == 0);
907         assertTrue(lifecycleCounts.mPauseCount == 0);
908     }
909 
testPinnedStackWithDockedStack()910     public void testPinnedStackWithDockedStack() throws Exception {
911         if (!supportsPip() || !supportsSplitScreenMultiWindow()) return;
912 
913         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
914         launchActivityInDockStack(LAUNCHING_ACTIVITY);
915         launchActivityToSide(true, false, TEST_ACTIVITY);
916         mAmWmState.assertVisibility(PIP_ACTIVITY, true);
917         mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
918         mAmWmState.assertVisibility(TEST_ACTIVITY, true);
919 
920         // Launch the activities again to take focus and make sure nothing is hidden
921         launchActivityInDockStack(LAUNCHING_ACTIVITY);
922         mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
923         mAmWmState.assertVisibility(TEST_ACTIVITY, true);
924 
925         launchActivityToSide(true, false, TEST_ACTIVITY);
926         mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
927         mAmWmState.assertVisibility(TEST_ACTIVITY, true);
928 
929         // Go to recents to make sure that fullscreen stack is invisible
930         // Some devices do not support recents or implement it differently (instead of using a
931         // separate stack id or as an activity), for those cases the visibility asserts will be
932         // ignored
933         pressAppSwitchButton();
934         if (mAmWmState.waitForRecentsActivityVisible(mDevice)) {
935             mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
936             mAmWmState.assertVisibility(TEST_ACTIVITY, false);
937         }
938     }
939 
testLaunchTaskByComponentMatchMultipleTasks()940     public void testLaunchTaskByComponentMatchMultipleTasks() throws Exception {
941         if (!supportsPip()) return;
942 
943         // Launch a fullscreen activity which will launch a PiP activity in a new task with the same
944         // affinity
945         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
946         launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY);
947         assertPinnedStackExists();
948 
949         // Launch the root activity again...
950         int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivityName(
951                 TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId;
952         launchHomeActivity();
953         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
954 
955         // ...and ensure that the root activity task is found and reused, and that the pinned stack
956         // is unaffected
957         assertPinnedStackExists();
958         mAmWmState.assertFocusedActivity("Expected root activity focused",
959                 TEST_ACTIVITY_WITH_SAME_AFFINITY);
960         assertTrue(rootActivityTaskId == mAmWmState.getAmState().getTaskByActivityName(
961                 TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId);
962     }
963 
testLaunchTaskByAffinityMatchMultipleTasks()964     public void testLaunchTaskByAffinityMatchMultipleTasks() throws Exception {
965         if (!supportsPip()) return;
966 
967         // Launch a fullscreen activity which will launch a PiP activity in a new task with the same
968         // affinity, and also launch another activity in the same task, while finishing itself. As
969         // a result, the task will not have a component matching the same activity as what it was
970         // started with
971         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY,
972                 EXTRA_START_ACTIVITY, getActivityComponentName(TEST_ACTIVITY),
973                 EXTRA_FINISH_SELF_ON_RESUME, "true");
974         mAmWmState.waitForValidState(mDevice, TEST_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
975         launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY);
976         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY_WITH_SAME_AFFINITY, PINNED_STACK_ID);
977         assertPinnedStackExists();
978 
979         // Launch the root activity again...
980         int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivityName(
981                 TEST_ACTIVITY).mTaskId;
982         launchHomeActivity();
983         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
984 
985         // ...and ensure that even while matching purely by task affinity, the root activity task is
986         // found and reused, and that the pinned stack is unaffected
987         assertPinnedStackExists();
988         mAmWmState.assertFocusedActivity("Expected root activity focused", TEST_ACTIVITY);
989         assertTrue(rootActivityTaskId == mAmWmState.getAmState().getTaskByActivityName(
990                 TEST_ACTIVITY).mTaskId);
991     }
992 
testLaunchTaskByAffinityMatchSingleTask()993     public void testLaunchTaskByAffinityMatchSingleTask() throws Exception {
994         if (!supportsPip()) return;
995 
996         // Launch an activity into the pinned stack with a fixed affinity
997         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY,
998                 EXTRA_ENTER_PIP, "true",
999                 EXTRA_START_ACTIVITY, getActivityComponentName(PIP_ACTIVITY),
1000                 EXTRA_FINISH_SELF_ON_RESUME, "true");
1001         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
1002         assertPinnedStackExists();
1003 
1004         // Launch the root activity again, of the matching task and ensure that we expand to
1005         // fullscreen
1006         int activityTaskId = mAmWmState.getAmState().getTaskByActivityName(
1007                 PIP_ACTIVITY).mTaskId;
1008         launchHomeActivity();
1009         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
1010         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
1011         assertPinnedStackDoesNotExist();
1012         assertTrue(activityTaskId == mAmWmState.getAmState().getTaskByActivityName(
1013                 PIP_ACTIVITY).mTaskId);
1014     }
1015 
1016     /** Test that reported display size corresponds to fullscreen after exiting PiP. */
testDisplayMetricsPinUnpin()1017     public void testDisplayMetricsPinUnpin() throws Exception {
1018         String logSeparator = clearLogcat();
1019         launchActivity(TEST_ACTIVITY);
1020         final int defaultDisplayStackId = mAmWmState.getAmState().getFocusedStackId();
1021         final ReportedSizes initialSizes = getLastReportedSizesForActivity(TEST_ACTIVITY,
1022                 logSeparator);
1023         final Rectangle initialAppBounds = readAppBounds(TEST_ACTIVITY, logSeparator);
1024         assertNotNull("Must report display dimensions", initialSizes);
1025         assertNotNull("Must report app bounds", initialAppBounds);
1026 
1027         logSeparator = clearLogcat();
1028         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
1029         mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
1030         final ReportedSizes pinnedSizes = getLastReportedSizesForActivity(PIP_ACTIVITY,
1031                 logSeparator);
1032         final Rectangle pinnedAppBounds = readAppBounds(PIP_ACTIVITY, logSeparator);
1033         assertFalse("Reported display size when pinned must be different from default",
1034                 initialSizes.equals(pinnedSizes));
1035         assertFalse("Reported app bounds when pinned must be different from default",
1036                 initialAppBounds.width == pinnedAppBounds.width
1037                         && initialAppBounds.height == pinnedAppBounds.height);
1038 
1039         logSeparator = clearLogcat();
1040         launchActivityInStack(PIP_ACTIVITY, defaultDisplayStackId);
1041         final ReportedSizes finalSizes = getLastReportedSizesForActivity(PIP_ACTIVITY,
1042                 logSeparator);
1043         final Rectangle finalAppBounds = readAppBounds(PIP_ACTIVITY, logSeparator);
1044         assertEquals("Must report default size after exiting PiP", initialSizes, finalSizes);
1045         assertEquals("Must report default app width after exiting PiP", initialAppBounds.width,
1046                 finalAppBounds.width);
1047         assertEquals("Must report default app height after exiting PiP", initialAppBounds.height,
1048                 finalAppBounds.height);
1049     }
1050 
1051     private static final Pattern sAppBoundsPattern = Pattern.compile(
1052             "(.+)appBounds=Rect\\((\\d+), (\\d+) - (\\d+), (\\d+)\\)(.*)");
1053 
1054     /** Read app bounds in last applied configuration from logs. */
readAppBounds(String activityName, String logSeparator)1055     private Rectangle readAppBounds(String activityName, String logSeparator) throws Exception {
1056         final String[] lines = getDeviceLogsForComponent(activityName, logSeparator);
1057         for (int i = lines.length - 1; i >= 0; i--) {
1058             final String line = lines[i].trim();
1059             final Matcher matcher = sAppBoundsPattern.matcher(line);
1060             if (matcher.matches()) {
1061                 final int left = Integer.parseInt(matcher.group(2));
1062                 final int top = Integer.parseInt(matcher.group(3));
1063                 final int right = Integer.parseInt(matcher.group(4));
1064                 final int bottom = Integer.parseInt(matcher.group(5));
1065                 return new Rectangle(left, top, right - left, bottom - top);
1066             }
1067         }
1068         return null;
1069     }
1070 
1071     /**
1072      * Called after the given {@param activityName} has been moved to the fullscreen stack. Ensures
1073      * that the {@param focusedStackId} is focused, and checks the top and/or bottom tasks in the
1074      * fullscreen stack if {@param expectTopTaskHasActivity} or {@param expectBottomTaskHasActivity}
1075      * are set respectively.
1076      */
assertPinnedStackStateOnMoveToFullscreen(String activityName, int focusedStackId, boolean expectTopTaskHasActivity, boolean expectBottomTaskHasActivity)1077     private void assertPinnedStackStateOnMoveToFullscreen(String activityName, int focusedStackId,
1078             boolean expectTopTaskHasActivity, boolean expectBottomTaskHasActivity)
1079                     throws Exception {
1080         mAmWmState.waitForFocusedStack(mDevice, focusedStackId);
1081         mAmWmState.assertFocusedStack("Wrong focused stack", focusedStackId);
1082         mAmWmState.waitForActivityState(mDevice, activityName, STATE_STOPPED);
1083         assertTrue(mAmWmState.getAmState().hasActivityState(activityName, STATE_STOPPED));
1084         assertPinnedStackDoesNotExist();
1085 
1086         if (expectTopTaskHasActivity) {
1087             ActivityTask topTask = mAmWmState.getAmState().getStackById(
1088                     FULLSCREEN_WORKSPACE_STACK_ID).getTopTask();
1089             assertTrue(topTask.containsActivity(ActivityManagerTestBase.getActivityComponentName(
1090                     activityName)));
1091         }
1092         if (expectBottomTaskHasActivity) {
1093             ActivityTask bottomTask = mAmWmState.getAmState().getStackById(
1094                     FULLSCREEN_WORKSPACE_STACK_ID).getBottomTask();
1095             assertTrue(bottomTask.containsActivity(ActivityManagerTestBase.getActivityComponentName(
1096                     activityName)));
1097         }
1098     }
1099 
1100     /**
1101      * Asserts that the pinned stack bounds does not intersect with the IME bounds.
1102      */
assertPinnedStackDoesNotIntersectIME()1103     private void assertPinnedStackDoesNotIntersectIME() throws Exception {
1104         // Ensure that the IME is visible
1105         WindowManagerState wmState = mAmWmState.getWmState();
1106         wmState.computeState(mDevice);
1107         WindowManagerState.WindowState imeWinState = wmState.getInputMethodWindowState();
1108         assertTrue(imeWinState != null);
1109 
1110         // Ensure that the PIP movement is constrained by the display bounds intersecting the
1111         // non-IME bounds
1112         Rectangle imeContentFrame = imeWinState.getContentFrame();
1113         Rectangle imeContentInsets = imeWinState.getGivenContentInsets();
1114         Rectangle imeBounds = new Rectangle(imeContentFrame.x + imeContentInsets.x,
1115                 imeContentFrame.y + imeContentInsets.y,
1116                 imeContentFrame.width - imeContentInsets.width,
1117                 imeContentFrame.height - imeContentInsets.height);
1118         wmState.computeState(mDevice);
1119         Rectangle pipMovementBounds = wmState.getPinnedStackMomentBounds();
1120         assertTrue(!pipMovementBounds.intersects(imeBounds));
1121     }
1122 
1123     /**
1124      * Asserts that the pinned stack bounds is contained in the display bounds.
1125      */
assertPinnedStackActivityIsInDisplayBounds(String activity)1126     private void assertPinnedStackActivityIsInDisplayBounds(String activity) throws Exception {
1127         final WindowManagerState.WindowState windowState = getWindowState(activity);
1128         final WindowManagerState.Display display = mAmWmState.getWmState().getDisplay(
1129                 windowState.getDisplayId());
1130         final Rectangle displayRect = display.getDisplayRect();
1131         final Rectangle pinnedStackBounds =
1132                 mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds();
1133         assertTrue(displayRect.contains(pinnedStackBounds));
1134     }
1135 
1136     /**
1137      * Asserts that the pinned stack exists.
1138      */
assertPinnedStackExists()1139     private void assertPinnedStackExists() throws Exception {
1140         mAmWmState.assertContainsStack("Must contain pinned stack.", PINNED_STACK_ID);
1141     }
1142 
1143     /**
1144      * Asserts that the pinned stack does not exist.
1145      */
assertPinnedStackDoesNotExist()1146     private void assertPinnedStackDoesNotExist() throws Exception {
1147         mAmWmState.assertDoesNotContainStack("Must not contain pinned stack.", PINNED_STACK_ID);
1148     }
1149 
1150     /**
1151      * Asserts that the pinned stack is the front stack.
1152      */
assertPinnedStackIsOnTop()1153     private void assertPinnedStackIsOnTop() throws Exception {
1154         mAmWmState.assertFrontStack("Pinned stack must always be on top.", PINNED_STACK_ID);
1155     }
1156 
1157     /**
1158      * Asserts that the activity received exactly one of each of the callbacks when entering and
1159      * exiting picture-in-picture.
1160      */
assertValidPictureInPictureCallbackOrder(String activityName, String logSeparator)1161     private void assertValidPictureInPictureCallbackOrder(String activityName, String logSeparator)
1162             throws Exception {
1163         final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(activityName,
1164                 logSeparator);
1165 
1166         if (lifecycleCounts.mConfigurationChangedCount != 1) {
1167             fail(activityName + " has received " + lifecycleCounts.mConfigurationChangedCount
1168                     + " onConfigurationChanged() calls, expecting 1");
1169         } else if (lifecycleCounts.mPictureInPictureModeChangedCount != 1) {
1170             fail(activityName + " has received " + lifecycleCounts.mPictureInPictureModeChangedCount
1171                     + " onPictureInPictureModeChanged() calls, expecting 1");
1172         } else if (lifecycleCounts.mMultiWindowModeChangedCount != 1) {
1173             fail(activityName + " has received " + lifecycleCounts.mMultiWindowModeChangedCount
1174                     + " onMultiWindowModeChanged() calls, expecting 1");
1175         } else {
1176             int lastPipLine = lifecycleCounts.mLastPictureInPictureModeChangedLineIndex;
1177             int lastMwLine = lifecycleCounts.mLastMultiWindowModeChangedLineIndex;
1178             int lastConfigLine = lifecycleCounts.mLastConfigurationChangedLineIndex;
1179             if (!(lastPipLine < lastMwLine && lastMwLine < lastConfigLine)) {
1180                 fail(activityName + " has received callbacks in unexpected order.  Expected:"
1181                         + " pip < mw < config change, but got line indices: " + lastPipLine + ", "
1182                         + lastMwLine + ", " + lastConfigLine + " respectively");
1183             }
1184         }
1185     }
1186 
1187     /**
1188      * @return the window state for the given {@param activity}'s window.
1189      */
getWindowState(String activity)1190     private WindowManagerState.WindowState getWindowState(String activity) throws Exception {
1191         String windowName = getWindowName(activity);
1192         mAmWmState.computeState(mDevice, new String[] {activity});
1193         final List<WindowManagerState.WindowState> tempWindowList = new ArrayList<>();
1194         mAmWmState.getWmState().getMatchingVisibleWindowState(windowName, tempWindowList);
1195         return tempWindowList.get(0);
1196     }
1197 
1198     /**
1199      * Compares two floats with a common epsilon.
1200      */
floatEquals(float f1, float f2)1201     private boolean floatEquals(float f1, float f2) {
1202         return Math.abs(f1 - f2) < FLOAT_COMPARE_EPSILON;
1203     }
1204 
1205     /**
1206      * Triggers a tap over the pinned stack bounds to trigger the PIP to close.
1207      */
tapToFinishPip()1208     private void tapToFinishPip() throws Exception {
1209         Rectangle pinnedStackBounds =
1210                 mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds();
1211         int tapX = pinnedStackBounds.x + pinnedStackBounds.width - 100;
1212         int tapY = pinnedStackBounds.y + pinnedStackBounds.height - 100;
1213         executeShellCommand(String.format("input tap %d %d", tapX, tapY));
1214     }
1215 
1216     /**
1217      * Launches the given {@param activityName} into the {@param taskId} as a task overlay.
1218      */
launchActivityAsTaskOverlay(String activityName, int taskId, int stackId)1219     private void launchActivityAsTaskOverlay(String activityName, int taskId, int stackId)
1220             throws Exception {
1221         executeShellCommand(getAmStartCmd(activityName) + " --task " + taskId + " --task-overlay");
1222 
1223         mAmWmState.waitForValidState(mDevice, activityName, stackId);
1224     }
1225 
1226     /**
1227      * Sets an app-ops op for a given package to a given mode.
1228      */
setAppOpsOpToMode(String packageName, int op, int mode)1229     private void setAppOpsOpToMode(String packageName, int op, int mode) throws Exception {
1230         executeShellCommand(String.format("appops set %s %d %d", packageName, op, mode));
1231     }
1232 
1233     /**
1234      * Triggers the window keycode.
1235      */
pressWindowButton()1236     private void pressWindowButton() throws Exception {
1237         executeShellCommand(INPUT_KEYEVENT_WINDOW);
1238     }
1239 
1240     /**
1241      * TODO: Improve tests check to actually check that apps are not interactive instead of checking
1242      *       if the stack is focused.
1243      */
pinnedStackTester(String startActivityCmd, String topActivityName, boolean moveTopToPinnedStack, boolean isFocusable)1244     private void pinnedStackTester(String startActivityCmd, String topActivityName,
1245             boolean moveTopToPinnedStack, boolean isFocusable) throws Exception {
1246 
1247         executeShellCommand(startActivityCmd);
1248         if (moveTopToPinnedStack) {
1249             executeShellCommand(AM_MOVE_TOP_ACTIVITY_TO_PINNED_STACK_COMMAND);
1250         }
1251 
1252         mAmWmState.waitForValidState(mDevice, topActivityName, PINNED_STACK_ID);
1253         mAmWmState.computeState(mDevice, null);
1254 
1255         if (supportsPip()) {
1256             final String windowName = getWindowName(topActivityName);
1257             assertPinnedStackExists();
1258             mAmWmState.assertFrontStack("Pinned stack must be the front stack.", PINNED_STACK_ID);
1259             mAmWmState.assertVisibility(topActivityName, true);
1260 
1261             if (isFocusable) {
1262                 mAmWmState.assertFocusedStack(
1263                         "Pinned stack must be the focused stack.", PINNED_STACK_ID);
1264                 mAmWmState.assertFocusedActivity(
1265                         "Pinned activity must be focused activity.", topActivityName);
1266                 mAmWmState.assertFocusedWindow(
1267                         "Pinned window must be focused window.", windowName);
1268                 // Not checking for resumed state here because PiP overlay can be launched on top
1269                 // in different task by SystemUI.
1270             } else {
1271                 // Don't assert that the stack is not focused as a focusable PiP overlay can be
1272                 // launched on top as a task overlay by SystemUI.
1273                 mAmWmState.assertNotFocusedActivity(
1274                         "Pinned activity can't be the focused activity.", topActivityName);
1275                 mAmWmState.assertNotResumedActivity(
1276                         "Pinned activity can't be the resumed activity.", topActivityName);
1277                 mAmWmState.assertNotFocusedWindow(
1278                         "Pinned window can't be focused window.", windowName);
1279             }
1280         } else {
1281             mAmWmState.assertDoesNotContainStack(
1282                     "Must not contain pinned stack.", PINNED_STACK_ID);
1283         }
1284     }
1285 }
1286