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