| /* |
| * 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); |
| } |
| } |