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