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 29 import static org.junit.Assert.assertEquals; 30 import static org.junit.Assert.assertFalse; 31 import static org.junit.Assert.assertNotNull; 32 import static org.junit.Assert.assertNull; 33 import static org.junit.Assert.assertTrue; 34 import static org.junit.Assume.assumeNotNull; 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.view.WindowMetrics; 49 50 import androidx.annotation.NonNull; 51 import androidx.annotation.Nullable; 52 import androidx.window.extensions.core.util.function.Predicate; 53 import androidx.window.extensions.embedding.ActivityEmbeddingComponent; 54 import androidx.window.extensions.embedding.SplitAttributes; 55 import androidx.window.extensions.embedding.SplitAttributes.LayoutDirection; 56 import androidx.window.extensions.embedding.SplitAttributes.SplitType; 57 import androidx.window.extensions.embedding.SplitInfo; 58 import androidx.window.extensions.embedding.SplitPairRule; 59 import androidx.window.extensions.embedding.SplitRule; 60 import androidx.window.extensions.layout.FoldingFeature; 61 import androidx.window.extensions.layout.WindowLayoutInfo; 62 63 import com.android.compatibility.common.util.PollingCheck; 64 import com.android.window.flags.Flags; 65 66 import java.util.ArrayList; 67 import java.util.Arrays; 68 import java.util.List; 69 70 /** 71 * Utility class for activity embedding tests. 72 */ 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 SplitAttributes DEFAULT_SPLIT_ATTRS = new SplitAttributes.Builder().build(); 78 79 public static final SplitAttributes EXPAND_SPLIT_ATTRS = new SplitAttributes.Builder() 80 .setSplitType(new SplitType.ExpandContainersSplitType()).build(); 81 82 public static final SplitAttributes HINGE_SPLIT_ATTRS = new SplitAttributes.Builder() 83 .setSplitType(new SplitType.HingeSplitType(SplitType.RatioSplitType.splitEqually())) 84 .build(); 85 86 public static final String EMBEDDED_ACTIVITY_ID = "embedded_activity_id"; 87 88 private static final long WAIT_PERIOD = 500; 89 90 @NonNull createWildcardSplitPairRule(boolean shouldClearTop)91 public static SplitPairRule createWildcardSplitPairRule(boolean shouldClearTop) { 92 // Build the split pair rule 93 return createSplitPairRuleBuilder( 94 // Any activity be split with any activity 95 activityActivityPair -> true, 96 // Any activity can launch any split intent 97 activityIntentPair -> true, 98 // Allow any parent bounds to show the split containers side by side 99 windowMetrics -> true) 100 .setDefaultSplitAttributes(DEFAULT_SPLIT_ATTRS) 101 .setShouldClearTop(shouldClearTop) 102 .build(); 103 } 104 105 @NonNull createWildcardSplitPairRuleWithPrimaryActivityClass( Class<? extends Activity> activityClass, boolean shouldClearTop)106 public static SplitPairRule createWildcardSplitPairRuleWithPrimaryActivityClass( 107 Class<? extends Activity> activityClass, boolean shouldClearTop) { 108 return createWildcardSplitPairRuleBuilderWithPrimaryActivityClass(activityClass, 109 shouldClearTop).build(); 110 } 111 112 @NonNull createWildcardSplitPairRuleBuilderWithPrimaryActivityClass( Class<? extends Activity> activityClass, boolean shouldClearTop)113 public static SplitPairRule.Builder createWildcardSplitPairRuleBuilderWithPrimaryActivityClass( 114 Class<? extends Activity> activityClass, boolean shouldClearTop) { 115 // Build the split pair rule 116 return createSplitPairRuleBuilder( 117 // The specified activity be split any activity 118 activityActivityPair -> activityActivityPair.first.getClass().equals(activityClass), 119 // The specified activity can launch any split intent 120 activityIntentPair -> activityIntentPair.first.getClass().equals(activityClass), 121 // Allow any parent bounds to show the split containers side by side 122 windowMetrics -> true) 123 .setDefaultSplitAttributes(DEFAULT_SPLIT_ATTRS) 124 .setShouldClearTop(shouldClearTop); 125 } 126 127 @NonNull createWildcardSplitPairRule()128 public static SplitPairRule createWildcardSplitPairRule() { 129 return createWildcardSplitPairRule(false /* shouldClearTop */); 130 } 131 132 /** 133 * A wrapper to create {@link SplitPairRule} builder with extensions core functional interface 134 * to prevent ambiguous issue when using lambda expressions. 135 */ 136 @NonNull createSplitPairRuleBuilder( @onNull Predicate<Pair<Activity, Activity>> activitiesPairPredicate, @NonNull Predicate<Pair<Activity, Intent>> activityIntentPairPredicate, @NonNull Predicate<WindowMetrics> windowMetricsPredicate)137 public static SplitPairRule.Builder createSplitPairRuleBuilder( 138 @NonNull Predicate<Pair<Activity, Activity>> activitiesPairPredicate, 139 @NonNull Predicate<Pair<Activity, Intent>> activityIntentPairPredicate, 140 @NonNull Predicate<WindowMetrics> windowMetricsPredicate) { 141 return new SplitPairRule.Builder(activitiesPairPredicate, activityIntentPairPredicate, 142 windowMetricsPredicate); 143 } 144 startActivityAndVerifyNotSplit( @onNull Activity activityLaunchingFrom)145 public static TestActivity startActivityAndVerifyNotSplit( 146 @NonNull Activity activityLaunchingFrom) { 147 final String secondActivityId = "secondActivityId"; 148 // Launch second activity 149 startActivityFromActivity(activityLaunchingFrom, TestActivityWithId.class, 150 secondActivityId); 151 // Verify both activities are in the correct lifecycle state 152 waitAndAssertResumed(secondActivityId); 153 assertFalse(isActivityResumed(activityLaunchingFrom)); 154 TestActivity secondActivity = getResumedActivityById(secondActivityId); 155 // Verify the second activity is not split with the first 156 waitAndAssertResumedAndFillsTask(secondActivity); 157 return secondActivity; 158 } 159 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)160 public static Activity startActivityAndVerifySplitAttributes( 161 @NonNull Activity activityLaunchingFrom, @NonNull Activity expectedPrimaryActivity, 162 @NonNull Class<? extends Activity> secondActivityClass, 163 @NonNull SplitAttributes splitAttributes, @NonNull String secondaryActivityId, 164 int expectedCallbackCount, 165 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) { 166 // Set the expected callback count 167 splitInfoConsumer.setCount(expectedCallbackCount); 168 169 // Start second activity 170 startActivityFromActivity(activityLaunchingFrom, secondActivityClass, secondaryActivityId); 171 172 // Wait for secondary activity to be resumed and verify that the newly sent split info 173 // contains the secondary activity. 174 waitAndAssertResumed(secondaryActivityId); 175 final Activity secondaryActivity = getResumedActivityById(secondaryActivityId); 176 177 assertSplitPairIsCorrect(expectedPrimaryActivity, secondaryActivity, splitAttributes, 178 splitInfoConsumer); 179 180 // Return second activity for easy access in calling method 181 return secondaryActivity; 182 } 183 assertSplitPairIsCorrect(@onNull Activity expectedPrimaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitAttributes splitAttributes, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer)184 public static void assertSplitPairIsCorrect(@NonNull Activity expectedPrimaryActivity, 185 @NonNull Activity secondaryActivity, @NonNull SplitAttributes splitAttributes, 186 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) { 187 // A split info callback should occur after the new activity is launched because the split 188 // states have changed. 189 List<SplitInfo> activeSplitStates; 190 try { 191 activeSplitStates = splitInfoConsumer.waitAndGet(); 192 } catch (InterruptedException e) { 193 throw new AssertionError("startActivityAndVerifySplitAttributes()", e); 194 } 195 assertNotNull("Active Split States cannot be null.", activeSplitStates); 196 197 assertSplitInfoTopSplitIsCorrect(activeSplitStates, expectedPrimaryActivity, 198 secondaryActivity, splitAttributes); 199 assertValidSplit(expectedPrimaryActivity, secondaryActivity, splitAttributes); 200 } 201 startActivityAndVerifyNoCallback(@onNull Activity activityLaunchingFrom, @NonNull Class secondActivityClass, @NonNull String secondaryActivityId, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer)202 public static void startActivityAndVerifyNoCallback(@NonNull Activity activityLaunchingFrom, 203 @NonNull Class secondActivityClass, @NonNull String secondaryActivityId, 204 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) throws Exception { 205 // We expect the actual count to be 0. Set to 1 to trigger the timeout and verify no calls. 206 splitInfoConsumer.setCount(1); 207 208 // Start second activity 209 startActivityFromActivity(activityLaunchingFrom, secondActivityClass, secondaryActivityId); 210 211 // A split info callback should occur after the new activity is launched because the split 212 // states have changed. 213 List<SplitInfo> activeSplitStates = splitInfoConsumer.waitAndGet(); 214 assertNull("Received SplitInfo value but did not expect none.", activeSplitStates); 215 } 216 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)217 public static Activity startActivityAndVerifySplitAttributes( 218 @NonNull Activity activityLaunchingFrom, @NonNull Activity expectedPrimaryActivity, 219 @NonNull Class<? extends Activity> secondActivityClass, 220 @NonNull SplitRule splitRule, @NonNull String secondaryActivityId, 221 int expectedCallbackCount, 222 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) { 223 return startActivityAndVerifySplitAttributes(activityLaunchingFrom, expectedPrimaryActivity, 224 secondActivityClass, splitRule.getDefaultSplitAttributes(), secondaryActivityId, 225 expectedCallbackCount, splitInfoConsumer); 226 } 227 startActivityAndVerifySplitAttributes(@onNull Activity primaryActivity, @NonNull Class<? extends Activity> secondActivityClass, @NonNull SplitPairRule splitPairRule, @NonNull String secondActivityId, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer)228 public static Activity startActivityAndVerifySplitAttributes(@NonNull Activity primaryActivity, 229 @NonNull Class<? extends Activity> secondActivityClass, 230 @NonNull SplitPairRule splitPairRule, @NonNull String secondActivityId, 231 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) { 232 return startActivityAndVerifySplitAttributes(primaryActivity, primaryActivity, 233 secondActivityClass, splitPairRule, secondActivityId, 1 /* expectedCallbackCount */, 234 splitInfoConsumer); 235 } 236 237 /** 238 * Attempts to start an activity from a different UID into a split, verifies that a new split 239 * is active. 240 */ startActivityCrossUidInSplit(@onNull Activity primaryActivity, @NonNull ComponentName secondActivityComponent, @NonNull SplitPairRule splitPairRule, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, @NonNull String secondActivityId, boolean verifySplitState)241 public static void startActivityCrossUidInSplit(@NonNull Activity primaryActivity, 242 @NonNull ComponentName secondActivityComponent, @NonNull SplitPairRule splitPairRule, 243 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer, 244 @NonNull String secondActivityId, boolean verifySplitState) { 245 startActivityFromActivity(primaryActivity, secondActivityComponent, secondActivityId, 246 Bundle.EMPTY); 247 if (!verifySplitState) { 248 return; 249 } 250 251 // Get updated split info 252 splitInfoConsumer.setCount(1); 253 List<SplitInfo> activeSplitStates = null; 254 try { 255 activeSplitStates = splitInfoConsumer.waitAndGet(); 256 } catch (InterruptedException e) { 257 throw new AssertionError("startActivityCrossUidInSplit()", e); 258 } 259 assertNotNull(activeSplitStates); 260 assertFalse(activeSplitStates.isEmpty()); 261 // Verify that the primary activity is on top of the primary stack 262 SplitInfo topSplit = activeSplitStates.get(activeSplitStates.size() - 1); 263 List<Activity> primaryStackActivities = topSplit.getPrimaryActivityStack() 264 .getActivities(); 265 assertEquals(primaryActivity, 266 primaryStackActivities.get(primaryStackActivities.size() - 1)); 267 // Verify that the secondary stack is reported as empty to developers 268 assertTrue(topSplit.getSecondaryActivityStack().getActivities().isEmpty()); 269 270 assertValidSplit(primaryActivity, null /* secondaryActivity */, 271 splitPairRule); 272 } 273 274 /** 275 * Attempts to start an activity from a different UID into a split, verifies that activity 276 * did not start on splitContainer successfully and no new split is active. 277 */ startActivityCrossUidInSplit_expectFail(@onNull Activity primaryActivity, @NonNull ComponentName secondActivityComponent, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer)278 public static void startActivityCrossUidInSplit_expectFail(@NonNull Activity primaryActivity, 279 @NonNull ComponentName secondActivityComponent, 280 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) { 281 startActivityFromActivity(primaryActivity, secondActivityComponent, "secondActivityId", 282 Bundle.EMPTY); 283 284 // No split should be active, primary activity should be covered by the new one. 285 assertNoSplit(primaryActivity, splitInfoConsumer); 286 } 287 288 /** 289 * Asserts that there is no split with the provided primary activity. 290 */ assertNoSplit(@onNull Activity primaryActivity, @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer)291 public static void assertNoSplit(@NonNull Activity primaryActivity, 292 @NonNull TestValueCountConsumer<List<SplitInfo>> splitInfoConsumer) { 293 waitForVisible(primaryActivity, false /* visible */); 294 List<SplitInfo> activeSplitStates = splitInfoConsumer.getLastReportedValue(); 295 assertTrue(activeSplitStates == null || activeSplitStates.isEmpty()); 296 } 297 298 @Nullable getSecondActivity(@ullable List<SplitInfo> activeSplitStates, @NonNull Activity primaryActivity, @NonNull String secondaryClassId)299 public static Activity getSecondActivity(@Nullable List<SplitInfo> activeSplitStates, 300 @NonNull Activity primaryActivity, @NonNull String secondaryClassId) { 301 if (activeSplitStates == null) { 302 Log.d(TAG, "Null split states"); 303 return null; 304 } 305 Log.d(TAG, "Active split states: " + activeSplitStates); 306 for (SplitInfo splitInfo : activeSplitStates) { 307 // Find the split info whose top activity in the primary container is the primary 308 // activity we are looking for 309 Activity primaryContainerTopActivity = getPrimaryStackTopActivity(splitInfo); 310 if (primaryActivity.equals(primaryContainerTopActivity)) { 311 Activity secondActivity = getSecondaryStackTopActivity(splitInfo); 312 // See if this activity is the secondary activity we expect 313 if (secondActivity != null && secondActivity instanceof TestActivityWithId 314 && secondaryClassId.equals(((TestActivityWithId) secondActivity).getId())) { 315 return secondActivity; 316 } 317 } 318 } 319 Log.d(TAG, "Second activity was not found: " + secondaryClassId); 320 return null; 321 } 322 323 /** 324 * Waits for and verifies a valid split. Can accept a null secondary activity if it belongs to 325 * a different process, in which case it will only verify the primary one. 326 */ assertValidSplit(@onNull Activity primaryActivity, @Nullable Activity secondaryActivity, @NonNull SplitRule splitRule)327 public static void assertValidSplit(@NonNull Activity primaryActivity, 328 @Nullable Activity secondaryActivity, @NonNull SplitRule splitRule) { 329 assertValidSplit(primaryActivity, secondaryActivity, splitRule.getDefaultSplitAttributes()); 330 } 331 332 /** 333 * Similar to {@link #assertValidSplit(Activity, Activity, SplitRule)}, but verifies 334 * {@link SplitAttributes} instead of {@link SplitRule#getDefaultSplitAttributes}. 335 */ assertValidSplit(@onNull Activity primaryActivity, @Nullable Activity secondaryActivity, @NonNull SplitAttributes splitAttributes)336 public static void assertValidSplit(@NonNull Activity primaryActivity, 337 @Nullable Activity secondaryActivity, @NonNull SplitAttributes splitAttributes) { 338 final boolean shouldExpandContainers = splitAttributes.getSplitType() 339 instanceof SplitType.ExpandContainersSplitType; 340 final List<Activity> resumedActivities = new ArrayList<>(2); 341 if (secondaryActivity == null) { 342 resumedActivities.add(primaryActivity); 343 } else if (shouldExpandContainers) { 344 resumedActivities.add(secondaryActivity); 345 } else { 346 resumedActivities.add(primaryActivity); 347 resumedActivities.add(secondaryActivity); 348 } 349 waitAndAssertResumed(resumedActivities); 350 351 final Pair<Rect, Rect> expectedBoundsPair = getExpectedBoundsPair( 352 shouldExpandContainers ? requireNonNull(secondaryActivity) : primaryActivity, 353 splitAttributes); 354 355 final ActivityEmbeddingComponent activityEmbeddingComponent = getWindowExtensions() 356 .getActivityEmbeddingComponent(); 357 358 // Verify that both activities are embedded and that the bounds are correct 359 if (!shouldExpandContainers) { 360 // If the split pair is stacked, ignore to check the bounds because the primary activity 361 // may have been occluded and the latest configuration may not be received. 362 waitForActivityBoundsEquals(primaryActivity, expectedBoundsPair.first); 363 assertTrue(activityEmbeddingComponent.isActivityEmbedded(primaryActivity)); 364 } 365 if (secondaryActivity != null) { 366 waitForActivityBoundsEquals(secondaryActivity, expectedBoundsPair.second); 367 assertEquals(!shouldExpandContainers, 368 activityEmbeddingComponent.isActivityEmbedded(secondaryActivity)); 369 } 370 } 371 372 /** 373 * Waits for the activity specified in {@code activityId} to be in resumed state and verifies 374 * if it fills the task. 375 */ waitAndAssertResumedAndFillsTask(@onNull String activityId)376 public static void waitAndAssertResumedAndFillsTask(@NonNull String activityId) { 377 waitAndAssertResumed(activityId); 378 final Activity activity = getResumedActivityById(activityId); 379 final Rect taskBounds = waitAndGetTaskBounds(activity, false /* shouldWaitForResume */); 380 PollingCheck.waitFor(WAIT_FOR_LIFECYCLE_TIMEOUT_MS, () -> 381 getActivityBounds(activity).equals(taskBounds)); 382 assertEquals(taskBounds, getActivityBounds(activity)); 383 } 384 385 /** Waits for the {@code activity} to be in resumed state and verifies if it fills the task. */ waitAndAssertResumedAndFillsTask(@onNull Activity activity)386 public static void waitAndAssertResumedAndFillsTask(@NonNull Activity activity) { 387 final Rect taskBounds = waitAndGetTaskBounds(activity, true /* shouldWaitForResume */); 388 PollingCheck.waitFor(WAIT_FOR_LIFECYCLE_TIMEOUT_MS, () -> 389 getActivityBounds(activity).equals(taskBounds)); 390 assertEquals(taskBounds, getActivityBounds(activity)); 391 } 392 393 @NonNull waitAndGetTaskBounds(@onNull Activity activity, boolean shouldWaitForResume)394 public static Rect waitAndGetTaskBounds(@NonNull Activity activity, 395 boolean shouldWaitForResume) { 396 final WindowManagerStateHelper wmState = new WindowManagerStateHelper(); 397 final ComponentName activityName = activity.getComponentName(); 398 if (shouldWaitForResume) { 399 wmState.waitAndAssertActivityState(activityName, STATE_RESUMED); 400 } else { 401 wmState.waitForValidState(activityName); 402 } 403 return wmState.getTaskByActivity(activityName).getBounds(); 404 } 405 406 /** Waits until the bounds of the activity matches the given bounds. */ waitForActivityBoundsEquals(@onNull Activity activity, @NonNull Rect bounds)407 public static void waitForActivityBoundsEquals(@NonNull Activity activity, 408 @NonNull Rect bounds) { 409 PollingCheck.waitFor(WAIT_FOR_LIFECYCLE_TIMEOUT_MS, 410 () -> getActivityBounds(activity).equals(bounds), 411 "Expected bounds: " + bounds + ", actual bounds:" + getActivityBounds(activity)); 412 } 413 waitForResumed( @onNull List<Activity> activityList)414 private static boolean waitForResumed( 415 @NonNull List<Activity> activityList) { 416 final long startTime = System.currentTimeMillis(); 417 while (System.currentTimeMillis() - startTime < WAIT_FOR_LIFECYCLE_TIMEOUT_MS) { 418 boolean allActivitiesResumed = true; 419 for (Activity activity : activityList) { 420 allActivitiesResumed &= WindowManagerJetpackTestBase.isActivityResumed(activity); 421 if (!allActivitiesResumed) { 422 break; 423 } 424 } 425 if (allActivitiesResumed) { 426 return true; 427 } 428 waitAndLog("resumed:" + activityList); 429 } 430 return false; 431 } 432 waitForResumed(@onNull String activityId)433 private static boolean waitForResumed(@NonNull String activityId) { 434 final long startTime = System.currentTimeMillis(); 435 while (System.currentTimeMillis() - startTime < WAIT_FOR_LIFECYCLE_TIMEOUT_MS) { 436 if (getResumedActivityById(activityId) != null) { 437 return true; 438 } 439 waitAndLog("resumed:" + activityId); 440 } 441 return false; 442 } 443 waitForResumed(@onNull Activity activity)444 private static boolean waitForResumed(@NonNull Activity activity) { 445 return waitForResumed(Arrays.asList(activity)); 446 } 447 waitAndAssertResumed(@onNull String activityId)448 public static void waitAndAssertResumed(@NonNull String activityId) { 449 assertTrue("Activity with id=" + activityId + " should be resumed", 450 waitForResumed(activityId)); 451 } 452 waitAndAssertResumed(@onNull Activity activity)453 public static void waitAndAssertResumed(@NonNull Activity activity) { 454 assertTrue(activity + " should be resumed", waitForResumed(activity)); 455 } 456 waitAndAssertResumed(@onNull List<Activity> activityList)457 public static void waitAndAssertResumed(@NonNull List<Activity> activityList) { 458 assertTrue("All activities in this list should be resumed:" + activityList, 459 waitForResumed(activityList)); 460 } 461 waitAndAssertNotResumed(@onNull String activityId)462 public static void waitAndAssertNotResumed(@NonNull String activityId) { 463 assertFalse("Activity with id=" + activityId + " should not be resumed", 464 waitForResumed(activityId)); 465 } 466 waitForVisible(@onNull Activity activity, boolean visible)467 public static boolean waitForVisible(@NonNull Activity activity, boolean visible) { 468 final long startTime = System.currentTimeMillis(); 469 while (System.currentTimeMillis() - startTime < WAIT_FOR_LIFECYCLE_TIMEOUT_MS) { 470 if (WindowManagerJetpackTestBase.isActivityVisible(activity) == visible) { 471 return true; 472 } 473 waitAndLog("visible:" + visible + " on " + activity); 474 } 475 return false; 476 } 477 waitAndAssertVisible(@onNull Activity activity)478 public static void waitAndAssertVisible(@NonNull Activity activity) { 479 assertTrue(activity + " should be visible", 480 waitForVisible(activity, true /* visible */)); 481 } 482 waitAndAssertNotVisible(@onNull Activity activity)483 public static void waitAndAssertNotVisible(@NonNull Activity activity) { 484 assertTrue(activity + " should not be visible", 485 waitForVisible(activity, false /* visible */)); 486 } 487 waitForFinishing(@onNull Activity activity)488 private static boolean waitForFinishing(@NonNull Activity activity) { 489 final long startTime = System.currentTimeMillis(); 490 while (System.currentTimeMillis() - startTime < WAIT_FOR_LIFECYCLE_TIMEOUT_MS) { 491 if (activity.isFinishing()) { 492 return true; 493 } 494 waitAndLog("finishing:" + activity); 495 } 496 return activity.isFinishing(); 497 } 498 waitAndAssertFinishing(@onNull Activity activity)499 public static void waitAndAssertFinishing(@NonNull Activity activity) { 500 assertTrue(activity + " should be finishing", waitForFinishing(activity)); 501 } 502 waitAndLog(String reason)503 private static void waitAndLog(String reason) { 504 Log.d(TAG, "** Waiting for " + reason); 505 SystemClock.sleep(WAIT_PERIOD); 506 } 507 508 @Nullable getPrimaryStackTopActivity(SplitInfo splitInfo)509 public static Activity getPrimaryStackTopActivity(SplitInfo splitInfo) { 510 List<Activity> primaryActivityStack = splitInfo.getPrimaryActivityStack().getActivities(); 511 if (primaryActivityStack.isEmpty()) { 512 return null; 513 } 514 return primaryActivityStack.get(primaryActivityStack.size() - 1); 515 } 516 517 @Nullable getSecondaryStackTopActivity(SplitInfo splitInfo)518 public static Activity getSecondaryStackTopActivity(SplitInfo splitInfo) { 519 List<Activity> secondaryActivityStack = splitInfo.getSecondaryActivityStack() 520 .getActivities(); 521 if (secondaryActivityStack.isEmpty()) { 522 return null; 523 } 524 return secondaryActivityStack.get(secondaryActivityStack.size() - 1); 525 } 526 527 /** Returns the expected bounds of the primary and secondary containers */ 528 @NonNull getExpectedBoundsPair(@onNull Activity activity, @NonNull SplitAttributes splitAttributes)529 private static Pair<Rect, Rect> getExpectedBoundsPair(@NonNull Activity activity, 530 @NonNull SplitAttributes splitAttributes) { 531 SplitType splitType = splitAttributes.getSplitType(); 532 533 final Rect parentTaskBounds = waitAndGetTaskBounds(activity, 534 false /* shouldWaitForResume */); 535 if (splitType instanceof SplitType.ExpandContainersSplitType) { 536 return new Pair<>(new Rect(parentTaskBounds), new Rect(parentTaskBounds)); 537 } 538 539 int layoutDir = (splitAttributes.getLayoutDirection() == LayoutDirection.LOCALE) 540 ? activity.getResources().getConfiguration().getLayoutDirection() 541 : splitAttributes.getLayoutDirection(); 542 final boolean isPrimaryRightOrBottomContainer = isPrimaryRightOrBottomContainer(layoutDir); 543 544 FoldingFeature foldingFeature; 545 try { 546 foldingFeature = getFoldingFeature(getExtensionWindowLayoutInfo(activity)); 547 } catch (InterruptedException e) { 548 foldingFeature = null; 549 } 550 if (splitType instanceof SplitAttributes.SplitType.HingeSplitType) { 551 if (shouldSplitByHinge(foldingFeature, splitAttributes)) { 552 // The split pair should be split by hinge if there's exactly one hinge 553 // at the current device state. 554 final Rect hingeArea = foldingFeature.getBounds(); 555 final Rect leftContainer = new Rect(parentTaskBounds.left, parentTaskBounds.top, 556 hingeArea.left, parentTaskBounds.bottom); 557 final Rect topContainer = new Rect(parentTaskBounds.left, parentTaskBounds.top, 558 parentTaskBounds.right, hingeArea.top); 559 final Rect rightContainer = new Rect(hingeArea.right, parentTaskBounds.top, 560 parentTaskBounds.right, parentTaskBounds.bottom); 561 final Rect bottomContainer = new Rect(parentTaskBounds.left, hingeArea.bottom, 562 parentTaskBounds.right, parentTaskBounds.bottom); 563 switch (layoutDir) { 564 case LayoutDirection.LEFT_TO_RIGHT: { 565 return new Pair<>(leftContainer, rightContainer); 566 } 567 case LayoutDirection.RIGHT_TO_LEFT: { 568 return new Pair<>(rightContainer, leftContainer); 569 } 570 case LayoutDirection.TOP_TO_BOTTOM: { 571 return new Pair<>(topContainer, bottomContainer); 572 } 573 case LayoutDirection.BOTTOM_TO_TOP: { 574 return new Pair<>(bottomContainer, topContainer); 575 } 576 default: 577 throw new UnsupportedOperationException("Unsupported layout direction: " 578 + layoutDir); 579 } 580 } else { 581 splitType = ((SplitType.HingeSplitType) splitType).getFallbackSplitType(); 582 } 583 } 584 585 assertTrue("The SplitType must be RatioSplitType", 586 splitType instanceof SplitType.RatioSplitType); 587 588 float splitRatio = ((SplitType.RatioSplitType) splitType).getRatio(); 589 // Normalize the split ratio so that parent start + (parent dimension * split ratio) is 590 // always the position of the split divider in the parent. 591 if (isPrimaryRightOrBottomContainer) { 592 splitRatio = 1 - splitRatio; 593 } 594 595 // Calculate the container bounds 596 final boolean isHorizontal = isHorizontal(layoutDir); 597 final Rect leftOrTopContainerBounds = isHorizontal 598 ? new Rect( 599 parentTaskBounds.left, 600 parentTaskBounds.top, 601 parentTaskBounds.right, 602 (int) (parentTaskBounds.top + parentTaskBounds.height() * splitRatio) 603 ) : new Rect( 604 parentTaskBounds.left, 605 parentTaskBounds.top, 606 (int) (parentTaskBounds.left + parentTaskBounds.width() * splitRatio), 607 parentTaskBounds.bottom); 608 609 final Rect rightOrBottomContainerBounds = isHorizontal 610 ? new Rect( 611 parentTaskBounds.left, 612 (int) (parentTaskBounds.top + parentTaskBounds.height() * splitRatio), 613 parentTaskBounds.right, 614 parentTaskBounds.bottom 615 ) : new Rect( 616 (int) (parentTaskBounds.left + parentTaskBounds.width() * splitRatio), 617 parentTaskBounds.top, 618 parentTaskBounds.right, 619 parentTaskBounds.bottom); 620 621 // Assign the primary and secondary bounds depending on layout direction 622 if (isPrimaryRightOrBottomContainer) { 623 return new Pair<>(rightOrBottomContainerBounds, leftOrTopContainerBounds); 624 } else { 625 return new Pair<>(leftOrTopContainerBounds, rightOrBottomContainerBounds); 626 } 627 } isHorizontal(int layoutDirection)628 private static boolean isHorizontal(int layoutDirection) { 629 switch (layoutDirection) { 630 case LayoutDirection.TOP_TO_BOTTOM: 631 case LayoutDirection.BOTTOM_TO_TOP: 632 return true; 633 default : 634 return false; 635 } 636 } 637 638 /** Indicates that whether the primary container is at right or bottom or not. */ isPrimaryRightOrBottomContainer(int layoutDirection)639 private static boolean isPrimaryRightOrBottomContainer(int layoutDirection) { 640 switch (layoutDirection) { 641 case LayoutDirection.RIGHT_TO_LEFT: 642 case LayoutDirection.BOTTOM_TO_TOP: 643 return true; 644 default: 645 return false; 646 } 647 } 648 649 /** 650 * Returns the folding feature if there is exact one in {@link WindowLayoutInfo}. Returns 651 * {@code null}, otherwise. 652 */ 653 @Nullable getFoldingFeature(@ullable WindowLayoutInfo windowLayoutInfo)654 private static FoldingFeature getFoldingFeature(@Nullable WindowLayoutInfo windowLayoutInfo) { 655 if (windowLayoutInfo == null) { 656 return null; 657 } 658 659 List<FoldingFeature> foldingFeatures = windowLayoutInfo.getDisplayFeatures() 660 .stream().filter(feature -> feature instanceof FoldingFeature) 661 .map(feature -> (FoldingFeature) feature) 662 .toList(); 663 664 // Cannot be followed by hinge if there's no or more than one hinges. 665 if (foldingFeatures.size() != 1) { 666 return null; 667 } 668 return foldingFeatures.get(0); 669 } 670 shouldSplitByHinge(@ullable FoldingFeature foldingFeature, @NonNull SplitAttributes splitAttributes)671 private static boolean shouldSplitByHinge(@Nullable FoldingFeature foldingFeature, 672 @NonNull SplitAttributes splitAttributes) { 673 // Don't need to check if SplitType is not HingeSplitType 674 if (!(splitAttributes.getSplitType() instanceof SplitAttributes.SplitType.HingeSplitType)) { 675 return false; 676 } 677 678 // Can't split by hinge because there's zero or multiple hinges. 679 if (foldingFeature == null) { 680 return false; 681 } 682 683 final Rect hingeArea = foldingFeature.getBounds(); 684 685 // Hinge orientation should match SplitAttributes layoutDirection. 686 return (hingeArea.width() > hingeArea.height()) 687 == ActivityEmbeddingUtil.isHorizontal(splitAttributes.getLayoutDirection()); 688 } 689 690 /** 691 * Assumes that WM Extensions - Activity Embedding feature is enabled on the device. 692 */ assumeActivityEmbeddingSupportedDevice()693 public static void assumeActivityEmbeddingSupportedDevice() { 694 assumeExtensionSupportedDevice(); 695 if (!Flags.enableWmExtensionsForAllFlag()) { 696 assumeNotNull("Device does not support ActivityEmbedding", 697 getWindowExtensions().getActivityEmbeddingComponent()); 698 } else { 699 // Devices are required to enable Activity Embedding with WM Extensions, unless the 700 // app's targetSDK is smaller than Android 15. 701 assertNotNull("Device with WM Extensions must support ActivityEmbedding", 702 getWindowExtensions().getActivityEmbeddingComponent()); 703 } 704 } 705 assertSplitInfoTopSplitIsCorrect(@onNull List<SplitInfo> splitInfoList, @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, @NonNull SplitAttributes splitAttributes)706 private static void assertSplitInfoTopSplitIsCorrect(@NonNull List<SplitInfo> splitInfoList, 707 @NonNull Activity primaryActivity, @NonNull Activity secondaryActivity, 708 @NonNull SplitAttributes splitAttributes) { 709 assertFalse("Split info callback should not be empty", splitInfoList.isEmpty()); 710 final SplitInfo topSplit = splitInfoList.get(splitInfoList.size() - 1); 711 assertEquals("Expect primary activity to match the top of the primary stack", 712 primaryActivity, getPrimaryStackTopActivity(topSplit)); 713 assertEquals("Expect secondary activity to match the top of the secondary stack", 714 secondaryActivity, getSecondaryStackTopActivity(topSplit)); 715 assertEquals(splitAttributes, topSplit.getSplitAttributes()); 716 } 717 } 718