blob: 72ce477b6cc2874ff107ae5397ee85f3bbc8b09a [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.wm;
import static android.app.ActivityManager.LOCK_TASK_MODE_NONE;
import static android.app.ActivityTaskManager.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.wm.ComponentNameUtils.getActivityName;
import static android.server.wm.ComponentNameUtils.getWindowName;
import static android.server.wm.UiDeviceUtils.pressWindowButton;
import static android.server.wm.WindowManagerState.STATE_PAUSED;
import static android.server.wm.WindowManagerState.STATE_RESUMED;
import static android.server.wm.WindowManagerState.STATE_STOPPED;
import static android.server.wm.app.Components.ALWAYS_FOCUSABLE_PIP_ACTIVITY;
import static android.server.wm.app.Components.LAUNCHING_ACTIVITY;
import static android.server.wm.app.Components.LAUNCH_ENTER_PIP_ACTIVITY;
import static android.server.wm.app.Components.LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY;
import static android.server.wm.app.Components.NON_RESIZEABLE_ACTIVITY;
import static android.server.wm.app.Components.PIP_ACTIVITY;
import static android.server.wm.app.Components.PIP_ACTIVITY2;
import static android.server.wm.app.Components.PIP_ACTIVITY_WITH_MINIMAL_SIZE;
import static android.server.wm.app.Components.PIP_ACTIVITY_WITH_SAME_AFFINITY;
import static android.server.wm.app.Components.PIP_ON_STOP_ACTIVITY;
import static android.server.wm.app.Components.PipActivity.ACTION_ENTER_PIP;
import static android.server.wm.app.Components.PipActivity.ACTION_FINISH;
import static android.server.wm.app.Components.PipActivity.ACTION_MOVE_TO_BACK;
import static android.server.wm.app.Components.PipActivity.ACTION_ON_PIP_REQUESTED;
import static android.server.wm.app.Components.PipActivity.EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP;
import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP;
import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR;
import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR;
import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_PAUSE;
import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_PIP_REQUESTED;
import static android.server.wm.app.Components.PipActivity.EXTRA_ENTER_PIP_ON_USER_LEAVE_HINT;
import static android.server.wm.app.Components.PipActivity.EXTRA_FINISH_SELF_ON_RESUME;
import static android.server.wm.app.Components.PipActivity.EXTRA_ON_PAUSE_DELAY;
import static android.server.wm.app.Components.PipActivity.EXTRA_PIP_ORIENTATION;
import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_DENOMINATOR;
import static android.server.wm.app.Components.PipActivity.EXTRA_SET_ASPECT_RATIO_NUMERATOR;
import static android.server.wm.app.Components.PipActivity.EXTRA_START_ACTIVITY;
import static android.server.wm.app.Components.PipActivity.EXTRA_TAP_TO_FINISH;
import static android.server.wm.app.Components.RESUME_WHILE_PAUSING_ACTIVITY;
import static android.server.wm.app.Components.TEST_ACTIVITY;
import static android.server.wm.app.Components.TEST_ACTIVITY_WITH_SAME_AFFINITY;
import static android.server.wm.app.Components.TRANSLUCENT_TEST_ACTIVITY;
import static android.server.wm.app.Components.TestActivity.EXTRA_CONFIGURATION;
import static android.server.wm.app.Components.TestActivity.EXTRA_FIXED_ORIENTATION;
import static android.server.wm.app.Components.TestActivity.TEST_ACTIVITY_ACTION_FINISH_SELF;
import static android.server.wm.app27.Components.SDK_27_LAUNCH_ENTER_PIP_ACTIVITY;
import static android.server.wm.app27.Components.SDK_27_PIP_ACTIVITY;
import static android.view.Display.DEFAULT_DISPLAY;
import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation;
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.content.pm.ActivityInfo;
import android.content.pm.PackageManager;
import android.content.res.Configuration;
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.wm.CommandSession.ActivityCallback;
import android.server.wm.CommandSession.SizeInfo;
import android.server.wm.TestJournalProvider.TestJournalContainer;
import android.server.wm.WindowManagerState.ActivityTask;
import android.server.wm.settings.SettingsSession;
import android.util.Log;
import android.util.Size;
import androidx.test.filters.FlakyTest;
import com.android.compatibility.common.util.AppOpsUtils;
import com.android.compatibility.common.util.SystemUtil;
import org.junit.Before;
import org.junit.Ignore;
import org.junit.Test;
import java.io.IOException;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Build/Install/Run:
* atest CtsWindowManagerDeviceTestCases:PinnedStackTests
*/
@Presubmit
@android.server.wm.annotation.Group2
public class PinnedStackTests extends ActivityManagerTestBase {
private static final String TAG = PinnedStackTests.class.getSimpleName();
private static final String APP_OPS_OP_ENTER_PICTURE_IN_PICTURE = "PICTURE_IN_PICTURE";
private static final int APP_OPS_MODE_IGNORED = 1;
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;
@Before
@Override
public void setUp() throws Exception {
super.setUp();
assumeTrue(supportsPip());
}
@Test
public void testMinimumDeviceSize() throws Exception {
mWmState.assertDeviceDefaultDisplaySize(
"Devices supporting picture-in-picture must be larger than the default minimum"
+ " task size");
}
@Test
public void testEnterPictureInPictureMode() throws Exception {
pinnedStackTester(getAmStartCmd(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"),
PIP_ACTIVITY, PIP_ACTIVITY, false /* moveTopToPinnedStack */,
false /* isFocusable */);
}
@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
@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 {
// 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();
mWmState.computeState(
new WaitForValidActivityState(PIP_ACTIVITY));
mWmState.assertVisibility(PIP_ACTIVITY, true);
}
@Test
public void testPinnedStackInBoundsAfterRotation() {
assumeTrue("Skipping test: no rotation support", supportsRotation());
// Launch an activity that is not fixed-orientation so that the display can rotate
launchActivity(TEST_ACTIVITY);
// 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
final RotationSession rotationSession = createManagedRotationSession();
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 {
// 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
mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
// Wait for animation complete since we are comparing bounds
waitForEnterPipAnimationComplete(PIP_ACTIVITY);
assertPinnedStackExists();
assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY);
}
// TODO: launch/size pip to a size smaller than limitation and verify the minWidth/minHeight
// is respected after b/149338177.
@Test
public void testEnterPipWithMinimalSize() throws Exception {
// Launch a PiP activity with minimal size specified
launchActivity(PIP_ACTIVITY_WITH_MINIMAL_SIZE, EXTRA_ENTER_PIP, "true");
// Wait for animation complete since we are comparing size
waitForEnterPipAnimationComplete(PIP_ACTIVITY_WITH_MINIMAL_SIZE);
assertPinnedStackExists();
// query the minimal size
final PackageManager pm = getInstrumentation().getTargetContext().getPackageManager();
final ActivityInfo info = pm.getActivityInfo(
PIP_ACTIVITY_WITH_MINIMAL_SIZE, 0 /* flags */);
final Size minSize = new Size(info.windowLayout.minWidth, info.windowLayout.minHeight);
// compare the bounds with minimal size
final Rect pipBounds = getPinnedStackBounds();
assertTrue("Pinned stack bounds is no smaller than minimal",
(pipBounds.width() == minSize.getWidth()
&& pipBounds.height() >= minSize.getHeight())
|| (pipBounds.height() == minSize.getHeight()
&& pipBounds.width() >= minSize.getWidth()));
}
@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 {
// Launch a test activity so that we're not over home
launchActivity(TEST_ACTIVITY);
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 {
// Launch a test activity so that we're not over home
launchActivity(TEST_ACTIVITY);
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 {
// Launch a test activity so that we're not over home
launchActivity(TEST_ACTIVITY);
// 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 {
// Launch a test activity so that we're not over home
launchActivity(TEST_ACTIVITY);
// 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 {
// 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
mWmState.waitForActivityState(PIP_ON_STOP_ACTIVITY, STATE_STOPPED);
// Assert that there is no pinned stack (that enterPictureInPicture() failed)
assertPinnedStackDoesNotExist();
}
@Test
public void testAutoEnterPictureInPicture() throws Exception {
// 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 testAutoEnterPictureInPictureOnUserLeaveHintWhenPipRequestedNotOverridden()
throws Exception {
// Launch a test activity so that we're not over home
launchActivity(TEST_ACTIVITY);
// Launch the PIP activity that enters PIP on user leave hint, not on PIP requested
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_USER_LEAVE_HINT, "true");
assertPinnedStackDoesNotExist();
// Go home and ensure that there is a pinned stack
separateTestJournal();
launchHomeActivity();
waitForEnterPipAnimationComplete(PIP_ACTIVITY);
assertPinnedStackExists();
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY);
// Check the order of the callbacks accounting for a task overlay activity that might show.
// The PIP request (with a user leave hint) should come before the pip mode change.
final int firstUserLeaveIndex =
lifecycleCounts.getFirstIndex(ActivityCallback.ON_USER_LEAVE_HINT);
final int firstPipRequestedIndex =
lifecycleCounts.getFirstIndex(ActivityCallback.ON_PICTURE_IN_PICTURE_REQUESTED);
final int firstPipModeChangedIndex =
lifecycleCounts.getFirstIndex(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED);
assertTrue("missing request", firstPipRequestedIndex != -1);
assertTrue("missing user leave", firstUserLeaveIndex != -1);
assertTrue("missing pip mode changed", firstPipModeChangedIndex != -1);
assertTrue("pip requested not before pause",
firstPipRequestedIndex < firstUserLeaveIndex);
assertTrue("unexpected user leave hint",
firstUserLeaveIndex < firstPipModeChangedIndex);
}
@Test
public void testAutoEnterPictureInPictureOnPictureInPictureRequested() throws Exception {
// Launch a test activity so that we're not over home
launchActivity(TEST_ACTIVITY);
// Launch the PIP activity on pip requested
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PIP_REQUESTED, "true");
assertPinnedStackDoesNotExist();
// Call onPictureInPictureRequested and verify activity enters pip
separateTestJournal();
mBroadcastActionTrigger.doAction(ACTION_ON_PIP_REQUESTED);
waitForEnterPipAnimationComplete(PIP_ACTIVITY);
assertPinnedStackExists();
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY);
// Check the order of the callbacks accounting for a task overlay activity that might show.
// The PIP request (without a user leave hint) should come before the pip mode change.
final int firstUserLeaveIndex =
lifecycleCounts.getFirstIndex(ActivityCallback.ON_USER_LEAVE_HINT);
final int firstPipRequestedIndex =
lifecycleCounts.getFirstIndex(ActivityCallback.ON_PICTURE_IN_PICTURE_REQUESTED);
final int firstPipModeChangedIndex =
lifecycleCounts.getFirstIndex(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED);
assertTrue("missing request", firstPipRequestedIndex != -1);
assertTrue("missing pip mode changed", firstPipModeChangedIndex != -1);
assertTrue("pip requested not before pause",
firstPipRequestedIndex < firstPipModeChangedIndex);
assertTrue("unexpected user leave hint",
firstUserLeaveIndex == -1 || firstUserLeaveIndex > firstPipModeChangedIndex);
}
@Test
public void testAutoEnterPictureInPictureLaunchActivity() throws Exception {
// 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));
mWmState.computeState(
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 {
// 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();
}
@Test
public void testAutoEnterPictureInPictureAspectRatio() throws Exception {
// 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);
}
@Test
public void testAutoEnterPictureInPictureOverPip() throws Exception {
// 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 ActivityTask pinnedStack = getPinnedStack();
assertEquals(getActivityName(ALWAYS_FOCUSABLE_PIP_ACTIVITY), pinnedStack.mRealActivity);
}
@Test
public void testDismissPipWhenLaunchNewOne() throws Exception {
// Launch another PIP activity
launchActivity(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY);
waitForEnterPip(ALWAYS_FOCUSABLE_PIP_ACTIVITY);
assertPinnedStackExists();
final ActivityTask pinnedStack = getPinnedStack();
launchActivityInNewTask(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY);
waitForEnterPip(ALWAYS_FOCUSABLE_PIP_ACTIVITY);
assertEquals(1, mWmState.countStacks(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD));
}
@Test
@FlakyTest(bugId=159062106)
public void testDisallowMultipleTasksInPinnedStack() throws Exception {
// 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");
waitForEnterPipAnimationComplete(PIP_ACTIVITY);
// Launch second PIP activity
launchActivity(PIP_ACTIVITY2, EXTRA_ENTER_PIP, "true");
final ActivityTask pinnedStack = getPinnedStack();
assertEquals(0, pinnedStack.getTasks().size());
assertTrue(mWmState.containsActivityInWindowingMode(
PIP_ACTIVITY2, WINDOWING_MODE_PINNED));
assertTrue(mWmState.containsActivityInWindowingMode(
PIP_ACTIVITY, WINDOWING_MODE_FULLSCREEN));
}
@Test
public void testPipUnPipOverHome() throws Exception {
// Go home
launchHomeActivity();
// Launch an auto pip activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
waitForEnterPip(PIP_ACTIVITY);
assertPinnedStackExists();
// Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip
launchActivity(PIP_ACTIVITY);
waitForExitPipToFullscreen(PIP_ACTIVITY);
mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
waitForEnterPipAnimationComplete(PIP_ACTIVITY);
mWmState.assertHomeActivityVisible(true);
}
@Test
public void testPipUnPipOverApp() throws Exception {
// 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");
waitForEnterPip(PIP_ACTIVITY);
assertPinnedStackExists();
// Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip
launchActivity(PIP_ACTIVITY);
waitForExitPipToFullscreen(PIP_ACTIVITY);
mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
waitForEnterPipAnimationComplete(PIP_ACTIVITY);
mWmState.assertVisibility(TEST_ACTIVITY, true);
}
@Test
public void testRemovePipWithNoFullscreenStack() throws Exception {
// 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);
}
@Test
public void testRemovePipWithVisibleFullscreenStack() throws Exception {
// 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);
}
@Test
public void testRemovePipWithHiddenFullscreenStack() throws Exception {
// 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 {
// 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)
mBroadcastActionTrigger.doAction(ACTION_MOVE_TO_BACK);
assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY,
WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
}
@Test
public void testMovePipToBackWithVisibleFullscreenStack() throws Exception {
// 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
mBroadcastActionTrigger.doAction(ACTION_MOVE_TO_BACK);
assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY,
WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD);
}
@Test
public void testMovePipToBackWithHiddenFullscreenStack() throws Exception {
// 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
mBroadcastActionTrigger.doAction(ACTION_MOVE_TO_BACK);
assertPinnedStackStateOnMoveToFullscreen(
PIP_ACTIVITY, WINDOWING_MODE_UNDEFINED, ACTIVITY_TYPE_HOME);
}
@Test
public void testPinnedStackAlwaysOnTop() throws Exception {
// 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 {
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 {
// 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 testLaunchStoppedActivityWithPiPInSameProcessPreQ() {
// Try to enter picture-in-picture from an activity that has more than one activity in the
// task and ensure that it works, for pre-Q app
launchActivity(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY,
EXTRA_ENTER_PIP, "true");
waitForEnterPip(SDK_27_PIP_ACTIVITY);
assertPinnedStackExists();
// Puts the host activity to stopped state
launchHomeActivity();
mWmState.assertHomeActivityVisible(true);
waitAndAssertActivityState(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY, STATE_STOPPED,
"Activity should become STOPPED");
mWmState.assertVisibility(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY, false);
// Host activity should be visible after re-launch and PiP window still exists
launchActivity(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY);
waitAndAssertActivityState(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY, STATE_RESUMED,
"Activity should become RESUMED");
mWmState.assertVisibility(SDK_27_LAUNCH_ENTER_PIP_ACTIVITY, true);
assertPinnedStackExists();
}
@Test
public void testEnterPipWithResumeWhilePausingActivityNoStop() throws Exception {
/*
* 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) Start the PiP activity that will enter picture-in-picture when paused in the
* fullscreen stack
* 3) Bring the activity in the dynamic stack forward to trigger PiP
*/
launchActivity(RESUME_WHILE_PAUSING_ACTIVITY);
// 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 {
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true");
ActivityTask task = mWmState.getStandardStackByWindowingMode(
WINDOWING_MODE_FULLSCREEN).getTopTask();
// Lock the task and ensure that we can't enter picture-in-picture both explicitly and
// when paused
SystemUtil.runWithShellPermissionIdentity(() -> {
try {
mAtm.startSystemLockTaskMode(task.mTaskId);
waitForOrFail("Task in lock mode", () -> {
return mAm.getLockTaskModeState() != LOCK_TASK_MODE_NONE;
});
mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
waitForEnterPip(PIP_ACTIVITY);
assertPinnedStackDoesNotExist();
launchHomeActivity();
assertPinnedStackDoesNotExist();
} finally {
mAtm.stopSystemLockTaskMode();
}
});
}
@Test
public void testConfigurationChangeOrderDuringTransition() throws Exception {
// 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);
separateTestJournal();
mBroadcastActionTrigger.doAction(ACTION_ENTER_PIP);
waitForEnterPip(PIP_ACTIVITY);
assertPinnedStackExists();
waitForValidPictureInPictureCallbacks(PIP_ACTIVITY);
assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY);
// Trigger it to go back to fullscreen and ensure that only triggered one configuration
// change as well
separateTestJournal();
launchActivity(PIP_ACTIVITY);
waitForValidPictureInPictureCallbacks(PIP_ACTIVITY);
assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY);
}
/** Helper class to save, set, and restore transition_animation_scale preferences. */
private static class TransitionAnimationScaleSession extends SettingsSession<Float> {
TransitionAnimationScaleSession() {
super(Settings.Global.getUriFor(Settings.Global.TRANSITION_ANIMATION_SCALE),
Settings.Global::getFloat,
Settings.Global::putFloat);
}
@Override
public void close() {
// Wait for the restored setting to apply before we continue on with the next test
final CountDownLatch waitLock = new CountDownLatch(1);
final Context context = getInstrumentation().getTargetContext();
context.getContentResolver().registerContentObserver(mUri, false,
new ContentObserver(new Handler(Looper.getMainLooper())) {
@Override
public void onChange(boolean selfChange) {
waitLock.countDown();
}
});
super.close();
try {
if (!waitLock.await(2, TimeUnit.SECONDS)) {
Log.i(TAG, "TransitionAnimationScaleSession value not restored");
}
} catch (InterruptedException impossible) {}
}
}
@Ignore("b/149946388")
@Test
public void testEnterPipInterruptedCallbacks() {
final TransitionAnimationScaleSession transitionAnimationScaleSession =
mObjectTracker.manage(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
separateTestJournal();
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);
assertEquals("onConfigurationChanged", 0,
lifecycleCounts.getCount(ActivityCallback.ON_CONFIGURATION_CHANGED));
assertEquals("onPictureInPictureModeChanged", 1,
lifecycleCounts.getCount(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED));
assertEquals("onMultiWindowModeChanged", 1,
lifecycleCounts.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
}
@Test
public void testStopBeforeMultiWindowCallbacksOnDismiss() throws Exception {
// Launch a PiP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
// Wait for animation complete so that system has reported pip mode change event to
// client and the last reported pip mode has updated.
waitForEnterPipAnimationComplete(PIP_ACTIVITY);
assertPinnedStackExists();
// Dismiss it
separateTestJournal();
removeStacksInWindowingModes(WINDOWING_MODE_PINNED);
waitForExitPipToFullscreen(PIP_ACTIVITY);
waitForValidPictureInPictureCallbacks(PIP_ACTIVITY);
// Confirm that we get stop before the multi-window and picture-in-picture mode change
// callbacks
final ActivityLifecycleCounts lifecycles = new ActivityLifecycleCounts(PIP_ACTIVITY);
assertEquals("onStop", 1, lifecycles.getCount(ActivityCallback.ON_STOP));
assertEquals("onPictureInPictureModeChanged", 1,
lifecycles.getCount(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED));
assertEquals("onMultiWindowModeChanged", 1,
lifecycles.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
final int lastStopIndex = lifecycles.getLastIndex(ActivityCallback.ON_STOP);
final int lastPipIndex = lifecycles.getLastIndex(
ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED);
final int lastMwIndex = lifecycles.getLastIndex(
ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED);
assertThat("onStop should be before onPictureInPictureModeChanged",
lastStopIndex, lessThan(lastPipIndex));
assertThat("onPictureInPictureModeChanged should be before onMultiWindowModeChanged",
lastPipIndex, lessThan(lastMwIndex));
}
@Test
public void testPreventSetAspectRatioWhileExpanding() throws Exception {
// 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
mBroadcastActionTrigger.expandPipWithAspectRatio("123456789", "100000000");
waitForExitPipToFullscreen(PIP_ACTIVITY);
assertPinnedStackDoesNotExist();
}
@Test
public void testSetRequestedOrientationWhilePinned() throws Exception {
// 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
mBroadcastActionTrigger.requestOrientationForPip(ORIENTATION_LANDSCAPE);
// Launch the activity back into fullscreen and ensure that it is now in landscape
launchActivity(PIP_ACTIVITY);
waitForExitPipToFullscreen(PIP_ACTIVITY);
assertPinnedStackDoesNotExist();
mWmState.waitForLastOrientation(ORIENTATION_LANDSCAPE);
assertEquals(ORIENTATION_LANDSCAPE, mWmState.getLastOrientation());
}
@Test
public void testWindowButtonEntersPip() throws Exception {
assumeTrue(!mWmState.isHomeRecentsComponent());
// Launch the PiP activity trigger the window button, ensure that we have entered PiP
launchActivity(PIP_ACTIVITY);
pressWindowButton();
waitForEnterPip(PIP_ACTIVITY);
assertPinnedStackExists();
}
@Test
@FlakyTest(bugId=156314330)
public void testFinishPipActivityWithTaskOverlay() throws Exception {
// Trigger PiP menu activity to properly lose focuse when going home
launchActivity(TEST_ACTIVITY);
// Launch PiP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
waitForEnterPip(PIP_ACTIVITY);
assertPinnedStackExists();
int taskId = mWmState.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
mBroadcastActionTrigger.doAction(ACTION_FINISH);
waitForPinnedStackRemoved();
assertPinnedStackDoesNotExist();
}
@Test
public void testNoResumeAfterTaskOverlayFinishes() throws Exception {
// Launch PiP activity
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
waitForEnterPip(PIP_ACTIVITY);
assertPinnedStackExists();
ActivityTask stack = mWmState.getStandardStackByWindowingMode(WINDOWING_MODE_PINNED);
int taskId = stack.getTopTask().mTaskId;
// Launch task overlay activity into PiP activity task
launchPinnedActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId);
// Finish the task overlay activity and ensure that the PiP activity never got resumed.
separateTestJournal();
mBroadcastActionTrigger.doAction(TEST_ACTIVITY_ACTION_FINISH_SELF);
mWmState.waitFor((amState) ->
!amState.containsActivity(TRANSLUCENT_TEST_ACTIVITY),
"Waiting for test activity to finish...");
final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY);
assertEquals("onResume", 0, lifecycleCounts.getCount(ActivityCallback.ON_RESUME));
assertEquals("onPause", 0, lifecycleCounts.getCount(ActivityCallback.ON_PAUSE));
}
@FlakyTest(bugId = 156003518)
@Test
public void testPinnedStackWithDockedStack() throws Exception {
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)
);
mWmState.assertVisibility(PIP_ACTIVITY, true);
mWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
mWmState.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)
);
mWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
mWmState.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();
mWmState.assertVisibility(LAUNCHING_ACTIVITY, true);
mWmState.assertVisibility(TEST_ACTIVITY, false);
}
@Test
public void testLaunchTaskByComponentMatchMultipleTasks() throws Exception {
// 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 = mWmState.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();
mWmState.assertFocusedActivity("Expected root activity focused",
TEST_ACTIVITY_WITH_SAME_AFFINITY);
assertEquals(rootActivityTaskId, mWmState.getTaskByActivity(
TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId);
}
@Test
public void testLaunchTaskByAffinityMatchMultipleTasks() throws Exception {
// 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");
mWmState.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 = mWmState.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();
mWmState.assertFocusedActivity("Expected root activity focused", TEST_ACTIVITY);
assertEquals(rootActivityTaskId, mWmState.getTaskByActivity(
TEST_ACTIVITY).mTaskId);
}
@Test
public void testLaunchTaskByAffinityMatchSingleTask() throws Exception {
// 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(PIP_ACTIVITY);
assertPinnedStackExists();
// Launch the root activity again, of the matching task and ensure that we expand to
// fullscreen
int activityTaskId = mWmState.getTaskByActivity(PIP_ACTIVITY).mTaskId;
launchHomeActivity();
launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY);
waitForExitPipToFullscreen(PIP_ACTIVITY);
assertPinnedStackDoesNotExist();
assertEquals(activityTaskId, mWmState.getTaskByActivity(
PIP_ACTIVITY).mTaskId);
}
/** Test that reported display size corresponds to fullscreen after exiting PiP. */
@Test
public void testDisplayMetricsPinUnpin() throws Exception {
separateTestJournal();
launchActivity(TEST_ACTIVITY);
final int defaultWindowingMode = mWmState
.getTaskByActivity(TEST_ACTIVITY).getWindowingMode();
final SizeInfo initialSizes = getLastReportedSizesForActivity(TEST_ACTIVITY);
final Rect initialAppBounds = getAppBounds(TEST_ACTIVITY);
assertNotNull("Must report display dimensions", initialSizes);
assertNotNull("Must report app bounds", initialAppBounds);
separateTestJournal();
launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true");
// Wait for animation complete since we are comparing bounds
waitForEnterPipAnimationComplete(PIP_ACTIVITY);
final SizeInfo pinnedSizes = getLastReportedSizesForActivity(PIP_ACTIVITY);
final Rect pinnedAppBounds = getAppBounds(PIP_ACTIVITY);
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);
separateTestJournal();
launchActivity(PIP_ACTIVITY, defaultWindowingMode);
final SizeInfo finalSizes = getLastReportedSizesForActivity(PIP_ACTIVITY);
final Rect finalAppBounds = getAppBounds(PIP_ACTIVITY);
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);
}
/** Get app bounds in last applied configuration. */
private Rect getAppBounds(ComponentName activityName) {
final Configuration config = TestJournalContainer.get(activityName).extras
.getParcelable(EXTRA_CONFIGURATION);
if (config != null) {
return config.windowConfiguration.getAppBounds();
}
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) {
mWmState.waitForFocusedStack(windowingMode, activityType);
mWmState.assertFocusedStack("Wrong focused stack", windowingMode, activityType);
waitAndAssertActivityState(activityName, STATE_STOPPED,
"Activity should go to STOPPED");
assertTrue(mWmState.containsActivityInWindowingMode(
activityName, WINDOWING_MODE_FULLSCREEN));
assertPinnedStackDoesNotExist();
}
/**
* 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.DisplayContent display = mWmState.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() {
mWmState.assertContainsStack("Must contain pinned stack.", WINDOWING_MODE_PINNED,
ACTIVITY_TYPE_STANDARD);
}
/**
* Asserts that the pinned stack does not exist.
*/
private void assertPinnedStackDoesNotExist() {
mWmState.assertDoesNotContainStack("Must not contain pinned stack.",
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
}
/**
* Asserts that the pinned stack is the front stack.
*/
private void assertPinnedStackIsOnTop() {
mWmState.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) {
final ActivityLifecycleCounts lifecycles = new ActivityLifecycleCounts(activityName);
assertEquals(getActivityName(activityName) + " onConfigurationChanged()",
1, lifecycles.getCount(ActivityCallback.ON_CONFIGURATION_CHANGED));
assertEquals(getActivityName(activityName) + " onPictureInPictureModeChanged()",
1, lifecycles.getCount(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED));
assertEquals(getActivityName(activityName) + " onMultiWindowModeChanged",
1, lifecycles.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED));
final int lastPipIndex = lifecycles
.getLastIndex(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED);
final int lastMwIndex = lifecycles
.getLastIndex(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED);
final int lastConfigIndex = lifecycles
.getLastIndex(ActivityCallback.ON_CONFIGURATION_CHANGED);
assertThat("onPictureInPictureModeChanged should be before onMultiWindowModeChanged",
lastPipIndex, lessThan(lastMwIndex));
assertThat("onMultiWindowModeChanged should be before onConfigurationChanged",
lastMwIndex, lessThan(lastConfigIndex));
}
/**
* Waits until the given activity has entered picture-in-picture mode (allowing for the
* subsequent animation to start).
*/
private void waitForEnterPip(ComponentName activityName) {
mWmState.waitForWithAmState(wmState -> {
ActivityTask task = wmState.getTaskByActivity(activityName);
return task != null && task.getWindowingMode() == WINDOWING_MODE_PINNED;
}, "checking task windowing mode");
}
/**
* Waits until the picture-in-picture animation has finished.
*/
private void waitForEnterPipAnimationComplete(ComponentName activityName) {
waitForEnterPip(activityName);
mWmState.waitForWithAmState(wmState -> {
ActivityTask task = wmState.getTaskByActivity(activityName);
if (task == null) {
return false;
}
WindowManagerState.Activity activity = task.getActivity(activityName);
return activity.getWindowingMode() == WINDOWING_MODE_PINNED
&& activity.getState().equals(STATE_PAUSED);
}, "checking activity windowing mode");
}
/**
* Waits until the pinned stack has been removed.
*/
private void waitForPinnedStackRemoved() {
mWmState.waitFor((amState) ->
!amState.containsStack(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD),
"pinned stack to be removed");
}
/**
* Waits until the picture-in-picture animation to fullscreen has finished.
*/
private void waitForExitPipToFullscreen(ComponentName activityName) {
mWmState.waitForWithAmState(wmState -> {
final ActivityTask task = wmState.getTaskByActivity(activityName);
if (task == null) {
return false;
}
final WindowManagerState.Activity activity = task.getActivity(activityName);
return activity.getWindowingMode() != WINDOWING_MODE_PINNED;
}, "checking activity windowing mode");
mWmState.waitForWithAmState(wmState -> {
final ActivityTask task = wmState.getTaskByActivity(activityName);
return task != null && task.getWindowingMode() != WINDOWING_MODE_PINNED;
}, "checking task windowing mode");
}
/**
* Waits until the expected picture-in-picture callbacks have been made.
*/
private void waitForValidPictureInPictureCallbacks(ComponentName activityName) {
mWmState.waitFor((amState) -> {
final ActivityLifecycleCounts lifecycles = new ActivityLifecycleCounts(activityName);
return lifecycles.getCount(ActivityCallback.ON_CONFIGURATION_CHANGED) == 1
&& lifecycles.getCount(ActivityCallback.ON_PICTURE_IN_PICTURE_MODE_CHANGED) == 1
&& lifecycles.getCount(ActivityCallback.ON_MULTI_WINDOW_MODE_CHANGED) == 1;
}, "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
mWmState.waitForWithAmState((state) -> {
Rect bounds = state.getStandardStackByWindowingMode(WINDOWING_MODE_PINNED).getBounds();
return floatEquals((float) bounds.width() / bounds.height(), (float) num / denom);
}, "valid aspect ratio");
}
/**
* @return the window state for the given {@param activityName}'s window.
*/
private WindowManagerState.WindowState getWindowState(ComponentName activityName) {
String windowName = getWindowName(activityName);
mWmState.computeState(activityName);
final List<WindowManagerState.WindowState> tempWindowList =
mWmState.getMatchingVisibleWindowState(windowName);
return tempWindowList.get(0);
}
/**
* @return the current pinned stack.
*/
private ActivityTask getPinnedStack() {
return mWmState.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;
tapOnDisplaySync(tapX, tapY, DEFAULT_DISPLAY);
}
/**
* 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");
mWmState.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();
}
/**
* Sets an app-ops op for a given package to a given mode.
*/
void setOpToMode(String op, int mode) {
try {
AppOpsUtils.setOpMode(mPackageName, op, mode);
} catch (Exception e) {
e.printStackTrace();
}
}
@Override
public void close() {
try {
AppOpsUtils.reset(mPackageName);
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* 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);
mWmState.waitForValidState(startActivity);
if (moveTopToPinnedStack) {
final int stackId = mWmState.getStackIdByActivity(topActivityName);
assertNotEquals(stackId, INVALID_STACK_ID);
moveTopActivityToPinnedStack(stackId);
}
mWmState.waitForValidState(new WaitForValidActivityState.Builder(topActivityName)
.setWindowingMode(WINDOWING_MODE_PINNED)
.setActivityType(ACTIVITY_TYPE_STANDARD)
.build());
mWmState.computeState();
if (supportsPip()) {
final String windowName = getWindowName(topActivityName);
assertPinnedStackExists();
mWmState.assertFrontStack("Pinned stack must be the front stack.",
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
mWmState.assertVisibility(topActivityName, true);
if (isFocusable) {
mWmState.assertFocusedStack("Pinned stack must be the focused stack.",
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
mWmState.assertFocusedActivity(
"Pinned activity must be focused activity.", topActivityName);
mWmState.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.
mWmState.assertNotFocusedActivity(
"Pinned activity can't be the focused activity.", topActivityName);
mWmState.assertNotResumedActivity(
"Pinned activity can't be the resumed activity.", topActivityName);
mWmState.assertNotFocusedWindow(
"Pinned window can't be focused window.", windowName);
}
} else {
mWmState.assertDoesNotContainStack("Must not contain pinned stack.",
WINDOWING_MODE_PINNED, ACTIVITY_TYPE_STANDARD);
}
}
}