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