/* * Copyright (C) 2016 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License */ package android.server.am; import static android.app.ActivityManager.StackId.INVALID_STACK_ID; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.server.am.ActivityManagerState.STATE_RESUMED; import static android.server.am.ActivityManagerState.STATE_STOPPED; import static android.server.am.ComponentNameUtils.getActivityName; import static android.server.am.ComponentNameUtils.getLogTag; import static android.server.am.ComponentNameUtils.getWindowName; import static android.server.am.Components.ALWAYS_FOCUSABLE_PIP_ACTIVITY; import static android.server.am.Components.LAUNCHING_ACTIVITY; import static android.server.am.Components.LAUNCH_ENTER_PIP_ACTIVITY; import static android.server.am.Components.LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY; import static android.server.am.Components.NON_RESIZEABLE_ACTIVITY; import static android.server.am.Components.NO_RELAUNCH_ACTIVITY; import static android.server.am.Components.PIP_ACTIVITY; import static android.server.am.Components.PIP_ACTIVITY2; import static android.server.am.Components.PIP_ACTIVITY_WITH_SAME_AFFINITY; import static android.server.am.Components.PIP_ON_STOP_ACTIVITY; import static android.server.am.Components.PipActivity.ACTION_ENTER_PIP; import static android.server.am.Components.PipActivity.ACTION_EXPAND_PIP; import static android.server.am.Components.PipActivity.ACTION_FINISH; import static android.server.am.Components.PipActivity.ACTION_MOVE_TO_BACK; import static android.server.am.Components.PipActivity.ACTION_SET_REQUESTED_ORIENTATION; import static android.server.am.Components.PipActivity.EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP; import static android.server.am.Components.PipActivity.EXTRA_ENTER_PIP; import static android.server.am.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR; import static android.server.am.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR; import static android.server.am.Components.PipActivity.EXTRA_ENTER_PIP_ON_PAUSE; import static android.server.am.Components.PipActivity.EXTRA_FINISH_SELF_ON_RESUME; import static android.server.am.Components.PipActivity.EXTRA_ON_PAUSE_DELAY; import static android.server.am.Components.PipActivity.EXTRA_PIP_ORIENTATION; import static android.server.am.Components.PipActivity.EXTRA_REENTER_PIP_ON_EXIT; import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_DENOMINATOR; import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_NUMERATOR; import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR; import static android.server.am.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR; import static android.server.am.Components.PipActivity.EXTRA_START_ACTIVITY; import static android.server.am.Components.PipActivity.EXTRA_TAP_TO_FINISH; import static android.server.am.Components.RESUME_WHILE_PAUSING_ACTIVITY; import static android.server.am.Components.TEST_ACTIVITY; import static android.server.am.Components.TEST_ACTIVITY_WITH_SAME_AFFINITY; import static android.server.am.Components.TRANSLUCENT_TEST_ACTIVITY; import static android.server.am.Components.TestActivity.EXTRA_FIXED_ORIENTATION; import static android.server.am.Components.TestActivity.TEST_ACTIVITY_ACTION_FINISH_SELF; import static android.server.am.UiDeviceUtils.pressWindowButton; import static android.view.Display.DEFAULT_DISPLAY; import static org.hamcrest.Matchers.lessThan; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.junit.Assert.fail; import static org.junit.Assume.assumeTrue; import android.content.ComponentName; import android.content.Context; import android.database.ContentObserver; import android.graphics.Rect; import android.os.Handler; import android.os.Looper; import android.platform.test.annotations.Presubmit; import android.provider.Settings; import android.server.am.ActivityManagerState.ActivityStack; import android.server.am.ActivityManagerState.ActivityTask; import android.server.am.WindowManagerState.WindowStack; import android.server.am.settings.SettingsSession; import android.support.test.filters.FlakyTest; import android.support.test.InstrumentationRegistry; import android.util.Log; import android.util.Size; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.regex.Matcher; import java.util.regex.Pattern; import org.junit.Ignore; import org.junit.Test; /** * Build/Install/Run: * atest CtsActivityManagerDeviceTestCases:ActivityManagerPinnedStackTests */ @FlakyTest(bugId = 71792368) public class ActivityManagerPinnedStackTests extends ActivityManagerTestBase { private static final String TAG = ActivityManagerPinnedStackTests.class.getSimpleName(); private static final String APP_OPS_OP_ENTER_PICTURE_IN_PICTURE = "PICTURE_IN_PICTURE"; private static final int APP_OPS_MODE_ALLOWED = 0; private static final int APP_OPS_MODE_IGNORED = 1; private static final int APP_OPS_MODE_ERRORED = 2; private static final int ROTATION_0 = 0; private static final int ROTATION_90 = 1; private static final int ROTATION_180 = 2; private static final int ROTATION_270 = 3; // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE private static final int ORIENTATION_LANDSCAPE = 0; // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT private static final int ORIENTATION_PORTRAIT = 1; private static final float FLOAT_COMPARE_EPSILON = 0.005f; // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio private static final int MIN_ASPECT_RATIO_NUMERATOR = 100; private static final int MIN_ASPECT_RATIO_DENOMINATOR = 239; private static final int BELOW_MIN_ASPECT_RATIO_DENOMINATOR = MIN_ASPECT_RATIO_DENOMINATOR + 1; // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio private static final int MAX_ASPECT_RATIO_NUMERATOR = 239; private static final int MAX_ASPECT_RATIO_DENOMINATOR = 100; private static final int ABOVE_MAX_ASPECT_RATIO_NUMERATOR = MAX_ASPECT_RATIO_NUMERATOR + 1; @Test public void testMinimumDeviceSize() throws Exception { assumeTrue(supportsPip()); mAmWmState.assertDeviceDefaultDisplaySize( "Devices supporting picture-in-picture must be larger than the default minimum" + " task size"); } @Presubmit @Test public void testEnterPictureInPictureMode() throws Exception { pinnedStackTester(getAmStartCmd(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"), PIP_ACTIVITY, PIP_ACTIVITY, false /* moveTopToPinnedStack */, false /* isFocusable */); } @FlakyTest(bugId = 71444628) @Presubmit @Test public void testMoveTopActivityToPinnedStack() throws Exception { pinnedStackTester(getAmStartCmd(PIP_ACTIVITY), PIP_ACTIVITY, PIP_ACTIVITY, true /* moveTopToPinnedStack */, false /* isFocusable */); } // This test is black-listed in cts-known-failures.xml (b/35314835). @Ignore @Test public void testAlwaysFocusablePipActivity() throws Exception { pinnedStackTester(getAmStartCmd(ALWAYS_FOCUSABLE_PIP_ACTIVITY), ALWAYS_FOCUSABLE_PIP_ACTIVITY, ALWAYS_FOCUSABLE_PIP_ACTIVITY, false /* moveTopToPinnedStack */, true /* isFocusable */); } // This test is black-listed in cts-known-failures.xml (b/35314835). @Ignore @Presubmit @Test public void testLaunchIntoPinnedStack() throws Exception { pinnedStackTester(getAmStartCmd(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY), LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY, ALWAYS_FOCUSABLE_PIP_ACTIVITY, false /* moveTopToPinnedStack */, true /* isFocusable */); } @Test public void testNonTappablePipActivity() throws Exception { assumeTrue(supportsPip()); // Launch the tap-to-finish activity at a specific place launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_TAP_TO_FINISH, "true"); // Wait for animation complete since we are tapping on specific bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); // Tap the screen at a known location in the pinned stack bounds, and ensure that it is // not passed down to the top task tapToFinishPip(); mAmWmState.computeState(false /* compareTaskAndStackBounds */, new WaitForValidActivityState(PIP_ACTIVITY)); mAmWmState.assertVisibility(PIP_ACTIVITY, true); } @Test public void testPinnedStackDefaultBounds() throws Exception { assumeTrue(supportsPip()); // Launch a PIP activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); // Wait for animation complete since we are comparing bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); try (final RotationSession rotationSession = new RotationSession()) { rotationSession.set(ROTATION_0); WindowManagerState wmState = mAmWmState.getWmState(); wmState.computeState(); Rect defaultPipBounds = wmState.getDefaultPinnedStackBounds(); Rect stableBounds = wmState.getStableBounds(); assertTrue(defaultPipBounds.width() > 0 && defaultPipBounds.height() > 0); assertTrue(stableBounds.contains(defaultPipBounds)); rotationSession.set(ROTATION_90); wmState = mAmWmState.getWmState(); wmState.computeState(); defaultPipBounds = wmState.getDefaultPinnedStackBounds(); stableBounds = wmState.getStableBounds(); assertTrue(defaultPipBounds.width() > 0 && defaultPipBounds.height() > 0); assertTrue(stableBounds.contains(defaultPipBounds)); } } @Test public void testPinnedStackMovementBounds() throws Exception { assumeTrue(supportsPip()); // Launch a PIP activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); // Wait for animation complete since we are comparing bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); try (final RotationSession rotationSession = new RotationSession()) { rotationSession.set(ROTATION_0); WindowManagerState wmState = mAmWmState.getWmState(); wmState.computeState(); Rect pipMovementBounds = wmState.getPinnedStackMovementBounds(); Rect stableBounds = wmState.getStableBounds(); assertTrue(pipMovementBounds.width() > 0 && pipMovementBounds.height() > 0); assertTrue(stableBounds.contains(pipMovementBounds)); rotationSession.set(ROTATION_90); wmState = mAmWmState.getWmState(); wmState.computeState(); pipMovementBounds = wmState.getPinnedStackMovementBounds(); stableBounds = wmState.getStableBounds(); assertTrue(pipMovementBounds.width() > 0 && pipMovementBounds.height() > 0); assertTrue(stableBounds.contains(pipMovementBounds)); } } @Test @FlakyTest // TODO: Reintroduce to presubmit once b/71508234 is resolved. public void testPinnedStackOutOfBoundsInsetsNonNegative() throws Exception { assumeTrue(supportsPip()); final WindowManagerState wmState = mAmWmState.getWmState(); // Launch an activity into the pinned stack launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_TAP_TO_FINISH, "true"); // Wait for animation complete since we are comparing bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); // Get the display dimensions WindowManagerState.WindowState windowState = getWindowState(PIP_ACTIVITY); WindowManagerState.Display display = wmState.getDisplay(windowState.getDisplayId()); Rect displayRect = display.getDisplayRect(); // Move the pinned stack offscreen final int stackId = getPinnedStack().mStackId; final int top = 0; final int left = displayRect.width() - 200; resizeStack(stackId, left, top, left + 500, top + 500); // Ensure that the surface insets are not negative windowState = getWindowState(PIP_ACTIVITY); Rect contentInsets = windowState.getContentInsets(); if (contentInsets != null) { assertTrue(contentInsets.left >= 0 && contentInsets.top >= 0 && contentInsets.width() >= 0 && contentInsets.height() >= 0); } } @Test public void testPinnedStackInBoundsAfterRotation() throws Exception { assumeTrue(supportsPip()); // Launch an activity into the pinned stack launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_TAP_TO_FINISH, "true"); // Wait for animation complete since we are comparing bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); // Ensure that the PIP stack is fully visible in each orientation try (final RotationSession rotationSession = new RotationSession()) { rotationSession.set(ROTATION_0); assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); rotationSession.set(ROTATION_90); assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); rotationSession.set(ROTATION_180); assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); rotationSession.set(ROTATION_270); assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); } } @Test public void testEnterPipToOtherOrientation() throws Exception { assumeTrue(supportsPip()); // Launch a portrait only app on the fullscreen stack launchActivity(TEST_ACTIVITY, EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT)); // Launch the PiP activity fixed as landscape launchActivity(PIP_ACTIVITY, EXTRA_PIP_ORIENTATION, String.valueOf(ORIENTATION_LANDSCAPE)); // Enter PiP, and assert that the PiP is within bounds now that the device is back in // portrait executeShellCommand("am broadcast -a " + ACTION_ENTER_PIP); // Wait for animation complete since we are comparing bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); } @Test public void testEnterPipAspectRatioMin() throws Exception { testEnterPipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR); } @Test public void testEnterPipAspectRatioMax() throws Exception { testEnterPipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR); } private void testEnterPipAspectRatio(int num, int denom) throws Exception { assumeTrue(supportsPip()); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num), EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)); // Wait for animation complete since we are comparing aspect ratio waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); // Assert that we have entered PIP and that the aspect ratio is correct Rect pinnedStackBounds = getPinnedStackBounds(); assertFloatEquals((float) pinnedStackBounds.width() / pinnedStackBounds.height(), (float) num / denom); } @Test public void testResizePipAspectRatioMin() throws Exception { testResizePipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR); } @Test public void testResizePipAspectRatioMax() throws Exception { testResizePipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR); } private void testResizePipAspectRatio(int num, int denom) throws Exception { assumeTrue(supportsPip()); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num), EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)); // Wait for animation complete since we are comparing aspect ratio waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); waitForValidAspectRatio(num, denom); Rect bounds = getPinnedStackBounds(); assertFloatEquals((float) bounds.width() / bounds.height(), (float) num / denom); } @Test public void testEnterPipExtremeAspectRatioMin() throws Exception { testEnterPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, BELOW_MIN_ASPECT_RATIO_DENOMINATOR); } @Test public void testEnterPipExtremeAspectRatioMax() throws Exception { testEnterPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR); } private void testEnterPipExtremeAspectRatio(int num, int denom) throws Exception { assumeTrue(supportsPip()); // Assert that we could not create a pinned stack with an extreme aspect ratio launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num), EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)); assertPinnedStackDoesNotExist(); } @Test public void testSetPipExtremeAspectRatioMin() throws Exception { testSetPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, BELOW_MIN_ASPECT_RATIO_DENOMINATOR); } @Test public void testSetPipExtremeAspectRatioMax() throws Exception { testSetPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR); } private void testSetPipExtremeAspectRatio(int num, int denom) throws Exception { assumeTrue(supportsPip()); // Try to resize the a normal pinned stack to an extreme aspect ratio and ensure that // fails (the aspect ratio remains the same) launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(MAX_ASPECT_RATIO_NUMERATOR), EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR), EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num), EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)); // Wait for animation complete since we are comparing aspect ratio waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); Rect pinnedStackBounds = getPinnedStackBounds(); assertFloatEquals((float) pinnedStackBounds.width() / pinnedStackBounds.height(), (float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR); } @Test public void testDisallowPipLaunchFromStoppedActivity() throws Exception { assumeTrue(supportsPip()); // Launch the bottom pip activity which will launch a new activity on top and attempt to // enter pip when it is stopped launchActivity(PIP_ON_STOP_ACTIVITY); // Wait for the bottom pip activity to be stopped mAmWmState.waitForActivityState(PIP_ON_STOP_ACTIVITY, STATE_STOPPED); // Assert that there is no pinned stack (that enterPictureInPicture() failed) assertPinnedStackDoesNotExist(); } @Test public void testAutoEnterPictureInPicture() throws Exception { assumeTrue(supportsPip()); // Launch a test activity so that we're not over home launchActivity(TEST_ACTIVITY); // Launch the PIP activity on pause launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true"); assertPinnedStackDoesNotExist(); // Go home and ensure that there is a pinned stack launchHomeActivity(); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); } @Test public void testAutoEnterPictureInPictureLaunchActivity() throws Exception { assumeTrue(supportsPip()); // Launch a test activity so that we're not over home launchActivity(TEST_ACTIVITY); // Launch the PIP activity on pause, and have it start another activity on // top of itself. Wait for the new activity to be visible and ensure that the pinned stack // was not created in the process launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true", EXTRA_START_ACTIVITY, getActivityName(NON_RESIZEABLE_ACTIVITY)); mAmWmState.computeState(false /* compareTaskAndStackBounds */, new WaitForValidActivityState(NON_RESIZEABLE_ACTIVITY)); assertPinnedStackDoesNotExist(); // Go home while the pip activity is open and ensure the previous activity is not PIPed launchHomeActivity(); assertPinnedStackDoesNotExist(); } @Test public void testAutoEnterPictureInPictureFinish() throws Exception { assumeTrue(supportsPip()); // Launch a test activity so that we're not over home launchActivity(TEST_ACTIVITY); // Launch the PIP activity on pause, and set it to finish itself after // some period. Wait for the previous activity to be visible, and ensure that the pinned // stack was not created in the process launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true", EXTRA_FINISH_SELF_ON_RESUME, "true"); assertPinnedStackDoesNotExist(); } @Presubmit @Test public void testAutoEnterPictureInPictureAspectRatio() throws Exception { assumeTrue(supportsPip()); // Launch the PIP activity on pause, and set the aspect ratio launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true", EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(MAX_ASPECT_RATIO_NUMERATOR), EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR)); // Go home while the pip activity is open to trigger auto-PIP launchHomeActivity(); // Wait for animation complete since we are comparing aspect ratio waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); waitForValidAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR); Rect bounds = getPinnedStackBounds(); assertFloatEquals((float) bounds.width() / bounds.height(), (float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR); } @Presubmit @Test public void testAutoEnterPictureInPictureOverPip() throws Exception { assumeTrue(supportsPip()); // Launch another PIP activity launchActivity(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY); waitForEnterPip(ALWAYS_FOCUSABLE_PIP_ACTIVITY); assertPinnedStackExists(); // Launch the PIP activity on pause launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true"); // Go home while the PIP activity is open to try to trigger auto-enter PIP launchHomeActivity(); assertPinnedStackExists(); // Ensure that auto-enter pip failed and that the resumed activity in the pinned stack is // still the first activity final ActivityStack pinnedStack = getPinnedStack(); assertEquals(1, pinnedStack.getTasks().size()); assertEquals(getActivityName(ALWAYS_FOCUSABLE_PIP_ACTIVITY), pinnedStack.getTasks().get(0).mRealActivity); } @Presubmit @Test public void testDisallowMultipleTasksInPinnedStack() throws Exception { assumeTrue(supportsPip()); // Launch a test activity so that we have multiple fullscreen tasks launchActivity(TEST_ACTIVITY); // Launch first PIP activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); // Launch second PIP activity launchActivity(PIP_ACTIVITY2, EXTRA_ENTER_PIP, "true"); final ActivityStack pinnedStack = getPinnedStack(); assertEquals(1, pinnedStack.getTasks().size()); assertTrue(mAmWmState.getAmState().containsActivityInWindowingMode( PIP_ACTIVITY2, WINDOWING_MODE_PINNED)); assertTrue(mAmWmState.getAmState().containsActivityInWindowingMode( PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN)); } @Test public void testPipUnPipOverHome() throws Exception { assumeTrue(supportsPip()); // Go home launchHomeActivity(); // Launch an auto pip activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_REENTER_PIP_ON_EXIT, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip launchActivity(PIP_ACTIVITY); mAmWmState.waitForWithAmState(amState -> amState.getFrontStackWindowingMode(DEFAULT_DISPLAY) == WINDOWING_MODE_FULLSCREEN, "Waiting for PIP to exit to fullscreen"); mAmWmState.waitForWithAmState(amState -> amState.getFrontStackWindowingMode(DEFAULT_DISPLAY) == WINDOWING_MODE_PINNED, "Waiting to re-enter PIP"); mAmWmState.assertHomeActivityVisible(true); } @Test public void testPipUnPipOverApp() throws Exception { assumeTrue(supportsPip()); // Launch a test activity so that we're not over home launchActivity(TEST_ACTIVITY); // Launch an auto pip activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true", EXTRA_REENTER_PIP_ON_EXIT, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip launchActivity(PIP_ACTIVITY); mAmWmState.waitForWithAmState(amState -> amState.getFrontStackWindowingMode(DEFAULT_DISPLAY) == WINDOWING_MODE_FULLSCREEN, "Waiting for PIP to exit to fullscreen"); waitForEnterPipAnimationComplete(PIP_ACTIVITY); mAmWmState.assertVisibility(TEST_ACTIVITY, true); } @Presubmit @Test public void testRemovePipWithNoFullscreenStack() throws Exception { assumeTrue(supportsPip()); // Launch a pip activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Remove the stack and ensure that the task is now in the fullscreen stack (when no // fullscreen stack existed before) removeStacksInWindowingModes(WINDOWING_MODE_PINNED); assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME); } @Presubmit @Test public void testRemovePipWithVisibleFullscreenStack() throws Exception { assumeTrue(supportsPip()); // Launch a fullscreen activity, and a pip activity over that launchActivity(TEST_ACTIVITY); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Remove the stack and ensure that the task is placed in the fullscreen stack, behind the // top fullscreen activity removeStacksInWindowingModes(WINDOWING_MODE_PINNED); assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); } @FlakyTest(bugId = 70746098) @Presubmit @Test public void testRemovePipWithHiddenFullscreenStack() throws Exception { assumeTrue(supportsPip()); // Launch a fullscreen activity, return home and while the fullscreen stack is hidden, // launch a pip activity over home launchActivity(TEST_ACTIVITY); launchHomeActivity(); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Remove the stack and ensure that the task is placed on top of the hidden fullscreen // stack, but that the home stack is still focused removeStacksInWindowingModes(WINDOWING_MODE_PINNED); assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME); } @Test public void testMovePipToBackWithNoFullscreenStack() throws Exception { assumeTrue(supportsPip()); // Start with a clean slate, remove all the stacks but home removeStacksWithActivityTypes(ALL_ACTIVITY_TYPE_BUT_HOME); // Launch a pip activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Remove the stack and ensure that the task is now in the fullscreen stack (when no // fullscreen stack existed before) executeShellCommand("am broadcast -a " + ACTION_MOVE_TO_BACK); assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME); } @FlakyTest(bugId = 70906499) @Presubmit @Test public void testMovePipToBackWithVisibleFullscreenStack() throws Exception { assumeTrue(supportsPip()); // Launch a fullscreen activity, and a pip activity over that launchActivity(TEST_ACTIVITY); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Remove the stack and ensure that the task is placed in the fullscreen stack, behind the // top fullscreen activity executeShellCommand("am broadcast -a " + ACTION_MOVE_TO_BACK); assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD); } @FlakyTest(bugId = 70906499) @Presubmit @Test public void testMovePipToBackWithHiddenFullscreenStack() throws Exception { assumeTrue(supportsPip()); // Launch a fullscreen activity, return home and while the fullscreen stack is hidden, // launch a pip activity over home launchActivity(TEST_ACTIVITY); launchHomeActivity(); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Remove the stack and ensure that the task is placed on top of the hidden fullscreen // stack, but that the home stack is still focused executeShellCommand("am broadcast -a " + ACTION_MOVE_TO_BACK); assertPinnedStackStateOnMoveToFullscreen( PIP_ACTIVITY, WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME); } @Test public void testPinnedStackAlwaysOnTop() throws Exception { assumeTrue(supportsPip()); // Launch activity into pinned stack and assert it's on top. launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); assertPinnedStackIsOnTop(); // Launch another activity in fullscreen stack and check that pinned stack is still on top. launchActivity(TEST_ACTIVITY); assertPinnedStackExists(); assertPinnedStackIsOnTop(); // Launch home and check that pinned stack is still on top. launchHomeActivity(); assertPinnedStackExists(); assertPinnedStackIsOnTop(); } @Test public void testAppOpsDenyPipOnPause() throws Exception { assumeTrue(supportsPip()); try (final AppOpsSession appOpsSession = new AppOpsSession(PIP_ACTIVITY)) { // Disable enter-pip and try to enter pip appOpsSession.setOpToMode(APP_OPS_OP_ENTER_PICTURE_IN_PICTURE, APP_OPS_MODE_IGNORED); // Launch the PIP activity on pause launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); assertPinnedStackDoesNotExist(); // Go home and ensure that there is no pinned stack launchHomeActivity(); assertPinnedStackDoesNotExist(); } } @Test public void testEnterPipFromTaskWithMultipleActivities() throws Exception { assumeTrue(supportsPip()); // Try to enter picture-in-picture from an activity that has more than one activity in the // task and ensure that it works launchActivity(LAUNCH_ENTER_PIP_ACTIVITY); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); } @Test public void testEnterPipWithResumeWhilePausingActivityNoStop() throws Exception { assumeTrue(supportsPip()); /* * Launch the resumeWhilePausing activity and ensure that the PiP activity did not get * stopped and actually went into the pinned stack. * * Note that this is a workaround because to trigger the path that we want to happen in * activity manager, we need to add the leaving activity to the stopping state, which only * happens when a hidden stack is brought forward. Normally, this happens when you go home, * but since we can't launch into the home stack directly, we have a workaround. * * 1) Launch an activity in a new dynamic stack * 2) Resize the dynamic stack to non-fullscreen bounds * 3) Start the PiP activity that will enter picture-in-picture when paused in the * fullscreen stack * 4) Bring the activity in the dynamic stack forward to trigger PiP */ int stackId = launchActivityInNewDynamicStack(RESUME_WHILE_PAUSING_ACTIVITY); resizeStack(stackId, 0, 0, 500, 500); // Launch an activity that will enter PiP when it is paused with a delay that is long enough // for the next resumeWhilePausing activity to finish resuming, but slow enough to not // trigger the current system pause timeout (currently 500ms) launchActivity(PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN, EXTRA_ENTER_PIP_ON_PAUSE, "true", EXTRA_ON_PAUSE_DELAY, "350", EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true"); launchActivity(RESUME_WHILE_PAUSING_ACTIVITY); assertPinnedStackExists(); } @Test public void testDisallowEnterPipActivityLocked() throws Exception { assumeTrue(supportsPip()); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true"); ActivityTask task = mAmWmState.getAmState().getStandardStackByWindowingMode( WINDOWING_MODE_FULLSCREEN).getTopTask(); // Lock the task and ensure that we can't enter picture-in-picture both explicitly and // when paused executeShellCommand("am task lock " + task.mTaskId); executeShellCommand("am broadcast -a " + ACTION_ENTER_PIP); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackDoesNotExist(); launchHomeActivity(); assertPinnedStackDoesNotExist(); executeShellCommand("am task lock stop"); } @FlakyTest(bugId = 70328524) @Presubmit @Test public void testConfigurationChangeOrderDuringTransition() throws Exception { assumeTrue(supportsPip()); // Launch a PiP activity and ensure configuration change only happened once, and that the // configuration change happened after the picture-in-picture and multi-window callbacks launchActivity(PIP_ACTIVITY); LogSeparator logSeparator = separateLogs(); executeShellCommand("am broadcast -a " + ACTION_ENTER_PIP); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); waitForValidPictureInPictureCallbacks(PIP_ACTIVITY, logSeparator); assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, logSeparator); // Trigger it to go back to fullscreen and ensure that only triggered one configuration // change as well logSeparator = separateLogs(); launchActivity(PIP_ACTIVITY); waitForValidPictureInPictureCallbacks(PIP_ACTIVITY, logSeparator); assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, logSeparator); } /** Helper class to save, set, and restore transition_animation_scale preferences. */ private static class TransitionAnimationScaleSession extends SettingsSession { TransitionAnimationScaleSession() { super(Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE), Settings.Global::getFloat, Settings.Global::putFloat); } @Override public void close() throws Exception { // Wait for the restored setting to apply before we continue on with the next test final CountDownLatch waitLock = new CountDownLatch(1); final Context context = InstrumentationRegistry.getTargetContext(); context.getContentResolver().registerContentObserver(mUri, false, new ContentObserver(new Handler(Looper.getMainLooper())) { @Override public void onChange(boolean selfChange) { waitLock.countDown(); } }); super.close(); if (!waitLock.await(2, TimeUnit.SECONDS)) { Log.i(TAG, "TransitionAnimationScaleSession value not restored"); } } } @Test public void testEnterPipInterruptedCallbacks() throws Exception { assumeTrue(supportsPip()); try (final TransitionAnimationScaleSession transitionAnimationScaleSession = new TransitionAnimationScaleSession()) { // Slow down the transition animations for this test transitionAnimationScaleSession.set(20f); // Launch a PiP activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); // Wait until the PiP activity has moved into the pinned stack (happens before the // transition has started) waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Relaunch the PiP activity back into fullscreen LogSeparator logSeparator = separateLogs(); launchActivity(PIP_ACTIVITY); // Wait until the PiP activity is reparented into the fullscreen stack (happens after // the transition has finished) waitForExitPipToFullscreen(PIP_ACTIVITY); // Ensure that we get the callbacks indicating that PiP/MW mode was cancelled, but no // configuration change (since none was sent) final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts( PIP_ACTIVITY, logSeparator); assertEquals("onConfigurationChanged", 0, lifecycleCounts.mConfigurationChangedCount); assertEquals("onPictureInPictureModeChanged", 1, lifecycleCounts.mPictureInPictureModeChangedCount); assertEquals("onMultiWindowModeChanged", 1, lifecycleCounts.mMultiWindowModeChangedCount); } } @FlakyTest(bugId = 71564769) @Presubmit @Test public void testStopBeforeMultiWindowCallbacksOnDismiss() throws Exception { assumeTrue(supportsPip()); // Launch a PiP activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Dismiss it LogSeparator logSeparator = separateLogs(); removeStacksInWindowingModes(WINDOWING_MODE_PINNED); waitForExitPipToFullscreen(PIP_ACTIVITY); // Confirm that we get stop before the multi-window and picture-in-picture mode change // callbacks final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY, logSeparator); assertEquals("onStop", 1, lifecycleCounts.mStopCount); assertEquals("onPictureInPictureModeChanged", 1, lifecycleCounts.mPictureInPictureModeChangedCount); assertEquals("onMultiWindowModeChanged", 1, lifecycleCounts.mMultiWindowModeChangedCount); final int lastStopLine = lifecycleCounts.mLastStopLineIndex; final int lastPipLine = lifecycleCounts.mLastPictureInPictureModeChangedLineIndex; final int lastMwLine = lifecycleCounts.mLastMultiWindowModeChangedLineIndex; assertThat("onStop should be before onPictureInPictureModeChanged", lastStopLine, lessThan(lastPipLine)); assertThat("onPictureInPictureModeChanged should be before onMultiWindowModeChanged", lastPipLine, lessThan(lastMwLine)); } @Test public void testPreventSetAspectRatioWhileExpanding() throws Exception { assumeTrue(supportsPip()); // Launch the PiP activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); // Trigger it to go back to fullscreen and try to set the aspect ratio, and ensure that the // call to set the aspect ratio did not prevent the PiP from returning to fullscreen executeShellCommand("am broadcast -a " + ACTION_EXPAND_PIP + " -e " + EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR + " 123456789" + " -e " + EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR + " 100000000"); waitForExitPipToFullscreen(PIP_ACTIVITY); assertPinnedStackDoesNotExist(); } @Test public void testSetRequestedOrientationWhilePinned() throws Exception { assumeTrue(supportsPip()); // Launch the PiP activity fixed as portrait, and enter picture-in-picture launchActivity(PIP_ACTIVITY, EXTRA_PIP_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT), EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); // Request that the orientation is set to landscape executeShellCommand("am broadcast -a " + ACTION_SET_REQUESTED_ORIENTATION + " -e " + EXTRA_PIP_ORIENTATION + " " + String.valueOf(ORIENTATION_LANDSCAPE)); // Launch the activity back into fullscreen and ensure that it is now in landscape launchActivity(PIP_ACTIVITY); waitForExitPipToFullscreen(PIP_ACTIVITY); assertPinnedStackDoesNotExist(); assertEquals(ORIENTATION_LANDSCAPE, mAmWmState.getWmState().getLastOrientation()); } @Test public void testWindowButtonEntersPip() throws Exception { assumeTrue(supportsPip()); assumeTrue(!mAmWmState.getAmState().isHomeRecentsComponent()); // Launch the PiP activity trigger the window button, ensure that we have entered PiP launchActivity(PIP_ACTIVITY); pressWindowButton(); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); } @Test public void testFinishPipActivityWithTaskOverlay() throws Exception { assumeTrue(supportsPip()); // Launch PiP activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); int taskId = mAmWmState.getAmState().getStandardStackByWindowingMode( WINDOWING_MODE_PINNED).getTopTask().mTaskId; // Ensure that we don't any any other overlays as a result of launching into PIP launchHomeActivity(); // Launch task overlay activity into PiP activity task launchPinnedActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId); // Finish the PiP activity and ensure that there is no pinned stack executeShellCommand("am broadcast -a " + ACTION_FINISH); waitForPinnedStackRemoved(); assertPinnedStackDoesNotExist(); } @Test public void testNoResumeAfterTaskOverlayFinishes() throws Exception { assumeTrue(supportsPip()); // Launch PiP activity launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); assertPinnedStackExists(); ActivityStack stack = mAmWmState.getAmState().getStandardStackByWindowingMode( WINDOWING_MODE_PINNED); int stackId = stack.mStackId; int taskId = stack.getTopTask().mTaskId; // Launch task overlay activity into PiP activity task launchPinnedActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId); // Finish the task overlay activity while animating and ensure that the PiP activity never // got resumed LogSeparator logSeparator = separateLogs(); executeShellCommand("am stack resize-animated " + stackId + " 20 20 500 500"); executeShellCommand("am broadcast -a " + TEST_ACTIVITY_ACTION_FINISH_SELF); mAmWmState.waitFor((amState, wmState) -> !amState.containsActivity(TRANSLUCENT_TEST_ACTIVITY), "Waiting for test activity to finish..."); final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY, logSeparator); assertEquals("onResume", 0, lifecycleCounts.mResumeCount); assertEquals("onPause", 0, lifecycleCounts.mPauseCount); } @Test public void testPinnedStackWithDockedStack() throws Exception { assumeTrue(supportsPip()); assumeTrue(supportsSplitScreenMultiWindow()); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPip(PIP_ACTIVITY); launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY) .setRandomData(true) .setMultipleTask(false) ); mAmWmState.assertVisibility(PIP_ACTIVITY, true); mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true); mAmWmState.assertVisibility(TEST_ACTIVITY, true); // Launch the activities again to take focus and make sure nothing is hidden launchActivitiesInSplitScreen( getLaunchActivityBuilder().setTargetActivity(LAUNCHING_ACTIVITY), getLaunchActivityBuilder().setTargetActivity(TEST_ACTIVITY) .setRandomData(true) .setMultipleTask(false) ); mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true); mAmWmState.assertVisibility(TEST_ACTIVITY, true); // Go to recents to make sure that fullscreen stack is invisible // Some devices do not support recents or implement it differently (instead of using a // separate stack id or as an activity), for those cases the visibility asserts will be // ignored pressAppSwitchButtonAndWaitForRecents(); mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true); mAmWmState.assertVisibility(TEST_ACTIVITY, false); } @Test public void testLaunchTaskByComponentMatchMultipleTasks() throws Exception { assumeTrue(supportsPip()); // Launch a fullscreen activity which will launch a PiP activity in a new task with the same // affinity launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY); launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY); assertPinnedStackExists(); // Launch the root activity again... int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivity( TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId; launchHomeActivity(); launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY); // ...and ensure that the root activity task is found and reused, and that the pinned stack // is unaffected assertPinnedStackExists(); mAmWmState.assertFocusedActivity("Expected root activity focused", TEST_ACTIVITY_WITH_SAME_AFFINITY); assertEquals(rootActivityTaskId, mAmWmState.getAmState().getTaskByActivity( TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId); } @Test public void testLaunchTaskByAffinityMatchMultipleTasks() throws Exception { assumeTrue(supportsPip()); // Launch a fullscreen activity which will launch a PiP activity in a new task with the same // affinity, and also launch another activity in the same task, while finishing itself. As // a result, the task will not have a component matching the same activity as what it was // started with launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY, EXTRA_START_ACTIVITY, getActivityName(TEST_ACTIVITY), EXTRA_FINISH_SELF_ON_RESUME, "true"); mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(TEST_ACTIVITY) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) .setActivityType(ACTIVITY_TYPE_STANDARD) .build()); launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY); waitForEnterPip(PIP_ACTIVITY_WITH_SAME_AFFINITY); assertPinnedStackExists(); // Launch the root activity again... int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivity( TEST_ACTIVITY).mTaskId; launchHomeActivity(); launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY); // ...and ensure that even while matching purely by task affinity, the root activity task is // found and reused, and that the pinned stack is unaffected assertPinnedStackExists(); mAmWmState.assertFocusedActivity("Expected root activity focused", TEST_ACTIVITY); assertEquals(rootActivityTaskId, mAmWmState.getAmState().getTaskByActivity( TEST_ACTIVITY).mTaskId); } @Test public void testLaunchTaskByAffinityMatchSingleTask() throws Exception { assumeTrue(supportsPip()); // Launch an activity into the pinned stack with a fixed affinity launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY, EXTRA_ENTER_PIP, "true", EXTRA_START_ACTIVITY, getActivityName(PIP_ACTIVITY), EXTRA_FINISH_SELF_ON_RESUME, "true"); waitForEnterPip(TEST_ACTIVITY_WITH_SAME_AFFINITY); assertPinnedStackExists(); // Launch the root activity again, of the matching task and ensure that we expand to // fullscreen int activityTaskId = mAmWmState.getAmState().getTaskByActivity(PIP_ACTIVITY).mTaskId; launchHomeActivity(); launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY); waitForExitPipToFullscreen(TEST_ACTIVITY_WITH_SAME_AFFINITY); assertPinnedStackDoesNotExist(); assertEquals(activityTaskId, mAmWmState.getAmState().getTaskByActivity( PIP_ACTIVITY).mTaskId); } /** Test that reported display size corresponds to fullscreen after exiting PiP. */ @FlakyTest @Presubmit @Test public void testDisplayMetricsPinUnpin() throws Exception { assumeTrue(supportsPip()); LogSeparator logSeparator = separateLogs(); launchActivity(TEST_ACTIVITY); final int defaultWindowingMode = mAmWmState.getAmState() .getTaskByActivity(TEST_ACTIVITY).getWindowingMode(); final ReportedSizes initialSizes = getLastReportedSizesForActivity(TEST_ACTIVITY, logSeparator); final Rect initialAppBounds = readAppBounds(TEST_ACTIVITY, logSeparator); assertNotNull("Must report display dimensions", initialSizes); assertNotNull("Must report app bounds", initialAppBounds); logSeparator = separateLogs(); launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); // Wait for animation complete since we are comparing bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); final ReportedSizes pinnedSizes = getLastReportedSizesForActivity(PIP_ACTIVITY, logSeparator); final Rect pinnedAppBounds = readAppBounds(PIP_ACTIVITY, logSeparator); assertNotEquals("Reported display size when pinned must be different from default", initialSizes, pinnedSizes); final Size initialAppSize = new Size(initialAppBounds.width(), initialAppBounds.height()); final Size pinnedAppSize = new Size(pinnedAppBounds.width(), pinnedAppBounds.height()); assertNotEquals("Reported app size when pinned must be different from default", initialAppSize, pinnedAppSize); logSeparator = separateLogs(); launchActivity(PIP_ACTIVITY, defaultWindowingMode); final ReportedSizes finalSizes = getLastReportedSizesForActivity(PIP_ACTIVITY, logSeparator); final Rect finalAppBounds = readAppBounds(PIP_ACTIVITY, logSeparator); final Size finalAppSize = new Size(finalAppBounds.width(), finalAppBounds.height()); assertEquals("Must report default size after exiting PiP", initialSizes, finalSizes); assertEquals("Must report default app size after exiting PiP", initialAppSize, finalAppSize); } @Presubmit @Test public void testEnterPictureInPictureSavePosition() throws Exception { assumeTrue(supportsPip()); // Ensure we have static shelf offset by running this test over a non-home activity launchActivity(NO_RELAUNCH_ACTIVITY); mAmWmState.waitForActivityState(mAmWmState.getAmState().getHomeActivityName(), STATE_STOPPED); // Launch PiP activity with auto-enter PiP, save the default position of the PiP // (while the PiP is still animating sleep) launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); // Wait for animation complete since we are comparing bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); // Move the PiP to a new position on screen final Rect initialBounds = new Rect(); final Rect offsetBounds = new Rect(); offsetPipWithinMovementBounds(100 /* offsetY */, initialBounds, offsetBounds); // Expand the PiP back to fullscreen and back into PiP and ensure that it is in the same // position as before we expanded (and that the default bounds reflect that) executeShellCommand("am broadcast -a " + ACTION_EXPAND_PIP); waitForExitPipToFullscreen(PIP_ACTIVITY); executeShellCommand("am broadcast -a " + ACTION_ENTER_PIP); waitForEnterPipAnimationComplete(PIP_ACTIVITY); mAmWmState.computeState(true); // Due to rounding in how we save and apply the snap fraction we may be a pixel off, so just // account for that in this check offsetBounds.inset(-1, -1); assertTrue("Expected offsetBounds=" + offsetBounds + " to contain bounds=" + getPinnedStackBounds(), offsetBounds.contains(getPinnedStackBounds())); // Expand the PiP, then launch an activity in a new task, and ensure that the PiP goes back // to the default position (and not the saved position) the next time it is launched executeShellCommand("am broadcast -a " + ACTION_EXPAND_PIP); waitForExitPipToFullscreen(PIP_ACTIVITY); launchActivity(TEST_ACTIVITY); executeShellCommand("am broadcast -a " + TEST_ACTIVITY_ACTION_FINISH_SELF); mAmWmState.waitForActivityState(PIP_ACTIVITY, STATE_RESUMED); mAmWmState.waitForAppTransitionIdle(); executeShellCommand("am broadcast -a " + ACTION_ENTER_PIP); waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); assertTrue("Expected initialBounds=" + initialBounds + " to equal bounds=" + getPinnedStackBounds(), initialBounds.equals(getPinnedStackBounds())); } @Presubmit @Test @FlakyTest(bugId = 71792368) public void testEnterPictureInPictureDiscardSavedPositionOnFinish() throws Exception { assumeTrue(supportsPip()); // Ensure we have static shelf offset by running this test over a non-home activity launchActivity(NO_RELAUNCH_ACTIVITY); mAmWmState.waitForActivityState(mAmWmState.getAmState().getHomeActivityName(), STATE_STOPPED); // Launch PiP activity with auto-enter PiP, save the default position of the PiP // (while the PiP is still animating sleep) launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); // Wait for animation complete since we are comparing bounds waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); // Move the PiP to a new position on screen final Rect initialBounds = new Rect(); final Rect offsetBounds = new Rect(); offsetPipWithinMovementBounds(100 /* offsetY */, initialBounds, offsetBounds); // Finish the activity executeShellCommand("am broadcast -a " + ACTION_FINISH); waitForPinnedStackRemoved(); assertPinnedStackDoesNotExist(); // Ensure that starting the same PiP activity after it finished will go to the default // bounds launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); waitForEnterPipAnimationComplete(PIP_ACTIVITY); assertPinnedStackExists(); assertTrue("Expected initialBounds=" + initialBounds + " to equal bounds=" + getPinnedStackBounds(), initialBounds.equals(getPinnedStackBounds())); } /** * Offsets the PiP in a direction by {@param offsetY} such that it is still within the movement * bounds. */ private void offsetPipWithinMovementBounds(int offsetY, Rect initialBoundsOut, Rect offsetBoundsOut) { final ActivityStack stack = getPinnedStack(); final Rect displayRect = mAmWmState.getWmState().getDisplay(stack.mDisplayId) .getDisplayRect(); initialBoundsOut.set(getPinnedStackBounds()); offsetBoundsOut.set(initialBoundsOut); if (initialBoundsOut.top < displayRect.centerY()) { // If the default gravity is top-aligned, offset down instead of up offsetBoundsOut.offset(0, offsetY); } else { offsetBoundsOut.offset(0, -offsetY); } resizeStack(stack.mStackId, offsetBoundsOut.left, offsetBoundsOut.top, offsetBoundsOut.right, offsetBoundsOut.bottom); } private static final Pattern sAppBoundsPattern = Pattern.compile( "(.+)mAppBounds=Rect\\((\\d+), (\\d+) - (\\d+), (\\d+)\\)(.*)"); /** Read app bounds in last applied configuration from logs. */ private Rect readAppBounds(ComponentName activityName, LogSeparator logSeparator) { final String[] lines = getDeviceLogsForComponents(logSeparator, getLogTag(activityName)); for (int i = lines.length - 1; i >= 0; i--) { final String line = lines[i].trim(); final Matcher matcher = sAppBoundsPattern.matcher(line); if (matcher.matches()) { final int left = Integer.parseInt(matcher.group(2)); final int top = Integer.parseInt(matcher.group(3)); final int right = Integer.parseInt(matcher.group(4)); final int bottom = Integer.parseInt(matcher.group(5)); return new Rect(left, top, right - left, bottom - top); } } return null; } /** * Called after the given {@param activityName} has been moved to the fullscreen stack. Ensures * that the stack matching the {@param windowingMode} and {@param activityType} is focused, and * checks the top and/or bottom tasks in the fullscreen stack if * {@param expectTopTaskHasActivity} or {@param expectBottomTaskHasActivity} are set * respectively. */ private void assertPinnedStackStateOnMoveToFullscreen(ComponentName activityName, int windowingMode, int activityType) { mAmWmState.waitForFocusedStack(windowingMode, activityType); mAmWmState.assertFocusedStack("Wrong focused stack", windowingMode, activityType); mAmWmState.waitForActivityState(activityName, STATE_STOPPED); assertTrue(mAmWmState.getAmState().hasActivityState(activityName, STATE_STOPPED)); assertTrue(mAmWmState.getAmState().containsActivityInWindowingMode( activityName, WINDOWING_MODE_FULLSCREEN)); assertPinnedStackDoesNotExist(); } /** * Asserts that the pinned stack bounds does not intersect with the IME bounds. */ private void assertPinnedStackDoesNotIntersectIME() { // Ensure that the IME is visible WindowManagerState wmState = mAmWmState.getWmState(); wmState.computeState(); WindowManagerState.WindowState imeWinState = wmState.getInputMethodWindowState(); assertTrue(imeWinState != null); // Ensure that the PIP movement is constrained by the display bounds intersecting the // non-IME bounds Rect imeContentFrame = imeWinState.getContentFrame(); Rect imeContentInsets = imeWinState.getGivenContentInsets(); Rect imeBounds = new Rect(imeContentFrame.left + imeContentInsets.left, imeContentFrame.top + imeContentInsets.top, imeContentFrame.right - imeContentInsets.width(), imeContentFrame.bottom - imeContentInsets.height()); wmState.computeState(); Rect pipMovementBounds = wmState.getPinnedStackMovementBounds(); assertTrue(!Rect.intersects(pipMovementBounds, imeBounds)); } /** * Asserts that the pinned stack bounds is contained in the display bounds. */ private void assertPinnedStackActivityIsInDisplayBounds(ComponentName activityName) { final WindowManagerState.WindowState windowState = getWindowState(activityName); final WindowManagerState.Display display = mAmWmState.getWmState().getDisplay( windowState.getDisplayId()); final Rect displayRect = display.getDisplayRect(); final Rect pinnedStackBounds = getPinnedStackBounds(); assertTrue(displayRect.contains(pinnedStackBounds)); } /** * Asserts that the pinned stack exists. */ private void assertPinnedStackExists() { mAmWmState.assertContainsStack("Must contain pinned stack.", WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); } /** * Asserts that the pinned stack does not exist. */ private void assertPinnedStackDoesNotExist() { mAmWmState.assertDoesNotContainStack("Must not contain pinned stack.", WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); } /** * Asserts that the pinned stack is the front stack. */ private void assertPinnedStackIsOnTop() { mAmWmState.assertFrontStack("Pinned stack must always be on top.", WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); } /** * Asserts that the activity received exactly one of each of the callbacks when entering and * exiting picture-in-picture. */ private void assertValidPictureInPictureCallbackOrder( ComponentName activityName, LogSeparator logSeparator) { final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(activityName, logSeparator); assertEquals(getActivityName(activityName) + " onConfigurationChanged()", 1, lifecycleCounts.mConfigurationChangedCount); assertEquals(getActivityName(activityName) + " onPictureInPictureModeChanged()", 1, lifecycleCounts.mPictureInPictureModeChangedCount); assertEquals(getActivityName(activityName) + " onMultiWindowModeChanged", 1, lifecycleCounts.mMultiWindowModeChangedCount); int lastPipLine = lifecycleCounts.mLastPictureInPictureModeChangedLineIndex; int lastMwLine = lifecycleCounts.mLastMultiWindowModeChangedLineIndex; int lastConfigLine = lifecycleCounts.mLastConfigurationChangedLineIndex; assertThat("onPictureInPictureModeChanged should be before onMultiWindowModeChanged", lastPipLine, lessThan(lastMwLine)); assertThat("onMultiWindowModeChanged should be before onConfigurationChanged", lastMwLine, lessThan(lastConfigLine)); } /** * Waits until the given activity has entered picture-in-picture mode (allowing for the * subsequent animation to start). */ private void waitForEnterPip(ComponentName activityName) { mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName) .setWindowingMode(WINDOWING_MODE_PINNED) .setActivityType(ACTIVITY_TYPE_STANDARD) .build()); } /** * Waits until the picture-in-picture animation has finished. */ private void waitForEnterPipAnimationComplete(ComponentName activityName) { waitForEnterPip(activityName); mAmWmState.waitFor((amState, wmState) -> { WindowStack stack = wmState.getStandardStackByWindowingMode(WINDOWING_MODE_PINNED); return stack != null && !stack.mAnimatingBounds; }, "Waiting for pinned stack bounds animation to finish"); } /** * Waits until the pinned stack has been removed. */ private void waitForPinnedStackRemoved() { mAmWmState.waitFor((amState, wmState) -> { return !amState.containsStack(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD) && !wmState.containsStack(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); }, "Waiting for pinned stack to be removed..."); } /** * Waits until the picture-in-picture animation to fullscreen has finished. */ private void waitForExitPipToFullscreen(ComponentName activityName) { mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName) .setWindowingMode(WINDOWING_MODE_FULLSCREEN) .setActivityType(ACTIVITY_TYPE_STANDARD) .build()); } /** * Waits until the expected picture-in-picture callbacks have been made. */ private void waitForValidPictureInPictureCallbacks(ComponentName activityName, LogSeparator logSeparator) { mAmWmState.waitFor((amState, wmState) -> { final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts( activityName, logSeparator); return lifecycleCounts.mConfigurationChangedCount == 1 && lifecycleCounts.mPictureInPictureModeChangedCount == 1 && lifecycleCounts.mMultiWindowModeChangedCount == 1; }, "Waiting for picture-in-picture activity callbacks..."); } private void waitForValidAspectRatio(int num, int denom) { // Hacky, but we need to wait for the auto-enter picture-in-picture animation to complete // and before we can check the pinned stack bounds mAmWmState.waitForWithAmState((state) -> { Rect bounds = state.getStandardStackByWindowingMode(WINDOWING_MODE_PINNED).getBounds(); return floatEquals((float) bounds.width() / bounds.height(), (float) num / denom); }, "waitForValidAspectRatio"); } /** * @return the window state for the given {@param activityName}'s window. */ private WindowManagerState.WindowState getWindowState(ComponentName activityName) { String windowName = getWindowName(activityName); mAmWmState.computeState(activityName); final List tempWindowList = mAmWmState.getWmState().getMatchingVisibleWindowState(windowName); return tempWindowList.get(0); } /** * @return the current pinned stack. */ private ActivityStack getPinnedStack() { return mAmWmState.getAmState().getStandardStackByWindowingMode(WINDOWING_MODE_PINNED); } /** * @return the current pinned stack bounds. */ private Rect getPinnedStackBounds() { return getPinnedStack().getBounds(); } /** * Compares two floats with a common epsilon. */ private void assertFloatEquals(float actual, float expected) { if (!floatEquals(actual, expected)) { fail(expected + " not equal to " + actual); } } private boolean floatEquals(float a, float b) { return Math.abs(a - b) < FLOAT_COMPARE_EPSILON; } /** * Triggers a tap over the pinned stack bounds to trigger the PIP to close. */ private void tapToFinishPip() { Rect pinnedStackBounds = getPinnedStackBounds(); int tapX = pinnedStackBounds.left + pinnedStackBounds.width() - 100; int tapY = pinnedStackBounds.top + pinnedStackBounds.height() - 100; executeShellCommand(String.format("input tap %d %d", tapX, tapY)); } /** * Launches the given {@param activityName} into the {@param taskId} as a task overlay. */ private void launchPinnedActivityAsTaskOverlay(ComponentName activityName, int taskId) { executeShellCommand(getAmStartCmd(activityName) + " --task " + taskId + " --task-overlay"); mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(activityName) .setWindowingMode(WINDOWING_MODE_PINNED) .setActivityType(ACTIVITY_TYPE_STANDARD) .build()); } private static class AppOpsSession implements AutoCloseable { private final String mPackageName; AppOpsSession(ComponentName activityName) { mPackageName = activityName.getPackageName(); } void setOpToMode(String op, int mode) { setAppOpsOpToMode(mPackageName, op, mode); } @Override public void close() { executeShellCommand("appops reset " + mPackageName); } /** * Sets an app-ops op for a given package to a given mode. */ private void setAppOpsOpToMode(String packageName, String op, int mode) { executeShellCommand(String.format("appops set %s %s %d", packageName, op, mode)); } } /** * TODO: Improve tests check to actually check that apps are not interactive instead of checking * if the stack is focused. */ private void pinnedStackTester(String startActivityCmd, ComponentName startActivity, ComponentName topActivityName, boolean moveTopToPinnedStack, boolean isFocusable) { executeShellCommand(startActivityCmd); mAmWmState.waitForValidState(startActivity); if (moveTopToPinnedStack) { final int stackId = mAmWmState.getAmState().getStackIdByActivity(topActivityName); assertNotEquals(stackId, INVALID_STACK_ID); executeShellCommand(getMoveToPinnedStackCommand(stackId)); } mAmWmState.waitForValidState(new WaitForValidActivityState.Builder(topActivityName) .setWindowingMode(WINDOWING_MODE_PINNED) .setActivityType(ACTIVITY_TYPE_STANDARD) .build()); mAmWmState.computeState(true); if (supportsPip()) { final String windowName = getWindowName(topActivityName); assertPinnedStackExists(); mAmWmState.assertFrontStack("Pinned stack must be the front stack.", WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); mAmWmState.assertVisibility(topActivityName, true); if (isFocusable) { mAmWmState.assertFocusedStack("Pinned stack must be the focused stack.", WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); mAmWmState.assertFocusedActivity( "Pinned activity must be focused activity.", topActivityName); mAmWmState.assertFocusedWindow( "Pinned window must be focused window.", windowName); // Not checking for resumed state here because PiP overlay can be launched on top // in different task by SystemUI. } else { // Don't assert that the stack is not focused as a focusable PiP overlay can be // launched on top as a task overlay by SystemUI. mAmWmState.assertNotFocusedActivity( "Pinned activity can't be the focused activity.", topActivityName); mAmWmState.assertNotResumedActivity( "Pinned activity can't be the resumed activity.", topActivityName); mAmWmState.assertNotFocusedWindow( "Pinned window can't be focused window.", windowName); } } else { mAmWmState.assertDoesNotContainStack("Must not contain pinned stack.", WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD); } } }