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.activity.lifecycle.LifecycleConstants.ON_CREATE;
20 import static android.server.wm.activity.lifecycle.LifecycleConstants.ON_PAUSE;
21 import static android.server.wm.activity.lifecycle.LifecycleConstants.ON_RESUME;
22 import static android.server.wm.activity.lifecycle.LifecycleConstants.ON_START;
23 import static android.server.wm.activity.lifecycle.TransitionVerifier.checkOrder;
24 import static android.server.wm.activity.lifecycle.TransitionVerifier.transition;
25 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.EXPAND_SPLIT_ATTRS;
26 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.createSplitPairRuleBuilder;
27 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.createWildcardSplitPairRuleBuilderWithPrimaryActivityClass;
28 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.startActivityAndVerifySplitAttributes;
29 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndAssertNotVisible;
30 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndAssertResumed;
31 import static android.server.wm.jetpack.utils.ActivityEmbeddingUtil.waitAndGetTaskBounds;
32 
33 import static org.junit.Assert.assertFalse;
34 import static org.junit.Assert.assertTrue;
35 
36 import android.app.Activity;
37 import android.graphics.Rect;
38 import android.platform.test.annotations.Presubmit;
39 import android.server.wm.jetpack.utils.TestActivityWithId;
40 import android.server.wm.jetpack.utils.TestConfigChangeHandlingActivity;
41 import android.util.Pair;
42 import android.util.Size;
43 
44 import androidx.annotation.NonNull;
45 import androidx.test.ext.junit.runners.AndroidJUnit4;
46 import androidx.window.extensions.embedding.SplitAttributes;
47 import androidx.window.extensions.embedding.SplitPairRule;
48 import androidx.window.extensions.embedding.SplitPinRule;
49 
50 import com.android.compatibility.common.util.ApiTest;
51 
52 import org.junit.Before;
53 import org.junit.Test;
54 import org.junit.runner.RunWith;
55 
56 import java.util.Collections;
57 import java.util.List;
58 
59 /**
60  * Tests for the {@link androidx.window.extensions} implementation provided on the device (and only
61  * if one is available) for the Activity Embedding functionality. Specifically tests pinning the
62  * top ActivityStack on a Task.
63  * <p>
64  * Build/Install/Run:
65  *     atest CtsWindowManagerJetpackTestCases:PinActivityStackTests
66  */
67 @Presubmit
68 @RunWith(AndroidJUnit4.class)
69 @ApiTest(apis = {
70         "androidx.window.extensions.embedding.ActivityEmbeddingComponent#pinTopActivityStack",
71         "androidx.window.extensions.embedding.ActivityEmbeddingComponent#unpinTopActivityStack"
72 })
73 public class PinActivityStackTests extends ActivityEmbeddingLifecycleTestBase {
74     private Activity mPrimaryActivity;
75     private Activity mPinnedActivity;
76     private String mPinnedActivityId = "pinActivity";
77     private SplitPairRule mWildcardSplitPairRule;
78     private int mTaskId;
79 
80     @Override
81     @Before
setUp()82     public void setUp() throws Exception {
83         super.setUp();
84         mPrimaryActivity = startFullScreenActivityNewTask(TestConfigChangeHandlingActivity.class);
85         mTaskId = mPrimaryActivity.getTaskId();
86         mWildcardSplitPairRule = createWildcardSplitPairRuleBuilderWithPrimaryActivityClass(
87                 TestConfigChangeHandlingActivity.class, false /* shouldClearTop */)
88                 .build();
89         mActivityEmbeddingComponent.setEmbeddingRules(
90                 Collections.singleton(mWildcardSplitPairRule));
91     }
92 
93     /**
94      * Verifies that the activity starts in a new container and the navigation are isolated after
95      * the top ActivityStack is pinned.
96      */
97     @Test
testPinTopActivityStack_createContainerForNewActivity()98     public void testPinTopActivityStack_createContainerForNewActivity() {
99         pinTopActivityStackAndVerifyLifecycle(true /* newPrimaryContainer */);
100     }
101 
102     /**
103      * Verifies that the activity starts in the same container and the navigation are isolated
104      * after the top ActivityStack is pinned.
105      */
106     @Test
testPinTopActivityStack_reuseContainerForNewActivity()107     public void testPinTopActivityStack_reuseContainerForNewActivity() {
108         pinTopActivityStackAndVerifyLifecycle(false /* newPrimaryContainer */);
109     }
110 
pinTopActivityStackAndVerifyLifecycle(boolean newPrimaryContainer)111     private void pinTopActivityStackAndVerifyLifecycle(boolean newPrimaryContainer) {
112         // Launch a secondary activity to side
113         mPinnedActivity = startActivityAndVerifySplitAttributes(mPrimaryActivity,
114                 TestActivityWithId.class, mWildcardSplitPairRule,
115                 mPinnedActivityId, mSplitInfoConsumer);
116 
117         // Pin the top ActivityStack
118         assertTrue(pinTopActivityStack());
119         mEventLog.clear();
120 
121         if (!newPrimaryContainer) {
122             mActivityEmbeddingComponent.setEmbeddingRules(Collections.emptySet());
123         }
124 
125         // Start an Activity from the primary ActivityStack
126         final String activityId1 = "Activity1";
127         startActivityFromActivity(mPrimaryActivity, TestActivityWithId.class, activityId1);
128 
129         // Verifies the activity in the primary ActivityStack is occluded by the new Activity.
130         waitAndAssertResumed(activityId1);
131         waitAndAssertResumed(mPinnedActivityId);
132         waitAndAssertNotVisible(mPrimaryActivity);
133         final List<Pair<String, String>> expectedLifecycle = List.of(
134                 transition(TestConfigChangeHandlingActivity.class, ON_PAUSE),
135                 transition(TestActivityWithId.class, ON_CREATE),
136                 transition(TestActivityWithId.class, ON_START),
137                 transition(TestActivityWithId.class, ON_RESUME));
138         assertTrue("Pause existing primary Activity before resuming another activity on top",
139                 mLifecycleTracker.waitForConditionWithTimeout(() ->
140                         checkOrder(mEventLog, expectedLifecycle)));
141         final Activity activity1 = getResumedActivityById(activityId1);
142 
143         // Start an Activity from the pinned ActivityStack
144         final String activityId2 = "Activity2";
145         startActivityFromActivity(mPinnedActivity, TestActivityWithId.class, activityId2);
146 
147         // Verifies the activity on the pinned ActivityStack is occluded by the new Activity.
148         waitAndAssertResumed(activityId2);
149         waitAndAssertResumed(activity1);
150         waitAndAssertNotVisible(mPrimaryActivity);
151         waitAndAssertNotVisible(mPinnedActivity);
152         final Activity activity2 = getResumedActivityById(activityId2);
153 
154         // Finishes activities on the pinned ActivityStack
155         activity2.finish();
156         mPinnedActivity.finish();
157 
158         waitAndAssertResumed(activity1);
159         if (newPrimaryContainer) {
160             // Verifies primary activity is resumed due to the two activities split side-by-side.
161             waitAndAssertResumed(mPrimaryActivity);
162         }
163     }
164 
165     /**
166      * Verifies that the activity navigation are not isolated after the top ActivityStack is
167      * unpinned.
168      */
169     @Test
testUnpinTopActivityStack()170     public void testUnpinTopActivityStack() {
171         // Launch a secondary activity to side
172         mPinnedActivity = startActivityAndVerifySplitAttributes(mPrimaryActivity,
173                 TestActivityWithId.class, mWildcardSplitPairRule,
174                 mPinnedActivityId, mSplitInfoConsumer);
175 
176         // Pin and unpin the top ActivityStack
177         assertTrue(pinTopActivityStack());
178         mActivityEmbeddingComponent.unpinTopActivityStack(mTaskId);
179 
180         // Verifies the activities still splits after unpin.
181         waitAndAssertResumed(mPrimaryActivity);
182         waitAndAssertResumed(mPinnedActivity);
183 
184         // Start an Activity from the primary ActivityStack
185         final String activityId1 = "Activity1";
186         startActivityFromActivity(mPrimaryActivity, TestActivityWithId.class, activityId1);
187 
188         // Verifies the activity in the secondary ActivityStack is occluded by the new Activity.
189         waitAndAssertResumed(activityId1);
190         waitAndAssertResumed(mPrimaryActivity);
191         waitAndAssertNotVisible(mPinnedActivity);
192     }
193 
194     /**
195      * Verifies that the activity is expanded if no split rules after unpinned.
196      */
197     @Test
testUnpinTopActivityStack_expands()198     public void testUnpinTopActivityStack_expands() {
199         // Launch a secondary activity to side
200         mPinnedActivity = startActivityAndVerifySplitAttributes(mPrimaryActivity,
201                 TestActivityWithId.class, mWildcardSplitPairRule,
202                 mPinnedActivityId, mSplitInfoConsumer);
203 
204         // Pin the top ActivityStack
205         assertTrue(pinTopActivityStack());
206         waitAndAssertResumed(mPrimaryActivity);
207         waitAndAssertResumed(mPinnedActivity);
208 
209         // Start an Activity from the primary ActivityStack
210         final String activityId1 = "Activity1";
211         startActivityFromActivity(mPrimaryActivity, TestActivityWithId.class, activityId1);
212 
213         // Verifies the activity in the primary ActivityStack is occluded by the new Activity.
214         waitAndAssertResumed(activityId1);
215         waitAndAssertResumed(mPinnedActivity);
216         waitAndAssertNotVisible(mPrimaryActivity);
217         final Activity activity1 = getResumedActivityById(activityId1);
218 
219         // Verifies the activity is expanded and occludes other activities after unpin.
220         mActivityEmbeddingComponent.unpinTopActivityStack(mTaskId);
221         waitAndAssertResumed(mPinnedActivity);
222         waitAndAssertNotVisible(mPrimaryActivity);
223         waitAndAssertNotVisible(activity1);
224     }
225 
226     /**
227      * Verifies that top ActivityStack cannot be pinned whenever it is not allowed.
228      */
229     @Test
testPinTopActivityStack_invalidPin()230     public void testPinTopActivityStack_invalidPin() {
231         // Cannot pin if there's no ActivityStack.
232         assertFalse(pinTopActivityStack());
233 
234         // Launch a secondary activity to side
235         mPinnedActivity = startActivityAndVerifySplitAttributes(mPrimaryActivity,
236                 TestActivityWithId.class, mWildcardSplitPairRule,
237                 mPinnedActivityId, mSplitInfoConsumer);
238 
239         // Cannot pin if no such task.
240         assertFalse(pinTopActivityStack(mTaskId + 1));
241 
242         // Cannot pin if parent window metric not large enough.
243         final SplitPinRule oversizeParentMetricsRule = new SplitPinRule.Builder(
244                 new SplitAttributes.Builder().build(),
245                 parentWindowMetrics -> false /* parentWindowMetricsPredicate */).build();
246         assertFalse(pinTopActivityStack(mTaskId, oversizeParentMetricsRule));
247 
248         // Pin the top ActivityStack
249         assertTrue(pinTopActivityStack());
250 
251         // Cannot pin once there's already a pinned ActivityStack.
252         assertFalse(pinTopActivityStack());
253     }
254 
255     /**
256      * Verifies that the pinned rule is sticky and applies whenever possible.
257      */
258     @Test
testPinTopActivityStack_resizeStickyPin()259     public void testPinTopActivityStack_resizeStickyPin() {
260         // Launch a secondary activity to side
261         Activity secondaryActivity = startActivityAndVerifySplitAttributes(mPrimaryActivity,
262                 TestActivityWithId.class, mWildcardSplitPairRule,
263                 mPinnedActivityId, mSplitInfoConsumer);
264 
265         pinExpandActivityAndResizeDisplay(secondaryActivity, true /* stickyPin */);
266 
267         // Verify the activities are still split
268         waitAndAssertResumed(secondaryActivity);
269         waitAndAssertResumed(mPinnedActivity);
270         waitAndAssertNotVisible(mPrimaryActivity);
271     }
272 
273     /**
274      * Verifies that the pinned rule is non-sticky and removed after resizing.
275      */
276     @Test
testPinTopActivityStack_resizeNonStickyPin()277     public void testPinTopActivityStack_resizeNonStickyPin() {
278         // Launch a secondary activity to side
279         Activity secondaryActivity = startActivityAndVerifySplitAttributes(mPrimaryActivity,
280                 TestActivityWithId.class, mWildcardSplitPairRule,
281                 mPinnedActivityId, mSplitInfoConsumer);
282 
283         pinExpandActivityAndResizeDisplay(secondaryActivity, false /* stickyPin */);
284 
285         // Verify the unpinned activity is expanded.
286         waitAndAssertResumed(mPinnedActivity);
287         waitAndAssertNotVisible(secondaryActivity);
288         waitAndAssertNotVisible(mPrimaryActivity);
289     }
290 
pinExpandActivityAndResizeDisplay(@onNull Activity secondaryActivity, boolean stickyPin)291     private void pinExpandActivityAndResizeDisplay(@NonNull Activity secondaryActivity,
292             boolean stickyPin) {
293         // Starts an Activity to always-expand
294         final SplitPairRule expandRule = createSplitPairRuleBuilder(activityActivityPair -> true,
295                 activityIntentPair -> true, windowMetrics -> true).setDefaultSplitAttributes(
296                 EXPAND_SPLIT_ATTRS).build();
297         mActivityEmbeddingComponent.setEmbeddingRules(
298                 Collections.singleton(expandRule));
299         mPinnedActivity = startActivityAndVerifySplitAttributes(secondaryActivity,
300                 TestActivityWithId.class, expandRule, "expandActivityId", mSplitInfoConsumer);
301 
302         // Pin the top ActivityStack
303         final Rect taskBounds = waitAndGetTaskBounds(mPinnedActivity,
304                 true /* shouldWaitForResume */);
305         final int originalTaskWidth = taskBounds.width();
306         final int originalTaskHeight = taskBounds.height();
307         final SplitPinRule stickySplitPinRule = new SplitPinRule.Builder(
308                 new SplitAttributes.Builder().build(),
309                 parentWindowMetrics -> parentWindowMetrics.getBounds().width() >= originalTaskWidth
310                         && parentWindowMetrics.getBounds().height() >= originalTaskHeight)
311                 .setSticky(stickyPin).build();
312 
313         // Verify the pinned activity is split with next-top activity
314         assertTrue(pinTopActivityStack(mTaskId, stickySplitPinRule));
315         waitAndAssertResumed(secondaryActivity);
316         waitAndAssertResumed(mPinnedActivity);
317         waitAndAssertNotVisible(mPrimaryActivity);
318 
319         // Shrink the display by 10% to make the activities stacked
320         final Size originalDisplaySize = mReportedDisplayMetrics.getSize();
321         mReportedDisplayMetrics.setSize(new Size((int) (originalDisplaySize.getWidth() * 0.9),
322                 (int) (originalDisplaySize.getHeight() * 0.9)));
323 
324         // Verify only the pinned activity is visible
325         waitAndAssertResumed(mPinnedActivity);
326         waitAndAssertNotVisible(secondaryActivity);
327         waitAndAssertNotVisible(mPrimaryActivity);
328 
329         // Restore to the original display size
330         mReportedDisplayMetrics.setSize(originalDisplaySize);
331     }
332 
pinTopActivityStack()333     private boolean pinTopActivityStack() {
334         return pinTopActivityStack(mTaskId);
335     }
336 
pinTopActivityStack(int taskId)337     private boolean pinTopActivityStack(int taskId) {
338         SplitPinRule splitPinRule = new SplitPinRule.Builder(new SplitAttributes.Builder().build(),
339                 parentWindowMetrics -> true /* parentWindowMetricsPredicate */).build();
340         return pinTopActivityStack(taskId, splitPinRule);
341     }
342 
pinTopActivityStack(int taskId, @NonNull SplitPinRule splitPinRule)343     private boolean pinTopActivityStack(int taskId, @NonNull SplitPinRule splitPinRule) {
344         return mActivityEmbeddingComponent.pinTopActivityStack(taskId, splitPinRule);
345     }
346 }
347