1 /* 2 * Copyright (C) 2021 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.utils; 18 19 import static android.server.wm.BuildUtils.HW_TIMEOUT_MULTIPLIER; 20 import static android.server.wm.WindowManagerState.STATE_RESUMED; 21 import static android.server.wm.jetpack.extensions.util.ExtensionsUtil.assumeExtensionSupportedDevice; 22 import static android.server.wm.jetpack.extensions.util.ExtensionsUtil.getExtensionWindowLayoutInfo; 23 import static android.server.wm.jetpack.extensions.util.ExtensionsUtil.getWindowExtensions; 24 import static android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.getActivityBounds; 25 import static android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.getResumedActivityById; 26 import static android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.isActivityResumed; 27 import static android.server.wm.jetpack.utils.WindowManagerJetpackTestBase.startActivityFromActivity; 28 import static android.util.TypedValue.COMPLEX_UNIT_DIP; 29 30 import static org.junit.Assert.assertEquals; 31 import static org.junit.Assert.assertFalse; 32 import static org.junit.Assert.assertNotNull; 33 import static org.junit.Assert.assertNull; 34 import static org.junit.Assert.assertTrue; 35 36 import static java.util.Objects.requireNonNull; 37 38 import android.app.Activity; 39 import android.content.ComponentName; 40 import android.content.Intent; 41 import android.graphics.Rect; 42 import android.os.Bundle; 43 import android.os.SystemClock; 44 import android.server.wm.WindowManagerStateHelper; 45 import android.server.wm.jetpack.extensions.util.TestValueCountConsumer; 46 import android.util.Log; 47 import android.util.Pair; 48 import android.util.TypedValue; 49 import android.view.WindowMetrics; 50 51 import androidx.annotation.NonNull; 52 import androidx.annotation.Nullable; 53 import androidx.window.extensions.core.util.function.Predicate; 54 import androidx.window.extensions.embedding.ActivityEmbeddingComponent; 55 import androidx.window.extensions.embedding.ActivityStack; 56 import androidx.window.extensions.embedding.DividerAttributes; 57 import androidx.window.extensions.embedding.SplitAttributes; 58 import androidx.window.extensions.embedding.SplitAttributes.LayoutDirection; 59 import androidx.window.extensions.embedding.SplitAttributes.SplitType; 60 import androidx.window.extensions.embedding.SplitInfo; 61 import androidx.window.extensions.embedding.SplitPairRule; 62 import androidx.window.extensions.embedding.SplitRule; 63 import androidx.window.extensions.layout.FoldingFeature; 64 import androidx.window.extensions.layout.WindowLayoutInfo; 65 66 import com.android.compatibility.common.util.PollingCheck; 67 68 import java.util.ArrayList; 69 import java.util.Arrays; 70 import java.util.List; 71 72 /** Utility class for activity embedding tests. */ 73 public class ActivityEmbeddingUtil { 74 75 public static final String TAG = "ActivityEmbeddingTests"; 76 public static final long WAIT_FOR_LIFECYCLE_TIMEOUT_MS = 3000L * HW_TIMEOUT_MULTIPLIER; 77 public static final long WAIT_FOR_COLD_LAUNCH_TIMEOUT_MS = 5000L * HW_TIMEOUT_MULTIPLIER; 78 public static final SplitAttributes DEFAULT_SPLIT_ATTRS = new SplitAttributes.Builder().build(); 79 80 public static final SplitAttributes EXPAND_SPLIT_ATTRS = new SplitAttributes.Builder() 81 .setSplitType(new SplitType.ExpandContainersSplitType()).build(); 82 83 public static final SplitAttributes HINGE_SPLIT_ATTRS = new SplitAttributes.Builder() 84 .setSplitType(new SplitType.HingeSplitType(SplitType.RatioSplitType.splitEqually())) 85 .build(); 86 87 public static final String EMBEDDED_ACTIVITY_ID = "embedded_activity_id"; 88 89 private static final long WAIT_PERIOD = 500; 90 91 @NonNull createWildcardSplitPairRule(boolean shouldClearTop)92 public static SplitPairRule createWildcardSplitPairRule(boolean shouldClearTop) { 93 // Build the split pair rule 94 return createSplitPairRuleBuilder( 95 // Any activity be split with any activity 96 activityActivityPair -> true, 97 // Any activity can launch any split intent 98 activityIntentPair -> true, 99 // Allow any parent bounds to show the split containers side by side 100 windowMetrics -> true) 101 .setDefaultSplitAttributes(DEFAULT_SPLIT_ATTRS) 102 .setShouldClearTop(shouldClearTop) 103 .build(); 104 } 105 106 @NonNull createWildcardSplitPairRuleWithPrimaryActivityClass( Class<? extends Activity> activityClass, boolean shouldClearTop)107 public static SplitPairRule createWildcardSplitPairRuleWithPrimaryActivityClass( 108 Class<? extends Activity> activityClass, boolean shouldClearTop) { 109 return createWildcardSplitPairRuleBuilderWithPrimaryActivityClass(activityClass, 110 shouldClearTop).build(); 111 } 112 113 @NonNull createWildcardSplitPairRuleBuilderWithPrimaryActivityClass( Class<? extends Activity> activityClass, boolean shouldClearTop)114 public static SplitPairRule.Builder createWildcardSplitPairRuleBuilderWithPrimaryActivityClass( 115 Class<? extends Activity> activityClass, boolean shouldClearTop) { 116 // Build the split pair rule 117 return createSplitPairRuleBuilder( 118 // The specified activity be split any activity 119 activityActivityPair -> activityActivityPair.first.getClass().equals(activityClass), 120 // The specified activity can launch any split intent 121 activityIntentPair -> activityIntentPair.first.getClass().equals(activityClass), 122 // Allow any parent bounds to show the split containers side by side 123 windowMetrics -> true) 124 .setDefaultSplitAttributes(DEFAULT_SPLIT_ATTRS) 125 .setShouldClearTop(shouldClearTop); 126 } 127 128 @NonNull createWildcardSplitPairRule()129 public static SplitPairRule createWildcardSplitPairRule() { 130 return createWildcardSplitPairRule(false /* shouldClearTop */); 131 } 132 133 /** 134 * A wrapper to create {@link SplitPairRule} builder with extensions core functional interface 135 * to prevent ambiguous issue when using lambda expressions. 136 */ 137 @NonNull createSplitPairRuleBuilder( @onNull Predicate<Pair<Activity, Activity>> activitiesPairPredicate, @NonNull Predicate<Pair<Activity, Intent>> activityIntentPairPredicate, @NonNull Predicate<WindowMetrics> windowMetricsPredicate)138 public static SplitPairRule.Builder createSplitPairRuleBuilder( 139 @NonNull Predicate<Pair<Activity, Activity>> activitiesPairPredicate, 140 @NonNull Predicate<Pair<Activity, Intent>> activityIntentPairPredicate, 141 @NonNull Predicate<WindowMetrics> windowMetricsPredicate) { 142 return new SplitPairRule.Builder(activitiesPairPredicate, activityIntentPairPredicate, 143 windowMetricsPredicate); 144 } 145 startActivityAndVerifyNotSplit( @onNull Activity activityLaunchingFrom)146 public static TestActivity startActivityAndVerifyNotSplit( 147 @NonNull Activity activityLaunchingFrom) { 148 final String secondActivityId = "secondActivityId"; 149 // Launch second activity 150 startActivityFromActivity(activityLaunchingFrom, TestActivityWithId.class, 151 secondActivityId); 152 // Verify both activities are in the correct lifecycle state 153 waitAndAssertResumed(secondActivityId); 154 assertFalse(isActivityResumed(activityLaunchingFrom)); 155 TestActivity secondActivity = getResumedActivityById(secondActivityId); 156 // Verify the second activity is not split with the first 157 waitAndAssertResumedAndFillsTask(secondActivity); 158 return secondActivity; 159 } 160 161 /** 162 * Starts an {@link Activity} and verifies the split states. 163 * 164 * @param activityLaunchingFrom the primary {@link Activity} to launch the secondary 165 * @param secondActivityClass the class of the secondary {@link Activity} 166 * @param secondaryActivityId the {@code String} ID of the secondary {@link Activity} 167 * @param expectedCallbackCount the expected count from {@code splitInfoConsumer} 168 * @param splitInfoConsumer the {@link SplitInfo} callback 169 * @param activityStackCallback the {@link ActivityStack} callback. It could be {@code null} if 170 * {@link ActivityEmbeddingComponent#registerActivityStackCallback} is not supported or we 171 * don't want to verify {@link ActivityStack}. 172 * @return the launched secondary {@link Activity} 173 */ 174 @NonNull startActivityAndVerifySplitAttributes( @onNull Activity activityLaunchingFrom, @NonNull Activity expectedPrimaryActivity, @NonNull Class<? extends Activity> secondActivityClass, @NonNull SplitAttributes splitAttributes, @NonNull String secondaryActivityId, int expectedCallbackCount, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback)175 public static Activity startActivityAndVerifySplitAttributes( 176 @NonNull Activity activityLaunchingFrom, 177 @NonNull Activity expectedPrimaryActivity, 178 @NonNull Class<? extends Activity> secondActivityClass, 179 @NonNull SplitAttributes splitAttributes, 180 @NonNull String secondaryActivityId, 181 int expectedCallbackCount, 182 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, 183 @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback) { 184 // Set the expected callback count 185 splitInfoConsumer.setCount(expectedCallbackCount); 186 187 // Start second activity 188 startActivityFromActivity(activityLaunchingFrom, secondActivityClass, secondaryActivityId); 189 190 // Wait for secondary activity to be resumed and verify that the newly sent split info 191 // contains the secondary activity. 192 waitAndAssertResumed(secondaryActivityId); 193 final Activity secondaryActivity = getResumedActivityById(secondaryActivityId); 194 195 assertSplitPairIsCorrect( 196 expectedPrimaryActivity, 197 secondaryActivity, 198 splitAttributes, 199 splitInfoConsumer, 200 activityStackCallback); 201 202 // Return second activity for easy access in calling method 203 return secondaryActivity; 204 } 205 206 /** 207 * Assert the split pair is correct. 208 * 209 * @param activityStackCallback if not {@code null}, check {@link ActivityStack activityStacks} 210 * is expected. Otherwise, don't verify {@code activityStacks}. 211 */ assertSplitPairIsCorrect( @onNull Activity expectedPrimaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitAttributes splitAttributes, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback)212 public static void assertSplitPairIsCorrect( 213 @NonNull Activity expectedPrimaryActivity, 214 @NonNull Activity secondaryActivity, 215 @NonNull SplitAttributes splitAttributes, 216 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, 217 @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback) { 218 // A split info callback should occur after the new activity is launched because the split 219 // states have changed. 220 List<SplitInfo> activeSplitStates; 221 try { 222 activeSplitStates = splitInfoConsumer.waitAndGet(); 223 } catch (InterruptedException e) { 224 throw new AssertionError("startActivityAndVerifySplitAttributes()", e); 225 } 226 assertNotNull("Active Split States cannot be null.", activeSplitStates); 227 228 assertSplitInfoTopSplitIsCorrect(activeSplitStates, expectedPrimaryActivity, 229 secondaryActivity, splitAttributes); 230 assertValidSplit(expectedPrimaryActivity, secondaryActivity, splitAttributes); 231 verifyActivityStacksIfNeeded( 232 activityStackCallback, expectedPrimaryActivity, secondaryActivity); 233 } 234 verifyActivityStacksIfNeeded( @ullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback, @NonNull Activity primaryActivity, @Nullable Activity secondaryActivity)235 private static void verifyActivityStacksIfNeeded( 236 @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback, 237 @NonNull Activity primaryActivity, 238 @Nullable Activity secondaryActivity) { 239 final List<ActivityStack> activityStacks = getLastActivityStacks(activityStackCallback); 240 if (activityStacks == null) { 241 return; 242 } 243 244 assertActivityStacksIsCorrect(activityStacks, primaryActivity, secondaryActivity); 245 } 246 247 @Nullable getLastActivityStacks( @ullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback)248 private static List<ActivityStack> getLastActivityStacks( 249 @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback) { 250 if (activityStackCallback == null) { 251 return null; 252 } 253 try { 254 return activityStackCallback.waitAndGet(); 255 } catch (InterruptedException e) { 256 throw new AssertionError("getLastActivityStacks()", e); 257 } 258 } 259 260 /** 261 * Starts an {@link Activity} from {@code activityLaunchingFrom} and verifies there's no {@link 262 * SplitInfo} callback. 263 * 264 * @param activityLaunchingFrom the {@link Activity} to launch a new {@link Activity} 265 * @param secondActivityClass the secondary {@link Activity} class 266 * @param secondaryActivityId the ID of the secondary {@link Activity} 267 * @param splitInfoConsumer the {@link SplitInfo} callback 268 * @throws InterruptedException if {@link TestValueCountConsumer#waitAndGet()} throws the 269 * exception 270 */ startActivityAndVerifyNoCallback( @onNull Activity activityLaunchingFrom, @NonNull Class<? extends Activity> secondActivityClass, @NonNull String secondaryActivityId, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer)271 public static void startActivityAndVerifyNoCallback( 272 @NonNull Activity activityLaunchingFrom, 273 @NonNull Class<? extends Activity> secondActivityClass, 274 @NonNull String secondaryActivityId, 275 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) 276 throws InterruptedException { 277 // We expect the actual count to be 0. Set to 1 to trigger the timeout and verify no calls. 278 splitInfoConsumer.setCount(1); 279 280 // Start second activity 281 startActivityFromActivity(activityLaunchingFrom, secondActivityClass, secondaryActivityId); 282 283 // A split info callback should occur after the new activity is launched because the split 284 // states have changed. 285 List<SplitInfo> activeSplitStates = splitInfoConsumer.waitAndGet(); 286 assertNull("Received SplitInfo value but did not expect none.", activeSplitStates); 287 } 288 289 /** 290 * Starts an {@link Activity} and verifies the split states. 291 * 292 * @param activityLaunchingFrom the primary {@link Activity} to launch the secondary 293 * @param secondActivityClass the class of the secondary {@link Activity} 294 * @param secondaryActivityId the {@code String} ID of the secondary {@link Activity} 295 * @param expectedCallbackCount the expected count from {@code splitInfoConsumer} 296 * @param splitInfoConsumer the {@link SplitInfo} callback 297 * @param activityStackCallback the {@link ActivityStack} callback. It could be {@code null} if 298 * we don't want to verify {@link ActivityStack}. 299 * @return the launched secondary {@link Activity} 300 */ 301 @NonNull startActivityAndVerifySplitAttributes( @onNull Activity activityLaunchingFrom, @NonNull Activity expectedPrimaryActivity, @NonNull Class<? extends Activity> secondActivityClass, @NonNull SplitRule splitRule, @NonNull String secondaryActivityId, int expectedCallbackCount, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback)302 public static Activity startActivityAndVerifySplitAttributes( 303 @NonNull Activity activityLaunchingFrom, 304 @NonNull Activity expectedPrimaryActivity, 305 @NonNull Class<? extends Activity> secondActivityClass, 306 @NonNull SplitRule splitRule, 307 @NonNull String secondaryActivityId, 308 int expectedCallbackCount, 309 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, 310 @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback) { 311 return startActivityAndVerifySplitAttributes( 312 activityLaunchingFrom, 313 expectedPrimaryActivity, 314 secondActivityClass, 315 splitRule.getDefaultSplitAttributes(), 316 secondaryActivityId, 317 expectedCallbackCount, 318 splitInfoConsumer, 319 activityStackCallback); 320 } 321 322 /** 323 * Starts an {@link Activity} and verifies the split states. 324 * 325 * @param primaryActivity the primary {@link Activity} to launch the secondary 326 * @param secondActivityClass the class of the secondary {@link Activity} 327 * @param splitPairRule the rule that matches the split pair 328 * @param secondActivityId the {@code String} ID of the secondary {@link Activity} 329 * @param splitInfoConsumer the {@link SplitInfo} callback 330 * @param activityStackCallback the {@link ActivityStack} callback. It could be {@code null} if 331 * {@link ActivityEmbeddingComponent#registerActivityStackCallback} is not supported or we 332 * don't want to verify {@link ActivityStack}. 333 * @return the launched secondary {@link Activity} 334 */ 335 @NonNull startActivityAndVerifySplitAttributes( @onNull Activity primaryActivity, @NonNull Class<? extends Activity> secondActivityClass, @NonNull SplitPairRule splitPairRule, @NonNull String secondActivityId, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback)336 public static Activity startActivityAndVerifySplitAttributes( 337 @NonNull Activity primaryActivity, 338 @NonNull Class<? extends Activity> secondActivityClass, 339 @NonNull SplitPairRule splitPairRule, 340 @NonNull String secondActivityId, 341 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, 342 @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback) { 343 return startActivityAndVerifySplitAttributes( 344 primaryActivity, 345 primaryActivity, 346 secondActivityClass, 347 splitPairRule, 348 secondActivityId, 349 1 /* expectedCallbackCount */, 350 splitInfoConsumer, 351 activityStackCallback); 352 } 353 354 /** 355 * Attempts to start an activity from a different UID into a split, verifies that a new split is 356 * active. 357 */ startActivityCrossUidInSplit( @onNull Activity primaryActivity, @NonNull ComponentName secondActivityComponent, @NonNull SplitPairRule splitPairRule, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback, @NonNull String secondActivityId, boolean verifySplitState)358 public static void startActivityCrossUidInSplit( 359 @NonNull Activity primaryActivity, 360 @NonNull ComponentName secondActivityComponent, 361 @NonNull SplitPairRule splitPairRule, 362 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, 363 @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback, 364 @NonNull String secondActivityId, 365 boolean verifySplitState) { 366 startActivityFromActivity(primaryActivity, secondActivityComponent, secondActivityId, 367 Bundle.EMPTY); 368 if (!verifySplitState) { 369 return; 370 } 371 372 // Get updated split info 373 splitInfoConsumer.setCount(1); 374 List<SplitInfo> activeSplitStates; 375 try { 376 activeSplitStates = splitInfoConsumer.waitAndGet(); 377 } catch (InterruptedException e) { 378 throw new AssertionError("startActivityCrossUidInSplit()", e); 379 } 380 assertNotNull(activeSplitStates); 381 assertFalse(activeSplitStates.isEmpty()); 382 // Verify that the primary activity is on top of the primary stack 383 SplitInfo topSplit = activeSplitStates.get(activeSplitStates.size() - 1); 384 List<Activity> primaryStackActivities = topSplit.getPrimaryActivityStack() 385 .getActivities(); 386 assertEquals(primaryActivity, 387 primaryStackActivities.get(primaryStackActivities.size() - 1)); 388 // Verify that the secondary stack is reported as empty to developers 389 assertTrue(topSplit.getSecondaryActivityStack().getActivities().isEmpty()); 390 391 assertValidSplit(primaryActivity, null /* secondaryActivity */, 392 splitPairRule); 393 394 verifyActivityStacksIfNeeded( 395 activityStackCallback, primaryActivity, null /* secondaryActivity */); 396 } 397 398 /** 399 * Attempts to start an activity from a different UID into a split, verifies that activity 400 * did not start on splitContainer successfully and no new split is active. 401 */ startActivityCrossUidInSplit_expectFail(@onNull Activity primaryActivity, @NonNull ComponentName secondActivityComponent, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer)402 public static void startActivityCrossUidInSplit_expectFail(@NonNull Activity primaryActivity, 403 @NonNull ComponentName secondActivityComponent, 404 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) { 405 startActivityFromActivity(primaryActivity, secondActivityComponent, "secondActivityId", 406 Bundle.EMPTY); 407 408 // No split should be active, primary activity should be covered by the new one. 409 assertNoSplit(primaryActivity, splitInfoConsumer); 410 } 411 412 /** 413 * Asserts that there is no split with the provided primary activity. 414 */ assertNoSplit(@onNull Activity primaryActivity, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer)415 public static void assertNoSplit(@NonNull Activity primaryActivity, 416 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) { 417 waitForVisible(primaryActivity, false /* visible */); 418 List<SplitInfo> activeSplitStates = splitInfoConsumer.getLastReportedValue(); 419 assertTrue(activeSplitStates == null || activeSplitStates.isEmpty()); 420 } 421 422 @Nullable getSecondActivity(@ullable List<SplitInfo> activeSplitStates, @NonNull Activity primaryActivity, @NonNull String secondaryClassId)423 public static Activity getSecondActivity(@Nullable List<SplitInfo> activeSplitStates, 424 @NonNull Activity primaryActivity, @NonNull String secondaryClassId) { 425 if (activeSplitStates == null) { 426 Log.d(TAG, "Null split states"); 427 return null; 428 } 429 Log.d(TAG, "Active split states: " + activeSplitStates); 430 for (SplitInfo splitInfo : activeSplitStates) { 431 // Find the split info whose top activity in the primary container is the primary 432 // activity we are looking for 433 Activity primaryContainerTopActivity = getPrimaryStackTopActivity(splitInfo); 434 if (primaryActivity.equals(primaryContainerTopActivity)) { 435 Activity secondActivity = getSecondaryStackTopActivity(splitInfo); 436 // See if this activity is the secondary activity we expect 437 if (secondActivity != null && secondActivity instanceof TestActivityWithId 438 && secondaryClassId.equals(((TestActivityWithId) secondActivity).getId())) { 439 return secondActivity; 440 } 441 } 442 } 443 Log.d(TAG, "Second activity was not found: " + secondaryClassId); 444 return null; 445 } 446 447 /** 448 * Waits for and verifies a valid split. Can accept a null secondary activity if it belongs to 449 * a different process, in which case it will only verify the primary one. 450 */ assertValidSplit(@onNull Activity primaryActivity, @Nullable Activity secondaryActivity, @NonNull SplitRule splitRule)451 public static void assertValidSplit(@NonNull Activity primaryActivity, 452 @Nullable Activity secondaryActivity, @NonNull SplitRule splitRule) { 453 assertValidSplit(primaryActivity, secondaryActivity, splitRule.getDefaultSplitAttributes()); 454 } 455 456 /** 457 * Similar to {@link #assertValidSplit(Activity, Activity, SplitRule)}, but verifies 458 * {@link SplitAttributes} instead of {@link SplitRule#getDefaultSplitAttributes}. 459 */ assertValidSplit(@onNull Activity primaryActivity, @Nullable Activity secondaryActivity, @NonNull SplitAttributes splitAttributes)460 public static void assertValidSplit(@NonNull Activity primaryActivity, 461 @Nullable Activity secondaryActivity, @NonNull SplitAttributes splitAttributes) { 462 final boolean shouldExpandContainers = splitAttributes.getSplitType() 463 instanceof SplitType.ExpandContainersSplitType; 464 final List<Activity> resumedActivities = new ArrayList<>(2); 465 if (secondaryActivity == null) { 466 resumedActivities.add(primaryActivity); 467 } else if (shouldExpandContainers) { 468 resumedActivities.add(secondaryActivity); 469 } else { 470 resumedActivities.add(primaryActivity); 471 resumedActivities.add(secondaryActivity); 472 } 473 waitAndAssertResumed(resumedActivities); 474 475 final Pair<Rect, Rect> expectedBoundsPair = getExpectedBoundsPair( 476 shouldExpandContainers ? requireNonNull(secondaryActivity) : primaryActivity, 477 splitAttributes); 478 479 final ActivityEmbeddingComponent activityEmbeddingComponent = getWindowExtensions() 480 .getActivityEmbeddingComponent(); 481 482 // Verify that both activities are embedded and that the bounds are correct 483 if (!shouldExpandContainers) { 484 // If the split pair is stacked, ignore to check the bounds because the primary activity 485 // may have been occluded and the latest configuration may not be received. 486 waitForActivityBoundsEquals(primaryActivity, expectedBoundsPair.first); 487 assertTrue(activityEmbeddingComponent.isActivityEmbedded(primaryActivity)); 488 } 489 if (secondaryActivity != null) { 490 waitForActivityBoundsEquals(secondaryActivity, expectedBoundsPair.second); 491 assertEquals(!shouldExpandContainers, 492 activityEmbeddingComponent.isActivityEmbedded(secondaryActivity)); 493 } 494 } 495 496 /** 497 * Waits for the activity specified in {@code activityId} to be in resumed state and verifies 498 * if it fills the task. 499 */ waitAndAssertResumedAndFillsTask(@onNull String activityId)500 public static void waitAndAssertResumedAndFillsTask(@NonNull String activityId) { 501 waitAndAssertResumed(activityId); 502 final Activity activity = getResumedActivityById(activityId); 503 final Rect taskBounds = waitAndGetTaskBounds(activity, false /* shouldWaitForResume */); 504 PollingCheck.waitFor(WAIT_FOR_LIFECYCLE_TIMEOUT_MS, () -> 505 getActivityBounds(activity).equals(taskBounds)); 506 assertEquals(taskBounds, getActivityBounds(activity)); 507 } 508 509 /** Waits for the {@code activity} to be in resumed state and verifies if it fills the task. */ waitAndAssertResumedAndFillsTask(@onNull Activity activity)510 public static void waitAndAssertResumedAndFillsTask(@NonNull Activity activity) { 511 final Rect taskBounds = waitAndGetTaskBounds(activity, true /* shouldWaitForResume */); 512 PollingCheck.waitFor(WAIT_FOR_LIFECYCLE_TIMEOUT_MS, () -> 513 getActivityBounds(activity).equals(taskBounds)); 514 assertEquals(taskBounds, getActivityBounds(activity)); 515 } 516 517 /** 518 * Verifies whether the value reported from {@code activityStackCallback} is expected. 519 * 520 * @param activity the {@link Activity} that must contain in the {@link ActivityStack} 521 */ verifyStandaloneActivityStackIfNeeded( @ullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback, @NonNull Activity activity)522 public static void verifyStandaloneActivityStackIfNeeded( 523 @Nullable TestValueCountConsumer<List<ActivityStack>> activityStackCallback, 524 @NonNull Activity activity) { 525 final List<ActivityStack> activityStacks = getLastActivityStacks(activityStackCallback); 526 if (activityStacks != null) { 527 assertActivityStackContainsActivity(activityStacks, activity); 528 } 529 } 530 531 @NonNull waitAndGetTaskBounds(@onNull Activity activity, boolean shouldWaitForResume)532 public static Rect waitAndGetTaskBounds(@NonNull Activity activity, 533 boolean shouldWaitForResume) { 534 final WindowManagerStateHelper wmState = new WindowManagerStateHelper(); 535 final ComponentName activityName = activity.getComponentName(); 536 wmState.waitForValidState(activityName); 537 if (shouldWaitForResume) { 538 wmState.waitAndAssertActivityState(activityName, STATE_RESUMED); 539 } 540 return wmState.getTaskByActivity(activityName).getBounds(); 541 } 542 543 /** Waits until the bounds of the activity matches the given bounds. */ waitForActivityBoundsEquals(@onNull Activity activity, @NonNull Rect bounds)544 public static void waitForActivityBoundsEquals(@NonNull Activity activity, 545 @NonNull Rect bounds) { 546 PollingCheck.waitFor(WAIT_FOR_LIFECYCLE_TIMEOUT_MS, 547 () -> getActivityBounds(activity).equals(bounds), 548 "Expected bounds: " + bounds + ", actual bounds:" + getActivityBounds(activity)); 549 } 550 waitForResumed( @onNull List<Activity> activityList)551 private static boolean waitForResumed( 552 @NonNull List<Activity> activityList) { 553 final long startTime = System.currentTimeMillis(); 554 while (System.currentTimeMillis() - startTime < WAIT_FOR_LIFECYCLE_TIMEOUT_MS) { 555 boolean allActivitiesResumed = true; 556 for (Activity activity : activityList) { 557 allActivitiesResumed &= WindowManagerJetpackTestBase.isActivityResumed(activity); 558 if (!allActivitiesResumed) { 559 break; 560 } 561 } 562 if (allActivitiesResumed) { 563 return true; 564 } 565 waitAndLog("resumed:" + activityList); 566 } 567 return false; 568 } 569 waitForResumed(@onNull String activityId)570 private static boolean waitForResumed(@NonNull String activityId) { 571 return waitForResumed(activityId, WAIT_FOR_LIFECYCLE_TIMEOUT_MS); 572 } 573 waitForResumed(@onNull String activityId, long timeout)574 private static boolean waitForResumed(@NonNull String activityId, long timeout) { 575 final long startTime = System.currentTimeMillis(); 576 while (System.currentTimeMillis() - startTime < timeout) { 577 if (getResumedActivityById(activityId) != null) { 578 return true; 579 } 580 waitAndLog("resumed:" + activityId); 581 } 582 return false; 583 } 584 waitForResumed(@onNull Activity activity)585 private static boolean waitForResumed(@NonNull Activity activity) { 586 return waitForResumed(Arrays.asList(activity)); 587 } 588 589 /** 590 * Similar to #waitAndAssertResumed, but with a longer timeout, since it may include a cold 591 * launch which involves process startup and application initialization. 592 */ waitAndAssertColdLaunch(@onNull String activityId)593 public static void waitAndAssertColdLaunch(@NonNull String activityId) { 594 assertTrue( 595 "Activity with id=" + activityId + " should be resumed", 596 waitForResumed(activityId, WAIT_FOR_COLD_LAUNCH_TIMEOUT_MS)); 597 } 598 waitAndAssertResumed(@onNull String activityId)599 public static void waitAndAssertResumed(@NonNull String activityId) { 600 assertTrue("Activity with id=" + activityId + " should be resumed", 601 waitForResumed(activityId)); 602 } 603 waitAndAssertResumed(@onNull Activity activity)604 public static void waitAndAssertResumed(@NonNull Activity activity) { 605 assertTrue(activity + " should be resumed", waitForResumed(activity)); 606 } 607 waitAndAssertResumed(@onNull List<Activity> activityList)608 public static void waitAndAssertResumed(@NonNull List<Activity> activityList) { 609 assertTrue("All activities in this list should be resumed:" + activityList, 610 waitForResumed(activityList)); 611 } 612 waitAndAssertNotResumed(@onNull String activityId)613 public static void waitAndAssertNotResumed(@NonNull String activityId) { 614 assertFalse("Activity with id=" + activityId + " should not be resumed", 615 waitForResumed(activityId)); 616 } 617 waitForVisible(@onNull Activity activity, boolean visible)618 public static boolean waitForVisible(@NonNull Activity activity, boolean visible) { 619 final long startTime = System.currentTimeMillis(); 620 while (System.currentTimeMillis() - startTime < WAIT_FOR_LIFECYCLE_TIMEOUT_MS) { 621 if (WindowManagerJetpackTestBase.isActivityVisible(activity) == visible) { 622 return true; 623 } 624 waitAndLog("visible:" + visible + " on " + activity); 625 } 626 return false; 627 } 628 waitAndAssertVisible(@onNull Activity activity)629 public static void waitAndAssertVisible(@NonNull Activity activity) { 630 assertTrue(activity + " should be visible", 631 waitForVisible(activity, true /* visible */)); 632 } 633 waitAndAssertNotVisible(@onNull Activity activity)634 public static void waitAndAssertNotVisible(@NonNull Activity activity) { 635 assertTrue(activity + " should not be visible", 636 waitForVisible(activity, false /* visible */)); 637 } 638 waitForFinishing(@onNull Activity activity)639 private static boolean waitForFinishing(@NonNull Activity activity) { 640 final long startTime = System.currentTimeMillis(); 641 while (System.currentTimeMillis() - startTime < WAIT_FOR_LIFECYCLE_TIMEOUT_MS) { 642 if (activity.isFinishing()) { 643 return true; 644 } 645 waitAndLog("finishing:" + activity); 646 } 647 return activity.isFinishing(); 648 } 649 waitAndAssertFinishing(@onNull Activity activity)650 public static void waitAndAssertFinishing(@NonNull Activity activity) { 651 assertTrue(activity + " should be finishing", waitForFinishing(activity)); 652 } 653 waitAndLog(String reason)654 private static void waitAndLog(String reason) { 655 Log.d(TAG, "** Waiting for " + reason); 656 SystemClock.sleep(WAIT_PERIOD); 657 } 658 659 @Nullable getPrimaryStackTopActivity(SplitInfo splitInfo)660 public static Activity getPrimaryStackTopActivity(SplitInfo splitInfo) { 661 List<Activity> primaryActivityStack = splitInfo.getPrimaryActivityStack().getActivities(); 662 if (primaryActivityStack.isEmpty()) { 663 return null; 664 } 665 return primaryActivityStack.get(primaryActivityStack.size() - 1); 666 } 667 668 @Nullable getSecondaryStackTopActivity(SplitInfo splitInfo)669 public static Activity getSecondaryStackTopActivity(SplitInfo splitInfo) { 670 List<Activity> secondaryActivityStack = splitInfo.getSecondaryActivityStack() 671 .getActivities(); 672 if (secondaryActivityStack.isEmpty()) { 673 return null; 674 } 675 return secondaryActivityStack.get(secondaryActivityStack.size() - 1); 676 } 677 678 /** Returns the expected bounds of the primary and secondary containers */ 679 @NonNull getExpectedBoundsPair(@onNull Activity activity, @NonNull SplitAttributes splitAttributes)680 private static Pair<Rect, Rect> getExpectedBoundsPair(@NonNull Activity activity, 681 @NonNull SplitAttributes splitAttributes) { 682 SplitType splitType = splitAttributes.getSplitType(); 683 684 final Rect parentTaskBounds = waitAndGetTaskBounds(activity, 685 false /* shouldWaitForResume */); 686 if (splitType instanceof SplitType.ExpandContainersSplitType) { 687 return new Pair<>(new Rect(parentTaskBounds), new Rect(parentTaskBounds)); 688 } 689 690 int layoutDir = (splitAttributes.getLayoutDirection() == LayoutDirection.LOCALE) 691 ? activity.getResources().getConfiguration().getLayoutDirection() 692 : splitAttributes.getLayoutDirection(); 693 final boolean isPrimaryRightOrBottomContainer = isPrimaryRightOrBottomContainer(layoutDir); 694 695 FoldingFeature foldingFeature; 696 try { 697 foldingFeature = getFoldingFeature(getExtensionWindowLayoutInfo(activity)); 698 } catch (InterruptedException e) { 699 foldingFeature = null; 700 } 701 if (splitType instanceof SplitAttributes.SplitType.HingeSplitType) { 702 if (shouldSplitByHinge(foldingFeature, splitAttributes)) { 703 // The split pair should be split by hinge if there's exactly one hinge 704 // at the current device state. 705 final Rect hingeArea = foldingFeature.getBounds(); 706 final Rect leftContainer = new Rect(parentTaskBounds.left, parentTaskBounds.top, 707 hingeArea.left, parentTaskBounds.bottom); 708 final Rect topContainer = new Rect(parentTaskBounds.left, parentTaskBounds.top, 709 parentTaskBounds.right, hingeArea.top); 710 final Rect rightContainer = new Rect(hingeArea.right, parentTaskBounds.top, 711 parentTaskBounds.right, parentTaskBounds.bottom); 712 final Rect bottomContainer = new Rect(parentTaskBounds.left, hingeArea.bottom, 713 parentTaskBounds.right, parentTaskBounds.bottom); 714 switch (layoutDir) { 715 case LayoutDirection.LEFT_TO_RIGHT: { 716 return new Pair<>(leftContainer, rightContainer); 717 } 718 case LayoutDirection.RIGHT_TO_LEFT: { 719 return new Pair<>(rightContainer, leftContainer); 720 } 721 case LayoutDirection.TOP_TO_BOTTOM: { 722 return new Pair<>(topContainer, bottomContainer); 723 } 724 case LayoutDirection.BOTTOM_TO_TOP: { 725 return new Pair<>(bottomContainer, topContainer); 726 } 727 default: 728 throw new UnsupportedOperationException("Unsupported layout direction: " 729 + layoutDir); 730 } 731 } else { 732 splitType = ((SplitType.HingeSplitType) splitType).getFallbackSplitType(); 733 } 734 } 735 736 assertTrue("The SplitType must be RatioSplitType", 737 splitType instanceof SplitType.RatioSplitType); 738 739 float splitRatio = ((SplitType.RatioSplitType) splitType).getRatio(); 740 // Normalize the split ratio so that parent start + (parent dimension * split ratio) is 741 // always the position of the split divider in the parent. 742 if (isPrimaryRightOrBottomContainer) { 743 splitRatio = 1 - splitRatio; 744 } 745 746 // Calculate the container bounds 747 final boolean isHorizontal = isHorizontal(layoutDir); 748 final int dividerOffsetLeftOrTop = getBoundsOffsetForDivider( 749 activity, splitAttributes, true /* isLeftOrTop */); 750 final int dividerOffsetRightOrBottom = getBoundsOffsetForDivider( 751 activity, splitAttributes, false /* isLeftOrTop */); 752 final Rect leftOrTopContainerBounds = isHorizontal 753 ? new Rect( 754 parentTaskBounds.left, 755 parentTaskBounds.top, 756 parentTaskBounds.right, 757 (int) (parentTaskBounds.top + parentTaskBounds.height() * splitRatio) 758 + dividerOffsetLeftOrTop 759 ) : new Rect( 760 parentTaskBounds.left, 761 parentTaskBounds.top, 762 (int) (parentTaskBounds.left + parentTaskBounds.width() * splitRatio) 763 + dividerOffsetLeftOrTop, 764 parentTaskBounds.bottom); 765 766 final Rect rightOrBottomContainerBounds = isHorizontal 767 ? new Rect( 768 parentTaskBounds.left, 769 (int) (parentTaskBounds.top + parentTaskBounds.height() * splitRatio) 770 + dividerOffsetRightOrBottom, 771 parentTaskBounds.right, 772 parentTaskBounds.bottom 773 ) : new Rect( 774 (int) (parentTaskBounds.left + parentTaskBounds.width() * splitRatio) 775 + dividerOffsetRightOrBottom, 776 parentTaskBounds.top, 777 parentTaskBounds.right, 778 parentTaskBounds.bottom); 779 780 // Assign the primary and secondary bounds depending on layout direction 781 if (isPrimaryRightOrBottomContainer) { 782 return new Pair<>(rightOrBottomContainerBounds, leftOrTopContainerBounds); 783 } else { 784 return new Pair<>(leftOrTopContainerBounds, rightOrBottomContainerBounds); 785 } 786 } 787 getBoundsOffsetForDivider( @onNull Activity activity, @NonNull SplitAttributes splitAttributes, boolean isLeftOrTop)788 private static int getBoundsOffsetForDivider( 789 @NonNull Activity activity, 790 @NonNull SplitAttributes splitAttributes, 791 boolean isLeftOrTop) { 792 final DividerAttributes dividerAttributes = splitAttributes.getDividerAttributes(); 793 if (dividerAttributes == null) { 794 return 0; 795 } 796 final int dividerWidthPx = (int) TypedValue.applyDimension( 797 COMPLEX_UNIT_DIP, dividerAttributes.getWidthDp(), 798 activity.getResources().getDisplayMetrics()); 799 final SplitType splitType = splitAttributes.getSplitType(); 800 801 if (splitType instanceof SplitType.ExpandContainersSplitType) { 802 // No divider offset is needed for the ExpandContainersSplitType. 803 return 0; 804 } 805 int primaryOffset; 806 if (splitType instanceof final SplitType.RatioSplitType splitRatio) { 807 primaryOffset = (int) (dividerWidthPx * splitRatio.getRatio()); 808 } else { 809 primaryOffset = dividerWidthPx / 2; 810 } 811 final int secondaryOffset = dividerWidthPx - primaryOffset; 812 return isLeftOrTop ? -primaryOffset : secondaryOffset; 813 } 814 isHorizontal(int layoutDirection)815 private static boolean isHorizontal(int layoutDirection) { 816 switch (layoutDirection) { 817 case LayoutDirection.TOP_TO_BOTTOM: 818 case LayoutDirection.BOTTOM_TO_TOP: 819 return true; 820 default : 821 return false; 822 } 823 } 824 825 /** Indicates that whether the primary container is at right or bottom or not. */ isPrimaryRightOrBottomContainer(int layoutDirection)826 private static boolean isPrimaryRightOrBottomContainer(int layoutDirection) { 827 switch (layoutDirection) { 828 case LayoutDirection.RIGHT_TO_LEFT: 829 case LayoutDirection.BOTTOM_TO_TOP: 830 return true; 831 default: 832 return false; 833 } 834 } 835 836 /** 837 * Returns the folding feature if there is exact one in {@link WindowLayoutInfo}. Returns 838 * {@code null}, otherwise. 839 */ 840 @Nullable getFoldingFeature(@ullable WindowLayoutInfo windowLayoutInfo)841 private static FoldingFeature getFoldingFeature(@Nullable WindowLayoutInfo windowLayoutInfo) { 842 if (windowLayoutInfo == null) { 843 return null; 844 } 845 846 List<FoldingFeature> foldingFeatures = windowLayoutInfo.getDisplayFeatures() 847 .stream().filter(feature -> feature instanceof FoldingFeature) 848 .map(feature -> (FoldingFeature) feature) 849 .toList(); 850 851 // Cannot be followed by hinge if there's no or more than one hinges. 852 if (foldingFeatures.size() != 1) { 853 return null; 854 } 855 return foldingFeatures.get(0); 856 } 857 shouldSplitByHinge( @ullable FoldingFeature foldingFeature, @NonNull SplitAttributes splitAttributes)858 private static boolean shouldSplitByHinge( 859 @Nullable FoldingFeature foldingFeature, @NonNull SplitAttributes splitAttributes) { 860 // Don't need to check if SplitType is not HingeSplitType 861 if (!(splitAttributes.getSplitType() instanceof SplitAttributes.SplitType.HingeSplitType)) { 862 return false; 863 } 864 865 // Can't split by hinge because there's zero or multiple hinges. 866 if (foldingFeature == null) { 867 return false; 868 } 869 870 final Rect hingeArea = foldingFeature.getBounds(); 871 872 // Hinge orientation should match SplitAttributes layoutDirection. 873 return (hingeArea.width() > hingeArea.height()) 874 == ActivityEmbeddingUtil.isHorizontal(splitAttributes.getLayoutDirection()); 875 } 876 877 /** Assumes that WM Extensions - Activity Embedding feature is enabled on the device. */ assumeActivityEmbeddingSupportedDevice()878 public static void assumeActivityEmbeddingSupportedDevice() { 879 assumeExtensionSupportedDevice(); 880 // Devices are required to enable Activity Embedding with WM Extensions, unless the 881 // app's targetSDK is smaller than Android 15. 882 assertNotNull( 883 "Device with WM Extensions must support ActivityEmbedding", 884 getWindowExtensions().getActivityEmbeddingComponent()); 885 } 886 assertSplitInfoTopSplitIsCorrect( @onNull List<SplitInfo> splitInfoList, @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitAttributes splitAttributes)887 private static void assertSplitInfoTopSplitIsCorrect( 888 @NonNull List<SplitInfo> splitInfoList, 889 @NonNull Activity primaryActivity, 890 @NonNull Activity secondaryActivity, 891 @NonNull SplitAttributes splitAttributes) { 892 assertFalse("Split info callback should not be empty", splitInfoList.isEmpty()); 893 final SplitInfo topSplit = splitInfoList.get(splitInfoList.size() - 1); 894 assertEquals( 895 "Expect primary activity to match the top of the primary stack", 896 primaryActivity, 897 getPrimaryStackTopActivity(topSplit)); 898 assertEquals( 899 "Expect secondary activity to match the top of the secondary stack", 900 secondaryActivity, 901 getSecondaryStackTopActivity(topSplit)); 902 assertEquals(splitAttributes, topSplit.getSplitAttributes()); 903 } 904 assertActivityStacksIsCorrect( @onNull List<ActivityStack> activityStacks, @NonNull Activity primaryActivity, @Nullable Activity secondaryActivity)905 private static void assertActivityStacksIsCorrect( 906 @NonNull List<ActivityStack> activityStacks, 907 @NonNull Activity primaryActivity, 908 @Nullable Activity secondaryActivity) { 909 assertActivityStackContainsActivity(activityStacks, primaryActivity); 910 911 if (secondaryActivity != null) { 912 final ActivityStack secondaryActivityStack = activityStacks.getLast(); 913 assertTrue( 914 "Secondary ActivityStack should contain " 915 + secondaryActivity 916 + ", but was" 917 + activityStacks, 918 secondaryActivityStack.getActivities().contains(secondaryActivity)); 919 } 920 } 921 assertActivityStackContainsActivity( @onNull List<ActivityStack> activityStacks, @NonNull Activity activity)922 private static void assertActivityStackContainsActivity( 923 @NonNull List<ActivityStack> activityStacks, @NonNull Activity activity) { 924 final List<ActivityStack> filteredActivityStacks = 925 activityStacks.stream() 926 .filter(activityStack -> activityStack.getActivities().contains(activity)) 927 .toList(); 928 assertEquals( 929 "There must exactly one ActivityStack containing Activity:" 930 + activity 931 + ", but was " 932 + filteredActivityStacks, 933 1, 934 filteredActivityStacks.size()); 935 } 936 } 937