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