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 = (TestActivity) startActivityAndVerifySplitAttributes( 102 primaryActivity, TestActivityWithId.class, splitPairRule, secondaryActivityId, 103 mSplitInfoConsumer); 104 105 // Resize multiple times to verify that the activities are correctly split or 106 // stacked depending on the parent bounds. Resizing multiple times simulates a foldable 107 // display is that folded and unfolded multiple times while running the same app. 108 final int numTimesToResize = 2; 109 final Size origDisplaySize = mReportedDisplayMetrics.getSize(); 110 mWmState.computeState( 111 primaryActivity.getComponentName(), secondaryActivity.getComponentName()); 112 // Primary and secondary activities should be in the same task 113 final Task task = mWmState.getTaskByActivity(primaryActivity.getComponentName()); 114 final Rect origTaskBounds = task.getBounds(); 115 final boolean taskInFreeformMode = task.getWindowingMode() == WINDOWING_MODE_FREEFORM; 116 for (int i = 0; i < numTimesToResize; i++) { 117 // Shrink by 10% to make the activities stacked. 118 // If the activity was launched in freeform windowing mode, resize the task bounds 119 // instead of resizing the display. 120 if (taskInFreeformMode) { 121 resizeActivityTask(primaryActivity.getComponentName(), 122 origTaskBounds.left, origTaskBounds.top, 123 origTaskBounds.left + (int) (origTaskBounds.width() * 0.9), 124 origTaskBounds.top + (int) (origTaskBounds.height() * 0.9)); 125 } else { 126 mReportedDisplayMetrics.setSize( 127 new Size((int) (origDisplaySize.getWidth() * 0.9), 128 (int) (origDisplaySize.getHeight() * 0.9))); 129 } 130 131 UiDevice.getInstance(mInstrumentation).waitForIdle(); 132 waitAndAssertResumedAndFillsTask(secondaryActivity); 133 waitAndAssertNotVisible(primaryActivity); 134 135 // Return the task/display to its original size and verify that the activities are split 136 if (taskInFreeformMode) { 137 resizeActivityTask(primaryActivity.getComponentName(), 138 origTaskBounds.left, origTaskBounds.top, 139 origTaskBounds.right,origTaskBounds.bottom); 140 } else { 141 mReportedDisplayMetrics.setSize(origDisplaySize); 142 } 143 144 UiDevice.getInstance(mInstrumentation).waitForIdle(); 145 assertValidSplit(primaryActivity, secondaryActivity, splitPairRule); 146 } 147 } 148 149 /** 150 * Tests that the activity bounds for activities in a split match the LTR layout direction 151 * provided in the {@link SplitPairRule}. 152 */ 153 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 154 + ".LayoutDirection#LEFT_TO_RIGHT"}) 155 @Test testLayoutDirection_LeftToRight()156 public void testLayoutDirection_LeftToRight() { 157 // Create a split pair rule with layout direction LEFT_TO_RIGHT and a split ratio that 158 // results in uneven bounds between the primary and secondary containers. 159 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule( 160 LayoutDirection.LEFT_TO_RIGHT); 161 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 162 163 // Start activities in a split and verify that the layout direction is LEFT_TO_RIGHT, 164 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 165 final Activity primaryActivity = startFullScreenActivityNewTask( 166 TestConfigChangeHandlingActivity.class, null /* activityId */, 167 getLaunchingDisplayId()); 168 startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class, 169 splitPairRule, "secondaryActivityId", mSplitInfoConsumer); 170 } 171 172 /** 173 * Tests that the activity bounds for activities in a split match the RTL layout direction 174 * provided in the {@link SplitPairRule}. 175 */ 176 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 177 + ".LayoutDirection#RIGHT_TO_LEFT"}) 178 @Test testLayoutDirection_RightToLeft()179 public void testLayoutDirection_RightToLeft() { 180 // Create a split pair rule with layout direction RIGHT_TO_LEFT and a split ratio that 181 // results in uneven bounds between the primary and secondary containers. 182 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule( 183 LayoutDirection.RIGHT_TO_LEFT); 184 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 185 186 // Start activities in a split and verify that the layout direction is RIGHT_TO_LEFT, 187 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 188 final Activity primaryActivity = startFullScreenActivityNewTask( 189 TestConfigChangeHandlingActivity.class, null /* activityId */, 190 getLaunchingDisplayId()); 191 startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class, 192 splitPairRule, "secondaryActivityId", mSplitInfoConsumer); 193 } 194 195 /** 196 * Tests that the activity bounds for activities in a split match the Locale layout direction 197 * provided in the {@link SplitPairRule}. 198 */ 199 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 200 + ".LayoutDirection#LOCALE"}) 201 @Test testLayoutDirection_Locale()202 public void testLayoutDirection_Locale() { 203 // Create a split pair rule with layout direction LOCALE and a split ratio that results in 204 // uneven bounds between the primary and secondary containers. 205 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule(LayoutDirection.LOCALE); 206 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 207 208 // Start activities in a split and verify that the layout direction is the device locale, 209 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 210 final Activity primaryActivity = startFullScreenActivityNewTask( 211 TestConfigChangeHandlingActivity.class, null /* activityId */, 212 getLaunchingDisplayId()); 213 startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class, 214 splitPairRule, "secondaryActivityId", mSplitInfoConsumer); 215 } 216 217 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 218 + ".LayoutDirection#TOP_TO_BOTTOM"}) 219 @Test testLayoutDirection_TopToBottom()220 public void testLayoutDirection_TopToBottom() { 221 // Create a split pair rule with layout direction TOP_TO_BOTTOM and a split ratio that 222 // results in uneven bounds between the primary and secondary containers. 223 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule( 224 LayoutDirection.TOP_TO_BOTTOM); 225 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 226 227 // Start activities in a split and verify that the layout direction is TOP_TO_BOTTOM, 228 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 229 final Activity primaryActivity = startFullScreenActivityNewTask( 230 TestConfigChangeHandlingActivity.class, null /* activityId */, 231 getLaunchingDisplayId()); 232 startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class, 233 splitPairRule, "secondaryActivityId", mSplitInfoConsumer); 234 } 235 236 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 237 + ".LayoutDirection#BOTTOM_TO_TOP"}) 238 @Test testLayoutDirection_BottomToTop()239 public void testLayoutDirection_BottomToTop() { 240 // Create a split pair rule with layout direction BOTTOM_TO_TOP and a split ratio that 241 // results in uneven bounds between the primary and secondary containers. 242 final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule( 243 LayoutDirection.TOP_TO_BOTTOM); 244 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 245 246 // Start activities in a split and verify that the layout direction is BOTTOM_TO_TOP, 247 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 248 final Activity primaryActivity = startFullScreenActivityNewTask( 249 TestConfigChangeHandlingActivity.class, null /* activityId */, 250 getLaunchingDisplayId()); 251 startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class, 252 splitPairRule, "secondaryActivityId", mSplitInfoConsumer); 253 } 254 255 /** 256 * Tests that when two activities enter a split, then their split ratio matches what is in their 257 * {@link SplitPairRule}, and is not assumed to be 0.5 or match the split ratio of the previous 258 * top-most activity split. 259 */ 260 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 261 + ".SplitType.RatioSplitType#getRatio"}) 262 @Test testSplitRatio()263 public void testSplitRatio() { 264 final String activityAId = "activityA"; 265 final String activityBId = "activityB"; 266 final String activityCId = "activityC"; 267 final SplitType activityABSplitRatio = new SplitType.RatioSplitType(0.37f); 268 final SplitType activityBCSplitRatio = new SplitType.RatioSplitType(0.85f); 269 270 // Create a split rule for activity A and activity B where the split ratio is 0.37. 271 final SplitPairRule splitPairRuleAB = createSplitPairRuleBuilder( 272 activityActivityPair -> false /* activityPairPredicate */, 273 activityIntentPair -> matchesActivityIntentPair(activityIntentPair, activityAId, 274 activityBId) /* activityIntentPredicate */, 275 parentWindowMetrics -> true /* parentWindowMetricsPredicate */) 276 .setDefaultSplitAttributes(new SplitAttributes.Builder().setSplitType( 277 activityABSplitRatio).build()) 278 .build(); 279 280 // Create a split rule for activity B and activity C where the split ratio is 0.65. 281 final SplitPairRule splitPairRuleBC = createSplitPairRuleBuilder( 282 activityActivityPair -> false /* activityPairPredicate */, 283 activityIntentPair -> matchesActivityIntentPair(activityIntentPair, activityBId, 284 activityCId) /* activityIntentPredicate */, 285 parentWindowMetrics -> true /* parentWindowMetricsPredicate */) 286 .setDefaultSplitAttributes(new SplitAttributes.Builder().setSplitType( 287 activityBCSplitRatio).build()) 288 .build(); 289 290 // Register the two split pair rules 291 mActivityEmbeddingComponent.setEmbeddingRules(Set.of(splitPairRuleAB, splitPairRuleBC)); 292 293 // Launch the activity A and B split and verify that the split ratio is 0.37 in 294 // {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 295 final Activity activityA = startFullScreenActivityNewTask( 296 TestActivityWithId.class, activityAId, getLaunchingDisplayId()); 297 Activity activityB = startActivityAndVerifySplitAttributes(activityA, 298 TestActivityWithId.class, splitPairRuleAB, activityBId, mSplitInfoConsumer); 299 300 // Launch the activity B and C split and verify that the split ratio is 0.65 in 301 // {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 302 Activity activityC = startActivityAndVerifySplitAttributes(activityB, 303 TestActivityWithId.class, splitPairRuleBC, activityCId, mSplitInfoConsumer); 304 305 // Finish activity C so that activity A and B are in a split again. Verify that the split 306 // ratio returns to 0.37 in {@link ActivityEmbeddingUtil#assertValidSplit}. 307 activityC.finish(); 308 assertValidSplit(activityA, activityB, splitPairRuleAB); 309 } 310 311 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes.HingeSplitType" 312 + "#HingeSplitType"}) 313 @Test testHingeSplitType()314 public void testHingeSplitType() { 315 SplitPairRule splitPairRule = createSplitPairRuleBuilder( 316 activityActivityPair -> true, 317 activityIntentPair -> true, 318 windowMetrics -> true) 319 .setDefaultSplitAttributes(HINGE_SPLIT_ATTRS) 320 .build(); 321 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 322 323 // Start another activity to split with the primary activity and verify that the split type 324 // is hinge. 325 final Activity primaryActivity = startFullScreenActivityNewTask( 326 TestConfigChangeHandlingActivity.class, null /* activityId */, 327 getLaunchingDisplayId()); 328 startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class, 329 splitPairRule, "secondaryActivityId", mSplitInfoConsumer); 330 } 331 332 /** Verifies {@link SplitAttributes.SplitType.ExpandContainersSplitType} behavior. */ 333 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes" 334 + ".ExpandContainersSplitType#ExpandContainersSplitType"}) 335 @Test testExpandSplitType()336 public void testExpandSplitType() { 337 SplitPairRule splitPairRule = createSplitPairRuleBuilder( 338 activityActivityPair -> true, 339 activityIntentPair -> true, 340 windowMetrics -> true 341 ) 342 .setDefaultSplitAttributes(EXPAND_SPLIT_ATTRS) 343 .build(); 344 mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule)); 345 346 // Start activities in a split and verify that the split type is expand, 347 // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}. 348 final Activity primaryActivity = startFullScreenActivityNewTask( 349 TestConfigChangeHandlingActivity.class, null /* activityId */, 350 getLaunchingDisplayId()); 351 startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class, 352 splitPairRule, "secondaryActivityId", mSplitInfoConsumer); 353 } 354 createUnevenWidthSplitPairRule(int layoutDir)355 private SplitPairRule createUnevenWidthSplitPairRule(int layoutDir) { 356 return createSplitPairRuleBuilder( 357 activityActivityPair -> true /* activityPairPredicate */, 358 activityIntentPair -> true /* activityIntentPredicate */, 359 parentWindowMetrics -> true /* parentWindowMetricsPredicate */) 360 .setDefaultSplitAttributes(new SplitAttributes.Builder() 361 .setSplitType(UNEVEN_CONTAINERS_DEFAULT_SPLIT_TYPE) 362 .setLayoutDirection(layoutDir) 363 .build()) 364 .build(); 365 } 366 matchesActivityIntentPair(@onNull Pair<Activity, Intent> activityIntentPair, @NonNull String primaryActivityId, @NonNull String secondaryActivityId)367 static boolean matchesActivityIntentPair(@NonNull Pair<Activity, Intent> activityIntentPair, 368 @NonNull String primaryActivityId, @NonNull String secondaryActivityId) { 369 if (!(activityIntentPair.first instanceof TestActivityWithId)) { 370 return false; 371 } 372 return primaryActivityId.equals(((TestActivityWithId) activityIntentPair.first).getId()) 373 && secondaryActivityId.equals(activityIntentPair.second.getStringExtra( 374 KEY_ACTIVITY_ID)); 375 } 376 } 377