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