blob: 1454c8e86a4596fd9b52251052761f6ce35ae234 [file] [log] [blame]
/*
* Copyright (C) 2019 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.content.pm.ActivityInfo.CONFIG_ORIENTATION;
import static android.content.pm.ActivityInfo.RESIZE_MODE_UNRESIZEABLE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
import static android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;
import static android.view.Surface.ROTATION_270;
import static android.view.Surface.ROTATION_90;
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.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.verify;
import static com.android.server.wm.ActivityStack.ActivityState.STOPPED;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.when;
import android.app.ActivityManager;
import android.app.ActivityManagerInternal;
import android.app.TaskStackListener;
import android.app.WindowConfiguration;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import androidx.test.filters.MediumTest;
import org.junit.Test;
import org.junit.runner.RunWith;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;
/**
* Tests for Size Compatibility mode.
*
* Build/Install/Run:
* atest WmTests:SizeCompatTests
*/
@MediumTest
@Presubmit
@RunWith(WindowTestRunner.class)
public class SizeCompatTests extends ActivityTestsBase {
private ActivityStack mStack;
private Task mTask;
private ActivityRecord mActivity;
private void setUpApp(ActivityDisplay display) {
mStack = new StackBuilder(mRootActivityContainer).setDisplay(display).build();
mTask = mStack.getChildAt(0);
mActivity = mTask.getTopNonFinishingActivity();
}
private void ensureActivityConfiguration() {
mActivity.ensureActivityConfiguration(0 /* globalChanges */, false /* preserveWindow */);
}
@Test
public void testRestartProcessIfVisible() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build());
doNothing().when(mSupervisor).scheduleRestartTimeout(mActivity);
mActivity.visible = true;
mActivity.setSavedState(null /* savedState */);
mActivity.setState(ActivityStack.ActivityState.RESUMED, "testRestart");
prepareUnresizable(1.5f /* maxAspect */, SCREEN_ORIENTATION_UNSPECIFIED);
final Rect originalOverrideBounds = new Rect(mActivity.getBounds());
resizeDisplay(mStack.getDisplay(), 600, 1200);
// The visible activity should recompute configuration according to the last parent bounds.
mService.restartActivityProcessIfVisible(mActivity.appToken);
assertEquals(ActivityStack.ActivityState.RESTARTING_PROCESS, mActivity.getState());
assertNotEquals(originalOverrideBounds, mActivity.getBounds());
}
@Test
public void testKeepBoundsWhenChangingFromFreeformToFullscreen() {
removeGlobalMinSizeRestriction();
// create freeform display and a freeform app
ActivityDisplay display = new TestActivityDisplay.Builder(mService, 2000, 1000)
.setCanRotate(false)
.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FREEFORM).build();
setUpApp(display);
// Put app window into freeform and then make it a compat app.
mTask.setBounds(100, 100, 400, 600);
prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
final Rect bounds = new Rect(mActivity.getBounds());
final int density = mActivity.getConfiguration().densityDpi;
// change display configuration to fullscreen
Configuration c = new Configuration(display.getRequestedOverrideConfiguration());
c.windowConfiguration.setWindowingMode(WindowConfiguration.WINDOWING_MODE_FULLSCREEN);
display.onRequestedOverrideConfigurationChanged(c);
// check if dimensions stay the same
assertTrue(mActivity.inSizeCompatMode());
assertEquals(bounds.width(), mActivity.getBounds().width());
assertEquals(bounds.height(), mActivity.getBounds().height());
assertEquals(density, mActivity.getConfiguration().densityDpi);
}
@Test
public void testFixedAspectRatioBoundsWithDecor() {
final int decorHeight = 200; // e.g. The device has cutout.
setUpApp(new TestActivityDisplay.Builder(mService, 600, 800)
.setNotch(decorHeight).build());
mActivity.info.minAspectRatio = mActivity.info.maxAspectRatio = 1;
prepareUnresizable(-1f, SCREEN_ORIENTATION_UNSPECIFIED);
// The parent configuration doesn't change since the first resolved configuration, so the
// activity shouldn't be in the size compatibility mode.
assertFalse(mActivity.inSizeCompatMode());
final Rect appBounds = mActivity.getWindowConfiguration().getAppBounds();
// Ensure the app bounds keep the declared aspect ratio.
assertEquals(appBounds.width(), appBounds.height());
// The decor height should be a part of the effective bounds.
assertEquals(mActivity.getBounds().height(), appBounds.height() + decorHeight);
mTask.getConfiguration().windowConfiguration.setRotation(ROTATION_90);
mActivity.onConfigurationChanged(mTask.getConfiguration());
// After changing orientation, the aspect ratio should be the same.
assertEquals(appBounds.width(), appBounds.height());
// The decor height will be included in width.
assertEquals(mActivity.getBounds().width(), appBounds.width() + decorHeight);
}
@Test
public void testFixedScreenConfigurationWhenMovingToDisplay() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build());
// Make a new less-tall display with lower density
final ActivityDisplay newDisplay =
new TestActivityDisplay.Builder(mService, 1000, 2000)
.setDensityDpi(200).build();
mActivity = new ActivityBuilder(mService)
.setTask(mTask)
.setResizeMode(RESIZE_MODE_UNRESIZEABLE)
.setMaxAspectRatio(1.5f)
.build();
mActivity.visible = true;
final Rect originalBounds = new Rect(mActivity.getBounds());
final int originalDpi = mActivity.getConfiguration().densityDpi;
// Move the non-resizable activity to the new display.
mStack.reparent(newDisplay.mDisplayContent, true /* onTop */);
assertEquals(originalBounds.width(), mActivity.getBounds().width());
assertEquals(originalBounds.height(), mActivity.getBounds().height());
assertEquals(originalDpi, mActivity.getConfiguration().densityDpi);
assertTrue(mActivity.inSizeCompatMode());
}
@Test
public void testFixedScreenBoundsWhenDisplaySizeChanged() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build());
prepareUnresizable(-1f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
assertFalse(mActivity.inSizeCompatMode());
final Rect origBounds = new Rect(mActivity.getBounds());
// Change the size of current display.
resizeDisplay(mStack.getDisplay(), 1000, 2000);
ensureActivityConfiguration();
assertEquals(origBounds.width(), mActivity.getWindowConfiguration().getBounds().width());
assertEquals(origBounds.height(), mActivity.getWindowConfiguration().getBounds().height());
assertTrue(mActivity.inSizeCompatMode());
// Change display size to a different orientation
resizeDisplay(mStack.getDisplay(), 2000, 1000);
ensureActivityConfiguration();
assertEquals(origBounds.width(), mActivity.getWindowConfiguration().getBounds().width());
assertEquals(origBounds.height(), mActivity.getWindowConfiguration().getBounds().height());
}
@Test
public void testLetterboxFullscreenBounds() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build());
// Fill out required fields on default display since WM-side is mocked out
prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_LANDSCAPE);
assertFalse(mActivity.inSizeCompatMode());
assertTrue(mActivity.getBounds().width() > mActivity.getBounds().height());
}
@Test
public void testMoveToDifferentOrientDisplay() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build());
final ActivityDisplay newDisplay =
new TestActivityDisplay.Builder(mService, 2000, 1000)
.setCanRotate(false).build();
prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
assertFalse(mActivity.inSizeCompatMode());
final Rect origBounds = new Rect(mActivity.getBounds());
// Move the non-resizable activity to the new display.
mStack.reparent(newDisplay.mDisplayContent, true /* onTop */);
ensureActivityConfiguration();
assertEquals(origBounds.width(), mActivity.getWindowConfiguration().getBounds().width());
assertEquals(origBounds.height(), mActivity.getWindowConfiguration().getBounds().height());
assertTrue(mActivity.inSizeCompatMode());
}
@Test
public void testFixedOrientRotateCutoutDisplay() {
// Create a display with a notch/cutout
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).setNotch(60).build());
prepareUnresizable(1.4f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
final Rect origBounds = new Rect(mActivity.getBounds());
final Rect origAppBounds = new Rect(mActivity.getWindowConfiguration().getAppBounds());
// Rotate the display
Configuration c = new Configuration();
mStack.getDisplay().mDisplayContent.getDisplayRotation().setRotation(ROTATION_270);
mStack.getDisplay().mDisplayContent.computeScreenConfiguration(c);
mStack.getDisplay().onRequestedOverrideConfigurationChanged(c);
// Make sure the app size is the same
assertEquals(ROTATION_270, mStack.getWindowConfiguration().getRotation());
assertEquals(origBounds.width(), mActivity.getWindowConfiguration().getBounds().width());
assertEquals(origBounds.height(), mActivity.getWindowConfiguration().getBounds().height());
assertEquals(origAppBounds.width(),
mActivity.getWindowConfiguration().getAppBounds().width());
assertEquals(origAppBounds.height(),
mActivity.getWindowConfiguration().getAppBounds().height());
}
@Test
public void testFixedAspOrientChangeOrient() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build());
prepareUnresizable(1.4f /* maxAspect */, SCREEN_ORIENTATION_LANDSCAPE);
assertTrue(mActivity.inSizeCompatMode());
final Rect originalBounds = new Rect(mActivity.getBounds());
final Rect originalAppBounds = new Rect(mActivity.getWindowConfiguration().getAppBounds());
// Change the fixed orientation
mActivity.mOrientation = SCREEN_ORIENTATION_PORTRAIT;
mActivity.info.screenOrientation = SCREEN_ORIENTATION_PORTRAIT;
// TaskRecord's configuration actually depends on the activity config right now for
// pillarboxing.
mActivity.getTask().onRequestedOverrideConfigurationChanged(
mActivity.getTask().getRequestedOverrideConfiguration());
assertEquals(originalBounds.width(), mActivity.getBounds().height());
assertEquals(originalBounds.height(), mActivity.getBounds().width());
assertEquals(originalAppBounds.width(),
mActivity.getWindowConfiguration().getAppBounds().height());
assertEquals(originalAppBounds.height(),
mActivity.getWindowConfiguration().getAppBounds().width());
}
@Test
public void testFixedScreenLayoutSizeBits() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build());
final int fixedScreenLayout = Configuration.SCREENLAYOUT_LONG_NO
| Configuration.SCREENLAYOUT_SIZE_NORMAL
| Configuration.SCREENLAYOUT_COMPAT_NEEDED;
final int layoutMask = Configuration.SCREENLAYOUT_LONG_MASK
| Configuration.SCREENLAYOUT_SIZE_MASK
| Configuration.SCREENLAYOUT_LAYOUTDIR_MASK
| Configuration.SCREENLAYOUT_COMPAT_NEEDED;
Configuration c = new Configuration(mTask.getRequestedOverrideConfiguration());
c.screenLayout = fixedScreenLayout | Configuration.SCREENLAYOUT_LAYOUTDIR_LTR;
mTask.onRequestedOverrideConfigurationChanged(c);
prepareUnresizable(1.5f, SCREEN_ORIENTATION_UNSPECIFIED);
// The initial configuration should inherit from parent.
assertEquals(fixedScreenLayout | Configuration.SCREENLAYOUT_LAYOUTDIR_LTR,
mActivity.getConfiguration().screenLayout & layoutMask);
mTask.getConfiguration().screenLayout = Configuration.SCREENLAYOUT_LAYOUTDIR_RTL
| Configuration.SCREENLAYOUT_LONG_YES | Configuration.SCREENLAYOUT_SIZE_LARGE;
mActivity.onConfigurationChanged(mTask.getConfiguration());
// The size and aspect ratio bits don't change, but the layout direction should be updated.
assertEquals(fixedScreenLayout | Configuration.SCREENLAYOUT_LAYOUTDIR_RTL,
mActivity.getConfiguration().screenLayout & layoutMask);
}
@Test
public void testResetNonVisibleActivity() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2500).build());
final ActivityDisplay display = mStack.getDisplay();
spyOn(display);
prepareUnresizable(1.5f, SCREEN_ORIENTATION_UNSPECIFIED);
mActivity.setState(STOPPED, "testSizeCompatMode");
mActivity.visible = false;
mActivity.app.setReportedProcState(ActivityManager.PROCESS_STATE_CACHED_ACTIVITY);
// Make the parent bounds to be different so the activity is in size compatibility mode.
mTask.getWindowConfiguration().setAppBounds(new Rect(0, 0, 600, 1200));
// Simulate the display changes orientation.
when(display.getLastOverrideConfigurationChanges()).thenReturn(
ActivityInfo.CONFIG_SCREEN_SIZE | CONFIG_ORIENTATION
| ActivityInfo.CONFIG_WINDOW_CONFIGURATION);
mActivity.onConfigurationChanged(mTask.getConfiguration());
when(display.getLastOverrideConfigurationChanges()).thenCallRealMethod();
// The override configuration should not change so it is still in size compatibility mode.
assertTrue(mActivity.inSizeCompatMode());
// Change display density
final DisplayContent displayContent = mStack.getDisplay().mDisplayContent;
displayContent.mBaseDisplayDensity = (int) (0.7f * displayContent.mBaseDisplayDensity);
final Configuration c = new Configuration();
displayContent.computeScreenConfiguration(c);
mService.mAmInternal = mock(ActivityManagerInternal.class);
mStack.getDisplay().onRequestedOverrideConfigurationChanged(c);
// The override configuration should be reset and the activity's process will be killed.
assertFalse(mActivity.inSizeCompatMode());
verify(mActivity).restartProcessIfVisible();
mLockRule.runWithScissors(mService.mH, () -> { }, TimeUnit.SECONDS.toMillis(3));
verify(mService.mAmInternal).killProcess(
eq(mActivity.app.mName), eq(mActivity.app.mUid), anyString());
}
/**
* Ensures that {@link TaskStackListener} can receive callback about the activity in size
* compatibility mode.
*/
@Test
public void testHandleActivitySizeCompatMode() {
setUpApp(new TestActivityDisplay.Builder(mService, 1000, 2000).build());
ActivityRecord activity = mActivity;
activity.setState(ActivityStack.ActivityState.RESUMED, "testHandleActivitySizeCompatMode");
prepareUnresizable(-1.f /* maxAspect */, SCREEN_ORIENTATION_PORTRAIT);
ensureActivityConfiguration();
assertFalse(mActivity.inSizeCompatMode());
final ArrayList<IBinder> compatTokens = new ArrayList<>();
mService.getTaskChangeNotificationController().registerTaskStackListener(
new TaskStackListener() {
@Override
public void onSizeCompatModeActivityChanged(int displayId,
IBinder activityToken) {
compatTokens.add(activityToken);
}
});
// Resize the display so that the activity exercises size-compat mode.
resizeDisplay(mStack.getDisplay(), 1000, 2500);
// Expect the exact token when the activity is in size compatibility mode.
assertEquals(1, compatTokens.size());
assertEquals(activity.appToken, compatTokens.get(0));
compatTokens.clear();
// Make the activity resizable again by restarting it
activity.info.resizeMode = ActivityInfo.RESIZE_MODE_RESIZEABLE;
activity.visible = true;
activity.restartProcessIfVisible();
// The full lifecycle isn't hooked up so manually set state to resumed
activity.setState(ActivityStack.ActivityState.RESUMED, "testHandleActivitySizeCompatMode");
mStack.getDisplay().handleActivitySizeCompatModeIfNeeded(activity);
// Expect null token when switching to non-size-compat mode activity.
assertEquals(1, compatTokens.size());
assertEquals(null, compatTokens.get(0));
}
/**
* Setup {@link #mActivity} as a size-compat-mode-able activity with fixed aspect and/or
* orientation.
*/
private void prepareUnresizable(float maxAspect, int screenOrientation) {
mActivity.info.resizeMode = RESIZE_MODE_UNRESIZEABLE;
mActivity.visible = true;
if (maxAspect >= 0) {
mActivity.info.maxAspectRatio = maxAspect;
}
if (screenOrientation != SCREEN_ORIENTATION_UNSPECIFIED) {
mActivity.mOrientation = screenOrientation;
mActivity.info.screenOrientation = screenOrientation;
// TaskRecord's configuration actually depends on the activity config right now for
// pillarboxing.
mActivity.getTask().onRequestedOverrideConfigurationChanged(
mActivity.getTask().getRequestedOverrideConfiguration());
}
ensureActivityConfiguration();
}
private void resizeDisplay(ActivityDisplay display, int width, int height) {
final DisplayContent displayContent = display.mDisplayContent;
displayContent.mBaseDisplayWidth = width;
displayContent.mBaseDisplayHeight = height;
Configuration c = new Configuration();
displayContent.computeScreenConfiguration(c);
display.onRequestedOverrideConfigurationChanged(c);
}
}