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