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