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.cts; 18 19 import static android.server.cts.ActivityAndWindowManagersState.DEFAULT_DISPLAY_ID; 20 import static android.server.cts.ActivityManagerState.STATE_STOPPED; 21 22 import android.server.cts.ActivityManagerState.Activity; 23 import android.server.cts.ActivityManagerState.ActivityStack; 24 import android.server.cts.ActivityManagerState.ActivityTask; 25 26 import java.awt.Rectangle; 27 import java.lang.Exception; 28 import java.lang.String; 29 import java.util.ArrayList; 30 import java.util.List; 31 import java.util.regex.Matcher; 32 import java.util.regex.Pattern; 33 34 /** 35 * Build: mmma -j32 cts/hostsidetests/services 36 * Run: cts/hostsidetests/services/activityandwindowmanager/util/run-test CtsServicesHostTestCases android.server.cts.ActivityManagerPinnedStackTests 37 */ 38 public class ActivityManagerPinnedStackTests extends ActivityManagerTestBase { 39 private static final String TEST_ACTIVITY = "TestActivity"; 40 private static final String TEST_ACTIVITY_WITH_SAME_AFFINITY = "TestActivityWithSameAffinity"; 41 private static final String TRANSLUCENT_TEST_ACTIVITY = "TranslucentTestActivity"; 42 private static final String NON_RESIZEABLE_ACTIVITY = "NonResizeableActivity"; 43 private static final String RESUME_WHILE_PAUSING_ACTIVITY = "ResumeWhilePausingActivity"; 44 private static final String PIP_ACTIVITY = "PipActivity"; 45 private static final String PIP_ACTIVITY2 = "PipActivity2"; 46 private static final String PIP_ACTIVITY_WITH_SAME_AFFINITY = "PipActivityWithSameAffinity"; 47 private static final String ALWAYS_FOCUSABLE_PIP_ACTIVITY = "AlwaysFocusablePipActivity"; 48 private static final String LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY = 49 "LaunchIntoPinnedStackPipActivity"; 50 private static final String LAUNCH_ENTER_PIP_ACTIVITY = "LaunchEnterPipActivity"; 51 private static final String PIP_ON_STOP_ACTIVITY = "PipOnStopActivity"; 52 53 private static final String EXTRA_FIXED_ORIENTATION = "fixed_orientation"; 54 private static final String EXTRA_ENTER_PIP = "enter_pip"; 55 private static final String EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR = 56 "enter_pip_aspect_ratio_numerator"; 57 private static final String EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR = 58 "enter_pip_aspect_ratio_denominator"; 59 private static final String EXTRA_SET_ASPECT_RATIO_NUMERATOR = "set_aspect_ratio_numerator"; 60 private static final String EXTRA_SET_ASPECT_RATIO_DENOMINATOR = "set_aspect_ratio_denominator"; 61 private static final String EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR = 62 "set_aspect_ratio_with_delay_numerator"; 63 private static final String EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR = 64 "set_aspect_ratio_with_delay_denominator"; 65 private static final String EXTRA_ENTER_PIP_ON_PAUSE = "enter_pip_on_pause"; 66 private static final String EXTRA_TAP_TO_FINISH = "tap_to_finish"; 67 private static final String EXTRA_START_ACTIVITY = "start_activity"; 68 private static final String EXTRA_FINISH_SELF_ON_RESUME = "finish_self_on_resume"; 69 private static final String EXTRA_REENTER_PIP_ON_EXIT = "reenter_pip_on_exit"; 70 private static final String EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP = "assert_no_on_stop_before_pip"; 71 private static final String EXTRA_ON_PAUSE_DELAY = "on_pause_delay"; 72 73 private static final String PIP_ACTIVITY_ACTION_ENTER_PIP = 74 "android.server.cts.PipActivity.enter_pip"; 75 private static final String PIP_ACTIVITY_ACTION_MOVE_TO_BACK = 76 "android.server.cts.PipActivity.move_to_back"; 77 private static final String PIP_ACTIVITY_ACTION_EXPAND_PIP = 78 "android.server.cts.PipActivity.expand_pip"; 79 private static final String PIP_ACTIVITY_ACTION_SET_REQUESTED_ORIENTATION = 80 "android.server.cts.PipActivity.set_requested_orientation"; 81 private static final String PIP_ACTIVITY_ACTION_FINISH = 82 "android.server.cts.PipActivity.finish"; 83 private static final String TEST_ACTIVITY_ACTION_FINISH = 84 "android.server.cts.TestActivity.finish_self"; 85 86 private static final int APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE = 67; 87 private static final int APP_OPS_MODE_ALLOWED = 0; 88 private static final int APP_OPS_MODE_IGNORED = 1; 89 private static final int APP_OPS_MODE_ERRORED = 2; 90 91 private static final int ROTATION_0 = 0; 92 private static final int ROTATION_90 = 1; 93 private static final int ROTATION_180 = 2; 94 private static final int ROTATION_270 = 3; 95 96 // Corresponds to ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE 97 private static final int ORIENTATION_LANDSCAPE = 0; 98 // Corresponds to ActivityInfo.SCREEN_ORIENTATION_PORTRAIT 99 private static final int ORIENTATION_PORTRAIT = 1; 100 101 private static final float FLOAT_COMPARE_EPSILON = 0.005f; 102 103 // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio 104 private static final int MIN_ASPECT_RATIO_NUMERATOR = 100; 105 private static final int MIN_ASPECT_RATIO_DENOMINATOR = 239; 106 private static final int BELOW_MIN_ASPECT_RATIO_DENOMINATOR = MIN_ASPECT_RATIO_DENOMINATOR + 1; 107 // Corresponds to com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio 108 private static final int MAX_ASPECT_RATIO_NUMERATOR = 239; 109 private static final int MAX_ASPECT_RATIO_DENOMINATOR = 100; 110 private static final int ABOVE_MAX_ASPECT_RATIO_NUMERATOR = MAX_ASPECT_RATIO_NUMERATOR + 1; 111 testMinimumDeviceSize()112 public void testMinimumDeviceSize() throws Exception { 113 if (!supportsPip()) return; 114 115 mAmWmState.assertDeviceDefaultDisplaySize(mDevice, 116 "Devices supporting picture-in-picture must be larger than the default minimum" 117 + " task size"); 118 } 119 testEnterPictureInPictureMode()120 public void testEnterPictureInPictureMode() throws Exception { 121 pinnedStackTester(getAmStartCmd(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"), PIP_ACTIVITY, 122 false /* moveTopToPinnedStack */, false /* isFocusable */); 123 } 124 testMoveTopActivityToPinnedStack()125 public void testMoveTopActivityToPinnedStack() throws Exception { 126 pinnedStackTester(getAmStartCmd(PIP_ACTIVITY), PIP_ACTIVITY, 127 true /* moveTopToPinnedStack */, false /* isFocusable */); 128 } 129 testAlwaysFocusablePipActivity()130 public void testAlwaysFocusablePipActivity() throws Exception { 131 pinnedStackTester(getAmStartCmd(ALWAYS_FOCUSABLE_PIP_ACTIVITY), 132 ALWAYS_FOCUSABLE_PIP_ACTIVITY, false /* moveTopToPinnedStack */, 133 true /* isFocusable */); 134 } 135 testLaunchIntoPinnedStack()136 public void testLaunchIntoPinnedStack() throws Exception { 137 pinnedStackTester(getAmStartCmd(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY), 138 ALWAYS_FOCUSABLE_PIP_ACTIVITY, false /* moveTopToPinnedStack */, 139 true /* isFocusable */); 140 } 141 testNonTappablePipActivity()142 public void testNonTappablePipActivity() throws Exception { 143 if (!supportsPip()) return; 144 145 // Launch the tap-to-finish activity at a specific place 146 launchActivity(PIP_ACTIVITY, 147 EXTRA_ENTER_PIP, "true", 148 EXTRA_TAP_TO_FINISH, "true"); 149 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 150 assertPinnedStackExists(); 151 152 // Tap the screen at a known location in the pinned stack bounds, and ensure that it is 153 // not passed down to the top task 154 tapToFinishPip(); 155 mAmWmState.computeState(mDevice, new String[] {PIP_ACTIVITY}, 156 false /* compareTaskAndStackBounds */); 157 mAmWmState.assertVisibility(PIP_ACTIVITY, true); 158 } 159 testPinnedStackDefaultBounds()160 public void testPinnedStackDefaultBounds() throws Exception { 161 if (!supportsPip()) return; 162 163 // Launch a PIP activity 164 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 165 166 setDeviceRotation(ROTATION_0); 167 WindowManagerState wmState = mAmWmState.getWmState(); 168 wmState.computeState(mDevice); 169 Rectangle defaultPipBounds = wmState.getDefaultPinnedStackBounds(); 170 Rectangle stableBounds = wmState.getStableBounds(); 171 assertTrue(defaultPipBounds.width > 0 && defaultPipBounds.height > 0); 172 assertTrue(stableBounds.contains(defaultPipBounds)); 173 174 setDeviceRotation(ROTATION_90); 175 wmState = mAmWmState.getWmState(); 176 wmState.computeState(mDevice); 177 defaultPipBounds = wmState.getDefaultPinnedStackBounds(); 178 stableBounds = wmState.getStableBounds(); 179 assertTrue(defaultPipBounds.width > 0 && defaultPipBounds.height > 0); 180 assertTrue(stableBounds.contains(defaultPipBounds)); 181 setDeviceRotation(ROTATION_0); 182 } 183 testPinnedStackMovementBounds()184 public void testPinnedStackMovementBounds() throws Exception { 185 if (!supportsPip()) return; 186 187 // Launch a PIP activity 188 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 189 190 setDeviceRotation(ROTATION_0); 191 WindowManagerState wmState = mAmWmState.getWmState(); 192 wmState.computeState(mDevice); 193 Rectangle pipMovementBounds = wmState.getPinnedStackMomentBounds(); 194 Rectangle stableBounds = wmState.getStableBounds(); 195 assertTrue(pipMovementBounds.width > 0 && pipMovementBounds.height > 0); 196 assertTrue(stableBounds.contains(pipMovementBounds)); 197 198 setDeviceRotation(ROTATION_90); 199 wmState = mAmWmState.getWmState(); 200 wmState.computeState(mDevice); 201 pipMovementBounds = wmState.getPinnedStackMomentBounds(); 202 stableBounds = wmState.getStableBounds(); 203 assertTrue(pipMovementBounds.width > 0 && pipMovementBounds.height > 0); 204 assertTrue(stableBounds.contains(pipMovementBounds)); 205 setDeviceRotation(ROTATION_0); 206 } 207 testPinnedStackOutOfBoundsInsetsNonNegative()208 public void testPinnedStackOutOfBoundsInsetsNonNegative() throws Exception { 209 if (!supportsPip()) return; 210 211 final WindowManagerState wmState = mAmWmState.getWmState(); 212 213 // Launch an activity into the pinned stack 214 launchActivity(PIP_ACTIVITY, 215 EXTRA_ENTER_PIP, "true", 216 EXTRA_TAP_TO_FINISH, "true"); 217 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 218 219 // Get the display dimensions 220 WindowManagerState.WindowState windowState = getWindowState(PIP_ACTIVITY); 221 WindowManagerState.Display display = wmState.getDisplay(windowState.getDisplayId()); 222 Rectangle displayRect = display.getDisplayRect(); 223 224 // Move the pinned stack offscreen 225 String moveStackOffscreenCommand = String.format("am stack resize 4 %d %d %d %d", 226 displayRect.width - 200, 0, displayRect.width + 200, 500); 227 executeShellCommand(moveStackOffscreenCommand); 228 229 // Ensure that the surface insets are not negative 230 windowState = getWindowState(PIP_ACTIVITY); 231 Rectangle contentInsets = windowState.getContentInsets(); 232 assertTrue(contentInsets.x >= 0 && contentInsets.y >= 0 && contentInsets.width >= 0 && 233 contentInsets.height >= 0); 234 } 235 testPinnedStackInBoundsAfterRotation()236 public void testPinnedStackInBoundsAfterRotation() throws Exception { 237 if (!supportsPip()) return; 238 239 // Launch an activity into the pinned stack 240 launchActivity(PIP_ACTIVITY, 241 EXTRA_ENTER_PIP, "true", 242 EXTRA_TAP_TO_FINISH, "true"); 243 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 244 245 // Ensure that the PIP stack is fully visible in each orientation 246 setDeviceRotation(ROTATION_0); 247 assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); 248 setDeviceRotation(ROTATION_90); 249 assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); 250 setDeviceRotation(ROTATION_180); 251 assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); 252 setDeviceRotation(ROTATION_270); 253 assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); 254 setDeviceRotation(ROTATION_0); 255 } 256 testEnterPipToOtherOrientation()257 public void testEnterPipToOtherOrientation() throws Exception { 258 if (!supportsPip()) return; 259 260 // Launch a portrait only app on the fullscreen stack 261 launchActivity(TEST_ACTIVITY, 262 EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT)); 263 // Launch the PiP activity fixed as landscape 264 launchActivity(PIP_ACTIVITY, 265 EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_LANDSCAPE)); 266 // Enter PiP, and assert that the PiP is within bounds now that the device is back in 267 // portrait 268 executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_ENTER_PIP); 269 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 270 assertPinnedStackExists(); 271 assertPinnedStackActivityIsInDisplayBounds(PIP_ACTIVITY); 272 } 273 testEnterPipAspectRatioMin()274 public void testEnterPipAspectRatioMin() throws Exception { 275 testEnterPipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR); 276 } 277 testEnterPipAspectRatioMax()278 public void testEnterPipAspectRatioMax() throws Exception { 279 testEnterPipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR); 280 } 281 testEnterPipAspectRatio(int num, int denom)282 private void testEnterPipAspectRatio(int num, int denom) throws Exception { 283 if (!supportsPip()) return; 284 285 launchActivity(PIP_ACTIVITY, 286 EXTRA_ENTER_PIP, "true", 287 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num), 288 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)); 289 assertPinnedStackExists(); 290 291 // Assert that we have entered PIP and that the aspect ratio is correct 292 Rectangle pinnedStackBounds = 293 mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds(); 294 assertTrue(floatEquals((float) pinnedStackBounds.width / pinnedStackBounds.height, 295 (float) num / denom)); 296 } 297 testResizePipAspectRatioMin()298 public void testResizePipAspectRatioMin() throws Exception { 299 testResizePipAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, MIN_ASPECT_RATIO_DENOMINATOR); 300 } 301 testResizePipAspectRatioMax()302 public void testResizePipAspectRatioMax() throws Exception { 303 testResizePipAspectRatio(MAX_ASPECT_RATIO_NUMERATOR, MAX_ASPECT_RATIO_DENOMINATOR); 304 } 305 testResizePipAspectRatio(int num, int denom)306 private void testResizePipAspectRatio(int num, int denom) throws Exception { 307 if (!supportsPip()) return; 308 309 launchActivity(PIP_ACTIVITY, 310 EXTRA_ENTER_PIP, "true", 311 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num), 312 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)); 313 assertPinnedStackExists(); 314 315 // Hacky, but we need to wait for the enterPictureInPicture animation to complete and 316 // the resize to be called before we can check the pinned stack bounds 317 final boolean[] result = new boolean[1]; 318 mAmWmState.waitForWithAmState(mDevice, (state) -> { 319 Rectangle pinnedStackBounds = state.getStackById(PINNED_STACK_ID).getBounds(); 320 boolean isValidAspectRatio = floatEquals( 321 (float) pinnedStackBounds.width / pinnedStackBounds.height, 322 (float) num / denom); 323 result[0] = isValidAspectRatio; 324 return isValidAspectRatio; 325 }, "Waiting for pinned stack to be resized"); 326 assertTrue(result[0]); 327 } 328 testEnterPipExtremeAspectRatioMin()329 public void testEnterPipExtremeAspectRatioMin() throws Exception { 330 testEnterPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, 331 BELOW_MIN_ASPECT_RATIO_DENOMINATOR); 332 } 333 testEnterPipExtremeAspectRatioMax()334 public void testEnterPipExtremeAspectRatioMax() throws Exception { 335 testEnterPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR, 336 MAX_ASPECT_RATIO_DENOMINATOR); 337 } 338 testEnterPipExtremeAspectRatio(int num, int denom)339 private void testEnterPipExtremeAspectRatio(int num, int denom) throws Exception { 340 if (!supportsPip()) return; 341 342 // Assert that we could not create a pinned stack with an extreme aspect ratio 343 launchActivity(PIP_ACTIVITY, 344 EXTRA_ENTER_PIP, "true", 345 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, Integer.toString(num), 346 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)); 347 assertPinnedStackDoesNotExist(); 348 } 349 testSetPipExtremeAspectRatioMin()350 public void testSetPipExtremeAspectRatioMin() throws Exception { 351 testSetPipExtremeAspectRatio(MIN_ASPECT_RATIO_NUMERATOR, 352 BELOW_MIN_ASPECT_RATIO_DENOMINATOR); 353 } 354 testSetPipExtremeAspectRatioMax()355 public void testSetPipExtremeAspectRatioMax() throws Exception { 356 testSetPipExtremeAspectRatio(ABOVE_MAX_ASPECT_RATIO_NUMERATOR, 357 MAX_ASPECT_RATIO_DENOMINATOR); 358 } 359 testSetPipExtremeAspectRatio(int num, int denom)360 private void testSetPipExtremeAspectRatio(int num, int denom) throws Exception { 361 if (!supportsPip()) return; 362 363 // Try to resize the a normal pinned stack to an extreme aspect ratio and ensure that 364 // fails (the aspect ratio remains the same) 365 launchActivity(PIP_ACTIVITY, 366 EXTRA_ENTER_PIP, "true", 367 EXTRA_ENTER_PIP_ASPECT_RATIO_NUMERATOR, 368 Integer.toString(MAX_ASPECT_RATIO_NUMERATOR), 369 EXTRA_ENTER_PIP_ASPECT_RATIO_DENOMINATOR, 370 Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR), 371 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(num), 372 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(denom)); 373 assertPinnedStackExists(); 374 Rectangle pinnedStackBounds = 375 mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds(); 376 assertTrue(floatEquals((float) pinnedStackBounds.width / pinnedStackBounds.height, 377 (float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR)); 378 } 379 testDisallowPipLaunchFromStoppedActivity()380 public void testDisallowPipLaunchFromStoppedActivity() throws Exception { 381 if (!supportsPip()) return; 382 383 // Launch the bottom pip activity 384 launchActivity(PIP_ON_STOP_ACTIVITY); 385 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 386 387 // Wait for the bottom pip activity to be stopped 388 mAmWmState.waitForActivityState(mDevice, PIP_ON_STOP_ACTIVITY, STATE_STOPPED); 389 390 // Assert that there is no pinned stack (that enterPictureInPicture() failed) 391 assertPinnedStackDoesNotExist(); 392 } 393 testAutoEnterPictureInPicture()394 public void testAutoEnterPictureInPicture() throws Exception { 395 if (!supportsPip()) return; 396 397 // Launch a test activity so that we're not over home 398 launchActivity(TEST_ACTIVITY); 399 400 // Launch the PIP activity on pause 401 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true"); 402 assertPinnedStackDoesNotExist(); 403 404 // Go home and ensure that there is a pinned stack 405 launchHomeActivity(); 406 assertPinnedStackExists(); 407 } 408 testAutoEnterPictureInPictureLaunchActivity()409 public void testAutoEnterPictureInPictureLaunchActivity() throws Exception { 410 if (!supportsPip()) return; 411 412 // Launch a test activity so that we're not over home 413 launchActivity(TEST_ACTIVITY); 414 415 // Launch the PIP activity on pause, and have it start another activity on 416 // top of itself. Wait for the new activity to be visible and ensure that the pinned stack 417 // was not created in the process 418 launchActivity(PIP_ACTIVITY, 419 EXTRA_ENTER_PIP_ON_PAUSE, "true", 420 EXTRA_START_ACTIVITY, getActivityComponentName(NON_RESIZEABLE_ACTIVITY)); 421 mAmWmState.computeState(mDevice, new String[] {NON_RESIZEABLE_ACTIVITY}, 422 false /* compareTaskAndStackBounds */); 423 assertPinnedStackDoesNotExist(); 424 425 // Go home while the pip activity is open and ensure the previous activity is not PIPed 426 launchHomeActivity(); 427 assertPinnedStackDoesNotExist(); 428 } 429 testAutoEnterPictureInPictureFinish()430 public void testAutoEnterPictureInPictureFinish() throws Exception { 431 if (!supportsPip()) return; 432 433 // Launch a test activity so that we're not over home 434 launchActivity(TEST_ACTIVITY); 435 436 // Launch the PIP activity on pause, and set it to finish itself after 437 // some period. Wait for the previous activity to be visible, and ensure that the pinned 438 // stack was not created in the process 439 launchActivity(PIP_ACTIVITY, 440 EXTRA_ENTER_PIP_ON_PAUSE, "true", 441 EXTRA_FINISH_SELF_ON_RESUME, "true"); 442 assertPinnedStackDoesNotExist(); 443 } 444 testAutoEnterPictureInPictureAspectRatio()445 public void testAutoEnterPictureInPictureAspectRatio() throws Exception { 446 if (!supportsPip()) return; 447 448 // Launch the PIP activity on pause, and set the aspect ratio 449 launchActivity(PIP_ACTIVITY, 450 EXTRA_ENTER_PIP_ON_PAUSE, "true", 451 EXTRA_SET_ASPECT_RATIO_NUMERATOR, Integer.toString(MAX_ASPECT_RATIO_NUMERATOR), 452 EXTRA_SET_ASPECT_RATIO_DENOMINATOR, Integer.toString(MAX_ASPECT_RATIO_DENOMINATOR)); 453 454 // Go home while the pip activity is open to trigger auto-PIP 455 launchHomeActivity(); 456 assertPinnedStackExists(); 457 458 // Hacky, but we need to wait for the auto-enter picture-in-picture animation to complete 459 // and before we can check the pinned stack bounds 460 final boolean[] result = new boolean[1]; 461 mAmWmState.waitForWithAmState(mDevice, (state) -> { 462 Rectangle pinnedStackBounds = state.getStackById(PINNED_STACK_ID).getBounds(); 463 boolean isValidAspectRatio = floatEquals( 464 (float) pinnedStackBounds.width / pinnedStackBounds.height, 465 (float) MAX_ASPECT_RATIO_NUMERATOR / MAX_ASPECT_RATIO_DENOMINATOR); 466 result[0] = isValidAspectRatio; 467 return isValidAspectRatio; 468 }, "Waiting for pinned stack to be resized"); 469 assertTrue(result[0]); 470 } 471 testAutoEnterPictureInPictureOverPip()472 public void testAutoEnterPictureInPictureOverPip() throws Exception { 473 if (!supportsPip()) return; 474 475 // Launch another PIP activity 476 launchActivity(LAUNCH_INTO_PINNED_STACK_PIP_ACTIVITY); 477 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 478 assertPinnedStackExists(); 479 480 // Launch the PIP activity on pause 481 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true"); 482 483 // Go home while the PIP activity is open to trigger auto-enter PIP 484 launchHomeActivity(); 485 assertPinnedStackExists(); 486 487 // Ensure that auto-enter pip failed and that the resumed activity in the pinned stack is 488 // still the first activity 489 final ActivityStack pinnedStack = mAmWmState.getAmState().getStackById(PINNED_STACK_ID); 490 assertTrue(pinnedStack.getTasks().size() == 1); 491 assertTrue(pinnedStack.getTasks().get(0).mRealActivity.equals(getActivityComponentName( 492 ALWAYS_FOCUSABLE_PIP_ACTIVITY))); 493 } 494 testDisallowMultipleTasksInPinnedStack()495 public void testDisallowMultipleTasksInPinnedStack() throws Exception { 496 if (!supportsPip()) return; 497 498 // Launch a test activity so that we have multiple fullscreen tasks 499 launchActivity(TEST_ACTIVITY); 500 501 // Launch first PIP activity 502 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 503 504 // Launch second PIP activity 505 launchActivity(PIP_ACTIVITY2, EXTRA_ENTER_PIP, "true"); 506 507 final ActivityStack pinnedStack = mAmWmState.getAmState().getStackById(PINNED_STACK_ID); 508 assertEquals(1, pinnedStack.getTasks().size()); 509 510 assertTrue(pinnedStack.getTasks().get(0).mRealActivity.equals(getActivityComponentName( 511 PIP_ACTIVITY2))); 512 513 final ActivityStack fullScreenStack = mAmWmState.getAmState().getStackById( 514 FULLSCREEN_WORKSPACE_STACK_ID); 515 assertTrue(fullScreenStack.getBottomTask().mRealActivity.equals(getActivityComponentName( 516 PIP_ACTIVITY))); 517 } 518 testPipUnPipOverHome()519 public void testPipUnPipOverHome() throws Exception { 520 if (!supportsPip()) return; 521 522 // Go home 523 launchHomeActivity(); 524 // Launch an auto pip activity 525 launchActivity(PIP_ACTIVITY, 526 EXTRA_ENTER_PIP, "true", 527 EXTRA_REENTER_PIP_ON_EXIT, "true"); 528 assertPinnedStackExists(); 529 530 // Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip 531 launchActivity(PIP_ACTIVITY); 532 mAmWmState.waitForWithAmState(mDevice, (amState) -> { 533 return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == FULLSCREEN_WORKSPACE_STACK_ID; 534 }, "Waiting for PIP to exit to fullscreen"); 535 mAmWmState.waitForWithAmState(mDevice, (amState) -> { 536 return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == PINNED_STACK_ID; 537 }, "Waiting to re-enter PIP"); 538 mAmWmState.assertFocusedStack("Expected home stack focused", HOME_STACK_ID); 539 } 540 testPipUnPipOverApp()541 public void testPipUnPipOverApp() throws Exception { 542 if (!supportsPip()) return; 543 544 // Launch a test activity so that we're not over home 545 launchActivity(TEST_ACTIVITY); 546 547 // Launch an auto pip activity 548 launchActivity(PIP_ACTIVITY, 549 EXTRA_ENTER_PIP, "true", 550 EXTRA_REENTER_PIP_ON_EXIT, "true"); 551 assertPinnedStackExists(); 552 553 // Relaunch the activity to fullscreen to trigger the activity to exit and re-enter pip 554 launchActivity(PIP_ACTIVITY); 555 mAmWmState.waitForWithAmState(mDevice, (amState) -> { 556 return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == FULLSCREEN_WORKSPACE_STACK_ID; 557 }, "Waiting for PIP to exit to fullscreen"); 558 mAmWmState.waitForWithAmState(mDevice, (amState) -> { 559 return amState.getFrontStackId(DEFAULT_DISPLAY_ID) == PINNED_STACK_ID; 560 }, "Waiting to re-enter PIP"); 561 mAmWmState.assertFocusedStack("Expected fullscreen stack focused", 562 FULLSCREEN_WORKSPACE_STACK_ID); 563 } 564 testRemovePipWithNoFullscreenStack()565 public void testRemovePipWithNoFullscreenStack() throws Exception { 566 if (!supportsPip()) return; 567 568 // Start with a clean slate, remove all the stacks but home 569 removeStacks(ALL_STACK_IDS_BUT_HOME); 570 571 // Launch a pip activity 572 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 573 assertPinnedStackExists(); 574 575 // Remove the stack and ensure that the task is now in the fullscreen stack (when no 576 // fullscreen stack existed before) 577 removeStacks(PINNED_STACK_ID); 578 assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID, 579 true /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */); 580 } 581 testRemovePipWithVisibleFullscreenStack()582 public void testRemovePipWithVisibleFullscreenStack() throws Exception { 583 if (!supportsPip()) return; 584 585 // Launch a fullscreen activity, and a pip activity over that 586 launchActivity(TEST_ACTIVITY); 587 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 588 assertPinnedStackExists(); 589 590 // Remove the stack and ensure that the task is placed in the fullscreen stack, behind the 591 // top fullscreen activity 592 removeStacks(PINNED_STACK_ID); 593 assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID, 594 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */); 595 } 596 testRemovePipWithHiddenFullscreenStack()597 public void testRemovePipWithHiddenFullscreenStack() throws Exception { 598 if (!supportsPip()) return; 599 600 // Launch a fullscreen activity, return home and while the fullscreen stack is hidden, 601 // launch a pip activity over home 602 launchActivity(TEST_ACTIVITY); 603 launchHomeActivity(); 604 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 605 assertPinnedStackExists(); 606 607 // Remove the stack and ensure that the task is placed on top of the hidden fullscreen 608 // stack, but that the home stack is still focused 609 removeStacks(PINNED_STACK_ID); 610 assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID, 611 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */); 612 } 613 testMovePipToBackWithNoFullscreenStack()614 public void testMovePipToBackWithNoFullscreenStack() throws Exception { 615 if (!supportsPip()) return; 616 617 // Start with a clean slate, remove all the stacks but home 618 removeStacks(ALL_STACK_IDS_BUT_HOME); 619 620 // Launch a pip activity 621 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 622 assertPinnedStackExists(); 623 624 // Remove the stack and ensure that the task is now in the fullscreen stack (when no 625 // fullscreen stack existed before) 626 executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK); 627 assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID, 628 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */); 629 } 630 testMovePipToBackWithVisibleFullscreenStack()631 public void testMovePipToBackWithVisibleFullscreenStack() throws Exception { 632 if (!supportsPip()) return; 633 634 // Launch a fullscreen activity, and a pip activity over that 635 launchActivity(TEST_ACTIVITY); 636 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 637 assertPinnedStackExists(); 638 639 // Remove the stack and ensure that the task is placed in the fullscreen stack, behind the 640 // top fullscreen activity 641 executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK); 642 assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID, 643 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */); 644 } 645 testMovePipToBackWithHiddenFullscreenStack()646 public void testMovePipToBackWithHiddenFullscreenStack() throws Exception { 647 if (!supportsPip()) return; 648 649 // Launch a fullscreen activity, return home and while the fullscreen stack is hidden, 650 // launch a pip activity over home 651 launchActivity(TEST_ACTIVITY); 652 launchHomeActivity(); 653 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 654 assertPinnedStackExists(); 655 656 // Remove the stack and ensure that the task is placed on top of the hidden fullscreen 657 // stack, but that the home stack is still focused 658 executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_MOVE_TO_BACK); 659 assertPinnedStackStateOnMoveToFullscreen(PIP_ACTIVITY, HOME_STACK_ID, 660 false /* expectTopTaskHasActivity */, true /* expectBottomTaskHasActivity */); 661 } 662 testPinnedStackAlwaysOnTop()663 public void testPinnedStackAlwaysOnTop() throws Exception { 664 if (!supportsPip()) return; 665 666 // Launch activity into pinned stack and assert it's on top. 667 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 668 assertPinnedStackExists(); 669 assertPinnedStackIsOnTop(); 670 671 // Launch another activity in fullscreen stack and check that pinned stack is still on top. 672 launchActivity(TEST_ACTIVITY); 673 assertPinnedStackExists(); 674 assertPinnedStackIsOnTop(); 675 676 // Launch home and check that pinned stack is still on top. 677 launchHomeActivity(); 678 assertPinnedStackExists(); 679 assertPinnedStackIsOnTop(); 680 } 681 testAppOpsDenyPipOnPause()682 public void testAppOpsDenyPipOnPause() throws Exception { 683 if (!supportsPip()) return; 684 685 // Disable enter-pip and try to enter pip 686 setAppOpsOpToMode(ActivityManagerTestBase.componentName, 687 APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE, APP_OPS_MODE_IGNORED); 688 689 // Launch the PIP activity on pause 690 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 691 assertPinnedStackDoesNotExist(); 692 693 // Go home and ensure that there is no pinned stack 694 launchHomeActivity(); 695 assertPinnedStackDoesNotExist(); 696 697 // Re-enable enter-pip-on-hide 698 setAppOpsOpToMode(ActivityManagerTestBase.componentName, 699 APP_OPS_OP_ENTER_PICTURE_IN_PICTURE_ON_HIDE, APP_OPS_MODE_ALLOWED); 700 } 701 testEnterPipFromTaskWithMultipleActivities()702 public void testEnterPipFromTaskWithMultipleActivities() throws Exception { 703 if (!supportsPip()) return; 704 705 // Try to enter picture-in-picture from an activity that has more than one activity in the 706 // task and ensure that it works 707 launchActivity(LAUNCH_ENTER_PIP_ACTIVITY); 708 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 709 assertPinnedStackExists(); 710 } 711 testEnterPipWithResumeWhilePausingActivityNoStop()712 public void testEnterPipWithResumeWhilePausingActivityNoStop() throws Exception { 713 if (!supportsPip()) return; 714 715 /* 716 * Launch the resumeWhilePausing activity and ensure that the PiP activity did not get 717 * stopped and actually went into the pinned stack. 718 * 719 * Note that this is a workaround because to trigger the path that we want to happen in 720 * activity manager, we need to add the leaving activity to the stopping state, which only 721 * happens when a hidden stack is brought forward. Normally, this happens when you go home, 722 * but since we can't launch into the home stack directly, we have a workaround. 723 * 724 * 1) Launch an activity in a new dynamic stack 725 * 2) Resize the dynamic stack to non-fullscreen bounds 726 * 3) Start the PiP activity that will enter picture-in-picture when paused in the 727 * fullscreen stack 728 * 4) Bring the activity in the dynamic stack forward to trigger PiP 729 */ 730 int stackId = launchActivityInNewDynamicStack(RESUME_WHILE_PAUSING_ACTIVITY); 731 resizeStack(stackId, 0, 0, 500, 500); 732 // Launch an activity that will enter PiP when it is paused with a delay that is long enough 733 // for the next resumeWhilePausing activity to finish resuming, but slow enough to not 734 // trigger the current system pause timeout (currently 500ms) 735 launchActivityInStack(PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID, 736 EXTRA_ENTER_PIP_ON_PAUSE, "true", 737 EXTRA_ON_PAUSE_DELAY, "350", 738 EXTRA_ASSERT_NO_ON_STOP_BEFORE_PIP, "true"); 739 launchActivity(RESUME_WHILE_PAUSING_ACTIVITY); 740 assertPinnedStackExists(); 741 } 742 testDisallowEnterPipActivityLocked()743 public void testDisallowEnterPipActivityLocked() throws Exception { 744 if (!supportsPip()) return; 745 746 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP_ON_PAUSE, "true"); 747 ActivityTask task = 748 mAmWmState.getAmState().getStackById(FULLSCREEN_WORKSPACE_STACK_ID).getTopTask(); 749 750 // Lock the task and ensure that we can't enter picture-in-picture both explicitly and 751 // when paused 752 executeShellCommand("am task lock " + task.mTaskId); 753 executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_ENTER_PIP); 754 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 755 assertPinnedStackDoesNotExist(); 756 launchHomeActivity(); 757 assertPinnedStackDoesNotExist(); 758 executeShellCommand("am task lock stop"); 759 } 760 testConfigurationChangeOrderDuringTransition()761 public void testConfigurationChangeOrderDuringTransition() throws Exception { 762 if (!supportsPip()) return; 763 764 // Launch a PiP activity and ensure configuration change only happened once, and that the 765 // configuration change happened after the picture-in-picture and multi-window callbacks 766 launchActivity(PIP_ACTIVITY); 767 String logSeparator = clearLogcat(); 768 executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_ENTER_PIP); 769 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 770 assertPinnedStackExists(); 771 assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, logSeparator); 772 773 // Trigger it to go back to fullscreen and ensure that only triggered one configuration 774 // change as well 775 logSeparator = clearLogcat(); 776 launchActivity(PIP_ACTIVITY); 777 assertValidPictureInPictureCallbackOrder(PIP_ACTIVITY, logSeparator); 778 } 779 testStopBeforeMultiWindowCallbacksOnDismiss()780 public void testStopBeforeMultiWindowCallbacksOnDismiss() throws Exception { 781 if (!supportsPip()) return; 782 783 // Launch a PiP activity 784 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 785 assertPinnedStackExists(); 786 787 // Dismiss it 788 String logSeparator = clearLogcat(); 789 removeStacks(PINNED_STACK_ID); 790 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID); 791 792 // Confirm that we get stop before the multi-window and picture-in-picture mode change 793 // callbacks 794 final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY, 795 logSeparator); 796 if (lifecycleCounts.mStopCount != 1) { 797 fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mStopCount 798 + " onStop() calls, expecting 1"); 799 } else if (lifecycleCounts.mPictureInPictureModeChangedCount != 1) { 800 fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mPictureInPictureModeChangedCount 801 + " onPictureInPictureModeChanged() calls, expecting 1"); 802 } else if (lifecycleCounts.mMultiWindowModeChangedCount != 1) { 803 fail(PIP_ACTIVITY + " has received " + lifecycleCounts.mMultiWindowModeChangedCount 804 + " onMultiWindowModeChanged() calls, expecting 1"); 805 } else { 806 int lastStopLine = lifecycleCounts.mLastStopLineIndex; 807 int lastPipLine = lifecycleCounts.mLastPictureInPictureModeChangedLineIndex; 808 int lastMwLine = lifecycleCounts.mLastMultiWindowModeChangedLineIndex; 809 if (!(lastStopLine < lastPipLine && lastPipLine < lastMwLine)) { 810 fail(PIP_ACTIVITY + " has received callbacks in unexpected order. Expected:" 811 + " stop < pip < mw, but got line indices: " + lastStopLine + ", " 812 + lastPipLine + ", " + lastMwLine + " respectively"); 813 } 814 } 815 } 816 testPreventSetAspectRatioWhileExpanding()817 public void testPreventSetAspectRatioWhileExpanding() throws Exception { 818 if (!supportsPip()) return; 819 820 // Launch the PiP activity 821 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 822 823 // Trigger it to go back to fullscreen and try to set the aspect ratio, and ensure that the 824 // call to set the aspect ratio did not prevent the PiP from returning to fullscreen 825 executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_EXPAND_PIP 826 + " -e " + EXTRA_SET_ASPECT_RATIO_WITH_DELAY_NUMERATOR + " 123456789" 827 + " -e " + EXTRA_SET_ASPECT_RATIO_WITH_DELAY_DENOMINATOR + " 100000000"); 828 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID); 829 assertPinnedStackDoesNotExist(); 830 } 831 testSetRequestedOrientationWhilePinned()832 public void testSetRequestedOrientationWhilePinned() throws Exception { 833 if (!supportsPip()) return; 834 835 // Launch the PiP activity fixed as portrait, and enter picture-in-picture 836 launchActivity(PIP_ACTIVITY, 837 EXTRA_FIXED_ORIENTATION, String.valueOf(ORIENTATION_PORTRAIT), 838 EXTRA_ENTER_PIP, "true"); 839 assertPinnedStackExists(); 840 841 // Request that the orientation is set to landscape 842 executeShellCommand("am broadcast -a " 843 + PIP_ACTIVITY_ACTION_SET_REQUESTED_ORIENTATION + " -e " 844 + EXTRA_FIXED_ORIENTATION + " " + String.valueOf(ORIENTATION_LANDSCAPE)); 845 846 // Launch the activity back into fullscreen and ensure that it is now in landscape 847 launchActivity(PIP_ACTIVITY); 848 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID); 849 assertPinnedStackDoesNotExist(); 850 assertTrue(mAmWmState.getWmState().getLastOrientation() == ORIENTATION_LANDSCAPE); 851 } 852 testWindowButtonEntersPip()853 public void testWindowButtonEntersPip() throws Exception { 854 if (!supportsPip()) return; 855 856 // Launch the PiP activity trigger the window button, ensure that we have entered PiP 857 launchActivity(PIP_ACTIVITY); 858 pressWindowButton(); 859 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 860 assertPinnedStackExists(); 861 } 862 testFinishPipActivityWithTaskOverlay()863 public void testFinishPipActivityWithTaskOverlay() throws Exception { 864 if (!supportsPip()) return; 865 866 // Launch PiP activity 867 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 868 assertPinnedStackExists(); 869 int taskId = mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getTopTask().mTaskId; 870 871 // Ensure that we don't any any other overlays as a result of launching into PIP 872 launchHomeActivity(); 873 874 // Launch task overlay activity into PiP activity task 875 launchActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId, PINNED_STACK_ID); 876 877 // Finish the PiP activity and ensure that there is no pinned stack 878 executeShellCommand("am broadcast -a " + PIP_ACTIVITY_ACTION_FINISH); 879 mAmWmState.waitForWithAmState(mDevice, (amState) -> { 880 ActivityStack stack = amState.getStackById(PINNED_STACK_ID); 881 return stack == null; 882 }, "Waiting for pinned stack to be removed..."); 883 assertPinnedStackDoesNotExist(); 884 } 885 testNoResumeAfterTaskOverlayFinishes()886 public void testNoResumeAfterTaskOverlayFinishes() throws Exception { 887 if (!supportsPip()) return; 888 889 // Launch PiP activity 890 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 891 assertPinnedStackExists(); 892 int taskId = mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getTopTask().mTaskId; 893 894 // Launch task overlay activity into PiP activity task 895 launchActivityAsTaskOverlay(TRANSLUCENT_TEST_ACTIVITY, taskId, PINNED_STACK_ID); 896 897 // Finish the task overlay activity while animating and ensure that the PiP activity never 898 // got resumed 899 String logSeparator = clearLogcat(); 900 executeShellCommand("am stack resize-animated 4 20 20 500 500"); 901 executeShellCommand("am broadcast -a " + TEST_ACTIVITY_ACTION_FINISH); 902 mAmWmState.waitFor(mDevice, (amState, wmState) -> !amState.containsActivity( 903 TRANSLUCENT_TEST_ACTIVITY), "Waiting for test activity to finish..."); 904 final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(PIP_ACTIVITY, 905 logSeparator); 906 assertTrue(lifecycleCounts.mResumeCount == 0); 907 assertTrue(lifecycleCounts.mPauseCount == 0); 908 } 909 testPinnedStackWithDockedStack()910 public void testPinnedStackWithDockedStack() throws Exception { 911 if (!supportsPip() || !supportsSplitScreenMultiWindow()) return; 912 913 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 914 launchActivityInDockStack(LAUNCHING_ACTIVITY); 915 launchActivityToSide(true, false, TEST_ACTIVITY); 916 mAmWmState.assertVisibility(PIP_ACTIVITY, true); 917 mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true); 918 mAmWmState.assertVisibility(TEST_ACTIVITY, true); 919 920 // Launch the activities again to take focus and make sure nothing is hidden 921 launchActivityInDockStack(LAUNCHING_ACTIVITY); 922 mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true); 923 mAmWmState.assertVisibility(TEST_ACTIVITY, true); 924 925 launchActivityToSide(true, false, TEST_ACTIVITY); 926 mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true); 927 mAmWmState.assertVisibility(TEST_ACTIVITY, true); 928 929 // Go to recents to make sure that fullscreen stack is invisible 930 // Some devices do not support recents or implement it differently (instead of using a 931 // separate stack id or as an activity), for those cases the visibility asserts will be 932 // ignored 933 pressAppSwitchButton(); 934 if (mAmWmState.waitForRecentsActivityVisible(mDevice)) { 935 mAmWmState.assertVisibility(LAUNCHING_ACTIVITY, true); 936 mAmWmState.assertVisibility(TEST_ACTIVITY, false); 937 } 938 } 939 testLaunchTaskByComponentMatchMultipleTasks()940 public void testLaunchTaskByComponentMatchMultipleTasks() throws Exception { 941 if (!supportsPip()) return; 942 943 // Launch a fullscreen activity which will launch a PiP activity in a new task with the same 944 // affinity 945 launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY); 946 launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY); 947 assertPinnedStackExists(); 948 949 // Launch the root activity again... 950 int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivityName( 951 TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId; 952 launchHomeActivity(); 953 launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY); 954 955 // ...and ensure that the root activity task is found and reused, and that the pinned stack 956 // is unaffected 957 assertPinnedStackExists(); 958 mAmWmState.assertFocusedActivity("Expected root activity focused", 959 TEST_ACTIVITY_WITH_SAME_AFFINITY); 960 assertTrue(rootActivityTaskId == mAmWmState.getAmState().getTaskByActivityName( 961 TEST_ACTIVITY_WITH_SAME_AFFINITY).mTaskId); 962 } 963 testLaunchTaskByAffinityMatchMultipleTasks()964 public void testLaunchTaskByAffinityMatchMultipleTasks() throws Exception { 965 if (!supportsPip()) return; 966 967 // Launch a fullscreen activity which will launch a PiP activity in a new task with the same 968 // affinity, and also launch another activity in the same task, while finishing itself. As 969 // a result, the task will not have a component matching the same activity as what it was 970 // started with 971 launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY, 972 EXTRA_START_ACTIVITY, getActivityComponentName(TEST_ACTIVITY), 973 EXTRA_FINISH_SELF_ON_RESUME, "true"); 974 mAmWmState.waitForValidState(mDevice, TEST_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID); 975 launchActivity(PIP_ACTIVITY_WITH_SAME_AFFINITY); 976 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY_WITH_SAME_AFFINITY, PINNED_STACK_ID); 977 assertPinnedStackExists(); 978 979 // Launch the root activity again... 980 int rootActivityTaskId = mAmWmState.getAmState().getTaskByActivityName( 981 TEST_ACTIVITY).mTaskId; 982 launchHomeActivity(); 983 launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY); 984 985 // ...and ensure that even while matching purely by task affinity, the root activity task is 986 // found and reused, and that the pinned stack is unaffected 987 assertPinnedStackExists(); 988 mAmWmState.assertFocusedActivity("Expected root activity focused", TEST_ACTIVITY); 989 assertTrue(rootActivityTaskId == mAmWmState.getAmState().getTaskByActivityName( 990 TEST_ACTIVITY).mTaskId); 991 } 992 testLaunchTaskByAffinityMatchSingleTask()993 public void testLaunchTaskByAffinityMatchSingleTask() throws Exception { 994 if (!supportsPip()) return; 995 996 // Launch an activity into the pinned stack with a fixed affinity 997 launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY, 998 EXTRA_ENTER_PIP, "true", 999 EXTRA_START_ACTIVITY, getActivityComponentName(PIP_ACTIVITY), 1000 EXTRA_FINISH_SELF_ON_RESUME, "true"); 1001 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 1002 assertPinnedStackExists(); 1003 1004 // Launch the root activity again, of the matching task and ensure that we expand to 1005 // fullscreen 1006 int activityTaskId = mAmWmState.getAmState().getTaskByActivityName( 1007 PIP_ACTIVITY).mTaskId; 1008 launchHomeActivity(); 1009 launchActivity(TEST_ACTIVITY_WITH_SAME_AFFINITY); 1010 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, FULLSCREEN_WORKSPACE_STACK_ID); 1011 assertPinnedStackDoesNotExist(); 1012 assertTrue(activityTaskId == mAmWmState.getAmState().getTaskByActivityName( 1013 PIP_ACTIVITY).mTaskId); 1014 } 1015 1016 /** Test that reported display size corresponds to fullscreen after exiting PiP. */ testDisplayMetricsPinUnpin()1017 public void testDisplayMetricsPinUnpin() throws Exception { 1018 String logSeparator = clearLogcat(); 1019 launchActivity(TEST_ACTIVITY); 1020 final int defaultDisplayStackId = mAmWmState.getAmState().getFocusedStackId(); 1021 final ReportedSizes initialSizes = getLastReportedSizesForActivity(TEST_ACTIVITY, 1022 logSeparator); 1023 final Rectangle initialAppBounds = readAppBounds(TEST_ACTIVITY, logSeparator); 1024 assertNotNull("Must report display dimensions", initialSizes); 1025 assertNotNull("Must report app bounds", initialAppBounds); 1026 1027 logSeparator = clearLogcat(); 1028 launchActivity(PIP_ACTIVITY, EXTRA_ENTER_PIP, "true"); 1029 mAmWmState.waitForValidState(mDevice, PIP_ACTIVITY, PINNED_STACK_ID); 1030 final ReportedSizes pinnedSizes = getLastReportedSizesForActivity(PIP_ACTIVITY, 1031 logSeparator); 1032 final Rectangle pinnedAppBounds = readAppBounds(PIP_ACTIVITY, logSeparator); 1033 assertFalse("Reported display size when pinned must be different from default", 1034 initialSizes.equals(pinnedSizes)); 1035 assertFalse("Reported app bounds when pinned must be different from default", 1036 initialAppBounds.width == pinnedAppBounds.width 1037 && initialAppBounds.height == pinnedAppBounds.height); 1038 1039 logSeparator = clearLogcat(); 1040 launchActivityInStack(PIP_ACTIVITY, defaultDisplayStackId); 1041 final ReportedSizes finalSizes = getLastReportedSizesForActivity(PIP_ACTIVITY, 1042 logSeparator); 1043 final Rectangle finalAppBounds = readAppBounds(PIP_ACTIVITY, logSeparator); 1044 assertEquals("Must report default size after exiting PiP", initialSizes, finalSizes); 1045 assertEquals("Must report default app width after exiting PiP", initialAppBounds.width, 1046 finalAppBounds.width); 1047 assertEquals("Must report default app height after exiting PiP", initialAppBounds.height, 1048 finalAppBounds.height); 1049 } 1050 1051 private static final Pattern sAppBoundsPattern = Pattern.compile( 1052 "(.+)appBounds=Rect\\((\\d+), (\\d+) - (\\d+), (\\d+)\\)(.*)"); 1053 1054 /** Read app bounds in last applied configuration from logs. */ readAppBounds(String activityName, String logSeparator)1055 private Rectangle readAppBounds(String activityName, String logSeparator) throws Exception { 1056 final String[] lines = getDeviceLogsForComponent(activityName, logSeparator); 1057 for (int i = lines.length - 1; i >= 0; i--) { 1058 final String line = lines[i].trim(); 1059 final Matcher matcher = sAppBoundsPattern.matcher(line); 1060 if (matcher.matches()) { 1061 final int left = Integer.parseInt(matcher.group(2)); 1062 final int top = Integer.parseInt(matcher.group(3)); 1063 final int right = Integer.parseInt(matcher.group(4)); 1064 final int bottom = Integer.parseInt(matcher.group(5)); 1065 return new Rectangle(left, top, right - left, bottom - top); 1066 } 1067 } 1068 return null; 1069 } 1070 1071 /** 1072 * Called after the given {@param activityName} has been moved to the fullscreen stack. Ensures 1073 * that the {@param focusedStackId} is focused, and checks the top and/or bottom tasks in the 1074 * fullscreen stack if {@param expectTopTaskHasActivity} or {@param expectBottomTaskHasActivity} 1075 * are set respectively. 1076 */ assertPinnedStackStateOnMoveToFullscreen(String activityName, int focusedStackId, boolean expectTopTaskHasActivity, boolean expectBottomTaskHasActivity)1077 private void assertPinnedStackStateOnMoveToFullscreen(String activityName, int focusedStackId, 1078 boolean expectTopTaskHasActivity, boolean expectBottomTaskHasActivity) 1079 throws Exception { 1080 mAmWmState.waitForFocusedStack(mDevice, focusedStackId); 1081 mAmWmState.assertFocusedStack("Wrong focused stack", focusedStackId); 1082 mAmWmState.waitForActivityState(mDevice, activityName, STATE_STOPPED); 1083 assertTrue(mAmWmState.getAmState().hasActivityState(activityName, STATE_STOPPED)); 1084 assertPinnedStackDoesNotExist(); 1085 1086 if (expectTopTaskHasActivity) { 1087 ActivityTask topTask = mAmWmState.getAmState().getStackById( 1088 FULLSCREEN_WORKSPACE_STACK_ID).getTopTask(); 1089 assertTrue(topTask.containsActivity(ActivityManagerTestBase.getActivityComponentName( 1090 activityName))); 1091 } 1092 if (expectBottomTaskHasActivity) { 1093 ActivityTask bottomTask = mAmWmState.getAmState().getStackById( 1094 FULLSCREEN_WORKSPACE_STACK_ID).getBottomTask(); 1095 assertTrue(bottomTask.containsActivity(ActivityManagerTestBase.getActivityComponentName( 1096 activityName))); 1097 } 1098 } 1099 1100 /** 1101 * Asserts that the pinned stack bounds does not intersect with the IME bounds. 1102 */ assertPinnedStackDoesNotIntersectIME()1103 private void assertPinnedStackDoesNotIntersectIME() throws Exception { 1104 // Ensure that the IME is visible 1105 WindowManagerState wmState = mAmWmState.getWmState(); 1106 wmState.computeState(mDevice); 1107 WindowManagerState.WindowState imeWinState = wmState.getInputMethodWindowState(); 1108 assertTrue(imeWinState != null); 1109 1110 // Ensure that the PIP movement is constrained by the display bounds intersecting the 1111 // non-IME bounds 1112 Rectangle imeContentFrame = imeWinState.getContentFrame(); 1113 Rectangle imeContentInsets = imeWinState.getGivenContentInsets(); 1114 Rectangle imeBounds = new Rectangle(imeContentFrame.x + imeContentInsets.x, 1115 imeContentFrame.y + imeContentInsets.y, 1116 imeContentFrame.width - imeContentInsets.width, 1117 imeContentFrame.height - imeContentInsets.height); 1118 wmState.computeState(mDevice); 1119 Rectangle pipMovementBounds = wmState.getPinnedStackMomentBounds(); 1120 assertTrue(!pipMovementBounds.intersects(imeBounds)); 1121 } 1122 1123 /** 1124 * Asserts that the pinned stack bounds is contained in the display bounds. 1125 */ assertPinnedStackActivityIsInDisplayBounds(String activity)1126 private void assertPinnedStackActivityIsInDisplayBounds(String activity) throws Exception { 1127 final WindowManagerState.WindowState windowState = getWindowState(activity); 1128 final WindowManagerState.Display display = mAmWmState.getWmState().getDisplay( 1129 windowState.getDisplayId()); 1130 final Rectangle displayRect = display.getDisplayRect(); 1131 final Rectangle pinnedStackBounds = 1132 mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds(); 1133 assertTrue(displayRect.contains(pinnedStackBounds)); 1134 } 1135 1136 /** 1137 * Asserts that the pinned stack exists. 1138 */ assertPinnedStackExists()1139 private void assertPinnedStackExists() throws Exception { 1140 mAmWmState.assertContainsStack("Must contain pinned stack.", PINNED_STACK_ID); 1141 } 1142 1143 /** 1144 * Asserts that the pinned stack does not exist. 1145 */ assertPinnedStackDoesNotExist()1146 private void assertPinnedStackDoesNotExist() throws Exception { 1147 mAmWmState.assertDoesNotContainStack("Must not contain pinned stack.", PINNED_STACK_ID); 1148 } 1149 1150 /** 1151 * Asserts that the pinned stack is the front stack. 1152 */ assertPinnedStackIsOnTop()1153 private void assertPinnedStackIsOnTop() throws Exception { 1154 mAmWmState.assertFrontStack("Pinned stack must always be on top.", PINNED_STACK_ID); 1155 } 1156 1157 /** 1158 * Asserts that the activity received exactly one of each of the callbacks when entering and 1159 * exiting picture-in-picture. 1160 */ assertValidPictureInPictureCallbackOrder(String activityName, String logSeparator)1161 private void assertValidPictureInPictureCallbackOrder(String activityName, String logSeparator) 1162 throws Exception { 1163 final ActivityLifecycleCounts lifecycleCounts = new ActivityLifecycleCounts(activityName, 1164 logSeparator); 1165 1166 if (lifecycleCounts.mConfigurationChangedCount != 1) { 1167 fail(activityName + " has received " + lifecycleCounts.mConfigurationChangedCount 1168 + " onConfigurationChanged() calls, expecting 1"); 1169 } else if (lifecycleCounts.mPictureInPictureModeChangedCount != 1) { 1170 fail(activityName + " has received " + lifecycleCounts.mPictureInPictureModeChangedCount 1171 + " onPictureInPictureModeChanged() calls, expecting 1"); 1172 } else if (lifecycleCounts.mMultiWindowModeChangedCount != 1) { 1173 fail(activityName + " has received " + lifecycleCounts.mMultiWindowModeChangedCount 1174 + " onMultiWindowModeChanged() calls, expecting 1"); 1175 } else { 1176 int lastPipLine = lifecycleCounts.mLastPictureInPictureModeChangedLineIndex; 1177 int lastMwLine = lifecycleCounts.mLastMultiWindowModeChangedLineIndex; 1178 int lastConfigLine = lifecycleCounts.mLastConfigurationChangedLineIndex; 1179 if (!(lastPipLine < lastMwLine && lastMwLine < lastConfigLine)) { 1180 fail(activityName + " has received callbacks in unexpected order. Expected:" 1181 + " pip < mw < config change, but got line indices: " + lastPipLine + ", " 1182 + lastMwLine + ", " + lastConfigLine + " respectively"); 1183 } 1184 } 1185 } 1186 1187 /** 1188 * @return the window state for the given {@param activity}'s window. 1189 */ getWindowState(String activity)1190 private WindowManagerState.WindowState getWindowState(String activity) throws Exception { 1191 String windowName = getWindowName(activity); 1192 mAmWmState.computeState(mDevice, new String[] {activity}); 1193 final List<WindowManagerState.WindowState> tempWindowList = new ArrayList<>(); 1194 mAmWmState.getWmState().getMatchingVisibleWindowState(windowName, tempWindowList); 1195 return tempWindowList.get(0); 1196 } 1197 1198 /** 1199 * Compares two floats with a common epsilon. 1200 */ floatEquals(float f1, float f2)1201 private boolean floatEquals(float f1, float f2) { 1202 return Math.abs(f1 - f2) < FLOAT_COMPARE_EPSILON; 1203 } 1204 1205 /** 1206 * Triggers a tap over the pinned stack bounds to trigger the PIP to close. 1207 */ tapToFinishPip()1208 private void tapToFinishPip() throws Exception { 1209 Rectangle pinnedStackBounds = 1210 mAmWmState.getAmState().getStackById(PINNED_STACK_ID).getBounds(); 1211 int tapX = pinnedStackBounds.x + pinnedStackBounds.width - 100; 1212 int tapY = pinnedStackBounds.y + pinnedStackBounds.height - 100; 1213 executeShellCommand(String.format("input tap %d %d", tapX, tapY)); 1214 } 1215 1216 /** 1217 * Launches the given {@param activityName} into the {@param taskId} as a task overlay. 1218 */ launchActivityAsTaskOverlay(String activityName, int taskId, int stackId)1219 private void launchActivityAsTaskOverlay(String activityName, int taskId, int stackId) 1220 throws Exception { 1221 executeShellCommand(getAmStartCmd(activityName) + " --task " + taskId + " --task-overlay"); 1222 1223 mAmWmState.waitForValidState(mDevice, activityName, stackId); 1224 } 1225 1226 /** 1227 * Sets an app-ops op for a given package to a given mode. 1228 */ setAppOpsOpToMode(String packageName, int op, int mode)1229 private void setAppOpsOpToMode(String packageName, int op, int mode) throws Exception { 1230 executeShellCommand(String.format("appops set %s %d %d", packageName, op, mode)); 1231 } 1232 1233 /** 1234 * Triggers the window keycode. 1235 */ pressWindowButton()1236 private void pressWindowButton() throws Exception { 1237 executeShellCommand(INPUT_KEYEVENT_WINDOW); 1238 } 1239 1240 /** 1241 * TODO: Improve tests check to actually check that apps are not interactive instead of checking 1242 * if the stack is focused. 1243 */ pinnedStackTester(String startActivityCmd, String topActivityName, boolean moveTopToPinnedStack, boolean isFocusable)1244 private void pinnedStackTester(String startActivityCmd, String topActivityName, 1245 boolean moveTopToPinnedStack, boolean isFocusable) throws Exception { 1246 1247 executeShellCommand(startActivityCmd); 1248 if (moveTopToPinnedStack) { 1249 executeShellCommand(AM_MOVE_TOP_ACTIVITY_TO_PINNED_STACK_COMMAND); 1250 } 1251 1252 mAmWmState.waitForValidState(mDevice, topActivityName, PINNED_STACK_ID); 1253 mAmWmState.computeState(mDevice, null); 1254 1255 if (supportsPip()) { 1256 final String windowName = getWindowName(topActivityName); 1257 assertPinnedStackExists(); 1258 mAmWmState.assertFrontStack("Pinned stack must be the front stack.", PINNED_STACK_ID); 1259 mAmWmState.assertVisibility(topActivityName, true); 1260 1261 if (isFocusable) { 1262 mAmWmState.assertFocusedStack( 1263 "Pinned stack must be the focused stack.", PINNED_STACK_ID); 1264 mAmWmState.assertFocusedActivity( 1265 "Pinned activity must be focused activity.", topActivityName); 1266 mAmWmState.assertFocusedWindow( 1267 "Pinned window must be focused window.", windowName); 1268 // Not checking for resumed state here because PiP overlay can be launched on top 1269 // in different task by SystemUI. 1270 } else { 1271 // Don't assert that the stack is not focused as a focusable PiP overlay can be 1272 // launched on top as a task overlay by SystemUI. 1273 mAmWmState.assertNotFocusedActivity( 1274 "Pinned activity can't be the focused activity.", topActivityName); 1275 mAmWmState.assertNotResumedActivity( 1276 "Pinned activity can't be the resumed activity.", topActivityName); 1277 mAmWmState.assertNotFocusedWindow( 1278 "Pinned window can't be focused window.", windowName); 1279 } 1280 } else { 1281 mAmWmState.assertDoesNotContainStack( 1282 "Must not contain pinned stack.", PINNED_STACK_ID); 1283 } 1284 } 1285 } 1286