blob: 262e4290ef44bec1c51950711fa89c65a6cc9ea6 [file] [log] [blame]
/*
* Copyright (C) 2020 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.wm.shell.pip;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue;
import android.graphics.Rect;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
import android.testing.TestableResources;
import android.util.Size;
import android.view.DisplayInfo;
import android.view.Gravity;
import androidx.test.filters.SmallTest;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.common.DisplayLayout;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
/**
* Unit tests against {@link PipBoundsAlgorithm}, including but not limited to:
* - default/movement bounds
* - save/restore PiP position on application lifecycle
* - save/restore PiP position on screen rotation
*/
@RunWith(AndroidTestingRunner.class)
@SmallTest
@TestableLooper.RunWithLooper(setAsMainLooper = true)
public class PipBoundsAlgorithmTest extends ShellTestCase {
private static final int ROUNDING_ERROR_MARGIN = 16;
private static final float ASPECT_RATIO_ERROR_MARGIN = 0.01f;
private static final float DEFAULT_ASPECT_RATIO = 1f;
private static final float MIN_ASPECT_RATIO = 0.5f;
private static final float MAX_ASPECT_RATIO = 2f;
private static final int DEFAULT_MIN_EDGE_SIZE = 100;
private PipBoundsAlgorithm mPipBoundsAlgorithm;
private DisplayInfo mDefaultDisplayInfo;
private PipBoundsState mPipBoundsState;
@Before
public void setUp() throws Exception {
initializeMockResources();
mPipBoundsState = new PipBoundsState(mContext);
mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
new PipSnapAlgorithm(), new PipKeepClearAlgorithm() {});
mPipBoundsState.setDisplayLayout(
new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true));
}
private void initializeMockResources() {
final TestableResources res = mContext.getOrCreateTestableResources();
res.addOverride(
R.dimen.config_pictureInPictureDefaultAspectRatio,
DEFAULT_ASPECT_RATIO);
res.addOverride(
R.integer.config_defaultPictureInPictureGravity,
Gravity.END | Gravity.BOTTOM);
res.addOverride(
R.dimen.default_minimal_size_pip_resizable_task,
DEFAULT_MIN_EDGE_SIZE);
res.addOverride(
R.string.config_defaultPictureInPictureScreenEdgeInsets,
"16x16");
res.addOverride(
com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio,
MIN_ASPECT_RATIO);
res.addOverride(
com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio,
MAX_ASPECT_RATIO);
mDefaultDisplayInfo = new DisplayInfo();
mDefaultDisplayInfo.displayId = 1;
mDefaultDisplayInfo.logicalWidth = 1000;
mDefaultDisplayInfo.logicalHeight = 1500;
}
@Test
public void getDefaultAspectRatio() {
assertEquals("Default aspect ratio matches resources",
DEFAULT_ASPECT_RATIO, mPipBoundsAlgorithm.getDefaultAspectRatio(),
ASPECT_RATIO_ERROR_MARGIN);
}
@Test
public void onConfigurationChanged_reloadResources() {
final float newDefaultAspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
final TestableResources res = mContext.getOrCreateTestableResources();
res.addOverride(R.dimen.config_pictureInPictureDefaultAspectRatio,
newDefaultAspectRatio);
mPipBoundsAlgorithm.onConfigurationChanged(mContext);
assertEquals("Default aspect ratio should be reloaded",
mPipBoundsAlgorithm.getDefaultAspectRatio(), newDefaultAspectRatio,
ASPECT_RATIO_ERROR_MARGIN);
}
@Test
public void getDefaultBounds_noOverrideMinSize_matchesDefaultSizeAndAspectRatio() {
final Size defaultSize = mPipBoundsAlgorithm.getSizeForAspectRatio(DEFAULT_ASPECT_RATIO,
DEFAULT_MIN_EDGE_SIZE, mDefaultDisplayInfo.logicalWidth,
mDefaultDisplayInfo.logicalHeight);
mPipBoundsState.setOverrideMinSize(null);
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
assertEquals(defaultSize, new Size(defaultBounds.width(), defaultBounds.height()));
assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds),
ASPECT_RATIO_ERROR_MARGIN);
}
@Test
public void getDefaultBounds_widerOverrideMinSize_matchesMinSizeWidthAndDefaultAspectRatio() {
overrideDefaultAspectRatio(1.0f);
// The min size's aspect ratio is greater than the default aspect ratio.
final Size overrideMinSize = new Size(150, 120);
mPipBoundsState.setOverrideMinSize(overrideMinSize);
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
// The default aspect ratio should trump the min size aspect ratio.
assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds),
ASPECT_RATIO_ERROR_MARGIN);
// The width of the min size is still used with the default aspect ratio.
assertEquals(overrideMinSize.getWidth(), defaultBounds.width());
}
@Test
public void getDefaultBounds_tallerOverrideMinSize_matchesMinSizeHeightAndDefaultAspectRatio() {
overrideDefaultAspectRatio(1.0f);
// The min size's aspect ratio is greater than the default aspect ratio.
final Size overrideMinSize = new Size(120, 150);
mPipBoundsState.setOverrideMinSize(overrideMinSize);
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
// The default aspect ratio should trump the min size aspect ratio.
assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds),
ASPECT_RATIO_ERROR_MARGIN);
// The height of the min size is still used with the default aspect ratio.
assertEquals(overrideMinSize.getHeight(), defaultBounds.height());
}
@Test
public void getDefaultBounds_imeShowing_offsetByImeHeight() {
final int imeHeight = 30;
mPipBoundsState.setImeVisibility(false, 0);
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
mPipBoundsState.setImeVisibility(true, imeHeight);
final Rect defaultBoundsWithIme = mPipBoundsAlgorithm.getDefaultBounds();
assertEquals(imeHeight, defaultBounds.top - defaultBoundsWithIme.top);
}
@Test
public void getDefaultBounds_shelfShowing_offsetByShelfHeight() {
final int shelfHeight = 30;
mPipBoundsState.setShelfVisibility(false, 0);
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
mPipBoundsState.setShelfVisibility(true, shelfHeight);
final Rect defaultBoundsWithShelf = mPipBoundsAlgorithm.getDefaultBounds();
assertEquals(shelfHeight, defaultBounds.top - defaultBoundsWithShelf.top);
}
@Test
public void getDefaultBounds_imeAndShelfShowing_offsetByTallest() {
final int imeHeight = 30;
final int shelfHeight = 40;
mPipBoundsState.setImeVisibility(false, 0);
mPipBoundsState.setShelfVisibility(false, 0);
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
mPipBoundsState.setImeVisibility(true, imeHeight);
mPipBoundsState.setShelfVisibility(true, shelfHeight);
final Rect defaultBoundsWithIme = mPipBoundsAlgorithm.getDefaultBounds();
assertEquals(shelfHeight, defaultBounds.top - defaultBoundsWithIme.top);
}
@Test
public void getDefaultBounds_boundsAtDefaultGravity() {
final Rect insetBounds = new Rect();
mPipBoundsAlgorithm.getInsetBounds(insetBounds);
overrideDefaultStackGravity(Gravity.END | Gravity.BOTTOM);
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
assertEquals(insetBounds.bottom, defaultBounds.bottom);
assertEquals(insetBounds.right, defaultBounds.right);
}
@Test
public void getNormalBounds_invalidAspectRatio_returnsDefaultBounds() {
final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
// Set an invalid current aspect ratio.
mPipBoundsState.setAspectRatio(MIN_ASPECT_RATIO / 2);
final Rect normalBounds = mPipBoundsAlgorithm.getNormalBounds();
assertEquals(defaultBounds, normalBounds);
}
@Test
public void getNormalBounds_validAspectRatio_returnsAdjustedDefaultBounds() {
final Rect defaultBoundsAdjustedToAspectRatio = mPipBoundsAlgorithm.getDefaultBounds();
mPipBoundsAlgorithm.transformBoundsToAspectRatio(defaultBoundsAdjustedToAspectRatio,
MIN_ASPECT_RATIO, false /* useCurrentMinEdgeSize */, false /* useCurrentSize */);
// Set a valid current aspect ratio different that the default.
mPipBoundsState.setAspectRatio(MIN_ASPECT_RATIO);
final Rect normalBounds = mPipBoundsAlgorithm.getNormalBounds();
assertEquals(defaultBoundsAdjustedToAspectRatio, normalBounds);
}
@Test
public void getEntryDestinationBounds_returnBoundsMatchesAspectRatio() {
final float[] aspectRatios = new float[] {
(MIN_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2,
DEFAULT_ASPECT_RATIO,
(MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
};
for (float aspectRatio : aspectRatios) {
mPipBoundsState.setAspectRatio(aspectRatio);
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
final float actualAspectRatio = getRectAspectRatio(destinationBounds);
assertEquals("Destination bounds matches the given aspect ratio",
aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
}
}
@Test
public void getEntryDestinationBounds_invalidAspectRatio_returnsDefaultAspectRatio() {
final float[] invalidAspectRatios = new float[] {
MIN_ASPECT_RATIO / 2,
MAX_ASPECT_RATIO * 2
};
for (float aspectRatio : invalidAspectRatios) {
mPipBoundsState.setAspectRatio(aspectRatio);
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
final float actualAspectRatio =
destinationBounds.width() / (destinationBounds.height() * 1f);
assertEquals("Destination bounds fallbacks to default aspect ratio",
mPipBoundsAlgorithm.getDefaultAspectRatio(), actualAspectRatio,
ASPECT_RATIO_ERROR_MARGIN);
}
}
@Test
public void getAdjustedDestinationBounds_returnBoundsMatchesAspectRatio() {
final float aspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
final Rect currentBounds = new Rect(0, 0, 0, 100);
currentBounds.right = (int) (currentBounds.height() * aspectRatio) + currentBounds.left;
mPipBoundsState.setAspectRatio(aspectRatio);
final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds(
currentBounds, aspectRatio);
final float actualAspectRatio =
destinationBounds.width() / (destinationBounds.height() * 1f);
assertEquals("Destination bounds matches the given aspect ratio",
aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
}
@Test
public void getEntryDestinationBounds_withMinSize_returnMinBounds() {
final float[] aspectRatios = new float[] {
(MIN_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2,
DEFAULT_ASPECT_RATIO,
(MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
};
final Size[] minimalSizes = new Size[] {
new Size((int) (100 * aspectRatios[0]), 100),
new Size((int) (100 * aspectRatios[1]), 100),
new Size((int) (100 * aspectRatios[2]), 100)
};
for (int i = 0; i < aspectRatios.length; i++) {
final float aspectRatio = aspectRatios[i];
final Size minimalSize = minimalSizes[i];
mPipBoundsState.setAspectRatio(aspectRatio);
mPipBoundsState.setOverrideMinSize(minimalSize);
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
assertTrue("Destination bounds is no smaller than minimal requirement",
(destinationBounds.width() == minimalSize.getWidth()
&& destinationBounds.height() >= minimalSize.getHeight())
|| (destinationBounds.height() == minimalSize.getHeight()
&& destinationBounds.width() >= minimalSize.getWidth()));
final float actualAspectRatio =
destinationBounds.width() / (destinationBounds.height() * 1f);
assertEquals("Destination bounds matches the given aspect ratio",
aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
}
}
@Test
public void getAdjustedDestinationBounds_ignoreMinBounds() {
final float aspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
final Rect currentBounds = new Rect(0, 0, 0, 100);
currentBounds.right = (int) (currentBounds.height() * aspectRatio) + currentBounds.left;
final Size minSize = new Size(currentBounds.width() / 2, currentBounds.height() / 2);
mPipBoundsState.setAspectRatio(aspectRatio);
mPipBoundsState.setOverrideMinSize(minSize);
final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds(
currentBounds, aspectRatio);
assertTrue("Destination bounds ignores minimal size",
destinationBounds.width() > minSize.getWidth()
&& destinationBounds.height() > minSize.getHeight());
}
@Test
public void getEntryDestinationBounds_reentryStateExists_restoreLastSize() {
mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
reentryBounds.scale(1.25f);
final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds);
mPipBoundsState.saveReentryState(
new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction);
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
assertEquals(reentryBounds.width(), destinationBounds.width());
assertEquals(reentryBounds.height(), destinationBounds.height());
}
@Test
public void getEntryDestinationBounds_reentryStateExists_restoreLastPosition() {
mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
reentryBounds.offset(0, -100);
final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds);
mPipBoundsState.saveReentryState(
new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction);
final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
assertBoundsInclusionWithMargin("restoreLastPosition", reentryBounds, destinationBounds);
}
@Test
public void setShelfHeight_offsetBounds() {
final int shelfHeight = 100;
mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
final Rect oldPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
mPipBoundsState.setShelfVisibility(true, shelfHeight);
final Rect newPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
oldPosition.offset(0, -shelfHeight);
assertBoundsInclusionWithMargin("offsetBounds by shelf", oldPosition, newPosition);
}
@Test
public void onImeVisibilityChanged_offsetBounds() {
final int imeHeight = 100;
mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
final Rect oldPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
mPipBoundsState.setImeVisibility(true, imeHeight);
final Rect newPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
oldPosition.offset(0, -imeHeight);
assertBoundsInclusionWithMargin("offsetBounds by IME", oldPosition, newPosition);
}
@Test
public void getEntryDestinationBounds_noReentryState_useDefaultBounds() {
mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
final Rect defaultBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
mPipBoundsState.clearReentryState();
final Rect actualBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
assertBoundsInclusionWithMargin("useDefaultBounds", defaultBounds, actualBounds);
}
@Test
public void adjustNormalBoundsToFitMenu_alreadyFits() {
final Rect normalBounds = new Rect(0, 0, 400, 711);
final Size minMenuSize = new Size(396, 292);
mPipBoundsState.setAspectRatio(
((float) normalBounds.width()) / ((float) normalBounds.height()));
final Rect bounds =
mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
assertEquals(normalBounds, bounds);
}
@Test
public void adjustNormalBoundsToFitMenu_widthTooSmall() {
final Rect normalBounds = new Rect(0, 0, 297, 528);
final Size minMenuSize = new Size(396, 292);
mPipBoundsState.setAspectRatio(
((float) normalBounds.width()) / ((float) normalBounds.height()));
final Rect bounds =
mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
assertEquals(minMenuSize.getWidth(), bounds.width());
assertEquals(minMenuSize.getWidth() / mPipBoundsState.getAspectRatio(),
bounds.height(), 0.3f);
}
@Test
public void adjustNormalBoundsToFitMenu_heightTooSmall() {
final Rect normalBounds = new Rect(0, 0, 400, 280);
final Size minMenuSize = new Size(396, 292);
mPipBoundsState.setAspectRatio(
((float) normalBounds.width()) / ((float) normalBounds.height()));
final Rect bounds =
mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
assertEquals(minMenuSize.getHeight(), bounds.height());
assertEquals(minMenuSize.getHeight() * mPipBoundsState.getAspectRatio(),
bounds.width(), 0.3f);
}
@Test
public void adjustNormalBoundsToFitMenu_widthAndHeightTooSmall() {
final Rect normalBounds = new Rect(0, 0, 350, 280);
final Size minMenuSize = new Size(396, 292);
mPipBoundsState.setAspectRatio(
((float) normalBounds.width()) / ((float) normalBounds.height()));
final Rect bounds =
mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
assertEquals(minMenuSize.getWidth(), bounds.width());
assertEquals(minMenuSize.getWidth() / mPipBoundsState.getAspectRatio(),
bounds.height(), 0.3f);
}
private void overrideDefaultAspectRatio(float aspectRatio) {
final TestableResources res = mContext.getOrCreateTestableResources();
res.addOverride(
R.dimen.config_pictureInPictureDefaultAspectRatio,
aspectRatio);
mPipBoundsAlgorithm.onConfigurationChanged(mContext);
}
private void overrideDefaultStackGravity(int stackGravity) {
final TestableResources res = mContext.getOrCreateTestableResources();
res.addOverride(
R.integer.config_defaultPictureInPictureGravity,
stackGravity);
mPipBoundsAlgorithm.onConfigurationChanged(mContext);
}
private void assertBoundsInclusionWithMargin(String from, Rect expected, Rect actual) {
final Rect expectedWithMargin = new Rect(expected);
expectedWithMargin.inset(-ROUNDING_ERROR_MARGIN, -ROUNDING_ERROR_MARGIN);
assertTrue(from + ": expect " + expected
+ " contains " + actual
+ " with error margin " + ROUNDING_ERROR_MARGIN,
expectedWithMargin.contains(actual));
}
private static float getRectAspectRatio(Rect rect) {
return rect.width() / (rect.height() * 1f);
}
}