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