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