1 /*
2  * Copyright (C) 2023 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.embedding;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.DEFAULT_SPLIT_ATTRS;
21 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.EXPAND_SPLIT_ATTRS;
22 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.HINGE_SPLIT_ATTRS;
23 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.assertValidSplit;
24 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.createSplitPairRuleBuilder;
25 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.startActivityAndVerifySplitAttributes;
26 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndAssertNotVisible;
27 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndAssertResumedAndFillsTask;
28 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndGetTaskBounds;
29 import static android.server.wm.jetpack.utils.TestActivityLauncher.KEY_ACTIVITY_ID;
30 
31 import android.app.Activity;
32 import android.content.Intent;
33 import android.graphics.Rect;
34 import android.platform.test.annotations.Presubmit;
35 import android.server.wm.WindowManagerState.Task;
36 import android.server.wm.jetpack.utils.TestActivity;
37 import android.server.wm.jetpack.utils.TestActivityWithId;
38 import android.server.wm.jetpack.utils.TestConfigChangeHandlingActivity;
39 import android.support.test.uiautomator.UiDevice;
40 import android.util.Pair;
41 import android.util.Size;
42 
43 import androidx.annotation.NonNull;
44 import androidx.test.ext.junit.runners.AndroidJUnit4;
45 import androidx.window.extensions.embedding.SplitAttributes;
46 import androidx.window.extensions.embedding.SplitAttributes.LayoutDirection;
47 import androidx.window.extensions.embedding.SplitAttributes.SplitType;
48 import androidx.window.extensions.embedding.SplitPairRule;
49 
50 import com.android.compatibility.common.util.ApiTest;
51 
52 import org.junit.Test;
53 import org.junit.runner.RunWith;
54 
55 import java.util.Collections;
56 import java.util.Set;
57 
58 /**
59  * Tests for the {@link androidx.window.extensions} implementation provided on the device (and only
60  * if one is available) for the Activity Embedding functionality. Specifically tests activity
61  * split bounds.
62  *
63  * Build/Install/Run:
64  *     atest CtsWindowManagerJetpackTestCases:ActivityEmbeddingBoundsTests
65  */
66 @ApiTest(apis = {"androidx.window.extensions.embedding.SplitPairRule#getDefaultSplitAttributes"})
67 @Presubmit
68 @RunWith(AndroidJUnit4.class)
69 public class ActivityEmbeddingBoundsTests extends ActivityEmbeddingTestBase {
70     public static SplitType UNEVEN_CONTAINERS_DEFAULT_SPLIT_TYPE =
71             new SplitType.RatioSplitType(0.7f);
72 
73     /**
74      * Tests that when two activities are in a split and the parent bounds shrink such that
75      * they can no longer support split activities, then the activities become stacked.
76      */
77     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitRule#checkParentMetrics"})
78     @Test
testParentWindowMetricsPredicate()79     public void testParentWindowMetricsPredicate() {
80         // Launch primary activity
81         final Activity primaryActivity = startFullScreenActivityNewTask(
82                 TestConfigChangeHandlingActivity.class, null /* activityId */,
83                 getLaunchingDisplayId());
84 
85         // Set split pair rule such that if the parent bounds is any smaller than it is now, then
86         // the parent cannot support a split.
87         final Rect taskBounds = waitAndGetTaskBounds(primaryActivity,
88                 true /* shouldWaitForResume */);
89         final int originalTaskWidth = taskBounds.width();
90         final int originalTaskHeight = taskBounds.height();
91         final SplitPairRule splitPairRule = createSplitPairRuleBuilder(
92                 activityActivityPair -> true /* activityPairPredicate */,
93                 activityIntentPair -> true /* activityIntentPredicate */,
94                 parentWindowMetrics -> parentWindowMetrics.getBounds().width() >= originalTaskWidth
95                         && parentWindowMetrics.getBounds().height() >= originalTaskHeight)
96                 .setDefaultSplitAttributes(DEFAULT_SPLIT_ATTRS).build();
97         mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule));
98 
99         // Launch the secondary activity
100         final String secondaryActivityId = "secondaryActivityId";
101         final TestActivity secondaryActivity = (TestActivity) startActivityAndVerifySplitAttributes(
102                 primaryActivity, TestActivityWithId.class, splitPairRule, secondaryActivityId,
103                 mSplitInfoConsumer);
104 
105         // Resize multiple times to verify that the activities are correctly split or
106         // stacked depending on the parent bounds. Resizing multiple times simulates a foldable
107         // display is that folded and unfolded multiple times while running the same app.
108         final int numTimesToResize = 2;
109         final Size origDisplaySize = mReportedDisplayMetrics.getSize();
110         mWmState.computeState(
111                 primaryActivity.getComponentName(), secondaryActivity.getComponentName());
112         // Primary and secondary activities should be in the same task
113         final Task task = mWmState.getTaskByActivity(primaryActivity.getComponentName());
114         final Rect origTaskBounds = task.getBounds();
115         final boolean taskInFreeformMode = task.getWindowingMode() == WINDOWING_MODE_FREEFORM;
116         for (int i = 0; i < numTimesToResize; i++) {
117             // Shrink by 10% to make the activities stacked.
118             // If the activity was launched in freeform windowing mode, resize the task bounds
119             // instead of resizing the display.
120             if (taskInFreeformMode) {
121                 resizeActivityTask(primaryActivity.getComponentName(),
122                         origTaskBounds.left, origTaskBounds.top,
123                         origTaskBounds.left + (int) (origTaskBounds.width() * 0.9),
124                         origTaskBounds.top + (int) (origTaskBounds.height() * 0.9));
125             } else {
126                 mReportedDisplayMetrics.setSize(
127                         new Size((int) (origDisplaySize.getWidth() * 0.9),
128                                  (int) (origDisplaySize.getHeight() * 0.9)));
129             }
130 
131             UiDevice.getInstance(mInstrumentation).waitForIdle();
132             waitAndAssertResumedAndFillsTask(secondaryActivity);
133             waitAndAssertNotVisible(primaryActivity);
134 
135             // Return the task/display to its original size and verify that the activities are split
136             if (taskInFreeformMode) {
137                 resizeActivityTask(primaryActivity.getComponentName(),
138                         origTaskBounds.left, origTaskBounds.top,
139                         origTaskBounds.right,origTaskBounds.bottom);
140             } else {
141                 mReportedDisplayMetrics.setSize(origDisplaySize);
142             }
143 
144             UiDevice.getInstance(mInstrumentation).waitForIdle();
145             assertValidSplit(primaryActivity, secondaryActivity, splitPairRule);
146         }
147     }
148 
149     /**
150      * Tests that the activity bounds for activities in a split match the LTR layout direction
151      * provided in the {@link SplitPairRule}.
152      */
153     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes"
154             + ".LayoutDirection#LEFT_TO_RIGHT"})
155     @Test
testLayoutDirection_LeftToRight()156     public void testLayoutDirection_LeftToRight() {
157         // Create a split pair rule with layout direction LEFT_TO_RIGHT and a split ratio that
158         // results in uneven bounds between the primary and secondary containers.
159         final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule(
160                 LayoutDirection.LEFT_TO_RIGHT);
161         mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule));
162 
163         // Start activities in a split and verify that the layout direction is LEFT_TO_RIGHT,
164         // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}.
165         final Activity primaryActivity = startFullScreenActivityNewTask(
166                 TestConfigChangeHandlingActivity.class, null /* activityId */,
167                 getLaunchingDisplayId());
168         startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class,
169                 splitPairRule, "secondaryActivityId", mSplitInfoConsumer);
170     }
171 
172     /**
173      * Tests that the activity bounds for activities in a split match the RTL layout direction
174      * provided in the {@link SplitPairRule}.
175      */
176     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes"
177             + ".LayoutDirection#RIGHT_TO_LEFT"})
178     @Test
testLayoutDirection_RightToLeft()179     public void testLayoutDirection_RightToLeft() {
180         // Create a split pair rule with layout direction RIGHT_TO_LEFT and a split ratio that
181         // results in uneven bounds between the primary and secondary containers.
182         final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule(
183                 LayoutDirection.RIGHT_TO_LEFT);
184         mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule));
185 
186         // Start activities in a split and verify that the layout direction is RIGHT_TO_LEFT,
187         // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}.
188         final Activity primaryActivity = startFullScreenActivityNewTask(
189                 TestConfigChangeHandlingActivity.class, null /* activityId */,
190                 getLaunchingDisplayId());
191         startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class,
192                 splitPairRule, "secondaryActivityId", mSplitInfoConsumer);
193     }
194 
195     /**
196      * Tests that the activity bounds for activities in a split match the Locale layout direction
197      * provided in the {@link SplitPairRule}.
198      */
199     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes"
200             + ".LayoutDirection#LOCALE"})
201     @Test
testLayoutDirection_Locale()202     public void testLayoutDirection_Locale() {
203         // Create a split pair rule with layout direction LOCALE and a split ratio that results in
204         // uneven bounds between the primary and secondary containers.
205         final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule(LayoutDirection.LOCALE);
206         mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule));
207 
208         // Start activities in a split and verify that the layout direction is the device locale,
209         // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}.
210         final Activity primaryActivity = startFullScreenActivityNewTask(
211                 TestConfigChangeHandlingActivity.class, null /* activityId */,
212                 getLaunchingDisplayId());
213         startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class,
214                 splitPairRule, "secondaryActivityId", mSplitInfoConsumer);
215     }
216 
217     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes"
218             + ".LayoutDirection#TOP_TO_BOTTOM"})
219     @Test
testLayoutDirection_TopToBottom()220     public void testLayoutDirection_TopToBottom() {
221         // Create a split pair rule with layout direction TOP_TO_BOTTOM and a split ratio that
222         // results in uneven bounds between the primary and secondary containers.
223         final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule(
224                 LayoutDirection.TOP_TO_BOTTOM);
225         mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule));
226 
227         // Start activities in a split and verify that the layout direction is TOP_TO_BOTTOM,
228         // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}.
229         final Activity primaryActivity = startFullScreenActivityNewTask(
230                 TestConfigChangeHandlingActivity.class, null /* activityId */,
231                 getLaunchingDisplayId());
232         startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class,
233                 splitPairRule, "secondaryActivityId", mSplitInfoConsumer);
234     }
235 
236     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes"
237             + ".LayoutDirection#BOTTOM_TO_TOP"})
238     @Test
testLayoutDirection_BottomToTop()239     public void testLayoutDirection_BottomToTop() {
240         // Create a split pair rule with layout direction BOTTOM_TO_TOP and a split ratio that
241         // results in uneven bounds between the primary and secondary containers.
242         final SplitPairRule splitPairRule = createUnevenWidthSplitPairRule(
243                 LayoutDirection.TOP_TO_BOTTOM);
244         mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule));
245 
246         // Start activities in a split and verify that the layout direction is BOTTOM_TO_TOP,
247         // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}.
248         final Activity primaryActivity = startFullScreenActivityNewTask(
249                 TestConfigChangeHandlingActivity.class, null /* activityId */,
250                 getLaunchingDisplayId());
251         startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class,
252                 splitPairRule, "secondaryActivityId", mSplitInfoConsumer);
253     }
254 
255     /**
256      * Tests that when two activities enter a split, then their split ratio matches what is in their
257      * {@link SplitPairRule}, and is not assumed to be 0.5 or match the split ratio of the previous
258      * top-most activity split.
259      */
260     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes"
261             + ".SplitType.RatioSplitType#getRatio"})
262     @Test
testSplitRatio()263     public void testSplitRatio() {
264         final String activityAId = "activityA";
265         final String activityBId = "activityB";
266         final String activityCId = "activityC";
267         final SplitType activityABSplitRatio = new SplitType.RatioSplitType(0.37f);
268         final SplitType activityBCSplitRatio = new SplitType.RatioSplitType(0.85f);
269 
270         // Create a split rule for activity A and activity B where the split ratio is 0.37.
271         final SplitPairRule splitPairRuleAB = createSplitPairRuleBuilder(
272                 activityActivityPair -> false /* activityPairPredicate */,
273                 activityIntentPair -> matchesActivityIntentPair(activityIntentPair, activityAId,
274                         activityBId) /* activityIntentPredicate */,
275                 parentWindowMetrics -> true /* parentWindowMetricsPredicate */)
276                 .setDefaultSplitAttributes(new SplitAttributes.Builder().setSplitType(
277                         activityABSplitRatio).build())
278                 .build();
279 
280         // Create a split rule for activity B and activity C where the split ratio is 0.65.
281         final SplitPairRule splitPairRuleBC = createSplitPairRuleBuilder(
282                 activityActivityPair -> false /* activityPairPredicate */,
283                 activityIntentPair -> matchesActivityIntentPair(activityIntentPair, activityBId,
284                         activityCId) /* activityIntentPredicate */,
285                 parentWindowMetrics -> true /* parentWindowMetricsPredicate */)
286                 .setDefaultSplitAttributes(new SplitAttributes.Builder().setSplitType(
287                         activityBCSplitRatio).build())
288                 .build();
289 
290         // Register the two split pair rules
291         mActivityEmbeddingComponent.setEmbeddingRules(Set.of(splitPairRuleAB, splitPairRuleBC));
292 
293         // Launch the activity A and B split and verify that the split ratio is 0.37 in
294         // {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}.
295         final Activity activityA = startFullScreenActivityNewTask(
296                 TestActivityWithId.class, activityAId, getLaunchingDisplayId());
297         Activity activityB = startActivityAndVerifySplitAttributes(activityA,
298                 TestActivityWithId.class, splitPairRuleAB, activityBId, mSplitInfoConsumer);
299 
300         // Launch the activity B and C split and verify that the split ratio is 0.65 in
301         // {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}.
302         Activity activityC = startActivityAndVerifySplitAttributes(activityB,
303                 TestActivityWithId.class, splitPairRuleBC, activityCId, mSplitInfoConsumer);
304 
305         // Finish activity C so that activity A and B are in a split again. Verify that the split
306         // ratio returns to 0.37 in {@link ActivityEmbeddingUtil#assertValidSplit}.
307         activityC.finish();
308         assertValidSplit(activityA, activityB, splitPairRuleAB);
309     }
310 
311     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes.HingeSplitType"
312             + "#HingeSplitType"})
313     @Test
testHingeSplitType()314     public void testHingeSplitType() {
315         SplitPairRule splitPairRule = createSplitPairRuleBuilder(
316                 activityActivityPair -> true,
317                 activityIntentPair -> true,
318                 windowMetrics -> true)
319                 .setDefaultSplitAttributes(HINGE_SPLIT_ATTRS)
320                 .build();
321         mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule));
322 
323         // Start another activity to split with the primary activity and verify that the split type
324         // is hinge.
325         final Activity primaryActivity = startFullScreenActivityNewTask(
326                 TestConfigChangeHandlingActivity.class, null /* activityId */,
327                 getLaunchingDisplayId());
328         startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class,
329                 splitPairRule, "secondaryActivityId", mSplitInfoConsumer);
330     }
331 
332     /** Verifies {@link SplitAttributes.SplitType.ExpandContainersSplitType} behavior. */
333     @ApiTest(apis = {"androidx.window.extensions.embedding.SplitAttributes"
334             + ".ExpandContainersSplitType#ExpandContainersSplitType"})
335     @Test
testExpandSplitType()336     public void testExpandSplitType() {
337         SplitPairRule splitPairRule = createSplitPairRuleBuilder(
338                 activityActivityPair -> true,
339                 activityIntentPair -> true,
340                 windowMetrics -> true
341         )
342                 .setDefaultSplitAttributes(EXPAND_SPLIT_ATTRS)
343                 .build();
344         mActivityEmbeddingComponent.setEmbeddingRules(Collections.singleton(splitPairRule));
345 
346         // Start activities in a split and verify that the split type is expand,
347         // which is checked in {@link ActivityEmbeddingUtil#startActivityAndVerifySplit}.
348         final Activity primaryActivity = startFullScreenActivityNewTask(
349                 TestConfigChangeHandlingActivity.class, null /* activityId */,
350                 getLaunchingDisplayId());
351         startActivityAndVerifySplitAttributes(primaryActivity, TestActivityWithId.class,
352                 splitPairRule, "secondaryActivityId", mSplitInfoConsumer);
353     }
354 
createUnevenWidthSplitPairRule(int layoutDir)355     private SplitPairRule createUnevenWidthSplitPairRule(int layoutDir) {
356         return createSplitPairRuleBuilder(
357                 activityActivityPair -> true /* activityPairPredicate */,
358                 activityIntentPair -> true /* activityIntentPredicate */,
359                 parentWindowMetrics -> true /* parentWindowMetricsPredicate */)
360                 .setDefaultSplitAttributes(new SplitAttributes.Builder()
361                         .setSplitType(UNEVEN_CONTAINERS_DEFAULT_SPLIT_TYPE)
362                         .setLayoutDirection(layoutDir)
363                         .build())
364                 .build();
365     }
366 
matchesActivityIntentPair(@onNull Pair<Activity, Intent> activityIntentPair, @NonNull String primaryActivityId, @NonNull String secondaryActivityId)367     static boolean matchesActivityIntentPair(@NonNull Pair<Activity, Intent> activityIntentPair,
368             @NonNull String primaryActivityId, @NonNull String secondaryActivityId) {
369         if (!(activityIntentPair.first instanceof TestActivityWithId)) {
370             return false;
371         }
372         return primaryActivityId.equals(((TestActivityWithId) activityIntentPair.first).getId())
373                 && secondaryActivityId.equals(activityIntentPair.second.getStringExtra(
374                 KEY_ACTIVITY_ID));
375     }
376 }
377