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_FREEFORM; 20 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.DEFAULT_SPLIT_ATTRS; 21 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.EXPAND_SPLIT_ATTRS; 22 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.HINGE_SPLIT_ATTRS; 23 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.assertValidSplit; 24 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.createSplitPairRuleBuilder; 25 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.startActivityAndVerifySplitAttributes; 26 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndAssertNotVisible; 27 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndAssertResumedAndFillsTask; 28 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndGetTaskBounds; 29 import static android.server.wm.jetpack.utils.TestActivityLauncher.KEY_ACTIVITY_ID; 30 31 import android.app.Activity; 32 import android.content.Intent; 33 import android.graphics.Rect; 34 import android.platform.test.annotations.Presubmit; 35 import android.server.wm.WindowManagerState.Task; 36 import android.server.wm.jetpack.utils.TestActivity; 37 import android.server.wm.jetpack.utils.TestActivityWithId; 38 import android.server.wm.jetpack.utils.TestConfigChangeHandlingActivity; 39 import android.support.test.uiautomator.UiDevice; 40 import android.util.Pair; 41 import android.util.Size; 42 43 import androidx.annotation.NonNull; 44 import androidx.test.ext.junit.runners.AndroidJUnit4; 45 import androidx.window.extensions.embedding.SplitAttributes; 46 import androidx.window.extensions.embedding.SplitAttributes.LayoutDirection; 47 import androidx.window.extensions.embedding.SplitAttributes.SplitType; 48 import androidx.window.extensions.embedding.SplitPairRule; 49 50 import com.android.compatibility.common.util.ApiTest; 51 52 import org.junit.Test; 53 import org.junit.runner.RunWith; 54 55 import java.util.Collections; 56 import java.util.Set; 57 58 /** 59 * Tests for the {@link androidx.window.extensions} implementation provided on the device (and only 60 * if one is available) for the Activity Embedding functionality. Specifically tests activity 61 * split bounds. 62 * 63 * Build/Install/Run: 64 * atest CtsWindowManagerJetpackTestCases:ActivityEmbeddingBoundsTests 65 */ 66 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitPairRule#getDefaultSplitAttributes"}) 67 @Presubmit 68 @RunWith(AndroidJUnit4.class) 69 public class ActivityEmbeddingBoundsTests extends ActivityEmbeddingTestBase { 70 public static SplitType UNEVEN_CONTAINERS_DEFAULT_SPLIT_TYPE = 71 new SplitType.RatioSplitType(0.7f); 72 73 /** 74 * Tests that when two activities are in a split and the parent bounds shrink such that 75 * they can no longer support split activities, then the activities become stacked. 76 */ 77 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitRule#checkParentMetrics"}) 78 @Test testParentWindowMetricsPredicate()79 public void testParentWindowMetricsPredicate() { 80 // Launch primary activity 81 final Activity primaryActivity = startFullScreenActivityNewTask( 82 TestConfigChangeHandlingActivity.class, null /* activityId */, 83 getLaunchingDisplayId()); 84 85 // Set split pair rule such that if the parent bounds is any smaller than it is now, then 86 // the parent cannot support a split. 87 final Rect taskBounds = waitAndGetTaskBounds(primaryActivity, 88 true /* shouldWaitForResume */); 89 final int originalTaskWidth = taskBounds.width(); 90 final int originalTaskHeight = taskBounds.height(); 91 final SplitPairRule splitPairRule = createSplitPairRuleBuilder( 92 activityActivityPair -> true /* activityPairPredicate */, 93 activityIntentPair -> true /* activityIntentPredicate */, 94 parentWindowMetrics -> parentWindowMetrics.getBounds().width() >= originalTaskWidth 95 && parentWindowMetrics.getBounds().height() >= originalTaskHeight) 96 .setDefaultSplitAttributes(DEFAULT_SPLIT_ATTRS).build(); 97 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 98 99 // Launch the secondary activity 100 final String secondaryActivityId = "secondaryActivityId"; 101 final TestActivity secondaryActivity = 102 (TestActivity) 103 startActivityAndVerifySplitAttributes( 104 primaryActivity, 105 TestActivityWithId.class, 106 splitPairRule, 107 secondaryActivityId, 108 mSplitInfoConsumer, 109 mActivityStackCallback); 110 111 // Resize multiple times to verify that the activities are correctly split or 112 // stacked depending on the parent bounds. Resizing multiple times simulates a foldable 113 // display is that folded and unfolded multiple times while running the same app. 114 final int numTimesToResize = 2; 115 final Size origDisplaySize = mReportedDisplayMetrics.getSize(); 116 mWmState.computeState( 117 primaryActivity.getComponentName(), secondaryActivity.getComponentName()); 118 // Primary and secondary activities should be in the same task 119 final Task task = mWmState.getTaskByActivity(primaryActivity.getComponentName()); 120 final Rect origTaskBounds = task.getBounds(); 121 final boolean taskInFreeformMode = task.getWindowingMode() == WINDOWING_MODE_FREEFORM; 122 for (int i = 0; i < numTimesToResize; i++) { 123 // Shrink by 10% to make the activities stacked. 124 // If the activity was launched in freeform windowing mode, resize the task bounds 125 // instead of resizing the display. 126 if (taskInFreeformMode) { 127 resizeActivityTask(primaryActivity.getComponentName(), 128 origTaskBounds.left, origTaskBounds.top, 129 origTaskBounds.left + (int) (origTaskBounds.width() * 0.9), 130 origTaskBounds.top + (int) (origTaskBounds.height() * 0.9)); 131 } else { 132 mReportedDisplayMetrics.setSize( 133 new Size((int) (origDisplaySize.getWidth() * 0.9), 134 (int) (origDisplaySize.getHeight() * 0.9))); 135 } 136 137 UiDevice.getInstance(mInstrumentation).waitForIdle(); 138 waitAndAssertResumedAndFillsTask(secondaryActivity); 139 waitAndAssertNotVisible(primaryActivity); 140 141 // Return the task/display to its original size and verify that the activities are split 142 if (taskInFreeformMode) { 143 resizeActivityTask(primaryActivity.getComponentName(), 144 origTaskBounds.left, origTaskBounds.top, 145 origTaskBounds.right,origTaskBounds.bottom); 146 } else { 147 mReportedDisplayMetrics.setSize(origDisplaySize); 148 } 149 150 UiDevice.getInstance(mInstrumentation).waitForIdle(); 151 assertValidSplit(primaryActivity, secondaryActivity, splitPairRule); 152 } 153 } 154 155 /** 156 * Tests that the activity bounds for activities in a split match the LTR layout direction 157 * provided in the {@link SplitPairRule}. 158 */ 159 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 160 + ".LayoutDirection#LEFT_TO_RIGHT"}) 161 @Test testLayoutDirection_LeftToRight()162 public void testLayoutDirection_LeftToRight() { 163 // Create a split pair rule with layout direction LEFT_TO_RIGHT and a split ratio that 164 // results in uneven bounds between the primary and secondary containers. 165 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule( 166 LayoutDirection.LEFT_TO_RIGHT); 167 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 168 169 // Start activities in a split and verify that the layout direction is LEFT_TO_RIGHT, 170 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 171 final Activity primaryActivity = startFullScreenActivityNewTask( 172 TestConfigChangeHandlingActivity.class, null /* activityId */, 173 getLaunchingDisplayId()); 174 startActivityAndVerifySplitAttributes( 175 primaryActivity, 176 TestActivityWithId.class, 177 splitPairRule, 178 "secondaryActivityId", 179 mSplitInfoConsumer, 180 mActivityStackCallback); 181 } 182 183 /** 184 * Tests that the activity bounds for activities in a split match the RTL layout direction 185 * provided in the {@link SplitPairRule}. 186 */ 187 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 188 + ".LayoutDirection#RIGHT_TO_LEFT"}) 189 @Test testLayoutDirection_RightToLeft()190 public void testLayoutDirection_RightToLeft() { 191 // Create a split pair rule with layout direction RIGHT_TO_LEFT and a split ratio that 192 // results in uneven bounds between the primary and secondary containers. 193 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule( 194 LayoutDirection.RIGHT_TO_LEFT); 195 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 196 197 // Start activities in a split and verify that the layout direction is RIGHT_TO_LEFT, 198 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 199 final Activity primaryActivity = startFullScreenActivityNewTask( 200 TestConfigChangeHandlingActivity.class, null /* activityId */, 201 getLaunchingDisplayId()); 202 startActivityAndVerifySplitAttributes( 203 primaryActivity, 204 TestActivityWithId.class, 205 splitPairRule, 206 "secondaryActivityId", 207 mSplitInfoConsumer, 208 mActivityStackCallback); 209 } 210 211 /** 212 * Tests that the activity bounds for activities in a split match the Locale layout direction 213 * provided in the {@link SplitPairRule}. 214 */ 215 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 216 + ".LayoutDirection#LOCALE"}) 217 @Test testLayoutDirection_Locale()218 public void testLayoutDirection_Locale() { 219 // Create a split pair rule with layout direction LOCALE and a split ratio that results in 220 // uneven bounds between the primary and secondary containers. 221 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule(LayoutDirection.LOCALE); 222 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 223 224 // Start activities in a split and verify that the layout direction is the device locale, 225 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 226 final Activity primaryActivity = startFullScreenActivityNewTask( 227 TestConfigChangeHandlingActivity.class, null /* activityId */, 228 getLaunchingDisplayId()); 229 startActivityAndVerifySplitAttributes( 230 primaryActivity, 231 TestActivityWithId.class, 232 splitPairRule, 233 "secondaryActivityId", 234 mSplitInfoConsumer, 235 mActivityStackCallback); 236 } 237 238 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 239 + ".LayoutDirection#TOP_TO_BOTTOM"}) 240 @Test testLayoutDirection_TopToBottom()241 public void testLayoutDirection_TopToBottom() { 242 // Create a split pair rule with layout direction TOP_TO_BOTTOM and a split ratio that 243 // results in uneven bounds between the primary and secondary containers. 244 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule( 245 LayoutDirection.TOP_TO_BOTTOM); 246 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 247 248 // Start activities in a split and verify that the layout direction is TOP_TO_BOTTOM, 249 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 250 final Activity primaryActivity = startFullScreenActivityNewTask( 251 TestConfigChangeHandlingActivity.class, null /* activityId */, 252 getLaunchingDisplayId()); 253 startActivityAndVerifySplitAttributes( 254 primaryActivity, 255 TestActivityWithId.class, 256 splitPairRule, 257 "secondaryActivityId", 258 mSplitInfoConsumer, 259 mActivityStackCallback); 260 } 261 262 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 263 + ".LayoutDirection#BOTTOM_TO_TOP"}) 264 @Test testLayoutDirection_BottomToTop()265 public void testLayoutDirection_BottomToTop() { 266 // Create a split pair rule with layout direction BOTTOM_TO_TOP and a split ratio that 267 // results in uneven bounds between the primary and secondary containers. 268 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule( 269 LayoutDirection.TOP_TO_BOTTOM); 270 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 271 272 // Start activities in a split and verify that the layout direction is BOTTOM_TO_TOP, 273 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 274 final Activity primaryActivity = startFullScreenActivityNewTask( 275 TestConfigChangeHandlingActivity.class, null /* activityId */, 276 getLaunchingDisplayId()); 277 startActivityAndVerifySplitAttributes( 278 primaryActivity, 279 TestActivityWithId.class, 280 splitPairRule, 281 "secondaryActivityId", 282 mSplitInfoConsumer, 283 mActivityStackCallback); 284 } 285 286 /** 287 * Tests that when two activities enter a split, then their split ratio matches what is in their 288 * {@link SplitPairRule}, and is not assumed to be 0.5 or match the split ratio of the previous 289 * top-most activity split. 290 */ 291 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 292 + ".SplitType.RatioSplitType#getRatio"}) 293 @Test testSplitRatio()294 public void testSplitRatio() { 295 final String activityAId = "activityA"; 296 final String activityBId = "activityB"; 297 final String activityCId = "activityC"; 298 final SplitType activityABSplitRatio = new SplitType.RatioSplitType(0.37f); 299 final SplitType activityBCSplitRatio = new SplitType.RatioSplitType(0.85f); 300 301 // Create a split rule for activity A and activity B where the split ratio is 0.37. 302 final SplitPairRule splitPairRuleAB = createSplitPairRuleBuilder( 303 activityActivityPair -> false /* activityPairPredicate */, 304 activityIntentPair -> matchesActivityIntentPair(activityIntentPair, activityAId, 305 activityBId) /* activityIntentPredicate */, 306 parentWindowMetrics -> true /* parentWindowMetricsPredicate */) 307 .setDefaultSplitAttributes(new SplitAttributes.Builder().setSplitType( 308 activityABSplitRatio).build()) 309 .build(); 310 311 // Create a split rule for activity B and activity C where the split ratio is 0.65. 312 final SplitPairRule splitPairRuleBC = createSplitPairRuleBuilder( 313 activityActivityPair -> false /* activityPairPredicate */, 314 activityIntentPair -> matchesActivityIntentPair(activityIntentPair, activityBId, 315 activityCId) /* activityIntentPredicate */, 316 parentWindowMetrics -> true /* parentWindowMetricsPredicate */) 317 .setDefaultSplitAttributes(new SplitAttributes.Builder().setSplitType( 318 activityBCSplitRatio).build()) 319 .build(); 320 321 // Register the two split pair rules 322 mActivityEmbeddingComponent.setEmbeddingRules(Set.of(splitPairRuleAB, splitPairRuleBC)); 323 324 // Launch the activity A and B split and verify that the split ratio is 0.37 in 325 // {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 326 final Activity activityA = startFullScreenActivityNewTask( 327 TestActivityWithId.class, activityAId, getLaunchingDisplayId()); 328 Activity activityB = 329 startActivityAndVerifySplitAttributes( 330 activityA, 331 TestActivityWithId.class, 332 splitPairRuleAB, 333 activityBId, 334 mSplitInfoConsumer, 335 mActivityStackCallback); 336 337 // Launch the activity B and C split and verify that the split ratio is 0.65 in 338 // {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 339 Activity activityC = 340 startActivityAndVerifySplitAttributes( 341 activityB, 342 TestActivityWithId.class, 343 splitPairRuleBC, 344 activityCId, 345 mSplitInfoConsumer, 346 mActivityStackCallback); 347 348 // Finish activity C so that activity A and B are in a split again. Verify that the split 349 // ratio returns to 0.37 in {@link ActivityEmbeddingUtil#assertValidSplit}. 350 activityC.finish(); 351 assertValidSplit(activityA, activityB, splitPairRuleAB); 352 } 353 354 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes.HingeSplitType" 355 + "#HingeSplitType"}) 356 @Test testHingeSplitType()357 public void testHingeSplitType() { 358 SplitPairRule splitPairRule = createSplitPairRuleBuilder( 359 activityActivityPair -> true, 360 activityIntentPair -> true, 361 windowMetrics -> true) 362 .setDefaultSplitAttributes(HINGE_SPLIT_ATTRS) 363 .build(); 364 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 365 366 // Start another activity to split with the primary activity and verify that the split type 367 // is hinge. 368 final Activity primaryActivity = startFullScreenActivityNewTask( 369 TestConfigChangeHandlingActivity.class, null /* activityId */, 370 getLaunchingDisplayId()); 371 startActivityAndVerifySplitAttributes( 372 primaryActivity, 373 TestActivityWithId.class, 374 splitPairRule, 375 "secondaryActivityId", 376 mSplitInfoConsumer, 377 mActivityStackCallback); 378 } 379 380 /** Verifies {@link SplitAttributes.SplitType.ExpandContainersSplitType} behavior. */ 381 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 382 + ".ExpandContainersSplitType#ExpandContainersSplitType"}) 383 @Test testExpandSplitType()384 public void testExpandSplitType() { 385 SplitPairRule splitPairRule = createSplitPairRuleBuilder( 386 activityActivityPair -> true, 387 activityIntentPair -> true, 388 windowMetrics -> true 389 ) 390 .setDefaultSplitAttributes(EXPAND_SPLIT_ATTRS) 391 .build(); 392 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 393 394 // Start activities in a split and verify that the split type is expand, 395 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 396 final Activity primaryActivity = startFullScreenActivityNewTask( 397 TestConfigChangeHandlingActivity.class, null /* activityId */, 398 getLaunchingDisplayId()); 399 startActivityAndVerifySplitAttributes( 400 primaryActivity, 401 TestActivityWithId.class, 402 splitPairRule, 403 "secondaryActivityId", 404 mSplitInfoConsumer, 405 mActivityStackCallback); 406 } 407 createUnevenWidthSplitPairRule(int layoutDir)408 private SplitPairRule createUnevenWidthSplitPairRule(int layoutDir) { 409 return createSplitPairRuleBuilder( 410 activityActivityPair -> true /* activityPairPredicate */, 411 activityIntentPair -> true /* activityIntentPredicate */, 412 parentWindowMetrics -> true /* parentWindowMetricsPredicate */) 413 .setDefaultSplitAttributes(new SplitAttributes.Builder() 414 .setSplitType(UNEVEN_CONTAINERS_DEFAULT_SPLIT_TYPE) 415 .setLayoutDirection(layoutDir) 416 .build()) 417 .build(); 418 } 419 matchesActivityIntentPair(@onNull Pair<Activity, Intent> activityIntentPair, @NonNull String primaryActivityId, @NonNull String secondaryActivityId)420 static boolean matchesActivityIntentPair(@NonNull Pair<Activity, Intent> activityIntentPair, 421 @NonNull String primaryActivityId, @NonNull String secondaryActivityId) { 422 if (!(activityIntentPair.first instanceof TestActivityWithId)) { 423 return false; 424 } 425 return primaryActivityId.equals(((TestActivityWithId) activityIntentPair.first).getId()) 426 && secondaryActivityId.equals(activityIntentPair.second.getStringExtra( 427 KEY_ACTIVITY_ID)); 428 } 429 } 430