blob: 55b41c860456882411d4357b8f071c85b6b6ab8a [file] [log] [blame]
/*
* 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.cts;
import static android.server.cts.ActivityAndWindowManagersState.DEFAULT_DISPLAY_ID;
import static android.server.cts.ActivityManagerState.STATE_STOPPED;
import android.server.cts.ActivityManagerState.Activity;
import android.server.cts.ActivityManagerState.ActivityStack;
import android.server.cts.ActivityManagerState.ActivityTask;
import java.awt.Rectangle;
import java.lang.Exception;
import java.lang.String;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Build: mmma -j32 cts/hostsidetests/services
* Run: cts/hostsidetests/services/activityandwindowmanager/util/run-test CtsServicesHostTestCases android.server.cts.ActivityManagerPinnedStackTests
*/
public class ActivityManagerPinnedStackTests extends ActivityManagerTestBase {
private static final String TEST_ACTIVITY = "TestActivity";
private static final String TEST_ACTIVITY_WITH_SAME_AFFINITY = "TestActivityWithSameAffinity";
private static final String TRANSLUCENT_TEST_ACTIVITY = "TranslucentTestActivity";
private static final String NON_RESIZEABLE_ACTIVITY = "NonResizeableActivity";
private static final String RESUME_WHILE_PAUSING_ACTIVITY = "ResumeWhilePausingActivity";
private static final String PIP_ACTIVITY = "PipActivity";
private static final String PIP_ACTIVITY2 = "PipActivity2";
private static final String PIP_ACTIVITY_WITH_SAME_AFFINITY = "PipActivityWithSameAffinity";
private static final String ALWAYS_FOCUSABLE_PIP_ACTIVITY = "AlwaysFocusablePipActivity";
private static final String LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY =
"LaunchIntoPinnedStackPipActivity";
private static final String LAUNCH_ENTER_PIP_ACTIVITY = "LaunchEnterPipActivity";
private static final String PIP_ON_STOP_ACTIVITY = "PipOnStopActivity";
private static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation";
private static final String EXTRA_ENTER_PIP = "enter_pip";
private static final String EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR =
"enter_pip_aspect_ratio_numerator";
private static final String EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR =
"enter_pip_aspect_ratio_denominator";
private static final String EXTRA_SET_ASPECT_RATIO_NUMERATOR = "set_aspect_ratio_numerator";
private static final String EXTRA_SET_ASPECT_RATIO_DENOMINATOR = "set_aspect_ratio_denominator";
private static final String EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR =
"set_aspect_ratio_with_delay_numerator";
private static final String EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR =
"set_aspect_ratio_with_delay_denominator";
private static final String EXTRA_ENTER_PIP_ON_PAUSE = "enter_pip_on_pause";
private static final String EXTRA_TAP_TO_FINISH = "tap_to_finish";
private static final String EXTRA_START_ACTIVITY = "start_activity";
private static final String EXTRA_FINISH_SELF_ON_RESUME = "finish_self_on_resume";
private static final String EXTRA_REENTER_PIP_ON_EXIT = "reenter_pip_on_exit";
private static final String EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP = "assert_no_on_stop_before_pip";
private static final String EXTRA_ON_PAUSE_DELAY = "on_pause_delay";
private static final String PIP_ACTIVITY_ACTION_ENTER_PIP =
"android.server.cts.PipActivity.enter_pip";
private static final String PIP_ACTIVITY_ACTION_MOVE_TO_BACK =
"android.server.cts.PipActivity.move_to_back";
private static final String PIP_ACTIVITY_ACTION_EXPAND_PIP =
"android.server.cts.PipActivity.expand_pip";
private static final String PIP_ACTIVITY_ACTION_SET_REQUESTED_ORIENTATION =
"android.server.cts.PipActivity.set_requested_orientation";
private static final String PIP_ACTIVITY_ACTION_FINISH =
"android.server.cts.PipActivity.finish";
private static final String TEST_ACTIVITY_ACTION_FINISH =
"android.server.cts.TestActivity.finish_self";
private static final int APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE = 67;
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;
public void testMinimumDeviceSize() throws Exception {
if (!supportsPip()) return;
mAmWmState.assertDeviceDefaultDisplaySize(mDevice,
"Devices supporting picture-in-picture must be larger than the default minimum"
+ " task size");
}
public void testEnterPictureInPictureMode() throws Exception {
pinnedStackTester(getAmStartCmd(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"), PIP_ACTIVITY,
false /* moveTopToPinnedStack */, false /* isFocusable */);
}
public void testMoveTopActivityToPinnedStack() throws Exception {
pinnedStackTester(getAmStartCmd(PIP_ACTIVITY), PIP_ACTIVITY,
true /* moveTopToPinnedStack */, false /* isFocusable */);
}
public void testAlwaysFocusablePipActivity() throws Exception {
pinnedStackTester(getAmStartCmd(ALWAYS_FOCUSABLE_PIP_ACTIVITY),
ALWAYS_FOCUSABLE_PIP_ACTIVITY, false /* moveTopToPinnedStack */,
true /* isFocusable */);
}
public void testLaunchIntoPinnedStack() throws Exception {
pinnedStackTester(getAmStartCmd(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY),
ALWAYS_FOCUSABLE_PIP_ACTIVITY, false /* moveTopToPinnedStack */,
true /* isFocusable */);
}
public void testNonTappablePipActivity() throws Exception {
if (!supportsPip()) return;
// Launch the tap-to-finish activity at a specific place
launchActivity(PIP_ACTIVITY,
EXTRA_ENTER_PIP, "true",
EXTRA_TAP_TO_FINISH, "true");
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
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(mDevice, new String[] {PIP_ACTIVITY},
false /* compareTaskAndStackBounds */);
mAmWmState.assertVisibility(PIP_ACTIVITY, true);
}
public void testPinnedStackDefaultBounds() throws Exception {
if (!supportsPip()) return;
// Launch a PIP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
setDeviceRotation(ROTATION_0);
WindowManagerState wmState = mAmWmState.getWmState();
wmState.computeState(mDevice);
Rectangle defaultPipBounds = wmState.getDefaultPinnedStackBounds();
Rectangle stableBounds = wmState.getStableBounds();
assertTrue(defaultPipBounds.width > 0 && defaultPipBounds.height > 0);
assertTrue(stableBounds.contains(defaultPipBounds));
setDeviceRotation(ROTATION_90);
wmState = mAmWmState.getWmState();
wmState.computeState(mDevice);
defaultPipBounds = wmState.getDefaultPinnedStackBounds();
stableBounds = wmState.getStableBounds();
assertTrue(defaultPipBounds.width > 0 && defaultPipBounds.height > 0);
assertTrue(stableBounds.contains(defaultPipBounds));
setDeviceRotation(ROTATION_0);
}
public void testPinnedStackMovementBounds() throws Exception {
if (!supportsPip()) return;
// Launch a PIP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
setDeviceRotation(ROTATION_0);
WindowManagerState wmState = mAmWmState.getWmState();
wmState.computeState(mDevice);
Rectangle pipMovementBounds = wmState.getPinnedStackMomentBounds();
Rectangle stableBounds = wmState.getStableBounds();
assertTrue(pipMovementBounds.width > 0 && pipMovementBounds.height > 0);
assertTrue(stableBounds.contains(pipMovementBounds));
setDeviceRotation(ROTATION_90);
wmState = mAmWmState.getWmState();
wmState.computeState(mDevice);
pipMovementBounds = wmState.getPinnedStackMomentBounds();
stableBounds = wmState.getStableBounds();
assertTrue(pipMovementBounds.width > 0 && pipMovementBounds.height > 0);
assertTrue(stableBounds.contains(pipMovementBounds));
setDeviceRotation(ROTATION_0);
}
public void testPinnedStackOutOfBoundsInsetsNonNegative() throws Exception {
if (!supportsPip()) return;
final WindowManagerState wmState = mAmWmState.getWmState();
// Launch an activity into the pinned stack
launchActivity(PIP_ACTIVITY,
EXTRA_ENTER_PIP, "true",
EXTRA_TAP_TO_FINISH, "true");
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
// Get the display dimensions
WindowManagerState.WindowState windowState = getWindowState(PIP_ACTIVITY);
WindowManagerState.Display display = wmState.getDisplay(windowState.getDisplayId());
Rectangle displayRect = display.getDisplayRect();
// Move the pinned stack offscreen
String moveStackOffscreenCommand = String.format("am stack resize 4 %d %d %d %d",
displayRect.width - 200, 0, displayRect.width + 200, 500);
executeShellCommand(moveStackOffscreenCommand);
// Ensure that the surface insets are not negative
windowState = getWindowState(PIP_ACTIVITY);
Rectangle contentInsets = windowState.getContentInsets();
assertTrue(contentInsets.x >= 0 && contentInsets.y >= 0 && contentInsets.width >= 0 &&
contentInsets.height >= 0);
}
public void testPinnedStackInBoundsAfterRotation() throws Exception {
if (!supportsPip()) return;
// Launch an activity into the pinned stack
launchActivity(PIP_ACTIVITY,
EXTRA_ENTER_PIP, "true",
EXTRA_TAP_TO_FINISH, "true");
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
// Ensure that the PIP stack is fully visible in each orientation
setDeviceRotation(ROTATION_0);
assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
setDeviceRotation(ROTATION_90);
assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
setDeviceRotation(ROTATION_180);
assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
setDeviceRotation(ROTATION_270);
assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
setDeviceRotation(ROTATION_0);
}
public void testEnterPipToOtherOrientation() throws Exception {
if (!supportsPip()) return;
// 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_FIXED_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 " + PIP_ACTIVITY_ACTION_ENTER_PIP);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
assertPinnedStackExists();
assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
}
public void testEnterPipAspectRatioMin() throws Exception {
testEnterPipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR);
}
public void testEnterPipAspectRatioMax() throws Exception {
testEnterPipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
}
private void testEnterPipAspectRatio(int num, int denom) throws Exception {
if (!supportsPip()) return;
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));
assertPinnedStackExists();
// Assert that we have entered PIP and that the aspect ratio is correct
Rectangle pinnedStackBounds =
mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds();
assertTrue(floatEquals((float) pinnedStackBounds.width / pinnedStackBounds.height,
(float) num / denom));
}
public void testResizePipAspectRatioMin() throws Exception {
testResizePipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR);
}
public void testResizePipAspectRatioMax() throws Exception {
testResizePipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR);
}
private void testResizePipAspectRatio(int num, int denom) throws Exception {
if (!supportsPip()) return;
launchActivity(PIP_ACTIVITY,
EXTRA_ENTER_PIP, "true",
EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num),
EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom));
assertPinnedStackExists();
// Hacky, but we need to wait for the enterPictureInPicture animation to complete and
// the resize to be called before we can check the pinned stack bounds
final boolean[] result = new boolean[1];
mAmWmState.waitForWithAmState(mDevice, (state) -> {
Rectangle pinnedStackBounds = state.getStackById(PINNED_STACK_ID).getBounds();
boolean isValidAspectRatio = floatEquals(
(float) pinnedStackBounds.width / pinnedStackBounds.height,
(float) num / denom);
result[0] = isValidAspectRatio;
return isValidAspectRatio;
}, "Waiting for pinned stack to be resized");
assertTrue(result[0]);
}
public void testEnterPipExtremeAspectRatioMin() throws Exception {
testEnterPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR,
BELOW_MIN_ASPECT_RATIO_DENOMINATOR);
}
public void testEnterPipExtremeAspectRatioMax() throws Exception {
testEnterPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR,
MAX_ASPECT_RATIO_DENOMINATOR);
}
private void testEnterPipExtremeAspectRatio(int num, int denom) throws Exception {
if (!supportsPip()) return;
// 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();
}
public void testSetPipExtremeAspectRatioMin() throws Exception {
testSetPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR,
BELOW_MIN_ASPECT_RATIO_DENOMINATOR);
}
public void testSetPipExtremeAspectRatioMax() throws Exception {
testSetPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR,
MAX_ASPECT_RATIO_DENOMINATOR);
}
private void testSetPipExtremeAspectRatio(int num, int denom) throws Exception {
if (!supportsPip()) return;
// 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));
assertPinnedStackExists();
Rectangle pinnedStackBounds =
mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds();
assertTrue(floatEquals((float) pinnedStackBounds.width / pinnedStackBounds.height,
(float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR));
}
public void testDisallowPipLaunchFromStoppedActivity() throws Exception {
if (!supportsPip()) return;
// Launch the bottom pip activity
launchActivity(PIP_ON_STOP_ACTIVITY);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
// Wait for the bottom pip activity to be stopped
mAmWmState.waitForActivityState(mDevice, PIP_ON_STOP_ACTIVITY, STATE_STOPPED);
// Assert that there is no pinned stack (that enterPictureInPicture() failed)
assertPinnedStackDoesNotExist();
}
public void testAutoEnterPictureInPicture() throws Exception {
if (!supportsPip()) return;
// 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();
assertPinnedStackExists();
}
public void testAutoEnterPictureInPictureLaunchActivity() throws Exception {
if (!supportsPip()) return;
// 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, getActivityComponentName(NON_RESIZEABLE_ACTIVITY));
mAmWmState.computeState(mDevice, new String[] {NON_RESIZEABLE_ACTIVITY},
false /* compareTaskAndStackBounds */);
assertPinnedStackDoesNotExist();
// Go home while the pip activity is open and ensure the previous activity is not PIPed
launchHomeActivity();
assertPinnedStackDoesNotExist();
}
public void testAutoEnterPictureInPictureFinish() throws Exception {
if (!supportsPip()) return;
// 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();
}
public void testAutoEnterPictureInPictureAspectRatio() throws Exception {
if (!supportsPip()) return;
// 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();
assertPinnedStackExists();
// 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
final boolean[] result = new boolean[1];
mAmWmState.waitForWithAmState(mDevice, (state) -> {
Rectangle pinnedStackBounds = state.getStackById(PINNED_STACK_ID).getBounds();
boolean isValidAspectRatio = floatEquals(
(float) pinnedStackBounds.width / pinnedStackBounds.height,
(float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR);
result[0] = isValidAspectRatio;
return isValidAspectRatio;
}, "Waiting for pinned stack to be resized");
assertTrue(result[0]);
}
public void testAutoEnterPictureInPictureOverPip() throws Exception {
if (!supportsPip()) return;
// Launch another PIP activity
launchActivity(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
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 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 = mAmWmState.getAmState().getStackById(PINNED_STACK_ID);
assertTrue(pinnedStack.getTasks().size() == 1);
assertTrue(pinnedStack.getTasks().get(0).mRealActivity.equals(getActivityComponentName(
ALWAYS_FOCUSABLE_PIP_ACTIVITY)));
}
public void testDisallowMultipleTasksInPinnedStack() throws Exception {
if (!supportsPip()) return;
// 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");
// Launch second PIP activity
launchActivity(PIP_ACTIVITY2, EXTRA_ENTER_PIP, "true");
final ActivityStack pinnedStack = mAmWmState.getAmState().getStackById(PINNED_STACK_ID);
assertEquals(1, pinnedStack.getTasks().size());
assertTrue(pinnedStack.getTasks().get(0).mRealActivity.equals(getActivityComponentName(
PIP_ACTIVITY2)));
final ActivityStack fullScreenStack = mAmWmState.getAmState().getStackById(
FULLSCREEN_WORKSPACE_STACK_ID);
assertTrue(fullScreenStack.getBottomTask().mRealActivity.equals(getActivityComponentName(
PIP_ACTIVITY)));
}
public void testPipUnPipOverHome() throws Exception {
if (!supportsPip()) return;
// Go home
launchHomeActivity();
// Launch an auto pip activity
launchActivity(PIP_ACTIVITY,
EXTRA_ENTER_PIP, "true",
EXTRA_REENTER_PIP_ON_EXIT, "true");
assertPinnedStackExists();
// Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip
launchActivity(PIP_ACTIVITY);
mAmWmState.waitForWithAmState(mDevice, (amState) -> {
return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == FULLSCREEN_WORKSPACE_STACK_ID;
}, "Waiting for PIP to exit to fullscreen");
mAmWmState.waitForWithAmState(mDevice, (amState) -> {
return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == PINNED_STACK_ID;
}, "Waiting to re-enter PIP");
mAmWmState.assertFocusedStack("Expected home stack focused", HOME_STACK_ID);
}
public void testPipUnPipOverApp() throws Exception {
if (!supportsPip()) return;
// 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");
assertPinnedStackExists();
// Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip
launchActivity(PIP_ACTIVITY);
mAmWmState.waitForWithAmState(mDevice, (amState) -> {
return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == FULLSCREEN_WORKSPACE_STACK_ID;
}, "Waiting for PIP to exit to fullscreen");
mAmWmState.waitForWithAmState(mDevice, (amState) -> {
return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == PINNED_STACK_ID;
}, "Waiting to re-enter PIP");
mAmWmState.assertFocusedStack("Expected fullscreen stack focused",
FULLSCREEN_WORKSPACE_STACK_ID);
}
public void testRemovePipWithNoFullscreenStack() throws Exception {
if (!supportsPip()) return;
// Start with a clean slate, remove all the stacks but home
removeStacks(ALL_STACK_IDS_BUT_HOME);
// Launch a pip activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
assertPinnedStackExists();
// Remove the stack and ensure that the task is now in the fullscreen stack (when no
// fullscreen stack existed before)
removeStacks(PINNED_STACK_ID);
assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID,
true /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
}
public void testRemovePipWithVisibleFullscreenStack() throws Exception {
if (!supportsPip()) return;
// Launch a fullscreen activity, and a pip activity over that
launchActivity(TEST_ACTIVITY);
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
assertPinnedStackExists();
// Remove the stack and ensure that the task is placed in the fullscreen stack, behind the
// top fullscreen activity
removeStacks(PINNED_STACK_ID);
assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID,
false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
}
public void testRemovePipWithHiddenFullscreenStack() throws Exception {
if (!supportsPip()) return;
// 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");
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
removeStacks(PINNED_STACK_ID);
assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID,
false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
}
public void testMovePipToBackWithNoFullscreenStack() throws Exception {
if (!supportsPip()) return;
// Start with a clean slate, remove all the stacks but home
removeStacks(ALL_STACK_IDS_BUT_HOME);
// Launch a pip activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
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 " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK);
assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID,
false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
}
public void testMovePipToBackWithVisibleFullscreenStack() throws Exception {
if (!supportsPip()) return;
// Launch a fullscreen activity, and a pip activity over that
launchActivity(TEST_ACTIVITY);
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
assertPinnedStackExists();
// Remove the stack and ensure that the task is placed in the fullscreen stack, behind the
// top fullscreen activity
executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK);
assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID,
false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
}
public void testMovePipToBackWithHiddenFullscreenStack() throws Exception {
if (!supportsPip()) return;
// 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");
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 " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK);
assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID,
false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */);
}
public void testPinnedStackAlwaysOnTop() throws Exception {
if (!supportsPip()) return;
// Launch activity into pinned stack and assert it's on top.
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
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();
}
public void testAppOpsDenyPipOnPause() throws Exception {
if (!supportsPip()) return;
// Disable enter-pip and try to enter pip
setAppOpsOpToMode(ActivityManagerTestBase.componentName,
APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE, 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();
// Re-enable enter-pip-on-hide
setAppOpsOpToMode(ActivityManagerTestBase.componentName,
APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE, APP_OPS_MODE_ALLOWED);
}
public void testEnterPipFromTaskWithMultipleActivities() throws Exception {
if (!supportsPip()) return;
// 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);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
assertPinnedStackExists();
}
public void testEnterPipWithResumeWhilePausingActivityNoStop() throws Exception {
if (!supportsPip()) return;
/*
* 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)
launchActivityInStack(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID,
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();
}
public void testDisallowEnterPipActivityLocked() throws Exception {
if (!supportsPip()) return;
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
ActivityTask task =
mAmWmState.getAmState().getStackById(FULLSCREEN_WORKSPACE_STACK_ID).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 " + PIP_ACTIVITY_ACTION_ENTER_PIP);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
assertPinnedStackDoesNotExist();
launchHomeActivity();
assertPinnedStackDoesNotExist();
executeShellCommand("am task lock stop");
}
public void testConfigurationChangeOrderDuringTransition() throws Exception {
if (!supportsPip()) return;
// 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);
String logSeparator = clearLogcat();
executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_ENTER_PIP);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
assertPinnedStackExists();
assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, logSeparator);
// Trigger it to go back to fullscreen and ensure that only triggered one configuration
// change as well
logSeparator = clearLogcat();
launchActivity(PIP_ACTIVITY);
assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, logSeparator);
}
public void testStopBeforeMultiWindowCallbacksOnDismiss() throws Exception {
if (!supportsPip()) return;
// Launch a PiP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
assertPinnedStackExists();
// Dismiss it
String logSeparator = clearLogcat();
removeStacks(PINNED_STACK_ID);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
// Confirm that we get stop before the multi-window and picture-in-picture mode change
// callbacks
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY,
logSeparator);
if (lifecycleCounts.mStopCount != 1) {
fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mStopCount
+ " onStop() calls, expecting 1");
} else if (lifecycleCounts.mPictureInPictureModeChangedCount != 1) {
fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mPictureInPictureModeChangedCount
+ " onPictureInPictureModeChanged() calls, expecting 1");
} else if (lifecycleCounts.mMultiWindowModeChangedCount != 1) {
fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mMultiWindowModeChangedCount
+ " onMultiWindowModeChanged() calls, expecting 1");
} else {
int lastStopLine = lifecycleCounts.mLastStopLineIndex;
int lastPipLine = lifecycleCounts.mLastPictureInPictureModeChangedLineIndex;
int lastMwLine = lifecycleCounts.mLastMultiWindowModeChangedLineIndex;
if (!(lastStopLine < lastPipLine && lastPipLine < lastMwLine)) {
fail(PIP_ACTIVITY + " has received callbacks in unexpected order. Expected:"
+ " stop < pip < mw, but got line indices: " + lastStopLine + ", "
+ lastPipLine + ", " + lastMwLine + " respectively");
}
}
}
public void testPreventSetAspectRatioWhileExpanding() throws Exception {
if (!supportsPip()) return;
// Launch the PiP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
// 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 " + PIP_ACTIVITY_ACTION_EXPAND_PIP
+ " -e " + EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR + " 123456789"
+ " -e " + EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR + " 100000000");
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
assertPinnedStackDoesNotExist();
}
public void testSetRequestedOrientationWhilePinned() throws Exception {
if (!supportsPip()) return;
// Launch the PiP activity fixed as portrait, and enter picture-in-picture
launchActivity(PIP_ACTIVITY,
EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT),
EXTRA_ENTER_PIP, "true");
assertPinnedStackExists();
// Request that the orientation is set to landscape
executeShellCommand("am broadcast -a "
+ PIP_ACTIVITY_ACTION_SET_REQUESTED_ORIENTATION + " -e "
+ EXTRA_FIXED_ORIENTATION + " " + String.valueOf(ORIENTATION_LANDSCAPE));
// Launch the activity back into fullscreen and ensure that it is now in landscape
launchActivity(PIP_ACTIVITY);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
assertPinnedStackDoesNotExist();
assertTrue(mAmWmState.getWmState().getLastOrientation() == ORIENTATION_LANDSCAPE);
}
public void testWindowButtonEntersPip() throws Exception {
if (!supportsPip()) return;
// Launch the PiP activity trigger the window button, ensure that we have entered PiP
launchActivity(PIP_ACTIVITY);
pressWindowButton();
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
assertPinnedStackExists();
}
public void testFinishPipActivityWithTaskOverlay() throws Exception {
if (!supportsPip()) return;
// Launch PiP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
assertPinnedStackExists();
int taskId = mAmWmState.getAmState().getStackById(PINNED_STACK_ID).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
launchActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId, PINNED_STACK_ID);
// Finish the PiP activity and ensure that there is no pinned stack
executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_FINISH);
mAmWmState.waitForWithAmState(mDevice, (amState) -> {
ActivityStack stack = amState.getStackById(PINNED_STACK_ID);
return stack == null;
}, "Waiting for pinned stack to be removed...");
assertPinnedStackDoesNotExist();
}
public void testNoResumeAfterTaskOverlayFinishes() throws Exception {
if (!supportsPip()) return;
// Launch PiP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
assertPinnedStackExists();
int taskId = mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getTopTask().mTaskId;
// Launch task overlay activity into PiP activity task
launchActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId, PINNED_STACK_ID);
// Finish the task overlay activity while animating and ensure that the PiP activity never
// got resumed
String logSeparator = clearLogcat();
executeShellCommand("am stack resize-animated 4 20 20 500 500");
executeShellCommand("am broadcast -a " + TEST_ACTIVITY_ACTION_FINISH);
mAmWmState.waitFor(mDevice, (amState, wmState) -> !amState.containsActivity(
TRANSLUCENT_TEST_ACTIVITY), "Waiting for test activity to finish...");
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY,
logSeparator);
assertTrue(lifecycleCounts.mResumeCount == 0);
assertTrue(lifecycleCounts.mPauseCount == 0);
}
public void testPinnedStackWithDockedStack() throws Exception {
if (!supportsPip() || !supportsSplitScreenMultiWindow()) return;
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
launchActivityInDockStack(LAUNCHING_ACTIVITY);
launchActivityToSide(true, false, TEST_ACTIVITY);
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
launchActivityInDockStack(LAUNCHING_ACTIVITY);
mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
mAmWmState.assertVisibility(TEST_ACTIVITY, true);
launchActivityToSide(true, false, TEST_ACTIVITY);
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
pressAppSwitchButton();
if (mAmWmState.waitForRecentsActivityVisible(mDevice)) {
mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
mAmWmState.assertVisibility(TEST_ACTIVITY, false);
}
}
public void testLaunchTaskByComponentMatchMultipleTasks() throws Exception {
if (!supportsPip()) return;
// 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().getTaskByActivityName(
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);
assertTrue(rootActivityTaskId == mAmWmState.getAmState().getTaskByActivityName(
TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId);
}
public void testLaunchTaskByAffinityMatchMultipleTasks() throws Exception {
if (!supportsPip()) return;
// 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, getActivityComponentName(TEST_ACTIVITY),
EXTRA_FINISH_SELF_ON_RESUME, "true");
mAmWmState.waitForValidState(mDevice, TEST_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY_WITH_SAME_AFFINITY, PINNED_STACK_ID);
assertPinnedStackExists();
// Launch the root activity again...
int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivityName(
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);
assertTrue(rootActivityTaskId == mAmWmState.getAmState().getTaskByActivityName(
TEST_ACTIVITY).mTaskId);
}
public void testLaunchTaskByAffinityMatchSingleTask() throws Exception {
if (!supportsPip()) return;
// Launch an activity into the pinned stack with a fixed affinity
launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY,
EXTRA_ENTER_PIP, "true",
EXTRA_START_ACTIVITY, getActivityComponentName(PIP_ACTIVITY),
EXTRA_FINISH_SELF_ON_RESUME, "true");
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
assertPinnedStackExists();
// Launch the root activity again, of the matching task and ensure that we expand to
// fullscreen
int activityTaskId = mAmWmState.getAmState().getTaskByActivityName(
PIP_ACTIVITY).mTaskId;
launchHomeActivity();
launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID);
assertPinnedStackDoesNotExist();
assertTrue(activityTaskId == mAmWmState.getAmState().getTaskByActivityName(
PIP_ACTIVITY).mTaskId);
}
/** Test that reported display size corresponds to fullscreen after exiting PiP. */
public void testDisplayMetricsPinUnpin() throws Exception {
String logSeparator = clearLogcat();
launchActivity(TEST_ACTIVITY);
final int defaultDisplayStackId = mAmWmState.getAmState().getFocusedStackId();
final ReportedSizes initialSizes = getLastReportedSizesForActivity(TEST_ACTIVITY,
logSeparator);
final Rectangle initialAppBounds = readAppBounds(TEST_ACTIVITY, logSeparator);
assertNotNull("Must report display dimensions", initialSizes);
assertNotNull("Must report app bounds", initialAppBounds);
logSeparator = clearLogcat();
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID);
final ReportedSizes pinnedSizes = getLastReportedSizesForActivity(PIP_ACTIVITY,
logSeparator);
final Rectangle pinnedAppBounds = readAppBounds(PIP_ACTIVITY, logSeparator);
assertFalse("Reported display size when pinned must be different from default",
initialSizes.equals(pinnedSizes));
assertFalse("Reported app bounds when pinned must be different from default",
initialAppBounds.width == pinnedAppBounds.width
&& initialAppBounds.height == pinnedAppBounds.height);
logSeparator = clearLogcat();
launchActivityInStack(PIP_ACTIVITY, defaultDisplayStackId);
final ReportedSizes finalSizes = getLastReportedSizesForActivity(PIP_ACTIVITY,
logSeparator);
final Rectangle finalAppBounds = readAppBounds(PIP_ACTIVITY, logSeparator);
assertEquals("Must report default size after exiting PiP", initialSizes, finalSizes);
assertEquals("Must report default app width after exiting PiP", initialAppBounds.width,
finalAppBounds.width);
assertEquals("Must report default app height after exiting PiP", initialAppBounds.height,
finalAppBounds.height);
}
private static final Pattern sAppBoundsPattern = Pattern.compile(
"(.+)appBounds=Rect\\((\\d+), (\\d+) - (\\d+), (\\d+)\\)(.*)");
/** Read app bounds in last applied configuration from logs. */
private Rectangle readAppBounds(String activityName, String logSeparator) throws Exception {
final String[] lines = getDeviceLogsForComponent(activityName, logSeparator);
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 Rectangle(left, top, right - left, bottom - top);
}
}
return null;
}
/**
* Called after the given {@param activityName} has been moved to the fullscreen stack. Ensures
* that the {@param focusedStackId} 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(String activityName, int focusedStackId,
boolean expectTopTaskHasActivity, boolean expectBottomTaskHasActivity)
throws Exception {
mAmWmState.waitForFocusedStack(mDevice, focusedStackId);
mAmWmState.assertFocusedStack("Wrong focused stack", focusedStackId);
mAmWmState.waitForActivityState(mDevice, activityName, STATE_STOPPED);
assertTrue(mAmWmState.getAmState().hasActivityState(activityName, STATE_STOPPED));
assertPinnedStackDoesNotExist();
if (expectTopTaskHasActivity) {
ActivityTask topTask = mAmWmState.getAmState().getStackById(
FULLSCREEN_WORKSPACE_STACK_ID).getTopTask();
assertTrue(topTask.containsActivity(ActivityManagerTestBase.getActivityComponentName(
activityName)));
}
if (expectBottomTaskHasActivity) {
ActivityTask bottomTask = mAmWmState.getAmState().getStackById(
FULLSCREEN_WORKSPACE_STACK_ID).getBottomTask();
assertTrue(bottomTask.containsActivity(ActivityManagerTestBase.getActivityComponentName(
activityName)));
}
}
/**
* Asserts that the pinned stack bounds does not intersect with the IME bounds.
*/
private void assertPinnedStackDoesNotIntersectIME() throws Exception {
// Ensure that the IME is visible
WindowManagerState wmState = mAmWmState.getWmState();
wmState.computeState(mDevice);
WindowManagerState.WindowState imeWinState = wmState.getInputMethodWindowState();
assertTrue(imeWinState != null);
// Ensure that the PIP movement is constrained by the display bounds intersecting the
// non-IME bounds
Rectangle imeContentFrame = imeWinState.getContentFrame();
Rectangle imeContentInsets = imeWinState.getGivenContentInsets();
Rectangle imeBounds = new Rectangle(imeContentFrame.x + imeContentInsets.x,
imeContentFrame.y + imeContentInsets.y,
imeContentFrame.width - imeContentInsets.width,
imeContentFrame.height - imeContentInsets.height);
wmState.computeState(mDevice);
Rectangle pipMovementBounds = wmState.getPinnedStackMomentBounds();
assertTrue(!pipMovementBounds.intersects(imeBounds));
}
/**
* Asserts that the pinned stack bounds is contained in the display bounds.
*/
private void assertPinnedStackActivityIsInDisplayBounds(String activity) throws Exception {
final WindowManagerState.WindowState windowState = getWindowState(activity);
final WindowManagerState.Display display = mAmWmState.getWmState().getDisplay(
windowState.getDisplayId());
final Rectangle displayRect = display.getDisplayRect();
final Rectangle pinnedStackBounds =
mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds();
assertTrue(displayRect.contains(pinnedStackBounds));
}
/**
* Asserts that the pinned stack exists.
*/
private void assertPinnedStackExists() throws Exception {
mAmWmState.assertContainsStack("Must contain pinned stack.", PINNED_STACK_ID);
}
/**
* Asserts that the pinned stack does not exist.
*/
private void assertPinnedStackDoesNotExist() throws Exception {
mAmWmState.assertDoesNotContainStack("Must not contain pinned stack.", PINNED_STACK_ID);
}
/**
* Asserts that the pinned stack is the front stack.
*/
private void assertPinnedStackIsOnTop() throws Exception {
mAmWmState.assertFrontStack("Pinned stack must always be on top.", PINNED_STACK_ID);
}
/**
* Asserts that the activity received exactly one of each of the callbacks when entering and
* exiting picture-in-picture.
*/
private void assertValidPictureInPictureCallbackOrder(String activityName, String logSeparator)
throws Exception {
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(activityName,
logSeparator);
if (lifecycleCounts.mConfigurationChangedCount != 1) {
fail(activityName + " has received " + lifecycleCounts.mConfigurationChangedCount
+ " onConfigurationChanged() calls, expecting 1");
} else if (lifecycleCounts.mPictureInPictureModeChangedCount != 1) {
fail(activityName + " has received " + lifecycleCounts.mPictureInPictureModeChangedCount
+ " onPictureInPictureModeChanged() calls, expecting 1");
} else if (lifecycleCounts.mMultiWindowModeChangedCount != 1) {
fail(activityName + " has received " + lifecycleCounts.mMultiWindowModeChangedCount
+ " onMultiWindowModeChanged() calls, expecting 1");
} else {
int lastPipLine = lifecycleCounts.mLastPictureInPictureModeChangedLineIndex;
int lastMwLine = lifecycleCounts.mLastMultiWindowModeChangedLineIndex;
int lastConfigLine = lifecycleCounts.mLastConfigurationChangedLineIndex;
if (!(lastPipLine < lastMwLine && lastMwLine < lastConfigLine)) {
fail(activityName + " has received callbacks in unexpected order. Expected:"
+ " pip < mw < config change, but got line indices: " + lastPipLine + ", "
+ lastMwLine + ", " + lastConfigLine + " respectively");
}
}
}
/**
* @return the window state for the given {@param activity}'s window.
*/
private WindowManagerState.WindowState getWindowState(String activity) throws Exception {
String windowName = getWindowName(activity);
mAmWmState.computeState(mDevice, new String[] {activity});
final List<WindowManagerState.WindowState> tempWindowList = new ArrayList<>();
mAmWmState.getWmState().getMatchingVisibleWindowState(windowName, tempWindowList);
return tempWindowList.get(0);
}
/**
* Compares two floats with a common epsilon.
*/
private boolean floatEquals(float f1, float f2) {
return Math.abs(f1 - f2) < FLOAT_COMPARE_EPSILON;
}
/**
* Triggers a tap over the pinned stack bounds to trigger the PIP to close.
*/
private void tapToFinishPip() throws Exception {
Rectangle pinnedStackBounds =
mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds();
int tapX = pinnedStackBounds.x + pinnedStackBounds.width - 100;
int tapY = pinnedStackBounds.y + 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 launchActivityAsTaskOverlay(String activityName, int taskId, int stackId)
throws Exception {
executeShellCommand(getAmStartCmd(activityName) + " --task " + taskId + " --task-overlay");
mAmWmState.waitForValidState(mDevice, activityName, stackId);
}
/**
* Sets an app-ops op for a given package to a given mode.
*/
private void setAppOpsOpToMode(String packageName, int op, int mode) throws Exception {
executeShellCommand(String.format("appops set %s %d %d", packageName, op, mode));
}
/**
* Triggers the window keycode.
*/
private void pressWindowButton() throws Exception {
executeShellCommand(INPUT_KEYEVENT_WINDOW);
}
/**
* 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, String topActivityName,
boolean moveTopToPinnedStack, boolean isFocusable) throws Exception {
executeShellCommand(startActivityCmd);
if (moveTopToPinnedStack) {
executeShellCommand(AM_MOVE_TOP_ACTIVITY_TO_PINNED_STACK_COMMAND);
}
mAmWmState.waitForValidState(mDevice, topActivityName, PINNED_STACK_ID);
mAmWmState.computeState(mDevice, null);
if (supportsPip()) {
final String windowName = getWindowName(topActivityName);
assertPinnedStackExists();
mAmWmState.assertFrontStack("Pinned stack must be the front stack.", PINNED_STACK_ID);
mAmWmState.assertVisibility(topActivityName, true);
if (isFocusable) {
mAmWmState.assertFocusedStack(
"Pinned stack must be the focused stack.", PINNED_STACK_ID);
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.", PINNED_STACK_ID);
}
}
}