1 /*
2  * Copyright (C) 2018 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 com.android.server.wm;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
22 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
23 import static android.view.Display.INVALID_DISPLAY;
24 
25 import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
26 import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
27 import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
28 import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
29 import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
30 import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
31 import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
32 import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
33 import static com.android.server.wm.ActivityStarter.Request;
34 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
35 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
36 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
37 import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
38 
39 import static org.junit.Assert.assertEquals;
40 import static org.junit.Assert.assertNotEquals;
41 
42 import android.app.ActivityOptions;
43 import android.content.ComponentName;
44 import android.content.pm.ActivityInfo.WindowLayout;
45 import android.graphics.Rect;
46 import android.platform.test.annotations.Presubmit;
47 import android.util.ArrayMap;
48 import android.util.SparseArray;
49 
50 import androidx.test.filters.MediumTest;
51 
52 import com.android.server.wm.LaunchParamsController.LaunchParams;
53 import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
54 
55 import org.junit.Before;
56 import org.junit.Test;
57 import org.junit.runner.RunWith;
58 
59 import java.util.Map;
60 
61 /**
62  * Tests for exercising {@link LaunchParamsController}.
63  *
64  * Build/Install/Run:
65  *  atest WmTests:LaunchParamsControllerTests
66  */
67 @MediumTest
68 @Presubmit
69 @RunWith(WindowTestRunner.class)
70 public class LaunchParamsControllerTests extends WindowTestsBase {
71     private LaunchParamsController mController;
72     private TestLaunchParamsPersister mPersister;
73 
74     @Before
setUp()75     public void setUp() throws Exception {
76         mPersister = new TestLaunchParamsPersister();
77         mController = new LaunchParamsController(mAtm, mPersister);
78     }
79 
80     /**
81      * Makes sure positioners get values passed to controller.
82      */
83     @Test
testArgumentPropagation()84     public void testArgumentPropagation() {
85         final LaunchParamsModifier
86                 positioner = mock(LaunchParamsModifier.class);
87         mController.registerModifier(positioner);
88 
89         final ActivityRecord record = new ActivityBuilder(mAtm).build();
90         final ActivityRecord source = new ActivityBuilder(mAtm).build();
91         final WindowLayout layout = new WindowLayout(0, 0, 0, 0, 0, 0, 0);
92         final ActivityOptions options = mock(ActivityOptions.class);
93         final Request request = new Request();
94 
95         mController.calculate(record.getTask(), layout, record, source, options, request,
96                 PHASE_BOUNDS, new LaunchParams());
97         verify(positioner, times(1)).onCalculate(eq(record.getTask()), eq(layout), eq(record),
98                 eq(source), eq(options), eq(request), anyInt(), any(), any());
99     }
100 
101     /**
102      * Makes sure controller passes stored params to modifiers.
103      */
104     @Test
testStoredParamsRecovery()105     public void testStoredParamsRecovery() {
106         final LaunchParamsModifier positioner = mock(LaunchParamsModifier.class);
107         mController.registerModifier(positioner);
108 
109         final ComponentName name = new ComponentName("com.android.foo", ".BarActivity");
110         final int userId = 0;
111         final ActivityRecord activity = new ActivityBuilder(mAtm).setComponent(name)
112                 .setUid(userId).build();
113         final LaunchParams expected = new LaunchParams();
114         expected.mPreferredTaskDisplayArea = mock(TaskDisplayArea.class);
115         expected.mWindowingMode = WINDOWING_MODE_PINNED;
116         expected.mBounds.set(200, 300, 400, 500);
117 
118         mPersister.putLaunchParams(userId, name, expected);
119 
120         mController.calculate(activity.getTask(), null /*layout*/, activity, null /*source*/,
121                 null /*options*/, null /*request*/, PHASE_BOUNDS, new LaunchParams());
122         verify(positioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
123                 anyInt(), eq(expected), any());
124     }
125 
126     /**
127      * Ensures positioners further down the chain are not called when RESULT_DONE is returned.
128      */
129     @Test
testEarlyExit()130     public void testEarlyExit() {
131         final LaunchParamsModifier
132                 ignoredPositioner = mock(LaunchParamsModifier.class);
133         final LaunchParamsModifier earlyExitPositioner =
134                 (task, layout, activity, source, options, phase, currentParams, outParams, request)
135                         -> RESULT_DONE;
136 
137         mController.registerModifier(ignoredPositioner);
138         mController.registerModifier(earlyExitPositioner);
139 
140         mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
141                 null /*source*/, null /*options*/, null /*request*/,
142                 PHASE_BOUNDS, new LaunchParams());
143         verify(ignoredPositioner, never()).onCalculate(any(), any(), any(), any(), any(), any(),
144                 anyInt(), any(), any());
145     }
146 
147     /**
148      * Ensures that positioners are called in the correct order.
149      */
150     @Test
testRegistration()151     public void testRegistration() {
152         LaunchParamsModifier earlyExitPositioner =
153                 new InstrumentedPositioner(RESULT_DONE, new LaunchParams());
154 
155         final LaunchParamsModifier firstPositioner = spy(earlyExitPositioner);
156 
157         mController.registerModifier(firstPositioner);
158 
159         mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
160                 null /*source*/, null /*options*/, null /*request*/, PHASE_BOUNDS,
161                 new LaunchParams());
162         verify(firstPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
163                 anyInt(), any(), any());
164 
165         final LaunchParamsModifier secondPositioner = spy(earlyExitPositioner);
166 
167         mController.registerModifier(secondPositioner);
168 
169         mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
170                 null /*source*/, null /*options*/, null /*request*/, PHASE_BOUNDS,
171                 new LaunchParams());
172         verify(firstPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
173                 anyInt(), any(), any());
174         verify(secondPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
175                 anyInt(), any(), any());
176     }
177 
178     /**
179      * Makes sure positioners further down the registration chain are called.
180      */
181     @Test
testPassThrough()182     public void testPassThrough() {
183         final LaunchParamsModifier
184                 positioner1 = mock(LaunchParamsModifier.class);
185         final LaunchParams params = new LaunchParams();
186         params.mWindowingMode = WINDOWING_MODE_FREEFORM;
187         params.mBounds.set(0, 0, 30, 20);
188         params.mPreferredTaskDisplayArea = mock(TaskDisplayArea.class);
189 
190         final InstrumentedPositioner positioner2 = new InstrumentedPositioner(RESULT_CONTINUE,
191                 params);
192 
193         mController.registerModifier(positioner1);
194         mController.registerModifier(positioner2);
195 
196         mController.calculate(null /*task*/, null /*layout*/, null /*activity*/, null /*source*/,
197                 null /*options*/, null /*request*/, PHASE_BOUNDS, new LaunchParams());
198 
199         verify(positioner1, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
200                 anyInt(), eq(positioner2.getLaunchParams()), any());
201     }
202 
203     /**
204      * Ensures skipped results are not propagated.
205      */
206     @Test
testSkip()207     public void testSkip() {
208         final LaunchParams params1 = new LaunchParams();
209         params1.mBounds.set(0, 0, 10, 10);
210         final InstrumentedPositioner positioner1 = new InstrumentedPositioner(RESULT_SKIP, params1);
211 
212         final LaunchParams params2 = new LaunchParams();
213         params2.mBounds.set(0, 0, 20, 30);
214         final InstrumentedPositioner positioner2 =
215                 new InstrumentedPositioner(RESULT_CONTINUE, params2);
216 
217         mController.registerModifier(positioner1);
218         mController.registerModifier(positioner2);
219 
220         final LaunchParams result = new LaunchParams();
221 
222         mController.calculate(null /*task*/, null /*layout*/, null /*activity*/, null /*source*/,
223                 null /*options*/, null /*request*/, PHASE_BOUNDS, result);
224 
225         assertEquals(result, positioner2.getLaunchParams());
226     }
227 
228     /**
229      * Tests preferred display id calculation for VR.
230      */
231     @Test
testVrPreferredDisplay()232     public void testVrPreferredDisplay() {
233         final TestDisplayContent vrDisplay = createNewDisplayContent();
234         mAtm.mVr2dDisplayId = vrDisplay.mDisplayId;
235 
236         final LaunchParams result = new LaunchParams();
237         final ActivityRecord vrActivity = new ActivityBuilder(mAtm).build();
238         vrActivity.requestedVrComponent = vrActivity.mActivityComponent;
239 
240         // VR activities should always land on default display.
241         mController.calculate(null /*task*/, null /*layout*/, vrActivity /*activity*/,
242                 null /*source*/, null /*options*/, null/*request*/, PHASE_BOUNDS, result);
243         assertEquals(mRootWindowContainer.getDefaultTaskDisplayArea(),
244                 result.mPreferredTaskDisplayArea);
245 
246         // Otherwise, always lands on VR 2D display.
247         final ActivityRecord vr2dActivity = new ActivityBuilder(mAtm).build();
248         mController.calculate(null /*task*/, null /*layout*/, vr2dActivity /*activity*/,
249                 null /*source*/, null /*options*/, null /*request*/, PHASE_BOUNDS, result);
250         assertEquals(vrDisplay.getDefaultTaskDisplayArea(), result.mPreferredTaskDisplayArea);
251         mController.calculate(null /*task*/, null /*layout*/, null /*activity*/, null /*source*/,
252                 null /*options*/, null /*request*/, PHASE_BOUNDS, result);
253         assertEquals(vrDisplay.getDefaultTaskDisplayArea(), result.mPreferredTaskDisplayArea);
254 
255         mAtm.mVr2dDisplayId = INVALID_DISPLAY;
256     }
257 
258 
259     /**
260      * Ensures that {@link LaunchParamsController} calculates to {@link PHASE_BOUNDS} phase by
261      * default.
262      */
263     @Test
testCalculatePhase()264     public void testCalculatePhase() {
265         final LaunchParamsModifier positioner = mock(LaunchParamsModifier.class);
266         mController.registerModifier(positioner);
267 
268         final ActivityRecord record = new ActivityBuilder(mAtm).build();
269         final ActivityRecord source = new ActivityBuilder(mAtm).build();
270         final WindowLayout layout = new WindowLayout(0, 0, 0, 0, 0, 0, 0);
271         final ActivityOptions options = mock(ActivityOptions.class);
272 
273         mController.calculate(record.getTask(), layout, record, source, options, null/*request*/,
274                 PHASE_BOUNDS, new LaunchParams());
275         verify(positioner, times(1)).onCalculate(eq(record.getTask()), eq(layout), eq(record),
276                 eq(source), eq(options), any(), eq(PHASE_BOUNDS), any(), any());
277     }
278 
279     /**
280      * Ensures that {@link LaunchParamsModifier} doesn't alter non-root tasks' windowingMode.
281      */
282     @Test
testLayoutNonRootTaskWindowingModeChange()283     public void testLayoutNonRootTaskWindowingModeChange() {
284         final LaunchParams params = new LaunchParams();
285         final int windowingMode = WINDOWING_MODE_FREEFORM;
286         params.mWindowingMode = windowingMode;
287         final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
288         final Task task = new TaskBuilder(mAtm.mTaskSupervisor).setCreateParentTask(true).build();
289         task.getRootTask().setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
290 
291         mController.registerModifier(positioner);
292 
293         final int beforeWindowMode = task.getWindowingMode();
294         assertNotEquals(windowingMode, beforeWindowMode);
295 
296         mController.layoutTask(task, null /* windowLayout */);
297 
298         final int afterWindowMode = task.getWindowingMode();
299         assertEquals(afterWindowMode, beforeWindowMode);
300     }
301 
302     /**
303      * Ensures that {@link LaunchParamsModifier} requests specifying bounds during
304      * layout are honored if window is in freeform.
305      */
306     @Test
testLayoutTaskBoundsChangeFreeformWindow()307     public void testLayoutTaskBoundsChangeFreeformWindow() {
308         final Rect expected = new Rect(10, 20, 30, 40);
309 
310         final LaunchParams params = new LaunchParams();
311         params.mBounds.set(expected);
312         final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
313         final Task task = new TaskBuilder(mAtm.mTaskSupervisor)
314                 .setWindowingMode(WINDOWING_MODE_FREEFORM).build();
315 
316         mController.registerModifier(positioner);
317 
318         assertNotEquals(expected, task.getBounds());
319 
320         mController.layoutTask(task, null /* windowLayout */);
321 
322         // Task will make adjustments to requested bounds. We only need to guarantee that the
323         // reuqested bounds are expected.
324         assertEquals(expected, task.getRequestedOverrideBounds());
325     }
326 
327     /**
328      * Ensures that {@link LaunchParamsModifier} requests specifying bounds during
329      * layout are honored if window is in multiwindow mode.
330      */
331     @Test
testLayoutTaskBoundsChangeMultiWindow()332     public void testLayoutTaskBoundsChangeMultiWindow() {
333         final Rect expected = new Rect(10, 20, 30, 40);
334 
335         final LaunchParams params = new LaunchParams();
336         params.mBounds.set(expected);
337         final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
338         final Task task = new TaskBuilder(mAtm.mTaskSupervisor)
339                 .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW).build();
340 
341         mController.registerModifier(positioner);
342 
343         assertNotEquals(expected, task.getBounds());
344 
345         mController.layoutTask(task, null /* windowLayout */);
346 
347         assertEquals(expected, task.getRequestedOverrideBounds());
348     }
349 
350     /**
351      * Ensures that {@link LaunchParamsModifier} requests specifying bounds during
352      * layout are set to last non-fullscreen bounds.
353      */
354     @Test
testLayoutTaskBoundsChangeFixedWindow()355     public void testLayoutTaskBoundsChangeFixedWindow() {
356         final Rect expected = new Rect(10, 20, 30, 40);
357 
358         final LaunchParams params = new LaunchParams();
359         params.mWindowingMode = WINDOWING_MODE_FULLSCREEN;
360         params.mBounds.set(expected);
361         final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
362         final Task task = new TaskBuilder(mAtm.mTaskSupervisor).build();
363 
364         mController.registerModifier(positioner);
365 
366         assertNotEquals(expected, task.getBounds());
367 
368         mController.layoutTask(task, null /* windowLayout */);
369 
370         assertNotEquals(expected, task.getBounds());
371         assertEquals(expected, task.mLastNonFullscreenBounds);
372     }
373 
374     public static class InstrumentedPositioner implements LaunchParamsModifier {
375 
376         private final int mReturnVal;
377         private final LaunchParams mParams;
378 
InstrumentedPositioner(int returnVal, LaunchParams params)379         InstrumentedPositioner(int returnVal, LaunchParams params) {
380             mReturnVal = returnVal;
381             mParams = params;
382         }
383 
384         @Override
onCalculate(Task task, WindowLayout layout, ActivityRecord activity, ActivityRecord source, ActivityOptions options, Request request, int phase, LaunchParams currentParams, LaunchParams outParams)385         public int onCalculate(Task task, WindowLayout layout, ActivityRecord activity,
386                 ActivityRecord source, ActivityOptions options, Request request, int phase,
387                 LaunchParams currentParams, LaunchParams outParams) {
388             outParams.set(mParams);
389             return mReturnVal;
390         }
391 
getLaunchParams()392         LaunchParams getLaunchParams() {
393             return mParams;
394         }
395     }
396 
397     /**
398      * Test double for {@link LaunchParamsPersister}. This class only manages an in-memory storage
399      * of a mapping from user ID and component name to launch params.
400      */
401     static class TestLaunchParamsPersister extends LaunchParamsPersister {
402 
403         private final SparseArray<Map<ComponentName, LaunchParams>> mMap =
404                 new SparseArray<>();
405         private final LaunchParams mTmpParams = new LaunchParams();
406 
TestLaunchParamsPersister()407         TestLaunchParamsPersister() {
408             super(null, null, null);
409         }
410 
putLaunchParams(int userId, ComponentName name, LaunchParams params)411         void putLaunchParams(int userId, ComponentName name, LaunchParams params) {
412             Map<ComponentName, LaunchParams> map = mMap.get(userId);
413             if (map == null) {
414                 map = new ArrayMap<>();
415                 mMap.put(userId, map);
416             }
417 
418             LaunchParams paramRecord = map.get(name);
419             if (paramRecord == null) {
420                 paramRecord = new LaunchParams();
421                 map.put(name, params);
422             }
423 
424             paramRecord.set(params);
425         }
426 
427         @Override
onUnlockUser(int userId)428         void onUnlockUser(int userId) {
429             if (mMap.get(userId) == null) {
430                 mMap.put(userId, new ArrayMap<>());
431             }
432         }
433 
434         @Override
saveTask(Task task, DisplayContent display)435         void saveTask(Task task, DisplayContent display) {
436             final int userId = task.mUserId;
437             final ComponentName realActivity = task.realActivity;
438             mTmpParams.mPreferredTaskDisplayArea = task.getDisplayArea();
439             mTmpParams.mWindowingMode = task.getWindowingMode();
440             if (task.mLastNonFullscreenBounds != null) {
441                 mTmpParams.mBounds.set(task.mLastNonFullscreenBounds);
442             } else {
443                 mTmpParams.mBounds.setEmpty();
444             }
445             putLaunchParams(userId, realActivity, mTmpParams);
446         }
447 
448         @Override
getLaunchParams(Task task, ActivityRecord activity, LaunchParams params)449         void getLaunchParams(Task task, ActivityRecord activity, LaunchParams params) {
450             final int userId = task != null ? task.mUserId : activity.mUserId;
451             final ComponentName name = task != null
452                     ? task.realActivity : activity.mActivityComponent;
453 
454             params.reset();
455             final Map<ComponentName, LaunchParams> map = mMap.get(userId);
456             if (map == null) {
457                 return;
458             }
459 
460             final LaunchParams paramsRecord = map.get(name);
461             if (paramsRecord != null) {
462                 params.set(paramsRecord);
463             }
464         }
465     }
466 
createNewDisplayContent()467     private TestDisplayContent createNewDisplayContent() {
468         return addNewDisplayContentAt(DisplayContent.POSITION_TOP);
469     }
470 }
471