blob: fc298b0d96a159e71201486927ad169e21fa93a4 [file] [log] [blame]
/*
* Copyright (C) 2018 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License
*/
package com.android.server.wm;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_SECONDARY;
import static android.view.Display.INVALID_DISPLAY;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.any;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyBoolean;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.anyInt;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doNothing;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.eq;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.never;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.times;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.server.wm.ActivityStarter.Request;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.PHASE_BOUNDS;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_CONTINUE;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_DONE;
import static com.android.server.wm.LaunchParamsController.LaunchParamsModifier.RESULT_SKIP;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotEquals;
import android.app.ActivityOptions;
import android.content.ComponentName;
import android.content.pm.ActivityInfo.WindowLayout;
import android.graphics.Rect;
import android.platform.test.annotations.Presubmit;
import android.util.ArrayMap;
import android.util.SparseArray;
import androidx.test.filters.MediumTest;
import com.android.server.wm.LaunchParamsController.LaunchParams;
import com.android.server.wm.LaunchParamsController.LaunchParamsModifier;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.Map;
/**
* Tests for exercising {@link LaunchParamsController}.
*
* Build/Install/Run:
* atest WmTests:LaunchParamsControllerTests
*/
@MediumTest
@Presubmit
@RunWith(WindowTestRunner.class)
public class LaunchParamsControllerTests extends WindowTestsBase {
private LaunchParamsController mController;
private TestLaunchParamsPersister mPersister;
@Before
public void setUp() throws Exception {
mPersister = new TestLaunchParamsPersister();
mController = new LaunchParamsController(mAtm, mPersister);
}
/**
* Makes sure positioners get values passed to controller.
*/
@Test
public void testArgumentPropagation() {
final LaunchParamsModifier
positioner = mock(LaunchParamsModifier.class);
mController.registerModifier(positioner);
final ActivityRecord record = new ActivityBuilder(mAtm).build();
final ActivityRecord source = new ActivityBuilder(mAtm).build();
final WindowLayout layout = new WindowLayout(0, 0, 0, 0, 0, 0, 0);
final ActivityOptions options = mock(ActivityOptions.class);
final Request request = new Request();
mController.calculate(record.getTask(), layout, record, source, options, request,
PHASE_BOUNDS, new LaunchParams());
verify(positioner, times(1)).onCalculate(eq(record.getTask()), eq(layout), eq(record),
eq(source), eq(options), eq(request), anyInt(), any(), any());
}
/**
* Makes sure controller passes stored params to modifiers.
*/
@Test
public void testStoredParamsRecovery() {
final LaunchParamsModifier positioner = mock(LaunchParamsModifier.class);
mController.registerModifier(positioner);
final ComponentName name = new ComponentName("com.android.foo", ".BarActivity");
final int userId = 0;
final ActivityRecord activity = new ActivityBuilder(mAtm).setComponent(name)
.setUid(userId).build();
final LaunchParams expected = new LaunchParams();
expected.mPreferredTaskDisplayArea = mock(TaskDisplayArea.class);
expected.mWindowingMode = WINDOWING_MODE_PINNED;
expected.mBounds.set(200, 300, 400, 500);
mPersister.putLaunchParams(userId, name, expected);
mController.calculate(activity.getTask(), null /*layout*/, activity, null /*source*/,
null /*options*/, null /*request*/, PHASE_BOUNDS, new LaunchParams());
verify(positioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
anyInt(), eq(expected), any());
}
/**
* Ensures positioners further down the chain are not called when RESULT_DONE is returned.
*/
@Test
public void testEarlyExit() {
final LaunchParamsModifier
ignoredPositioner = mock(LaunchParamsModifier.class);
final LaunchParamsModifier earlyExitPositioner =
(task, layout, activity, source, options, phase, currentParams, outParams, request)
-> RESULT_DONE;
mController.registerModifier(ignoredPositioner);
mController.registerModifier(earlyExitPositioner);
mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
null /*source*/, null /*options*/, null /*request*/,
PHASE_BOUNDS, new LaunchParams());
verify(ignoredPositioner, never()).onCalculate(any(), any(), any(), any(), any(), any(),
anyInt(), any(), any());
}
/**
* Ensures that positioners are called in the correct order.
*/
@Test
public void testRegistration() {
LaunchParamsModifier earlyExitPositioner =
new InstrumentedPositioner(RESULT_DONE, new LaunchParams());
final LaunchParamsModifier firstPositioner = spy(earlyExitPositioner);
mController.registerModifier(firstPositioner);
mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
null /*source*/, null /*options*/, null /*request*/, PHASE_BOUNDS,
new LaunchParams());
verify(firstPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
anyInt(), any(), any());
final LaunchParamsModifier secondPositioner = spy(earlyExitPositioner);
mController.registerModifier(secondPositioner);
mController.calculate(null /*task*/, null /*layout*/, null /*activity*/,
null /*source*/, null /*options*/, null /*request*/, PHASE_BOUNDS,
new LaunchParams());
verify(firstPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
anyInt(), any(), any());
verify(secondPositioner, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
anyInt(), any(), any());
}
/**
* Makes sure positioners further down the registration chain are called.
*/
@Test
public void testPassThrough() {
final LaunchParamsModifier
positioner1 = mock(LaunchParamsModifier.class);
final LaunchParams params = new LaunchParams();
params.mWindowingMode = WINDOWING_MODE_FREEFORM;
params.mBounds.set(0, 0, 30, 20);
params.mPreferredTaskDisplayArea = mock(TaskDisplayArea.class);
final InstrumentedPositioner positioner2 = new InstrumentedPositioner(RESULT_CONTINUE,
params);
mController.registerModifier(positioner1);
mController.registerModifier(positioner2);
mController.calculate(null /*task*/, null /*layout*/, null /*activity*/, null /*source*/,
null /*options*/, null /*request*/, PHASE_BOUNDS, new LaunchParams());
verify(positioner1, times(1)).onCalculate(any(), any(), any(), any(), any(), any(),
anyInt(), eq(positioner2.getLaunchParams()), any());
}
/**
* Ensures skipped results are not propagated.
*/
@Test
public void testSkip() {
final LaunchParams params1 = new LaunchParams();
params1.mBounds.set(0, 0, 10, 10);
final InstrumentedPositioner positioner1 = new InstrumentedPositioner(RESULT_SKIP, params1);
final LaunchParams params2 = new LaunchParams();
params2.mBounds.set(0, 0, 20, 30);
final InstrumentedPositioner positioner2 =
new InstrumentedPositioner(RESULT_CONTINUE, params2);
mController.registerModifier(positioner1);
mController.registerModifier(positioner2);
final LaunchParams result = new LaunchParams();
mController.calculate(null /*task*/, null /*layout*/, null /*activity*/, null /*source*/,
null /*options*/, null /*request*/, PHASE_BOUNDS, result);
assertEquals(result, positioner2.getLaunchParams());
}
/**
* Tests preferred display id calculation for VR.
*/
@Test
public void testVrPreferredDisplay() {
final TestDisplayContent vrDisplay = createNewDisplayContent();
mAtm.mVr2dDisplayId = vrDisplay.mDisplayId;
final LaunchParams result = new LaunchParams();
final ActivityRecord vrActivity = new ActivityBuilder(mAtm).build();
vrActivity.requestedVrComponent = vrActivity.mActivityComponent;
// VR activities should always land on default display.
mController.calculate(null /*task*/, null /*layout*/, vrActivity /*activity*/,
null /*source*/, null /*options*/, null/*request*/, PHASE_BOUNDS, result);
assertEquals(mRootWindowContainer.getDefaultTaskDisplayArea(),
result.mPreferredTaskDisplayArea);
// Otherwise, always lands on VR 2D display.
final ActivityRecord vr2dActivity = new ActivityBuilder(mAtm).build();
mController.calculate(null /*task*/, null /*layout*/, vr2dActivity /*activity*/,
null /*source*/, null /*options*/, null /*request*/, PHASE_BOUNDS, result);
assertEquals(vrDisplay.getDefaultTaskDisplayArea(), result.mPreferredTaskDisplayArea);
mController.calculate(null /*task*/, null /*layout*/, null /*activity*/, null /*source*/,
null /*options*/, null /*request*/, PHASE_BOUNDS, result);
assertEquals(vrDisplay.getDefaultTaskDisplayArea(), result.mPreferredTaskDisplayArea);
mAtm.mVr2dDisplayId = INVALID_DISPLAY;
}
/**
* Ensures that {@link LaunchParamsController} calculates to {@link PHASE_BOUNDS} phase by
* default.
*/
@Test
public void testCalculatePhase() {
final LaunchParamsModifier positioner = mock(LaunchParamsModifier.class);
mController.registerModifier(positioner);
final ActivityRecord record = new ActivityBuilder(mAtm).build();
final ActivityRecord source = new ActivityBuilder(mAtm).build();
final WindowLayout layout = new WindowLayout(0, 0, 0, 0, 0, 0, 0);
final ActivityOptions options = mock(ActivityOptions.class);
mController.calculate(record.getTask(), layout, record, source, options, null/*request*/,
PHASE_BOUNDS, new LaunchParams());
verify(positioner, times(1)).onCalculate(eq(record.getTask()), eq(layout), eq(record),
eq(source), eq(options), any(), eq(PHASE_BOUNDS), any(), any());
}
/**
* Ensures that {@link LaunchParamsModifier} requests specifying display id during
* layout are honored.
*/
@Test
public void testLayoutTaskPreferredDisplayChange() {
final LaunchParams params = new LaunchParams();
final TestDisplayContent display = createNewDisplayContent();
final TaskDisplayArea preferredTaskDisplayArea = display.getDefaultTaskDisplayArea();
params.mPreferredTaskDisplayArea = preferredTaskDisplayArea;
final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
final Task task = new TaskBuilder(mAtm.mTaskSupervisor).build();
mController.registerModifier(positioner);
doNothing().when(mRootWindowContainer).moveRootTaskToTaskDisplayArea(anyInt(), any(),
anyBoolean());
mController.layoutTask(task, null /* windowLayout */);
verify(mRootWindowContainer, times(1)).moveRootTaskToTaskDisplayArea(
eq(task.getRootTaskId()), eq(preferredTaskDisplayArea), anyBoolean());
}
/**
* Ensures that {@link LaunchParamsModifier} requests specifying windowingMode during
* layout are honored.
*/
@Test
public void testLayoutTaskWindowingModeChange() {
final LaunchParams params = new LaunchParams();
final int windowingMode = WINDOWING_MODE_FREEFORM;
params.mWindowingMode = windowingMode;
final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
final Task task = new TaskBuilder(mAtm.mTaskSupervisor).build();
mController.registerModifier(positioner);
final int beforeWindowMode = task.getRootTask().getWindowingMode();
assertNotEquals(windowingMode, beforeWindowMode);
mController.layoutTask(task, null /* windowLayout */);
final int afterWindowMode = task.getRootTask().getWindowingMode();
assertEquals(windowingMode, afterWindowMode);
}
/**
* Ensures that {@link LaunchParamsModifier} doesn't alter non-root tasks' windowingMode.
*/
@Test
public void testLayoutNonRootTaskWindowingModeChange() {
final LaunchParams params = new LaunchParams();
final int windowingMode = WINDOWING_MODE_FREEFORM;
params.mWindowingMode = windowingMode;
final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
final Task task = new TaskBuilder(mAtm.mTaskSupervisor).setCreateParentTask(true).build();
task.getRootTask().setWindowingMode(WINDOWING_MODE_SPLIT_SCREEN_SECONDARY);
mController.registerModifier(positioner);
final int beforeWindowMode = task.getWindowingMode();
assertNotEquals(windowingMode, beforeWindowMode);
mController.layoutTask(task, null /* windowLayout */);
final int afterWindowMode = task.getWindowingMode();
assertEquals(afterWindowMode, beforeWindowMode);
}
/**
* Ensures that {@link LaunchParamsModifier} requests specifying bounds during
* layout are honored if window is in freeform.
*/
@Test
public void testLayoutTaskBoundsChangeFreeformWindow() {
final Rect expected = new Rect(10, 20, 30, 40);
final LaunchParams params = new LaunchParams();
params.mWindowingMode = WINDOWING_MODE_FREEFORM;
params.mBounds.set(expected);
final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
final Task task = new TaskBuilder(mAtm.mTaskSupervisor).build();
mController.registerModifier(positioner);
assertNotEquals(expected, task.getBounds());
mController.layoutTask(task, null /* windowLayout */);
// Task will make adjustments to requested bounds. We only need to guarantee that the
// reuqested bounds are expected.
assertEquals(expected, task.getRequestedOverrideBounds());
}
/**
* Ensures that {@link LaunchParamsModifier} requests specifying bounds during
* layout are honored if window is in multiwindow mode.
*/
@Test
public void testLayoutTaskBoundsChangeMultiWindow() {
final Rect expected = new Rect(10, 20, 30, 40);
final LaunchParams params = new LaunchParams();
params.mWindowingMode = WINDOWING_MODE_MULTI_WINDOW;
params.mBounds.set(expected);
final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
final Task task = new TaskBuilder(mAtm.mTaskSupervisor).build();
mController.registerModifier(positioner);
assertNotEquals(expected, task.getBounds());
mController.layoutTask(task, null /* windowLayout */);
assertEquals(expected, task.getRequestedOverrideBounds());
}
/**
* Ensures that {@link LaunchParamsModifier} requests specifying bounds during
* layout are set to last non-fullscreen bounds.
*/
@Test
public void testLayoutTaskBoundsChangeFixedWindow() {
final Rect expected = new Rect(10, 20, 30, 40);
final LaunchParams params = new LaunchParams();
params.mWindowingMode = WINDOWING_MODE_FULLSCREEN;
params.mBounds.set(expected);
final InstrumentedPositioner positioner = new InstrumentedPositioner(RESULT_DONE, params);
final Task task = new TaskBuilder(mAtm.mTaskSupervisor).build();
mController.registerModifier(positioner);
assertNotEquals(expected, task.getBounds());
mController.layoutTask(task, null /* windowLayout */);
assertNotEquals(expected, task.getBounds());
assertEquals(expected, task.mLastNonFullscreenBounds);
}
public static class InstrumentedPositioner implements LaunchParamsModifier {
private final int mReturnVal;
private final LaunchParams mParams;
InstrumentedPositioner(int returnVal, LaunchParams params) {
mReturnVal = returnVal;
mParams = params;
}
@Override
public int onCalculate(Task task, WindowLayout layout, ActivityRecord activity,
ActivityRecord source, ActivityOptions options, Request request, int phase,
LaunchParams currentParams, LaunchParams outParams) {
outParams.set(mParams);
return mReturnVal;
}
LaunchParams getLaunchParams() {
return mParams;
}
}
/**
* Test double for {@link LaunchParamsPersister}. This class only manages an in-memory storage
* of a mapping from user ID and component name to launch params.
*/
static class TestLaunchParamsPersister extends LaunchParamsPersister {
private final SparseArray<Map<ComponentName, LaunchParams>> mMap =
new SparseArray<>();
private final LaunchParams mTmpParams = new LaunchParams();
TestLaunchParamsPersister() {
super(null, null, null);
}
void putLaunchParams(int userId, ComponentName name, LaunchParams params) {
Map<ComponentName, LaunchParams> map = mMap.get(userId);
if (map == null) {
map = new ArrayMap<>();
mMap.put(userId, map);
}
LaunchParams paramRecord = map.get(name);
if (paramRecord == null) {
paramRecord = new LaunchParams();
map.put(name, params);
}
paramRecord.set(params);
}
@Override
void onUnlockUser(int userId) {
if (mMap.get(userId) == null) {
mMap.put(userId, new ArrayMap<>());
}
}
@Override
void saveTask(Task task, DisplayContent display) {
final int userId = task.mUserId;
final ComponentName realActivity = task.realActivity;
mTmpParams.mPreferredTaskDisplayArea = task.getDisplayArea();
mTmpParams.mWindowingMode = task.getWindowingMode();
if (task.mLastNonFullscreenBounds != null) {
mTmpParams.mBounds.set(task.mLastNonFullscreenBounds);
} else {
mTmpParams.mBounds.setEmpty();
}
putLaunchParams(userId, realActivity, mTmpParams);
}
@Override
void getLaunchParams(Task task, ActivityRecord activity, LaunchParams params) {
final int userId = task != null ? task.mUserId : activity.mUserId;
final ComponentName name = task != null
? task.realActivity : activity.mActivityComponent;
params.reset();
final Map<ComponentName, LaunchParams> map = mMap.get(userId);
if (map == null) {
return;
}
final LaunchParams paramsRecord = map.get(name);
if (paramsRecord != null) {
params.set(paramsRecord);
}
}
}
private TestDisplayContent createNewDisplayContent() {
return addNewDisplayContentAt(DisplayContent.POSITION_TOP);
}
}