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