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