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.wm;
18 
19 import static android.app.ActivityTaskManager.INVALID_STACK_ID;
20 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
21 import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
23 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
24 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
25 import static android.content.pm.PackageManager.FEATURE_LEANBACK;
26 import static android.server.wm.ActivityManagerState.STATE_RESUMED;
27 import static android.server.wm.ActivityManagerState.STATE_STOPPED;
28 import static android.server.wm.ComponentNameUtils.getActivityName;
29 import static android.server.wm.ComponentNameUtils.getWindowName;
30 import static android.server.wm.UiDeviceUtils.pressWindowButton;
31 import static android.server.wm.app.Components.ALWAYS_FOCUSABLE_PIP_ACTIVITY;
32 import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
33 import static android.server.wm.app.Components.LAUNCH_ENTER_PIP_ACTIVITY;
34 import static android.server.wm.app.Components.LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY;
35 import static android.server.wm.app.Components.NON_RESIZEABLE_ACTIVITY;
36 import static android.server.wm.app.Components.NO_RELAUNCH_ACTIVITY;
37 import static android.server.wm.app.Components.PIP_ACTIVITY;
38 import static android.server.wm.app.Components.PIP_ACTIVITY2;
39 import static android.server.wm.app.Components.PIP_ACTIVITY_WITH_SAME_AFFINITY;
40 import static android.server.wm.app.Components.PIP_ON_STOP_ACTIVITY;
41 import static android.server.wm.app.Components.PipActivity.ACTION_ENTER_PIP;
42 import static android.server.wm.app.Components.PipActivity.ACTION_EXPAND_PIP;
43 import static android.server.wm.app.Components.PipActivity.ACTION_FINISH;
44 import static android.server.wm.app.Components.PipActivity.ACTION_MOVE_TO_BACK;
45 import static android.server.wm.app.Components.PipActivity.EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP;
46 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP;
47 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR;
48 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR;
49 import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_PAUSE;
50 import static android.server.wm.app.Components.PipActivity.EXTRA_FINISH_SELF_ON_RESUME;
51 import static android.server.wm.app.Components.PipActivity.EXTRA_ON_PAUSE_DELAY;
52 import static android.server.wm.app.Components.PipActivity.EXTRA_PIP_ORIENTATION;
53 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_DENOMINATOR;
54 import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_NUMERATOR;
55 import static android.server.wm.app.Components.PipActivity.EXTRA_START_ACTIVITY;
56 import static android.server.wm.app.Components.PipActivity.EXTRA_TAP_TO_FINISH;
57 import static android.server.wm.app.Components.RESUME_WHILE_PAUSING_ACTIVITY;
58 import static android.server.wm.app.Components.TEST_ACTIVITY;
59 import static android.server.wm.app.Components.TEST_ACTIVITY_WITH_SAME_AFFINITY;
60 import static android.server.wm.app.Components.TRANSLUCENT_TEST_ACTIVITY;
61 import static android.server.wm.app.Components.TestActivity.EXTRA_CONFIGURATION;
62 import static android.server.wm.app.Components.TestActivity.EXTRA_FIXED_ORIENTATION;
63 import static android.server.wm.app.Components.TestActivity.TEST_ACTIVITY_ACTION_FINISH_SELF;
64 import static android.server.wm.app27.Components.SDK_27_LAUNCH_ENTER_PIP_ACTIVITY;
65 import static android.server.wm.app27.Components.SDK_27_PIP_ACTIVITY;
66 import static android.view.Display.DEFAULT_DISPLAY;
67 
68 import static androidx.test.InstrumentationRegistry.getInstrumentation;
69 
70 import static org.hamcrest.Matchers.lessThan;
71 import static org.junit.Assert.assertEquals;
72 import static org.junit.Assert.assertNotEquals;
73 import static org.junit.Assert.assertNotNull;
74 import static org.junit.Assert.assertThat;
75 import static org.junit.Assert.assertTrue;
76 import static org.junit.Assert.fail;
77 import static org.junit.Assume.assumeFalse;
78 import static org.junit.Assume.assumeTrue;
79 
80 import android.content.ComponentName;
81 import android.content.Context;
82 import android.content.res.Configuration;
83 import android.database.ContentObserver;
84 import android.graphics.Rect;
85 import android.os.Handler;
86 import android.os.Looper;
87 import android.platform.test.annotations.Presubmit;
88 import android.provider.Settings;
89 import android.server.wm.ActivityManagerState.ActivityStack;
90 import android.server.wm.ActivityManagerState.ActivityTask;
91 import android.server.wm.CommandSession.ActivityCallback;
92 import android.server.wm.CommandSession.SizeInfo;
93 import android.server.wm.TestJournalProvider.TestJournalContainer;
94 import android.server.wm.WindowManagerState.WindowStack;
95 import android.server.wm.settings.SettingsSession;
96 import android.util.Log;
97 import android.util.Size;
98 
99 import androidx.test.filters.FlakyTest;
100 
101 import com.android.compatibility.common.util.AppOpsUtils;
102 import com.android.compatibility.common.util.SystemUtil;
103 
104 import org.junit.Before;
105 import org.junit.Ignore;
106 import org.junit.Test;
107 
108 import java.io.IOException;
109 import java.util.List;
110 import java.util.concurrent.CountDownLatch;
111 import java.util.concurrent.TimeUnit;
112 
113 /**
114  * Build/Install/Run:
115  * atest CtsWindowManagerDeviceTestCases:PinnedStackTests
116  */
117 @Presubmit
118 @FlakyTest(bugId = 71792368)
119 public class PinnedStackTests extends ActivityManagerTestBase {
120     private static final String TAG = PinnedStackTests.class.getSimpleName();
121 
122     private static final String APP_OPS_OP_ENTER_PICTURE_IN_PICTURE = "PICTURE_IN_PICTURE";
123     private static final int APP_OPS_MODE_ALLOWED = 0;
124     private static final int APP_OPS_MODE_IGNORED = 1;
125     private static final int APP_OPS_MODE_ERRORED = 2;
126 
127     private static final int ROTATION_0 = 0;
128     private static final int ROTATION_90 = 1;
129     private static final int ROTATION_180 = 2;
130     private static final int ROTATION_270 = 3;
131 
132     // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
133     private static final int ORIENTATION_LANDSCAPE = 0;
134     // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
135     private static final int ORIENTATION_PORTRAIT = 1;
136 
137     private static final float FLOAT_COMPARE_EPSILON = 0.005f;
138 
139     // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio
140     private static final int MIN_ASPECT_RATIO_NUMERATOR = 100;
141     private static final int MIN_ASPECT_RATIO_DENOMINATOR = 239;
142     private static final int BELOW_MIN_ASPECT_RATIO_DENOMINATOR = MIN_ASPECT_RATIO_DENOMINATOR + 1;
143     // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio
144     private static final int MAX_ASPECT_RATIO_NUMERATOR = 239;
145     private static final int MAX_ASPECT_RATIO_DENOMINATOR = 100;
146     private static final int ABOVE_MAX_ASPECT_RATIO_NUMERATOR = MAX_ASPECT_RATIO_NUMERATOR + 1;
147 
148     @Before
149     @Override
setUp()150     public void setUp() throws Exception {
151         super.setUp();
152         assumeTrue(supportsPip());
153     }
154 
155     @Test
testMinimumDeviceSize()156     public void testMinimumDeviceSize() throws Exception {
157         mAmWmState.assertDeviceDefaultDisplaySize(
158                 "Devices supporting picture-in-picture must be larger than the default minimum"
159                         + " task size");
160     }
161 
162     @Test
testEnterPictureInPictureMode()163     public void testEnterPictureInPictureMode() throws Exception {
164         pinnedStackTester(getAmStartCmd(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"),
165                 PIP_ACTIVITY, PIP_ACTIVITY, false /* moveTopToPinnedStack */,
166                 false /* isFocusable */);
167     }
168 
169     @Test
testMoveTopActivityToPinnedStack()170     public void testMoveTopActivityToPinnedStack() throws Exception {
171         pinnedStackTester(getAmStartCmd(PIP_ACTIVITY), PIP_ACTIVITY, PIP_ACTIVITY,
172                 true /* moveTopToPinnedStack */, false /* isFocusable */);
173     }
174 
175     // This test is black-listed in cts-known-failures.xml (b/35314835).
176     @Ignore
177     @Test
testAlwaysFocusablePipActivity()178     public void testAlwaysFocusablePipActivity() throws Exception {
179         pinnedStackTester(getAmStartCmd(ALWAYS_FOCUSABLE_PIP_ACTIVITY),
180                 ALWAYS_FOCUSABLE_PIP_ACTIVITY, ALWAYS_FOCUSABLE_PIP_ACTIVITY,
181                 false /* moveTopToPinnedStack */, true /* isFocusable */);
182     }
183 
184     // This test is black-listed in cts-known-failures.xml (b/35314835).
185     @Ignore
186     @Test
testLaunchIntoPinnedStack()187     public void testLaunchIntoPinnedStack() throws Exception {
188         pinnedStackTester(getAmStartCmd(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY),
189                 LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY, ALWAYS_FOCUSABLE_PIP_ACTIVITY,
190                 false /* moveTopToPinnedStack */, true /* isFocusable */);
191     }
192 
193     @Test
testNonTappablePipActivity()194     public void testNonTappablePipActivity() throws Exception {
195         assumeFalse(hasDeviceFeature(FEATURE_LEANBACK));
196         // Launch the tap-to-finish activity at a specific place
197         launchActivity(PIP_ACTIVITY,
198                 EXTRA_ENTER_PIP, "true",
199                 EXTRA_TAP_TO_FINISH, "true");
200         // Wait for animation complete since we are tapping on specific bounds
201         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
202         assertPinnedStackExists();
203 
204         // Tap the screen at a known location in the pinned stack bounds, and ensure that it is
205         // not passed down to the top task
206         tapToFinishPip();
207         mAmWmState.computeState(false /* compareTaskAndStackBounds */,
208                 new WaitForValidActivityState(PIP_ACTIVITY));
209         mAmWmState.assertVisibility(PIP_ACTIVITY, true);
210     }
211 
212     @Test
testPinnedStackDefaultBounds()213     public void testPinnedStackDefaultBounds() throws Exception {
214         // Launch a PIP activity
215         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
216         // Wait for animation complete since we are comparing bounds
217         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
218 
219         try (final RotationSession rotationSession = new RotationSession()) {
220             rotationSession.set(ROTATION_0);
221             mAmWmState.waitForWithWmState((wmState1) -> {
222                 Rect db = wmState1.getDefaultPinnedStackBounds();
223                 Rect sb = wmState1.getStableBounds();
224                 return (db.width() > 0 && db.height() > 0) &&
225                         (sb.contains(db));
226             }, "Waiting for valid bounds..");
227             WindowManagerState wmState = mAmWmState.getWmState();
228             wmState.computeState();
229             Rect defaultPipBounds = wmState.getDefaultPinnedStackBounds();
230             Rect stableBounds = wmState.getStableBounds();
231             assertTrue(defaultPipBounds.width() > 0 && defaultPipBounds.height() > 0);
232             assertTrue(stableBounds.contains(defaultPipBounds));
233 
234             rotationSession.set(ROTATION_90);
235             mAmWmState.waitForWithWmState((wmState1) -> {
236                 Rect db = wmState1.getDefaultPinnedStackBounds();
237                 Rect sb = wmState1.getStableBounds();
238                 return (db.width() > 0 && db.height() > 0) &&
239                         (sb.contains(db));
240             }, "Waiting for valid bounds...");
241             wmState = mAmWmState.getWmState();
242             wmState.computeState();
243             defaultPipBounds = wmState.getDefaultPinnedStackBounds();
244             stableBounds = wmState.getStableBounds();
245             assertTrue(defaultPipBounds.width() > 0 && defaultPipBounds.height() > 0);
246             assertTrue(stableBounds.contains(defaultPipBounds));
247         }
248     }
249 
250     @Test
testPinnedStackMovementBounds()251     public void testPinnedStackMovementBounds() throws Exception {
252         // Launch a PIP activity
253         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
254         // Wait for animation complete since we are comparing bounds
255         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
256 
257         try (final RotationSession rotationSession = new RotationSession()) {
258             rotationSession.set(ROTATION_0);
259             mAmWmState.waitForWithWmState((wmState1) -> {
260                 Rect db = wmState1.getPinnedStackMovementBounds();
261                 Rect sb = wmState1.getStableBounds();
262                 return (db.width() > 0 && db.height() > 0) &&
263                         (sb.contains(db));
264             }, "Waiting for valid bounds...");
265             WindowManagerState wmState = mAmWmState.getWmState();
266             wmState.computeState();
267             Rect pipMovementBounds = wmState.getPinnedStackMovementBounds();
268             Rect stableBounds = wmState.getStableBounds();
269             assertTrue(pipMovementBounds.width() > 0 && pipMovementBounds.height() > 0);
270             assertTrue(stableBounds.contains(pipMovementBounds));
271 
272             rotationSession.set(ROTATION_90);
273             mAmWmState.waitForWithWmState((wmState1) -> {
274                 Rect db = wmState1.getPinnedStackMovementBounds();
275                 Rect sb = wmState1.getStableBounds();
276                 return (db.width() > 0 && db.height() > 0) &&
277                         (sb.contains(db));
278             }, "Waiting for valid bounds...");
279             wmState = mAmWmState.getWmState();
280             wmState.computeState();
281             pipMovementBounds = wmState.getPinnedStackMovementBounds();
282             stableBounds = wmState.getStableBounds();
283             assertTrue(pipMovementBounds.width() > 0 && pipMovementBounds.height() > 0);
284             assertTrue(stableBounds.contains(pipMovementBounds));
285         }
286     }
287 
288     @Test
289     @FlakyTest // TODO: Reintroduce to presubmit once b/71508234 is resolved.
testPinnedStackOutOfBoundsInsetsNonNegative()290     public void testPinnedStackOutOfBoundsInsetsNonNegative() throws Exception {
291         final WindowManagerState wmState = mAmWmState.getWmState();
292 
293         // Launch an activity into the pinned stack
294         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true",
295                 EXTRA_TAP_TO_FINISH, "true");
296         // Wait for animation complete since we are comparing bounds
297         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
298 
299         // Get the display dimensions
300         WindowManagerState.WindowState windowState = getWindowState(PIP_ACTIVITY);
301         WindowManagerState.Display display = wmState.getDisplay(windowState.getDisplayId());
302         Rect displayRect = display.getDisplayRect();
303 
304         // Move the pinned stack offscreen
305         final int stackId = getPinnedStack().mStackId;
306         final int top = 0;
307         final int left = displayRect.width() - 200;
308         resizeStack(stackId, left, top, left + 500, top + 500);
309 
310         // Ensure that the surface insets are not negative
311         windowState = getWindowState(PIP_ACTIVITY);
312         Rect contentInsets = windowState.getContentInsets();
313         if (contentInsets != null) {
314             assertTrue(contentInsets.left >= 0 && contentInsets.top >= 0
315                     && contentInsets.width() >= 0 && contentInsets.height() >= 0);
316         }
317     }
318 
319     @Test
testPinnedStackInBoundsAfterRotation()320     public void testPinnedStackInBoundsAfterRotation() throws Exception {
321         // Launch an activity into the pinned stack
322         launchActivity(PIP_ACTIVITY,
323                 EXTRA_ENTER_PIP, "true",
324                 EXTRA_TAP_TO_FINISH, "true");
325         // Wait for animation complete since we are comparing bounds
326         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
327 
328         // Ensure that the PIP stack is fully visible in each orientation
329         try (final RotationSession rotationSession = new RotationSession()) {
330             rotationSession.set(ROTATION_0);
331             assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
332             rotationSession.set(ROTATION_90);
333             assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
334             rotationSession.set(ROTATION_180);
335             assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
336             rotationSession.set(ROTATION_270);
337             assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
338         }
339     }
340 
341     @Test
testEnterPipToOtherOrientation()342     public void testEnterPipToOtherOrientation() throws Exception {
343         // Launch a portrait only app on the fullscreen stack
344         launchActivity(TEST_ACTIVITY,
345                 EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT));
346         // Launch the PiP activity fixed as landscape
347         launchActivity(PIP_ACTIVITY,
348                 EXTRA_PIP_ORIENTATION, String.valueOf(ORIENTATION_LANDSCAPE));
349         // Enter PiP, and assert that the PiP is within bounds now that the device is back in
350         // portrait
351         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
352         // Wait for animation complete since we are comparing bounds
353         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
354         assertPinnedStackExists();
355         assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
356     }
357 
358     @Test
testEnterPipAspectRatioMin()359     public void testEnterPipAspectRatioMin() throws Exception {
360         testEnterPipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR);
361     }
362 
363     @Test
testEnterPipAspectRatioMax()364     public void testEnterPipAspectRatioMax() throws Exception {
365         testEnterPipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
366     }
367 
testEnterPipAspectRatio(int num, int denom)368     private void testEnterPipAspectRatio(int num, int denom) throws Exception {
369         launchActivity(PIP_ACTIVITY,
370                 EXTRA_ENTER_PIP, "true",
371                 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
372                 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
373         // Wait for animation complete since we are comparing aspect ratio
374         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
375         assertPinnedStackExists();
376 
377         // Assert that we have entered PIP and that the aspect ratio is correct
378         Rect pinnedStackBounds = getPinnedStackBounds();
379         assertFloatEquals((float) pinnedStackBounds.width() / pinnedStackBounds.height(),
380                 (float) num / denom);
381     }
382 
383     @Test
testResizePipAspectRatioMin()384     public void testResizePipAspectRatioMin() throws Exception {
385         testResizePipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR);
386     }
387 
388     @Test
testResizePipAspectRatioMax()389     public void testResizePipAspectRatioMax() throws Exception {
390         testResizePipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
391     }
392 
testResizePipAspectRatio(int num, int denom)393     private void testResizePipAspectRatio(int num, int denom) throws Exception {
394         launchActivity(PIP_ACTIVITY,
395                 EXTRA_ENTER_PIP, "true",
396                 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
397                 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
398         // Wait for animation complete since we are comparing aspect ratio
399         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
400         assertPinnedStackExists();
401         waitForValidAspectRatio(num, denom);
402         Rect bounds = getPinnedStackBounds();
403         assertFloatEquals((float) bounds.width() / bounds.height(), (float) num / denom);
404     }
405 
406     @Test
testEnterPipExtremeAspectRatioMin()407     public void testEnterPipExtremeAspectRatioMin() throws Exception {
408         testEnterPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR,
409                 BELOW_MIN_ASPECT_RATIO_DENOMINATOR);
410     }
411 
412     @Test
testEnterPipExtremeAspectRatioMax()413     public void testEnterPipExtremeAspectRatioMax() throws Exception {
414         testEnterPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR,
415                 MAX_ASPECT_RATIO_DENOMINATOR);
416     }
417 
testEnterPipExtremeAspectRatio(int num, int denom)418     private void testEnterPipExtremeAspectRatio(int num, int denom) throws Exception {
419         // Assert that we could not create a pinned stack with an extreme aspect ratio
420         launchActivity(PIP_ACTIVITY,
421                 EXTRA_ENTER_PIP, "true",
422                 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
423                 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
424         assertPinnedStackDoesNotExist();
425     }
426 
427     @Test
testSetPipExtremeAspectRatioMin()428     public void testSetPipExtremeAspectRatioMin() throws Exception {
429         testSetPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR,
430                 BELOW_MIN_ASPECT_RATIO_DENOMINATOR);
431     }
432 
433     @Test
testSetPipExtremeAspectRatioMax()434     public void testSetPipExtremeAspectRatioMax() throws Exception {
435         testSetPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR,
436                 MAX_ASPECT_RATIO_DENOMINATOR);
437     }
438 
testSetPipExtremeAspectRatio(int num, int denom)439     private void testSetPipExtremeAspectRatio(int num, int denom) throws Exception {
440         // Try to resize the a normal pinned stack to an extreme aspect ratio and ensure that
441         // fails (the aspect ratio remains the same)
442         launchActivity(PIP_ACTIVITY,
443                 EXTRA_ENTER_PIP, "true",
444                 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR,
445                         Integer.toString(MAX_ASPECT_RATIO_NUMERATOR),
446                 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR,
447                         Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR),
448                 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
449                 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
450         // Wait for animation complete since we are comparing aspect ratio
451         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
452         assertPinnedStackExists();
453         Rect pinnedStackBounds = getPinnedStackBounds();
454         assertFloatEquals((float) pinnedStackBounds.width() / pinnedStackBounds.height(),
455                 (float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR);
456     }
457 
458     @Test
testDisallowPipLaunchFromStoppedActivity()459     public void testDisallowPipLaunchFromStoppedActivity() throws Exception {
460         // Launch the bottom pip activity which will launch a new activity on top and attempt to
461         // enter pip when it is stopped
462         launchActivity(PIP_ON_STOP_ACTIVITY);
463 
464         // Wait for the bottom pip activity to be stopped
465         mAmWmState.waitForActivityState(PIP_ON_STOP_ACTIVITY, STATE_STOPPED);
466 
467         // Assert that there is no pinned stack (that enterPictureInPicture() failed)
468         assertPinnedStackDoesNotExist();
469     }
470 
471     @Test
testAutoEnterPictureInPicture()472     public void testAutoEnterPictureInPicture() throws Exception {
473         // Launch a test activity so that we're not over home
474         launchActivity(TEST_ACTIVITY);
475 
476         // Launch the PIP activity on pause
477         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
478         assertPinnedStackDoesNotExist();
479 
480         // Go home and ensure that there is a pinned stack
481         launchHomeActivity();
482         waitForEnterPip(PIP_ACTIVITY);
483         assertPinnedStackExists();
484     }
485 
486     @Test
testAutoEnterPictureInPictureLaunchActivity()487     public void testAutoEnterPictureInPictureLaunchActivity() throws Exception {
488         // Launch a test activity so that we're not over home
489         launchActivity(TEST_ACTIVITY);
490 
491         // Launch the PIP activity on pause, and have it start another activity on
492         // top of itself.  Wait for the new activity to be visible and ensure that the pinned stack
493         // was not created in the process
494         launchActivity(PIP_ACTIVITY,
495                 EXTRA_ENTER_PIP_ON_PAUSE, "true",
496                 EXTRA_START_ACTIVITY, getActivityName(NON_RESIZEABLE_ACTIVITY));
497         mAmWmState.computeState(false /* compareTaskAndStackBounds */,
498                 new WaitForValidActivityState(NON_RESIZEABLE_ACTIVITY));
499         assertPinnedStackDoesNotExist();
500 
501         // Go home while the pip activity is open and ensure the previous activity is not PIPed
502         launchHomeActivity();
503         assertPinnedStackDoesNotExist();
504     }
505 
506     @Test
testAutoEnterPictureInPictureFinish()507     public void testAutoEnterPictureInPictureFinish() throws Exception {
508         // Launch a test activity so that we're not over home
509         launchActivity(TEST_ACTIVITY);
510 
511         // Launch the PIP activity on pause, and set it to finish itself after
512         // some period.  Wait for the previous activity to be visible, and ensure that the pinned
513         // stack was not created in the process
514         launchActivity(PIP_ACTIVITY,
515                 EXTRA_ENTER_PIP_ON_PAUSE, "true",
516                 EXTRA_FINISH_SELF_ON_RESUME, "true");
517         assertPinnedStackDoesNotExist();
518     }
519 
520     @Test
testAutoEnterPictureInPictureAspectRatio()521     public void testAutoEnterPictureInPictureAspectRatio() throws Exception {
522         // Launch the PIP activity on pause, and set the aspect ratio
523         launchActivity(PIP_ACTIVITY,
524                 EXTRA_ENTER_PIP_ON_PAUSE, "true",
525                 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(MAX_ASPECT_RATIO_NUMERATOR),
526                 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR));
527 
528         // Go home while the pip activity is open to trigger auto-PIP
529         launchHomeActivity();
530         // Wait for animation complete since we are comparing aspect ratio
531         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
532         assertPinnedStackExists();
533 
534         waitForValidAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
535         Rect bounds = getPinnedStackBounds();
536         assertFloatEquals((float) bounds.width() / bounds.height(),
537                 (float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR);
538     }
539 
540     @Test
testAutoEnterPictureInPictureOverPip()541     public void testAutoEnterPictureInPictureOverPip() throws Exception {
542         // Launch another PIP activity
543         launchActivity(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY);
544         waitForEnterPip(ALWAYS_FOCUSABLE_PIP_ACTIVITY);
545         assertPinnedStackExists();
546 
547         // Launch the PIP activity on pause
548         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
549 
550         // Go home while the PIP activity is open to try to trigger auto-enter PIP
551         launchHomeActivity();
552         assertPinnedStackExists();
553 
554         // Ensure that auto-enter pip failed and that the resumed activity in the pinned stack is
555         // still the first activity
556         final ActivityStack pinnedStack = getPinnedStack();
557         assertEquals(1, pinnedStack.getTasks().size());
558         assertEquals(getActivityName(ALWAYS_FOCUSABLE_PIP_ACTIVITY),
559                 pinnedStack.getTasks().get(0).mRealActivity);
560     }
561 
562     @Test
testDisallowMultipleTasksInPinnedStack()563     public void testDisallowMultipleTasksInPinnedStack() throws Exception {
564         // Launch a test activity so that we have multiple fullscreen tasks
565         launchActivity(TEST_ACTIVITY);
566 
567         // Launch first PIP activity
568         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
569         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
570 
571         // Launch second PIP activity
572         launchActivity(PIP_ACTIVITY2, EXTRA_ENTER_PIP, "true");
573 
574         final ActivityStack pinnedStack = getPinnedStack();
575         assertEquals(1, pinnedStack.getTasks().size());
576         assertTrue(mAmWmState.getAmState().containsActivityInWindowingMode(
577                 PIP_ACTIVITY2, WINDOWING_MODE_PINNED));
578         assertTrue(mAmWmState.getAmState().containsActivityInWindowingMode(
579                 PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN));
580     }
581 
582     @Test
testPipUnPipOverHome()583     public void testPipUnPipOverHome() throws Exception {
584         // Go home
585         launchHomeActivity();
586         // Launch an auto pip activity
587         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
588         waitForEnterPip(PIP_ACTIVITY);
589         assertPinnedStackExists();
590 
591         // Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip
592         launchActivity(PIP_ACTIVITY);
593         waitForExitPipToFullscreen(PIP_ACTIVITY);
594         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
595         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
596         mAmWmState.assertHomeActivityVisible(true);
597     }
598 
599     @Test
testPipUnPipOverApp()600     public void testPipUnPipOverApp() throws Exception {
601         // Launch a test activity so that we're not over home
602         launchActivity(TEST_ACTIVITY);
603 
604         // Launch an auto pip activity
605         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
606         waitForEnterPip(PIP_ACTIVITY);
607         assertPinnedStackExists();
608 
609         // Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip
610         launchActivity(PIP_ACTIVITY);
611         waitForExitPipToFullscreen(PIP_ACTIVITY);
612         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
613         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
614         mAmWmState.assertVisibility(TEST_ACTIVITY, true);
615     }
616 
617     @Test
testRemovePipWithNoFullscreenStack()618     public void testRemovePipWithNoFullscreenStack() throws Exception {
619         // Launch a pip activity
620         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
621         waitForEnterPip(PIP_ACTIVITY);
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         removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
627         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY,
628                 WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
629     }
630 
631     @Test
testRemovePipWithVisibleFullscreenStack()632     public void testRemovePipWithVisibleFullscreenStack() throws Exception {
633         // Launch a fullscreen activity, and a pip activity over that
634         launchActivity(TEST_ACTIVITY);
635         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
636         waitForEnterPip(PIP_ACTIVITY);
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         removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
642         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY,
643                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
644     }
645 
646     @FlakyTest(bugId = 70746098)
647     @Test
testRemovePipWithHiddenFullscreenStack()648     public void testRemovePipWithHiddenFullscreenStack() throws Exception {
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         waitForEnterPip(PIP_ACTIVITY);
655         assertPinnedStackExists();
656 
657         // Remove the stack and ensure that the task is placed on top of the hidden fullscreen
658         // stack, but that the home stack is still focused
659         removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
660         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY,
661                 WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
662     }
663 
664     @Test
testMovePipToBackWithNoFullscreenStack()665     public void testMovePipToBackWithNoFullscreenStack() throws Exception {
666         // Start with a clean slate, remove all the stacks but home
667         removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME);
668 
669         // Launch a pip activity
670         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
671         waitForEnterPip(PIP_ACTIVITY);
672         assertPinnedStackExists();
673 
674         // Remove the stack and ensure that the task is now in the fullscreen stack (when no
675         // fullscreen stack existed before)
676         mBroadcastActionTrigger.doAction(ACTION_MOVE_TO_BACK);
677         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY,
678                 WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
679     }
680 
681     @FlakyTest(bugId = 70906499)
682     @Test
testMovePipToBackWithVisibleFullscreenStack()683     public void testMovePipToBackWithVisibleFullscreenStack() throws Exception {
684         // Launch a fullscreen activity, and a pip activity over that
685         launchActivity(TEST_ACTIVITY);
686         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
687         waitForEnterPip(PIP_ACTIVITY);
688         assertPinnedStackExists();
689 
690         // Remove the stack and ensure that the task is placed in the fullscreen stack, behind the
691         // top fullscreen activity
692         mBroadcastActionTrigger.doAction(ACTION_MOVE_TO_BACK);
693         assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY,
694                 WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
695     }
696 
697     @FlakyTest(bugId = 70906499)
698     @Test
testMovePipToBackWithHiddenFullscreenStack()699     public void testMovePipToBackWithHiddenFullscreenStack() throws Exception {
700         // Launch a fullscreen activity, return home and while the fullscreen stack is hidden,
701         // launch a pip activity over home
702         launchActivity(TEST_ACTIVITY);
703         launchHomeActivity();
704         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
705         waitForEnterPip(PIP_ACTIVITY);
706         assertPinnedStackExists();
707 
708         // Remove the stack and ensure that the task is placed on top of the hidden fullscreen
709         // stack, but that the home stack is still focused
710         mBroadcastActionTrigger.doAction(ACTION_MOVE_TO_BACK);
711         assertPinnedStackStateOnMoveToFullscreen(
712                 PIP_ACTIVITY, WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
713     }
714 
715     @Test
testPinnedStackAlwaysOnTop()716     public void testPinnedStackAlwaysOnTop() throws Exception {
717         // Launch activity into pinned stack and assert it's on top.
718         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
719         waitForEnterPip(PIP_ACTIVITY);
720         assertPinnedStackExists();
721         assertPinnedStackIsOnTop();
722 
723         // Launch another activity in fullscreen stack and check that pinned stack is still on top.
724         launchActivity(TEST_ACTIVITY);
725         assertPinnedStackExists();
726         assertPinnedStackIsOnTop();
727 
728         // Launch home and check that pinned stack is still on top.
729         launchHomeActivity();
730         assertPinnedStackExists();
731         assertPinnedStackIsOnTop();
732     }
733 
734     @Test
testAppOpsDenyPipOnPause()735     public void testAppOpsDenyPipOnPause() throws Exception {
736         try (final AppOpsSession appOpsSession = new AppOpsSession(PIP_ACTIVITY)) {
737             // Disable enter-pip and try to enter pip
738             appOpsSession.setOpToMode(APP_OPS_OP_ENTER_PICTURE_IN_PICTURE, APP_OPS_MODE_IGNORED);
739 
740             // Launch the PIP activity on pause
741             launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
742             assertPinnedStackDoesNotExist();
743 
744             // Go home and ensure that there is no pinned stack
745             launchHomeActivity();
746             assertPinnedStackDoesNotExist();
747         }
748     }
749 
750     @Test
testEnterPipFromTaskWithMultipleActivities()751     public void testEnterPipFromTaskWithMultipleActivities() throws Exception {
752         // Try to enter picture-in-picture from an activity that has more than one activity in the
753         // task and ensure that it works
754         launchActivity(LAUNCH_ENTER_PIP_ACTIVITY);
755         waitForEnterPip(PIP_ACTIVITY);
756         assertPinnedStackExists();
757     }
758 
759     @Test
testLaunchStoppedActivityWithPiPInSameProcessPreQ()760     public void testLaunchStoppedActivityWithPiPInSameProcessPreQ() {
761         // Try to enter picture-in-picture from an activity that has more than one activity in the
762         // task and ensure that it works, for pre-Q app
763         launchActivity(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY,
764                 EXTRA_ENTER_PIP, "true");
765         waitForEnterPip(SDK_27_PIP_ACTIVITY);
766         assertPinnedStackExists();
767 
768         // Puts the host activity to stopped state
769         launchHomeActivity();
770         mAmWmState.assertHomeActivityVisible(true);
771         waitAndAssertActivityState(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY, STATE_STOPPED,
772                 "Activity should become STOPPED");
773         mAmWmState.assertVisibility(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY, false);
774 
775         // Host activity should be visible after re-launch and PiP window still exists
776         launchActivity(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY);
777         waitAndAssertActivityState(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY, STATE_RESUMED,
778                 "Activity should become RESUMED");
779         mAmWmState.assertVisibility(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY, true);
780         assertPinnedStackExists();
781     }
782 
783     @Test
testEnterPipWithResumeWhilePausingActivityNoStop()784     public void testEnterPipWithResumeWhilePausingActivityNoStop() throws Exception {
785         /*
786          * Launch the resumeWhilePausing activity and ensure that the PiP activity did not get
787          * stopped and actually went into the pinned stack.
788          *
789          * Note that this is a workaround because to trigger the path that we want to happen in
790          * activity manager, we need to add the leaving activity to the stopping state, which only
791          * happens when a hidden stack is brought forward. Normally, this happens when you go home,
792          * but since we can't launch into the home stack directly, we have a workaround.
793          *
794          * 1) Launch an activity in a new dynamic stack
795          * 2) Start the PiP activity that will enter picture-in-picture when paused in the
796          *    fullscreen stack
797          * 3) Bring the activity in the dynamic stack forward to trigger PiP
798          */
799         launchActivity(RESUME_WHILE_PAUSING_ACTIVITY);
800         // Launch an activity that will enter PiP when it is paused with a delay that is long enough
801         // for the next resumeWhilePausing activity to finish resuming, but slow enough to not
802         // trigger the current system pause timeout (currently 500ms)
803         launchActivity(PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN,
804                 EXTRA_ENTER_PIP_ON_PAUSE, "true",
805                 EXTRA_ON_PAUSE_DELAY, "350",
806                 EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true");
807         launchActivity(RESUME_WHILE_PAUSING_ACTIVITY);
808         assertPinnedStackExists();
809     }
810 
811     @Test
testDisallowEnterPipActivityLocked()812     public void testDisallowEnterPipActivityLocked() throws Exception {
813         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
814         ActivityTask task = mAmWmState.getAmState().getStandardStackByWindowingMode(
815                 WINDOWING_MODE_FULLSCREEN).getTopTask();
816 
817         // Lock the task and ensure that we can't enter picture-in-picture both explicitly and
818         // when paused
819         SystemUtil.runWithShellPermissionIdentity(() -> {
820             try {
821                 mAtm.startSystemLockTaskMode(task.mTaskId);
822                 mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
823                 waitForEnterPip(PIP_ACTIVITY);
824                 assertPinnedStackDoesNotExist();
825                 launchHomeActivity();
826                 assertPinnedStackDoesNotExist();
827             } finally {
828                 mAtm.stopSystemLockTaskMode();
829             }
830         });
831     }
832 
833     @FlakyTest(bugId = 70328524)
834     @Test
testConfigurationChangeOrderDuringTransition()835     public void testConfigurationChangeOrderDuringTransition() throws Exception {
836         // Launch a PiP activity and ensure configuration change only happened once, and that the
837         // configuration change happened after the picture-in-picture and multi-window callbacks
838         launchActivity(PIP_ACTIVITY);
839         separateTestJournal();
840         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
841         waitForEnterPip(PIP_ACTIVITY);
842         assertPinnedStackExists();
843         waitForValidPictureInPictureCallbacks(PIP_ACTIVITY);
844         assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY);
845 
846         // Trigger it to go back to fullscreen and ensure that only triggered one configuration
847         // change as well
848         separateTestJournal();
849         launchActivity(PIP_ACTIVITY);
850         waitForValidPictureInPictureCallbacks(PIP_ACTIVITY);
851         assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY);
852     }
853 
854     /** Helper class to save, set, and restore transition_animation_scale preferences. */
855     private static class TransitionAnimationScaleSession extends SettingsSession<Float> {
TransitionAnimationScaleSession()856         TransitionAnimationScaleSession() {
857             super(Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE),
858                     Settings.Global::getFloat,
859                     Settings.Global::putFloat);
860         }
861 
862         @Override
close()863         public void close() throws Exception {
864             // Wait for the restored setting to apply before we continue on with the next test
865             final CountDownLatch waitLock = new CountDownLatch(1);
866             final Context context = getInstrumentation().getTargetContext();
867             context.getContentResolver().registerContentObserver(mUri, false,
868                     new ContentObserver(new Handler(Looper.getMainLooper())) {
869                         @Override
870                         public void onChange(boolean selfChange) {
871                             waitLock.countDown();
872                         }
873                     });
874             super.close();
875             if (!waitLock.await(2, TimeUnit.SECONDS)) {
876                 Log.i(TAG, "TransitionAnimationScaleSession value not restored");
877             }
878         }
879     }
880 
881     @Test
testEnterPipInterruptedCallbacks()882     public void testEnterPipInterruptedCallbacks() throws Exception {
883         try (final TransitionAnimationScaleSession transitionAnimationScaleSession =
884                 new TransitionAnimationScaleSession()) {
885             // Slow down the transition animations for this test
886             transitionAnimationScaleSession.set(20f);
887 
888             // Launch a PiP activity
889             launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
890             // Wait until the PiP activity has moved into the pinned stack (happens before the
891             // transition has started)
892             waitForEnterPip(PIP_ACTIVITY);
893             assertPinnedStackExists();
894 
895             // Relaunch the PiP activity back into fullscreen
896             separateTestJournal();
897             launchActivity(PIP_ACTIVITY);
898             // Wait until the PiP activity is reparented into the fullscreen stack (happens after
899             // the transition has finished)
900             waitForExitPipToFullscreen(PIP_ACTIVITY);
901 
902             // Ensure that we get the callbacks indicating that PiP/MW mode was cancelled, but no
903             // configuration change (since none was sent)
904             final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(
905                     PIP_ACTIVITY);
906             assertEquals("onConfigurationChanged", 0,
907                     lifecycleCounts.getCount(ActivityCallback.ON_CONFIGURATION_CHANGED));
908             assertEquals("onPictureInPictureModeChanged", 1,
909                     lifecycleCounts.getCount(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED));
910             assertEquals("onMultiWindowModeChanged", 1,
911                     lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
912         }
913     }
914 
915     @Test
testStopBeforeMultiWindowCallbacksOnDismiss()916     public void testStopBeforeMultiWindowCallbacksOnDismiss() throws Exception {
917         // Launch a PiP activity
918         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
919         // Wait for animation complete so that system has reported pip mode change event to
920         // client and the last reported pip mode has updated.
921         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
922         assertPinnedStackExists();
923 
924         // Dismiss it
925         separateTestJournal();
926         removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
927         waitForExitPipToFullscreen(PIP_ACTIVITY);
928 
929         // Confirm that we get stop before the multi-window and picture-in-picture mode change
930         // callbacks
931         final ActivityLifecycleCounts lifecycles = new ActivityLifecycleCounts(PIP_ACTIVITY);
932         assertEquals("onStop", 1, lifecycles.getCount(ActivityCallback.ON_STOP));
933         assertEquals("onPictureInPictureModeChanged", 1,
934                 lifecycles.getCount(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED));
935         assertEquals("onMultiWindowModeChanged", 1,
936                 lifecycles.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
937         final int lastStopIndex = lifecycles.getLastIndex(ActivityCallback.ON_STOP);
938         final int lastPipIndex = lifecycles.getLastIndex(
939                 ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED);
940         final int lastMwIndex = lifecycles.getLastIndex(
941                 ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED);
942         assertThat("onStop should be before onPictureInPictureModeChanged",
943                 lastStopIndex, lessThan(lastPipIndex));
944         assertThat("onPictureInPictureModeChanged should be before onMultiWindowModeChanged",
945                 lastPipIndex, lessThan(lastMwIndex));
946     }
947 
948     @Test
testPreventSetAspectRatioWhileExpanding()949     public void testPreventSetAspectRatioWhileExpanding() throws Exception {
950         // Launch the PiP activity
951         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
952         waitForEnterPip(PIP_ACTIVITY);
953 
954         // Trigger it to go back to fullscreen and try to set the aspect ratio, and ensure that the
955         // call to set the aspect ratio did not prevent the PiP from returning to fullscreen
956         mBroadcastActionTrigger.expandPipWithAspectRatio("123456789", "100000000");
957         waitForExitPipToFullscreen(PIP_ACTIVITY);
958         assertPinnedStackDoesNotExist();
959     }
960 
961     @Test
testSetRequestedOrientationWhilePinned()962     public void testSetRequestedOrientationWhilePinned() throws Exception {
963         assumeTrue("Skipping test: no rotation support", supportsRotation());
964         // Launch the PiP activity fixed as portrait, and enter picture-in-picture
965         launchActivity(PIP_ACTIVITY,
966                 EXTRA_PIP_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT),
967                 EXTRA_ENTER_PIP, "true");
968         waitForEnterPip(PIP_ACTIVITY);
969         assertPinnedStackExists();
970 
971         // Request that the orientation is set to landscape
972         mBroadcastActionTrigger.requestOrientationForPip(ORIENTATION_LANDSCAPE);
973 
974         // Launch the activity back into fullscreen and ensure that it is now in landscape
975         launchActivity(PIP_ACTIVITY);
976         waitForExitPipToFullscreen(PIP_ACTIVITY);
977         assertPinnedStackDoesNotExist();
978         assertEquals(ORIENTATION_LANDSCAPE, mAmWmState.getWmState().getLastOrientation());
979     }
980 
981     @Test
testWindowButtonEntersPip()982     public void testWindowButtonEntersPip() throws Exception {
983         assumeTrue(!mAmWmState.getAmState().isHomeRecentsComponent());
984 
985         // Launch the PiP activity trigger the window button, ensure that we have entered PiP
986         launchActivity(PIP_ACTIVITY);
987         pressWindowButton();
988         waitForEnterPip(PIP_ACTIVITY);
989         assertPinnedStackExists();
990     }
991 
992     @Test
testFinishPipActivityWithTaskOverlay()993     public void testFinishPipActivityWithTaskOverlay() throws Exception {
994         // Launch PiP activity
995         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
996         waitForEnterPip(PIP_ACTIVITY);
997         assertPinnedStackExists();
998         int taskId = mAmWmState.getAmState().getStandardStackByWindowingMode(
999                 WINDOWING_MODE_PINNED).getTopTask().mTaskId;
1000 
1001         // Ensure that we don't any any other overlays as a result of launching into PIP
1002         launchHomeActivity();
1003 
1004         // Launch task overlay activity into PiP activity task
1005         launchPinnedActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId);
1006 
1007         // Finish the PiP activity and ensure that there is no pinned stack
1008         mBroadcastActionTrigger.doAction(ACTION_FINISH);
1009         waitForPinnedStackRemoved();
1010         assertPinnedStackDoesNotExist();
1011     }
1012 
1013     @Test
testNoResumeAfterTaskOverlayFinishes()1014     public void testNoResumeAfterTaskOverlayFinishes() throws Exception {
1015         // Launch PiP activity
1016         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
1017         waitForEnterPip(PIP_ACTIVITY);
1018         assertPinnedStackExists();
1019         ActivityStack stack = mAmWmState.getAmState().getStandardStackByWindowingMode(
1020                 WINDOWING_MODE_PINNED);
1021         int stackId = stack.mStackId;
1022         int taskId = stack.getTopTask().mTaskId;
1023 
1024         // Launch task overlay activity into PiP activity task
1025         launchPinnedActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId);
1026 
1027         // Finish the task overlay activity while animating and ensure that the PiP activity never
1028         // got resumed.
1029         separateTestJournal();
1030         SystemUtil.runWithShellPermissionIdentity(
1031                 () -> mAtm.resizeStack(stackId, new Rect(20, 20, 500, 500), true /* animate */));
1032         mBroadcastActionTrigger.doAction(TEST_ACTIVITY_ACTION_FINISH_SELF);
1033         mAmWmState.waitFor((amState, wmState) ->
1034                         !amState.containsActivity(TRANSLUCENT_TEST_ACTIVITY),
1035                 "Waiting for test activity to finish...");
1036         final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY);
1037         assertEquals("onResume", 0, lifecycleCounts.getCount(ActivityCallback.ON_RESUME));
1038         assertEquals("onPause", 0, lifecycleCounts.getCount(ActivityCallback.ON_PAUSE));
1039     }
1040 
1041     @Test
testPinnedStackWithDockedStack()1042     public void testPinnedStackWithDockedStack() throws Exception {
1043         assumeTrue(supportsSplitScreenMultiWindow());
1044 
1045         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
1046         waitForEnterPip(PIP_ACTIVITY);
1047         launchActivitiesInSplitScreen(
1048                 getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
1049                 getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)
1050                         .setRandomData(true)
1051                         .setMultipleTask(false)
1052         );
1053         mAmWmState.assertVisibility(PIP_ACTIVITY, true);
1054         mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
1055         mAmWmState.assertVisibility(TEST_ACTIVITY, true);
1056 
1057         // Launch the activities again to take focus and make sure nothing is hidden
1058         launchActivitiesInSplitScreen(
1059                 getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY),
1060                 getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY)
1061                         .setRandomData(true)
1062                         .setMultipleTask(false)
1063         );
1064         mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
1065         mAmWmState.assertVisibility(TEST_ACTIVITY, true);
1066 
1067         // Go to recents to make sure that fullscreen stack is invisible
1068         // Some devices do not support recents or implement it differently (instead of using a
1069         // separate stack id or as an activity), for those cases the visibility asserts will be
1070         // ignored
1071         pressAppSwitchButtonAndWaitForRecents();
1072         mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
1073         mAmWmState.assertVisibility(TEST_ACTIVITY, false);
1074     }
1075 
1076     @Test
testLaunchTaskByComponentMatchMultipleTasks()1077     public void testLaunchTaskByComponentMatchMultipleTasks() throws Exception {
1078         // Launch a fullscreen activity which will launch a PiP activity in a new task with the same
1079         // affinity
1080         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
1081         launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY);
1082         assertPinnedStackExists();
1083 
1084         // Launch the root activity again...
1085         int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivity(
1086                 TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId;
1087         launchHomeActivity();
1088         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
1089 
1090         // ...and ensure that the root activity task is found and reused, and that the pinned stack
1091         // is unaffected
1092         assertPinnedStackExists();
1093         mAmWmState.assertFocusedActivity("Expected root activity focused",
1094                 TEST_ACTIVITY_WITH_SAME_AFFINITY);
1095         assertEquals(rootActivityTaskId, mAmWmState.getAmState().getTaskByActivity(
1096                 TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId);
1097     }
1098 
1099     @Test
testLaunchTaskByAffinityMatchMultipleTasks()1100     public void testLaunchTaskByAffinityMatchMultipleTasks() throws Exception {
1101         // Launch a fullscreen activity which will launch a PiP activity in a new task with the same
1102         // affinity, and also launch another activity in the same task, while finishing itself. As
1103         // a result, the task will not have a component matching the same activity as what it was
1104         // started with
1105         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY,
1106                 EXTRA_START_ACTIVITY, getActivityName(TEST_ACTIVITY),
1107                 EXTRA_FINISH_SELF_ON_RESUME, "true");
1108         mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(TEST_ACTIVITY)
1109                 .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
1110                 .setActivityType(ACTIVITY_TYPE_STANDARD)
1111                 .build());
1112         launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY);
1113         waitForEnterPip(PIP_ACTIVITY_WITH_SAME_AFFINITY);
1114         assertPinnedStackExists();
1115 
1116         // Launch the root activity again...
1117         int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivity(
1118                 TEST_ACTIVITY).mTaskId;
1119         launchHomeActivity();
1120         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
1121 
1122         // ...and ensure that even while matching purely by task affinity, the root activity task is
1123         // found and reused, and that the pinned stack is unaffected
1124         assertPinnedStackExists();
1125         mAmWmState.assertFocusedActivity("Expected root activity focused", TEST_ACTIVITY);
1126         assertEquals(rootActivityTaskId, mAmWmState.getAmState().getTaskByActivity(
1127                 TEST_ACTIVITY).mTaskId);
1128     }
1129 
1130     @Test
testLaunchTaskByAffinityMatchSingleTask()1131     public void testLaunchTaskByAffinityMatchSingleTask() throws Exception {
1132         // Launch an activity into the pinned stack with a fixed affinity
1133         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY,
1134                 EXTRA_ENTER_PIP, "true",
1135                 EXTRA_START_ACTIVITY, getActivityName(PIP_ACTIVITY),
1136                 EXTRA_FINISH_SELF_ON_RESUME, "true");
1137         waitForEnterPip(TEST_ACTIVITY_WITH_SAME_AFFINITY);
1138         assertPinnedStackExists();
1139 
1140         // Launch the root activity again, of the matching task and ensure that we expand to
1141         // fullscreen
1142         int activityTaskId = mAmWmState.getAmState().getTaskByActivity(PIP_ACTIVITY).mTaskId;
1143         launchHomeActivity();
1144         launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
1145         waitForExitPipToFullscreen(TEST_ACTIVITY_WITH_SAME_AFFINITY);
1146         assertPinnedStackDoesNotExist();
1147         assertEquals(activityTaskId, mAmWmState.getAmState().getTaskByActivity(
1148                 PIP_ACTIVITY).mTaskId);
1149     }
1150 
1151     /** Test that reported display size corresponds to fullscreen after exiting PiP. */
1152     @FlakyTest
1153     @Test
testDisplayMetricsPinUnpin()1154     public void testDisplayMetricsPinUnpin() throws Exception {
1155         separateTestJournal();
1156         launchActivity(TEST_ACTIVITY);
1157         final int defaultWindowingMode = mAmWmState.getAmState()
1158                 .getTaskByActivity(TEST_ACTIVITY).getWindowingMode();
1159         final SizeInfo initialSizes = getLastReportedSizesForActivity(TEST_ACTIVITY);
1160         final Rect initialAppBounds = getAppBounds(TEST_ACTIVITY);
1161         assertNotNull("Must report display dimensions", initialSizes);
1162         assertNotNull("Must report app bounds", initialAppBounds);
1163 
1164         separateTestJournal();
1165         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
1166         // Wait for animation complete since we are comparing bounds
1167         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
1168         final SizeInfo pinnedSizes = getLastReportedSizesForActivity(PIP_ACTIVITY);
1169         final Rect pinnedAppBounds = getAppBounds(PIP_ACTIVITY);
1170         assertNotEquals("Reported display size when pinned must be different from default",
1171                 initialSizes, pinnedSizes);
1172         final Size initialAppSize = new Size(initialAppBounds.width(), initialAppBounds.height());
1173         final Size pinnedAppSize = new Size(pinnedAppBounds.width(), pinnedAppBounds.height());
1174         assertNotEquals("Reported app size when pinned must be different from default",
1175                 initialAppSize, pinnedAppSize);
1176 
1177         separateTestJournal();
1178         launchActivity(PIP_ACTIVITY, defaultWindowingMode);
1179         final SizeInfo finalSizes = getLastReportedSizesForActivity(PIP_ACTIVITY);
1180         final Rect finalAppBounds = getAppBounds(PIP_ACTIVITY);
1181         final Size finalAppSize = new Size(finalAppBounds.width(), finalAppBounds.height());
1182         assertEquals("Must report default size after exiting PiP", initialSizes, finalSizes);
1183         assertEquals("Must report default app size after exiting PiP", initialAppSize,
1184                 finalAppSize);
1185     }
1186 
1187     @Test
testEnterPictureInPictureSavePosition()1188     public void testEnterPictureInPictureSavePosition() throws Exception {
1189         // Ensure we have static shelf offset by running this test over a non-home activity
1190         launchActivity(NO_RELAUNCH_ACTIVITY);
1191         mAmWmState.waitForActivityState(mAmWmState.getAmState().getHomeActivityName(),
1192                 STATE_STOPPED);
1193 
1194         // Launch PiP activity with auto-enter PiP, save the default position of the PiP
1195         // (while the PiP is still animating sleep)
1196         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
1197         // Wait for animation complete since we are comparing bounds
1198         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
1199         assertPinnedStackExists();
1200 
1201         // Move the PiP to a new position on screen
1202         final Rect initialBounds = new Rect();
1203         final Rect offsetBounds = new Rect();
1204         offsetPipWithinMovementBounds(100 /* offsetY */, initialBounds, offsetBounds);
1205 
1206         // Expand the PiP back to fullscreen and back into PiP and ensure that it is in the same
1207         // position as before we expanded (and that the default bounds reflect that)
1208         mBroadcastActionTrigger.doAction(ACTION_EXPAND_PIP);
1209         waitForExitPipToFullscreen(PIP_ACTIVITY);
1210         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
1211         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
1212         mAmWmState.computeState(true);
1213         // Due to rounding in how we save and apply the snap fraction we may be a pixel off, so just
1214         // account for that in this check
1215         offsetBounds.inset(-1, -1);
1216         assertTrue("Expected offsetBounds=" + offsetBounds + " to contain bounds="
1217                 + getPinnedStackBounds(), offsetBounds.contains(getPinnedStackBounds()));
1218 
1219         // Expand the PiP, then launch an activity in a new task, and ensure that the PiP goes back
1220         // to the default position (and not the saved position) the next time it is launched
1221         mBroadcastActionTrigger.doAction(ACTION_EXPAND_PIP);
1222         waitForExitPipToFullscreen(PIP_ACTIVITY);
1223         launchActivity(TEST_ACTIVITY);
1224         mBroadcastActionTrigger.doAction(TEST_ACTIVITY_ACTION_FINISH_SELF);
1225         mAmWmState.waitForActivityState(PIP_ACTIVITY, STATE_RESUMED);
1226         mAmWmState.waitForAppTransitionIdleOnDisplay(DEFAULT_DISPLAY);
1227         mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
1228         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
1229         assertPinnedStackExists();
1230         assertTrue("Expected initialBounds=" + initialBounds + " to equal bounds="
1231                 + getPinnedStackBounds(), initialBounds.equals(getPinnedStackBounds()));
1232     }
1233 
1234     @Test
1235     @FlakyTest(bugId = 71792368)
testEnterPictureInPictureDiscardSavedPositionOnFinish()1236     public void testEnterPictureInPictureDiscardSavedPositionOnFinish() throws Exception {
1237         // Ensure we have static shelf offset by running this test over a non-home activity
1238         launchActivity(NO_RELAUNCH_ACTIVITY);
1239         mAmWmState.waitForActivityState(mAmWmState.getAmState().getHomeActivityName(),
1240                 STATE_STOPPED);
1241 
1242         // Launch PiP activity with auto-enter PiP, save the default position of the PiP
1243         // (while the PiP is still animating sleep)
1244         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
1245         // Wait for animation complete since we are comparing bounds
1246         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
1247         assertPinnedStackExists();
1248 
1249         // Move the PiP to a new position on screen
1250         final Rect initialBounds = new Rect();
1251         final Rect offsetBounds = new Rect();
1252         offsetPipWithinMovementBounds(100 /* offsetY */, initialBounds, offsetBounds);
1253 
1254         // Finish the activity
1255         mBroadcastActionTrigger.doAction(ACTION_FINISH);
1256         waitForPinnedStackRemoved();
1257         assertPinnedStackDoesNotExist();
1258 
1259         // Ensure that starting the same PiP activity after it finished will go to the default
1260         // bounds
1261         launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
1262         waitForEnterPipAnimationComplete(PIP_ACTIVITY);
1263         assertPinnedStackExists();
1264         assertTrue("Expected initialBounds=" + initialBounds + " to equal bounds="
1265                 + getPinnedStackBounds(), initialBounds.equals(getPinnedStackBounds()));
1266     }
1267 
1268     /**
1269      * Offsets the PiP in a direction by {@param offsetY} such that it is still within the movement
1270      * bounds.
1271      */
offsetPipWithinMovementBounds(int offsetY, Rect initialBoundsOut, Rect offsetBoundsOut)1272     private void offsetPipWithinMovementBounds(int offsetY, Rect initialBoundsOut,
1273             Rect offsetBoundsOut) {
1274         final ActivityStack stack = getPinnedStack();
1275         final Rect displayRect = mAmWmState.getWmState().getDisplay(stack.mDisplayId)
1276                 .getDisplayRect();
1277         initialBoundsOut.set(getPinnedStackBounds());
1278         offsetBoundsOut.set(initialBoundsOut);
1279         if (initialBoundsOut.top < displayRect.centerY()) {
1280             // If the default gravity is top-aligned, offset down instead of up
1281             offsetBoundsOut.offset(0, offsetY);
1282         } else {
1283             offsetBoundsOut.offset(0, -offsetY);
1284         }
1285         resizeStack(stack.mStackId, offsetBoundsOut.left, offsetBoundsOut.top,
1286                 offsetBoundsOut.right, offsetBoundsOut.bottom);
1287     }
1288 
1289     /** Get app bounds in last applied configuration. */
getAppBounds(ComponentName activityName)1290     private Rect getAppBounds(ComponentName activityName) {
1291         final Configuration config = TestJournalContainer.get(activityName).extras
1292                 .getParcelable(EXTRA_CONFIGURATION);
1293         if (config != null) {
1294             return config.windowConfiguration.getAppBounds();
1295         }
1296         return null;
1297     }
1298 
1299     /**
1300      * Called after the given {@param activityName} has been moved to the fullscreen stack. Ensures
1301      * that the stack matching the {@param windowingMode} and {@param activityType} is focused, and
1302      * checks the top and/or bottom tasks in the fullscreen stack if
1303      * {@param expectTopTaskHasActivity} or {@param expectBottomTaskHasActivity} are set
1304      * respectively.
1305      */
assertPinnedStackStateOnMoveToFullscreen(ComponentName activityName, int windowingMode, int activityType)1306     private void assertPinnedStackStateOnMoveToFullscreen(ComponentName activityName,
1307             int windowingMode, int activityType) {
1308         mAmWmState.waitForFocusedStack(windowingMode, activityType);
1309         mAmWmState.assertFocusedStack("Wrong focused stack", windowingMode, activityType);
1310         waitAndAssertActivityState(activityName, STATE_STOPPED,
1311                 "Activity should go to STOPPED");
1312         assertTrue(mAmWmState.getAmState().containsActivityInWindowingMode(
1313                 activityName, WINDOWING_MODE_FULLSCREEN));
1314         assertPinnedStackDoesNotExist();
1315     }
1316 
1317     /**
1318      * Asserts that the pinned stack bounds does not intersect with the IME bounds.
1319      */
assertPinnedStackDoesNotIntersectIME()1320     private void assertPinnedStackDoesNotIntersectIME() {
1321         // Ensure that the IME is visible
1322         WindowManagerState wmState = mAmWmState.getWmState();
1323         wmState.computeState();
1324         WindowManagerState.WindowState imeWinState = wmState.getInputMethodWindowState();
1325         assertTrue(imeWinState != null);
1326 
1327         // Ensure that the PIP movement is constrained by the display bounds intersecting the
1328         // non-IME bounds
1329         Rect imeContentFrame = imeWinState.getContentFrame();
1330         Rect imeContentInsets = imeWinState.getGivenContentInsets();
1331         Rect imeBounds = new Rect(imeContentFrame.left + imeContentInsets.left,
1332                 imeContentFrame.top + imeContentInsets.top,
1333                 imeContentFrame.right - imeContentInsets.width(),
1334                 imeContentFrame.bottom - imeContentInsets.height());
1335         wmState.computeState();
1336         Rect pipMovementBounds = wmState.getPinnedStackMovementBounds();
1337         assertTrue(!Rect.intersects(pipMovementBounds, imeBounds));
1338     }
1339 
1340     /**
1341      * Asserts that the pinned stack bounds is contained in the display bounds.
1342      */
assertPinnedStackActivityIsInDisplayBounds(ComponentName activityName)1343     private void assertPinnedStackActivityIsInDisplayBounds(ComponentName activityName) {
1344         final WindowManagerState.WindowState windowState = getWindowState(activityName);
1345         final WindowManagerState.Display display = mAmWmState.getWmState().getDisplay(
1346                 windowState.getDisplayId());
1347         final Rect displayRect = display.getDisplayRect();
1348         final Rect pinnedStackBounds = getPinnedStackBounds();
1349         assertTrue(displayRect.contains(pinnedStackBounds));
1350     }
1351 
1352     /**
1353      * Asserts that the pinned stack exists.
1354      */
assertPinnedStackExists()1355     private void assertPinnedStackExists() {
1356         mAmWmState.assertContainsStack("Must contain pinned stack.", WINDOWING_MODE_PINNED,
1357                 ACTIVITY_TYPE_STANDARD);
1358     }
1359 
1360     /**
1361      * Asserts that the pinned stack does not exist.
1362      */
assertPinnedStackDoesNotExist()1363     private void assertPinnedStackDoesNotExist() {
1364         mAmWmState.assertDoesNotContainStack("Must not contain pinned stack.",
1365                 WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
1366     }
1367 
1368     /**
1369      * Asserts that the pinned stack is the front stack.
1370      */
assertPinnedStackIsOnTop()1371     private void assertPinnedStackIsOnTop() {
1372         mAmWmState.assertFrontStack("Pinned stack must always be on top.",
1373                 WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
1374     }
1375 
1376     /**
1377      * Asserts that the activity received exactly one of each of the callbacks when entering and
1378      * exiting picture-in-picture.
1379      */
assertValidPictureInPictureCallbackOrder(ComponentName activityName)1380     private void assertValidPictureInPictureCallbackOrder(ComponentName activityName) {
1381         final ActivityLifecycleCounts lifecycles = new ActivityLifecycleCounts(activityName);
1382 
1383         assertEquals(getActivityName(activityName) + " onConfigurationChanged()",
1384                 1, lifecycles.getCount(ActivityCallback.ON_CONFIGURATION_CHANGED));
1385         assertEquals(getActivityName(activityName) + " onPictureInPictureModeChanged()",
1386                 1, lifecycles.getCount(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED));
1387         assertEquals(getActivityName(activityName) + " onMultiWindowModeChanged",
1388                 1, lifecycles.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
1389         final int lastPipIndex = lifecycles
1390                 .getLastIndex(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED);
1391         final int lastMwIndex = lifecycles
1392                 .getLastIndex(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED);
1393         final int lastConfigIndex = lifecycles
1394                 .getLastIndex(ActivityCallback.ON_CONFIGURATION_CHANGED);
1395         assertThat("onPictureInPictureModeChanged should be before onMultiWindowModeChanged",
1396                 lastPipIndex, lessThan(lastMwIndex));
1397         assertThat("onMultiWindowModeChanged should be before onConfigurationChanged",
1398                 lastMwIndex, lessThan(lastConfigIndex));
1399     }
1400 
1401     /**
1402      * Waits until the given activity has entered picture-in-picture mode (allowing for the
1403      * subsequent animation to start).
1404      */
waitForEnterPip(ComponentName activityName)1405     private void waitForEnterPip(ComponentName activityName) {
1406         mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
1407                 .setWindowingMode(WINDOWING_MODE_PINNED)
1408                 .setActivityType(ACTIVITY_TYPE_STANDARD)
1409                 .build());
1410     }
1411 
1412     /**
1413      * Waits until the picture-in-picture animation has finished.
1414      */
waitForEnterPipAnimationComplete(ComponentName activityName)1415     private void waitForEnterPipAnimationComplete(ComponentName activityName) {
1416         waitForEnterPip(activityName);
1417         mAmWmState.waitFor((amState, wmState) -> {
1418                 WindowStack stack = wmState.getStandardStackByWindowingMode(WINDOWING_MODE_PINNED);
1419                 return stack != null && !stack.mAnimatingBounds;
1420             }, "Waiting for pinned stack bounds animation to finish");
1421     }
1422 
1423     /**
1424      * Waits until the pinned stack has been removed.
1425      */
waitForPinnedStackRemoved()1426     private void waitForPinnedStackRemoved() {
1427         mAmWmState.waitFor((amState, wmState) -> {
1428             return !amState.containsStack(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD)
1429                     && !wmState.containsStack(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
1430         }, "Waiting for pinned stack to be removed...");
1431     }
1432 
1433     /**
1434      * Waits until the picture-in-picture animation to fullscreen has finished.
1435      */
waitForExitPipToFullscreen(ComponentName activityName)1436     private void waitForExitPipToFullscreen(ComponentName activityName) {
1437         mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
1438                 .setWindowingMode(WINDOWING_MODE_FULLSCREEN)
1439                 .setActivityType(ACTIVITY_TYPE_STANDARD)
1440                 .build());
1441     }
1442 
1443     /**
1444      * Waits until the expected picture-in-picture callbacks have been made.
1445      */
waitForValidPictureInPictureCallbacks(ComponentName activityName)1446     private void waitForValidPictureInPictureCallbacks(ComponentName activityName) {
1447         mAmWmState.waitFor((amState, wmState) -> {
1448             final ActivityLifecycleCounts lifecycles = new ActivityLifecycleCounts(activityName);
1449             return lifecycles.getCount(ActivityCallback.ON_CONFIGURATION_CHANGED) == 1
1450                     && lifecycles.getCount(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED) == 1
1451                     && lifecycles.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED) == 1;
1452         }, "Waiting for picture-in-picture activity callbacks...");
1453     }
1454 
waitForValidAspectRatio(int num, int denom)1455     private void waitForValidAspectRatio(int num, int denom) {
1456         // Hacky, but we need to wait for the auto-enter picture-in-picture animation to complete
1457         // and before we can check the pinned stack bounds
1458         mAmWmState.waitForWithAmState((state) -> {
1459             Rect bounds = state.getStandardStackByWindowingMode(WINDOWING_MODE_PINNED).getBounds();
1460             return floatEquals((float) bounds.width() / bounds.height(), (float) num / denom);
1461         }, "waitForValidAspectRatio");
1462     }
1463 
1464     /**
1465      * @return the window state for the given {@param activityName}'s window.
1466      */
getWindowState(ComponentName activityName)1467     private WindowManagerState.WindowState getWindowState(ComponentName activityName) {
1468         String windowName = getWindowName(activityName);
1469         mAmWmState.computeState(activityName);
1470         final List<WindowManagerState.WindowState> tempWindowList =
1471                 mAmWmState.getWmState().getMatchingVisibleWindowState(windowName);
1472         return tempWindowList.get(0);
1473     }
1474 
1475     /**
1476      * @return the current pinned stack.
1477      */
getPinnedStack()1478     private ActivityStack getPinnedStack() {
1479         return mAmWmState.getAmState().getStandardStackByWindowingMode(WINDOWING_MODE_PINNED);
1480     }
1481 
1482     /**
1483      * @return the current pinned stack bounds.
1484      */
getPinnedStackBounds()1485     private Rect getPinnedStackBounds() {
1486         return getPinnedStack().getBounds();
1487     }
1488 
1489     /**
1490      * Compares two floats with a common epsilon.
1491      */
assertFloatEquals(float actual, float expected)1492     private void assertFloatEquals(float actual, float expected) {
1493         if (!floatEquals(actual, expected)) {
1494             fail(expected + " not equal to " + actual);
1495         }
1496     }
1497 
floatEquals(float a, float b)1498     private boolean floatEquals(float a, float b) {
1499         return Math.abs(a - b) < FLOAT_COMPARE_EPSILON;
1500     }
1501 
1502     /**
1503      * Triggers a tap over the pinned stack bounds to trigger the PIP to close.
1504      */
tapToFinishPip()1505     private void tapToFinishPip() {
1506         Rect pinnedStackBounds = getPinnedStackBounds();
1507         int tapX = pinnedStackBounds.left + pinnedStackBounds.width() - 100;
1508         int tapY = pinnedStackBounds.top + pinnedStackBounds.height() - 100;
1509         tapOnDisplay(tapX, tapY, DEFAULT_DISPLAY);
1510     }
1511 
1512     /**
1513      * Launches the given {@param activityName} into the {@param taskId} as a task overlay.
1514      */
launchPinnedActivityAsTaskOverlay(ComponentName activityName, int taskId)1515     private void launchPinnedActivityAsTaskOverlay(ComponentName activityName, int taskId) {
1516         executeShellCommand(getAmStartCmd(activityName) + " --task " + taskId + " --task-overlay");
1517 
1518         mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName)
1519                 .setWindowingMode(WINDOWING_MODE_PINNED)
1520                 .setActivityType(ACTIVITY_TYPE_STANDARD)
1521                 .build());
1522     }
1523 
1524     private static class AppOpsSession implements AutoCloseable {
1525 
1526         private final String mPackageName;
1527 
AppOpsSession(ComponentName activityName)1528         AppOpsSession(ComponentName activityName) {
1529             mPackageName = activityName.getPackageName();
1530         }
1531 
1532         /**
1533          * Sets an app-ops op for a given package to a given mode.
1534          */
setOpToMode(String op, int mode)1535         void setOpToMode(String op, int mode) {
1536             try {
1537                 AppOpsUtils.setOpMode(mPackageName, op, mode);
1538             } catch (Exception e) {
1539                 e.printStackTrace();
1540             }
1541         }
1542 
1543         @Override
close()1544         public void close() {
1545             try {
1546                 AppOpsUtils.reset(mPackageName);
1547             } catch (IOException e) {
1548                 e.printStackTrace();
1549             }
1550         }
1551     }
1552 
1553     /**
1554      * TODO: Improve tests check to actually check that apps are not interactive instead of checking
1555      *       if the stack is focused.
1556      */
pinnedStackTester(String startActivityCmd, ComponentName startActivity, ComponentName topActivityName, boolean moveTopToPinnedStack, boolean isFocusable)1557     private void pinnedStackTester(String startActivityCmd, ComponentName startActivity,
1558             ComponentName topActivityName, boolean moveTopToPinnedStack, boolean isFocusable) {
1559         executeShellCommand(startActivityCmd);
1560         mAmWmState.waitForValidState(startActivity);
1561 
1562         if (moveTopToPinnedStack) {
1563             final int stackId = mAmWmState.getAmState().getStackIdByActivity(topActivityName);
1564 
1565             assertNotEquals(stackId, INVALID_STACK_ID);
1566             moveTopActivityToPinnedStack(stackId);
1567         }
1568 
1569         mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(topActivityName)
1570                 .setWindowingMode(WINDOWING_MODE_PINNED)
1571                 .setActivityType(ACTIVITY_TYPE_STANDARD)
1572                 .build());
1573         mAmWmState.computeState(true);
1574 
1575         if (supportsPip()) {
1576             final String windowName = getWindowName(topActivityName);
1577             assertPinnedStackExists();
1578             mAmWmState.assertFrontStack("Pinned stack must be the front stack.",
1579                     WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
1580             mAmWmState.assertVisibility(topActivityName, true);
1581 
1582             if (isFocusable) {
1583                 mAmWmState.assertFocusedStack("Pinned stack must be the focused stack.",
1584                         WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
1585                 mAmWmState.assertFocusedActivity(
1586                         "Pinned activity must be focused activity.", topActivityName);
1587                 mAmWmState.assertFocusedWindow(
1588                         "Pinned window must be focused window.", windowName);
1589                 // Not checking for resumed state here because PiP overlay can be launched on top
1590                 // in different task by SystemUI.
1591             } else {
1592                 // Don't assert that the stack is not focused as a focusable PiP overlay can be
1593                 // launched on top as a task overlay by SystemUI.
1594                 mAmWmState.assertNotFocusedActivity(
1595                         "Pinned activity can't be the focused activity.", topActivityName);
1596                 mAmWmState.assertNotResumedActivity(
1597                         "Pinned activity can't be the resumed activity.", topActivityName);
1598                 mAmWmState.assertNotFocusedWindow(
1599                         "Pinned window can't be focused window.", windowName);
1600             }
1601         } else {
1602             mAmWmState.assertDoesNotContainStack("Must not contain pinned stack.",
1603                     WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
1604         }
1605     }
1606 }
1607