1 /* 2 * Copyright (C) 2023 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.jetpack.embedding; 18 19 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; 20 import static android.server.wm.activity.lifecycle.LifecycleConstants.ON_CREATE; 21 import static android.server.wm.activity.lifecycle.LifecycleConstants.ON_PAUSE; 22 import static android.server.wm.activity.lifecycle.LifecycleConstants.ON_RESUME; 23 import static android.server.wm.activity.lifecycle.LifecycleConstants.ON_START; 24 import static android.server.wm.activity.lifecycle.TransitionVerifier.checkOrder; 25 import static android.server.wm.activity.lifecycle.TransitionVerifier.transition; 26 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.EXPAND_SPLIT_ATTRS; 27 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.createSplitPairRuleBuilder; 28 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.createWildcardSplitPairRuleBuilderWithPrimaryActivityClass; 29 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.startActivityAndVerifySplitAttributes; 30 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndAssertNotVisible; 31 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndAssertResumed; 32 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndGetTaskBounds; 33 34 import static org.junit.Assert.assertFalse; 35 import static org.junit.Assert.assertTrue; 36 37 import android.app.Activity; 38 import android.graphics.Rect; 39 import android.platform.test.annotations.Presubmit; 40 import android.server.wm.WindowManagerState.Task; 41 import android.server.wm.jetpack.utils.TestActivityWithId; 42 import android.server.wm.jetpack.utils.TestConfigChangeHandlingActivity; 43 import android.util.Pair; 44 import android.util.Size; 45 46 import androidx.annotation.NonNull; 47 import androidx.test.ext.junit.runners.AndroidJUnit4; 48 import androidx.window.extensions.embedding.SplitAttributes; 49 import androidx.window.extensions.embedding.SplitPairRule; 50 import androidx.window.extensions.embedding.SplitPinRule; 51 52 import com.android.compatibility.common.util.ApiTest; 53 54 import org.junit.Before; 55 import org.junit.Test; 56 import org.junit.runner.RunWith; 57 58 import java.util.Collections; 59 import java.util.List; 60 61 /** 62 * Tests for the {@link androidx.window.extensions} implementation provided on the device (and only 63 * if one is available) for the Activity Embedding functionality. Specifically tests pinning the 64 * top ActivityStack on a Task. 65 * <p> 66 * Build/Install/Run: 67 * atest CtsWindowManagerJetpackTestCases:PinActivityStackTests 68 */ 69 @Presubmit 70 @RunWith(AndroidJUnit4.class) 71 @ApiTest(apis = { 72 "androidx.window.extensions.embedding.ActivityEmbeddingComponent#pinTopActivityStack", 73 "androidx.window.extensions.embedding.ActivityEmbeddingComponent#unpinTopActivityStack" 74 }) 75 public class PinActivityStackTests extends ActivityEmbeddingLifecycleTestBase { 76 private Activity mPrimaryActivity; 77 private Activity mPinnedActivity; 78 private String mPinnedActivityId = "pinActivity"; 79 private SplitPairRule mWildcardSplitPairRule; 80 private int mTaskId; 81 82 @Override 83 @Before setUp()84 public void setUp() throws Exception { 85 super.setUp(); 86 87 mPrimaryActivity = startFullScreenActivityNewTask(TestConfigChangeHandlingActivity.class); 88 mTaskId = mPrimaryActivity.getTaskId(); 89 mWildcardSplitPairRule = createWildcardSplitPairRuleBuilderWithPrimaryActivityClass( 90 TestConfigChangeHandlingActivity.class, false /* shouldClearTop */) 91 .build(); 92 mActivityEmbeddingComponent.setEmbeddingRules( 93 Collections.singleton(mWildcardSplitPairRule)); 94 } 95 96 /** 97 * Verifies that the activity starts in a new container and the navigation are isolated after 98 * the top ActivityStack is pinned. 99 */ 100 @Test testPinTopActivityStack_createContainerForNewActivity()101 public void testPinTopActivityStack_createContainerForNewActivity() { 102 pinTopActivityStackAndVerifyLifecycle(true /* newPrimaryContainer */); 103 } 104 105 /** 106 * Verifies that the activity starts in the same container and the navigation are isolated 107 * after the top ActivityStack is pinned. 108 */ 109 @Test testPinTopActivityStack_reuseContainerForNewActivity()110 public void testPinTopActivityStack_reuseContainerForNewActivity() { 111 pinTopActivityStackAndVerifyLifecycle(false /* newPrimaryContainer */); 112 } 113 pinTopActivityStackAndVerifyLifecycle(boolean newPrimaryContainer)114 private void pinTopActivityStackAndVerifyLifecycle(boolean newPrimaryContainer) { 115 // Launch a secondary activity to side 116 mPinnedActivity = 117 startActivityAndVerifySplitAttributes( 118 mPrimaryActivity, 119 TestActivityWithId.class, 120 mWildcardSplitPairRule, 121 mPinnedActivityId, 122 mSplitInfoConsumer, 123 mActivityStackCallback); 124 125 // Pin the top ActivityStack 126 assertTrue(pinTopActivityStack()); 127 mEventLog.clear(); 128 129 if (!newPrimaryContainer) { 130 mActivityEmbeddingComponent.setEmbeddingRules(Collections.emptySet()); 131 } 132 133 // Start an Activity from the primary ActivityStack 134 final String activityId1 = "Activity1"; 135 startActivityFromActivity(mPrimaryActivity, TestActivityWithId.class, activityId1); 136 137 // Verifies the activity in the primary ActivityStack is occluded by the new Activity. 138 waitAndAssertResumed(activityId1); 139 waitAndAssertResumed(mPinnedActivityId); 140 waitAndAssertNotVisible(mPrimaryActivity); 141 final List<Pair<String, String>> expectedLifecycle = List.of( 142 transition(TestConfigChangeHandlingActivity.class, ON_PAUSE), 143 transition(TestActivityWithId.class, ON_CREATE), 144 transition(TestActivityWithId.class, ON_START), 145 transition(TestActivityWithId.class, ON_RESUME)); 146 assertTrue("Pause existing primary Activity before resuming another activity on top", 147 mLifecycleTracker.waitForConditionWithTimeout(() -> 148 checkOrder(mEventLog, expectedLifecycle))); 149 final Activity activity1 = getResumedActivityById(activityId1); 150 151 // Start an Activity from the pinned ActivityStack 152 final String activityId2 = "Activity2"; 153 startActivityFromActivity(mPinnedActivity, TestActivityWithId.class, activityId2); 154 155 // Verifies the activity on the pinned ActivityStack is occluded by the new Activity. 156 waitAndAssertResumed(activityId2); 157 waitAndAssertResumed(activity1); 158 waitAndAssertNotVisible(mPrimaryActivity); 159 waitAndAssertNotVisible(mPinnedActivity); 160 final Activity activity2 = getResumedActivityById(activityId2); 161 162 // Finishes activities on the pinned ActivityStack 163 activity2.finish(); 164 mPinnedActivity.finish(); 165 166 waitAndAssertResumed(activity1); 167 if (newPrimaryContainer) { 168 // Verifies primary activity is resumed due to the two activities split side-by-side. 169 waitAndAssertResumed(mPrimaryActivity); 170 } 171 } 172 173 /** 174 * Verifies that the activity navigation are not isolated after the top ActivityStack is 175 * unpinned. 176 */ 177 @Test testUnpinTopActivityStack()178 public void testUnpinTopActivityStack() { 179 // Launch a secondary activity to side 180 mPinnedActivity = 181 startActivityAndVerifySplitAttributes( 182 mPrimaryActivity, 183 TestActivityWithId.class, 184 mWildcardSplitPairRule, 185 mPinnedActivityId, 186 mSplitInfoConsumer, 187 mActivityStackCallback); 188 189 // Pin and unpin the top ActivityStack 190 assertTrue(pinTopActivityStack()); 191 mActivityEmbeddingComponent.unpinTopActivityStack(mTaskId); 192 193 // Verifies the activities still splits after unpin. 194 waitAndAssertResumed(mPrimaryActivity); 195 waitAndAssertResumed(mPinnedActivity); 196 197 // Start an Activity from the primary ActivityStack 198 final String activityId1 = "Activity1"; 199 startActivityFromActivity(mPrimaryActivity, TestActivityWithId.class, activityId1); 200 201 // Verifies the activity in the secondary ActivityStack is occluded by the new Activity. 202 waitAndAssertResumed(activityId1); 203 waitAndAssertResumed(mPrimaryActivity); 204 waitAndAssertNotVisible(mPinnedActivity); 205 } 206 207 /** 208 * Verifies that the activity is expanded if no split rules after unpinned. 209 */ 210 @Test testUnpinTopActivityStack_expands()211 public void testUnpinTopActivityStack_expands() { 212 // Launch a secondary activity to side 213 mPinnedActivity = 214 startActivityAndVerifySplitAttributes( 215 mPrimaryActivity, 216 TestActivityWithId.class, 217 mWildcardSplitPairRule, 218 mPinnedActivityId, 219 mSplitInfoConsumer, 220 mActivityStackCallback); 221 222 // Pin the top ActivityStack 223 assertTrue(pinTopActivityStack()); 224 waitAndAssertResumed(mPrimaryActivity); 225 waitAndAssertResumed(mPinnedActivity); 226 227 // Start an Activity from the primary ActivityStack 228 final String activityId1 = "Activity1"; 229 startActivityFromActivity(mPrimaryActivity, TestActivityWithId.class, activityId1); 230 231 // Verifies the activity in the primary ActivityStack is occluded by the new Activity. 232 waitAndAssertResumed(activityId1); 233 waitAndAssertResumed(mPinnedActivity); 234 waitAndAssertNotVisible(mPrimaryActivity); 235 final Activity activity1 = getResumedActivityById(activityId1); 236 237 // Verifies the activity is expanded and occludes other activities after unpin. 238 mActivityEmbeddingComponent.unpinTopActivityStack(mTaskId); 239 waitAndAssertResumed(mPinnedActivity); 240 waitAndAssertNotVisible(mPrimaryActivity); 241 waitAndAssertNotVisible(activity1); 242 } 243 244 /** 245 * Verifies that top ActivityStack cannot be pinned whenever it is not allowed. 246 */ 247 @Test testPinTopActivityStack_invalidPin()248 public void testPinTopActivityStack_invalidPin() { 249 // Cannot pin if there's no ActivityStack. 250 assertFalse(pinTopActivityStack()); 251 252 // Launch a secondary activity to side 253 mPinnedActivity = 254 startActivityAndVerifySplitAttributes( 255 mPrimaryActivity, 256 TestActivityWithId.class, 257 mWildcardSplitPairRule, 258 mPinnedActivityId, 259 mSplitInfoConsumer, 260 mActivityStackCallback); 261 262 // Cannot pin if no such task. 263 assertFalse(pinTopActivityStack(mTaskId + 1)); 264 265 // Cannot pin if parent window metric not large enough. 266 final SplitPinRule oversizeParentMetricsRule = new SplitPinRule.Builder( 267 new SplitAttributes.Builder().build(), 268 parentWindowMetrics -> false /* parentWindowMetricsPredicate */).build(); 269 assertFalse(pinTopActivityStack(mTaskId, oversizeParentMetricsRule)); 270 271 // Pin the top ActivityStack 272 assertTrue(pinTopActivityStack()); 273 274 // Cannot pin once there's already a pinned ActivityStack. 275 assertFalse(pinTopActivityStack()); 276 } 277 278 /** 279 * Verifies that the pinned rule is sticky and applies whenever possible. 280 */ 281 @Test testPinTopActivityStack_resizeStickyPin()282 public void testPinTopActivityStack_resizeStickyPin() { 283 // Launch a secondary activity to side 284 Activity secondaryActivity = 285 startActivityAndVerifySplitAttributes( 286 mPrimaryActivity, 287 TestActivityWithId.class, 288 mWildcardSplitPairRule, 289 mPinnedActivityId, 290 mSplitInfoConsumer, 291 mActivityStackCallback); 292 293 pinExpandActivityAndResizeDisplayOrTask(secondaryActivity, true /* stickyPin */); 294 295 // Verify the activities are still split 296 waitAndAssertResumed(secondaryActivity); 297 waitAndAssertResumed(mPinnedActivity); 298 waitAndAssertNotVisible(mPrimaryActivity); 299 } 300 301 /** 302 * Verifies that the pinned rule is non-sticky and removed after resizing. 303 */ 304 @Test testPinTopActivityStack_resizeNonStickyPin()305 public void testPinTopActivityStack_resizeNonStickyPin() { 306 // Launch a secondary activity to side 307 Activity secondaryActivity = 308 startActivityAndVerifySplitAttributes( 309 mPrimaryActivity, 310 TestActivityWithId.class, 311 mWildcardSplitPairRule, 312 mPinnedActivityId, 313 mSplitInfoConsumer, 314 mActivityStackCallback); 315 316 pinExpandActivityAndResizeDisplayOrTask(secondaryActivity, false /* stickyPin */); 317 318 // Verify the unpinned activity is expanded. 319 waitAndAssertResumed(mPinnedActivity); 320 waitAndAssertNotVisible(secondaryActivity); 321 waitAndAssertNotVisible(mPrimaryActivity); 322 } 323 pinExpandActivityAndResizeDisplayOrTask(@onNull Activity secondaryActivity, boolean stickyPin)324 private void pinExpandActivityAndResizeDisplayOrTask(@NonNull Activity secondaryActivity, 325 boolean stickyPin) { 326 // Starts an Activity to always-expand 327 final SplitPairRule expandRule = createSplitPairRuleBuilder(activityActivityPair -> true, 328 activityIntentPair -> true, windowMetrics -> true).setDefaultSplitAttributes( 329 EXPAND_SPLIT_ATTRS).build(); 330 mActivityEmbeddingComponent.setEmbeddingRules( 331 Collections.singleton(expandRule)); 332 mPinnedActivity = 333 startActivityAndVerifySplitAttributes( 334 secondaryActivity, 335 TestActivityWithId.class, 336 expandRule, 337 "expandActivityId", 338 mSplitInfoConsumer, 339 mActivityStackCallback); 340 341 // Pin the top ActivityStack 342 final Rect taskBounds = waitAndGetTaskBounds(mPinnedActivity, 343 true /* shouldWaitForResume */); 344 final int originalTaskWidth = taskBounds.width(); 345 final int originalTaskHeight = taskBounds.height(); 346 final SplitPinRule stickySplitPinRule = new SplitPinRule.Builder( 347 new SplitAttributes.Builder().build(), 348 parentWindowMetrics -> parentWindowMetrics.getBounds().width() >= originalTaskWidth 349 && parentWindowMetrics.getBounds().height() >= originalTaskHeight) 350 .setSticky(stickyPin).build(); 351 352 // Verify the pinned activity is split with next-top activity 353 assertTrue(pinTopActivityStack(mTaskId, stickySplitPinRule)); 354 waitAndAssertResumed(secondaryActivity); 355 waitAndAssertResumed(mPinnedActivity); 356 waitAndAssertNotVisible(mPrimaryActivity); 357 358 mWmState.computeState(); 359 final Size originalDisplaySize = mReportedDisplayMetrics.getSize(); 360 final Task topTask = 361 mWmState.getTaskByActivity(mPrimaryActivity.getComponentName()); 362 final Rect origTaskBounds = topTask.getBounds(); 363 if (topTask.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { 364 // Shrink the task by 10% to make the activities stacked 365 // Note: Shrinking the task might have an effect on the task behind this task 366 // in the containing root task on automotive. The task behind will become visible 367 // as the task above is not filling the parent anymore. 368 // This behavior is okay in the specific tests that involve shrinking tasks as the 369 // assertions are unaffected by this behavior. 370 resizeActivityTask(mPrimaryActivity.getComponentName(), 371 origTaskBounds.left, origTaskBounds.top, 372 origTaskBounds.left + (int) (origTaskBounds.width() * 0.9), 373 origTaskBounds.top + (int) (origTaskBounds.height() * 0.9)); 374 } else { 375 // Shrink the display by 10% to make the activities stacked 376 mReportedDisplayMetrics.setSize(new Size((int) (originalDisplaySize.getWidth() * 0.9), 377 (int) (originalDisplaySize.getHeight() * 0.9))); 378 } 379 // Verify only the pinned activity is visible 380 waitAndAssertResumed(mPinnedActivity); 381 waitAndAssertNotVisible(secondaryActivity); 382 waitAndAssertNotVisible(mPrimaryActivity); 383 384 385 if (topTask.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { 386 // Restore to the original task size 387 resizeActivityTask(mPrimaryActivity.getComponentName(), 388 origTaskBounds.left, origTaskBounds.top, 389 origTaskBounds.left + (int) (origTaskBounds.width()), 390 origTaskBounds.top + (int) (origTaskBounds.height())); 391 } else { 392 // Restore to the original display size 393 mReportedDisplayMetrics.setSize(originalDisplaySize); 394 } 395 } 396 pinTopActivityStack()397 private boolean pinTopActivityStack() { 398 return pinTopActivityStack(mTaskId); 399 } 400 pinTopActivityStack(int taskId)401 private boolean pinTopActivityStack(int taskId) { 402 SplitPinRule splitPinRule = new SplitPinRule.Builder(new SplitAttributes.Builder().build(), 403 parentWindowMetrics -> true /* parentWindowMetricsPredicate */).build(); 404 return pinTopActivityStack(taskId, splitPinRule); 405 } 406 pinTopActivityStack(int taskId, @NonNull SplitPinRule splitPinRule)407 private boolean pinTopActivityStack(int taskId, @NonNull SplitPinRule splitPinRule) { 408 return mActivityEmbeddingComponent.pinTopActivityStack(taskId, splitPinRule); 409 } 410 } 411