• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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