1 /*
2  * Copyright (C) 2020 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.wm.shell.pip;
18 
19 import static org.junit.Assert.assertEquals;
20 import static org.junit.Assert.assertTrue;
21 
22 import android.graphics.Rect;
23 import android.testing.AndroidTestingRunner;
24 import android.testing.TestableLooper;
25 import android.testing.TestableResources;
26 import android.util.Size;
27 import android.view.DisplayInfo;
28 import android.view.Gravity;
29 
30 import androidx.test.filters.SmallTest;
31 
32 import com.android.wm.shell.R;
33 import com.android.wm.shell.ShellTestCase;
34 import com.android.wm.shell.common.DisplayLayout;
35 import com.android.wm.shell.common.pip.PhoneSizeSpecSource;
36 import com.android.wm.shell.common.pip.PipBoundsAlgorithm;
37 import com.android.wm.shell.common.pip.PipBoundsState;
38 import com.android.wm.shell.common.pip.PipDisplayLayoutState;
39 import com.android.wm.shell.common.pip.PipKeepClearAlgorithmInterface;
40 import com.android.wm.shell.common.pip.PipSnapAlgorithm;
41 import com.android.wm.shell.common.pip.SizeSpecSource;
42 
43 import org.junit.Before;
44 import org.junit.Test;
45 import org.junit.runner.RunWith;
46 
47 /**
48  * Unit tests against {@link PipBoundsAlgorithm}, including but not limited to:
49  * - default/movement bounds
50  * - save/restore PiP position on application lifecycle
51  * - save/restore PiP position on screen rotation
52  */
53 @RunWith(AndroidTestingRunner.class)
54 @SmallTest
55 @TestableLooper.RunWithLooper(setAsMainLooper = true)
56 public class PipBoundsAlgorithmTest extends ShellTestCase {
57     private static final int ROUNDING_ERROR_MARGIN = 16;
58     private static final float ASPECT_RATIO_ERROR_MARGIN = 0.01f;
59     private static final float DEFAULT_ASPECT_RATIO = 1f;
60     private static final float MIN_ASPECT_RATIO = 0.5f;
61     private static final float MAX_ASPECT_RATIO = 2f;
62     private static final int DEFAULT_MIN_EDGE_SIZE = 100;
63 
64     /** The minimum possible size of the override min size's width or height */
65     private static final int OVERRIDABLE_MIN_SIZE = 40;
66 
67     private PipBoundsAlgorithm mPipBoundsAlgorithm;
68     private DisplayInfo mDefaultDisplayInfo;
69     private PipBoundsState mPipBoundsState;
70     private SizeSpecSource mSizeSpecSource;
71     private PipDisplayLayoutState mPipDisplayLayoutState;
72 
73 
74     @Before
setUp()75     public void setUp() throws Exception {
76         initializeMockResources();
77         mPipDisplayLayoutState = new PipDisplayLayoutState(mContext);
78 
79         mSizeSpecSource = new PhoneSizeSpecSource(mContext, mPipDisplayLayoutState);
80         mPipBoundsState = new PipBoundsState(mContext, mSizeSpecSource, mPipDisplayLayoutState);
81         mPipBoundsAlgorithm = new PipBoundsAlgorithm(mContext, mPipBoundsState,
82                 new PipSnapAlgorithm(), new PipKeepClearAlgorithmInterface() {},
83                 mPipDisplayLayoutState, mSizeSpecSource);
84 
85         DisplayLayout layout =
86                 new DisplayLayout(mDefaultDisplayInfo, mContext.getResources(), true, true);
87         mPipDisplayLayoutState.setDisplayLayout(layout);
88     }
89 
initializeMockResources()90     private void initializeMockResources() {
91         final TestableResources res = mContext.getOrCreateTestableResources();
92         res.addOverride(
93                 R.dimen.config_pictureInPictureDefaultAspectRatio,
94                 DEFAULT_ASPECT_RATIO);
95         res.addOverride(
96                 R.integer.config_defaultPictureInPictureGravity,
97                 Gravity.END | Gravity.BOTTOM);
98         res.addOverride(
99                 R.dimen.default_minimal_size_pip_resizable_task,
100                 DEFAULT_MIN_EDGE_SIZE);
101         res.addOverride(
102                 R.dimen.overridable_minimal_size_pip_resizable_task,
103                 OVERRIDABLE_MIN_SIZE);
104         res.addOverride(
105                 R.string.config_defaultPictureInPictureScreenEdgeInsets,
106                 "16x16");
107         res.addOverride(
108                 com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio,
109                 MIN_ASPECT_RATIO);
110         res.addOverride(
111                 com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio,
112                 MAX_ASPECT_RATIO);
113 
114         mDefaultDisplayInfo = new DisplayInfo();
115         mDefaultDisplayInfo.displayId = 1;
116         mDefaultDisplayInfo.logicalWidth = 1000;
117         mDefaultDisplayInfo.logicalHeight = 1500;
118     }
119 
120     @Test
getDefaultAspectRatio()121     public void getDefaultAspectRatio() {
122         assertEquals("Default aspect ratio matches resources",
123                 DEFAULT_ASPECT_RATIO, mPipBoundsAlgorithm.getDefaultAspectRatio(),
124                 ASPECT_RATIO_ERROR_MARGIN);
125     }
126 
127     @Test
onConfigurationChanged_reloadResources()128     public void onConfigurationChanged_reloadResources() {
129         final float newDefaultAspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
130         final TestableResources res = mContext.getOrCreateTestableResources();
131         res.addOverride(R.dimen.config_pictureInPictureDefaultAspectRatio,
132                 newDefaultAspectRatio);
133 
134         mPipBoundsAlgorithm.onConfigurationChanged(mContext);
135 
136         assertEquals("Default aspect ratio should be reloaded",
137                 mPipBoundsAlgorithm.getDefaultAspectRatio(), newDefaultAspectRatio,
138                 ASPECT_RATIO_ERROR_MARGIN);
139     }
140 
141     @Test
getDefaultBounds_noOverrideMinSize_matchesDefaultSizeAndAspectRatio()142     public void getDefaultBounds_noOverrideMinSize_matchesDefaultSizeAndAspectRatio() {
143         final Size defaultSize = mSizeSpecSource.getDefaultSize(DEFAULT_ASPECT_RATIO);
144 
145         mPipBoundsState.setOverrideMinSize(null);
146         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
147 
148         assertEquals(defaultSize, new Size(defaultBounds.width(), defaultBounds.height()));
149         assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds),
150                 ASPECT_RATIO_ERROR_MARGIN);
151     }
152 
153     @Test
getDefaultBounds_widerOverrideMinSize_matchesMinSizeWidthAndDefaultAspectRatio()154     public void getDefaultBounds_widerOverrideMinSize_matchesMinSizeWidthAndDefaultAspectRatio() {
155         overrideDefaultAspectRatio(1.0f);
156         // The min size's aspect ratio is greater than the default aspect ratio.
157         final Size overrideMinSize = new Size(150, 120);
158 
159         mPipBoundsState.setOverrideMinSize(overrideMinSize);
160         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
161 
162         // The default aspect ratio should trump the min size aspect ratio.
163         assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds),
164                 ASPECT_RATIO_ERROR_MARGIN);
165         // The width of the min size is still used with the default aspect ratio.
166         assertEquals(overrideMinSize.getWidth(), defaultBounds.width());
167     }
168 
169     @Test
getDefaultBounds_tallerOverrideMinSize_matchesMinSizeHeightAndDefaultAspectRatio()170     public void getDefaultBounds_tallerOverrideMinSize_matchesMinSizeHeightAndDefaultAspectRatio() {
171         overrideDefaultAspectRatio(1.0f);
172         // The min size's aspect ratio is greater than the default aspect ratio.
173         final Size overrideMinSize = new Size(120, 150);
174 
175         mPipBoundsState.setOverrideMinSize(overrideMinSize);
176         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
177 
178         // The default aspect ratio should trump the min size aspect ratio.
179         assertEquals(DEFAULT_ASPECT_RATIO, getRectAspectRatio(defaultBounds),
180                 ASPECT_RATIO_ERROR_MARGIN);
181         // The height of the min size is still used with the default aspect ratio.
182         assertEquals(overrideMinSize.getHeight(), defaultBounds.height());
183     }
184 
185     @Test
getDefaultBounds_imeShowing_offsetByImeHeight()186     public void getDefaultBounds_imeShowing_offsetByImeHeight() {
187         final int imeHeight = 30;
188         mPipBoundsState.setImeVisibility(false, 0);
189         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
190 
191         mPipBoundsState.setImeVisibility(true, imeHeight);
192         final Rect defaultBoundsWithIme = mPipBoundsAlgorithm.getDefaultBounds();
193 
194         assertEquals(imeHeight, defaultBounds.top - defaultBoundsWithIme.top);
195     }
196 
197     @Test
getDefaultBounds_shelfShowing_offsetByShelfHeight()198     public void getDefaultBounds_shelfShowing_offsetByShelfHeight() {
199         final int shelfHeight = 30;
200         mPipBoundsState.setShelfVisibility(false, 0);
201         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
202 
203         mPipBoundsState.setShelfVisibility(true, shelfHeight);
204         final Rect defaultBoundsWithShelf = mPipBoundsAlgorithm.getDefaultBounds();
205 
206         assertEquals(shelfHeight, defaultBounds.top - defaultBoundsWithShelf.top);
207     }
208 
209     @Test
getDefaultBounds_imeAndShelfShowing_offsetByTallest()210     public void getDefaultBounds_imeAndShelfShowing_offsetByTallest() {
211         final int imeHeight = 30;
212         final int shelfHeight = 40;
213         mPipBoundsState.setImeVisibility(false, 0);
214         mPipBoundsState.setShelfVisibility(false, 0);
215         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
216 
217         mPipBoundsState.setImeVisibility(true, imeHeight);
218         mPipBoundsState.setShelfVisibility(true, shelfHeight);
219         final Rect defaultBoundsWithIme = mPipBoundsAlgorithm.getDefaultBounds();
220 
221         assertEquals(shelfHeight, defaultBounds.top - defaultBoundsWithIme.top);
222     }
223 
224     @Test
getDefaultBounds_boundsAtDefaultGravity()225     public void getDefaultBounds_boundsAtDefaultGravity() {
226         final Rect insetBounds = new Rect();
227         mPipBoundsAlgorithm.getInsetBounds(insetBounds);
228         overrideDefaultStackGravity(Gravity.END | Gravity.BOTTOM);
229 
230         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
231 
232         assertEquals(insetBounds.bottom, defaultBounds.bottom);
233         assertEquals(insetBounds.right, defaultBounds.right);
234     }
235 
236     @Test
getNormalBounds_invalidAspectRatio_returnsDefaultBounds()237     public void getNormalBounds_invalidAspectRatio_returnsDefaultBounds() {
238         final Rect defaultBounds = mPipBoundsAlgorithm.getDefaultBounds();
239 
240         // Set an invalid current aspect ratio.
241         mPipBoundsState.setAspectRatio(MIN_ASPECT_RATIO / 2);
242         final Rect normalBounds = mPipBoundsAlgorithm.getNormalBounds();
243 
244         assertEquals(defaultBounds, normalBounds);
245     }
246 
247     @Test
getNormalBounds_validAspectRatio_returnsAdjustedDefaultBounds()248     public void getNormalBounds_validAspectRatio_returnsAdjustedDefaultBounds() {
249         final Rect defaultBoundsAdjustedToAspectRatio = mPipBoundsAlgorithm.getDefaultBounds();
250         mPipBoundsAlgorithm.transformBoundsToAspectRatio(defaultBoundsAdjustedToAspectRatio,
251                 MIN_ASPECT_RATIO, false /* useCurrentMinEdgeSize */, false /* useCurrentSize */);
252 
253         // Set a valid current aspect ratio different that the default.
254         mPipBoundsState.setAspectRatio(MIN_ASPECT_RATIO);
255         final Rect normalBounds = mPipBoundsAlgorithm.getNormalBounds();
256 
257         assertEquals(defaultBoundsAdjustedToAspectRatio, normalBounds);
258     }
259 
260     @Test
getEntryDestinationBounds_returnBoundsMatchesAspectRatio()261     public void getEntryDestinationBounds_returnBoundsMatchesAspectRatio() {
262         final float[] aspectRatios = new float[] {
263                 (MIN_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2,
264                 DEFAULT_ASPECT_RATIO,
265                 (MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
266         };
267         for (float aspectRatio : aspectRatios) {
268             mPipBoundsState.setAspectRatio(aspectRatio);
269             final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
270             final float actualAspectRatio = getRectAspectRatio(destinationBounds);
271             assertEquals("Destination bounds matches the given aspect ratio",
272                     aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
273         }
274     }
275 
276     @Test
getEntryDestinationBounds_invalidAspectRatio_returnsDefaultAspectRatio()277     public void getEntryDestinationBounds_invalidAspectRatio_returnsDefaultAspectRatio() {
278         final float[] invalidAspectRatios = new float[] {
279                 MIN_ASPECT_RATIO / 2,
280                 MAX_ASPECT_RATIO * 2
281         };
282         for (float aspectRatio : invalidAspectRatios) {
283             mPipBoundsState.setAspectRatio(aspectRatio);
284             final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
285             final float actualAspectRatio =
286                     destinationBounds.width() / (destinationBounds.height() * 1f);
287             assertEquals("Destination bounds fallbacks to default aspect ratio",
288                     mPipBoundsAlgorithm.getDefaultAspectRatio(), actualAspectRatio,
289                     ASPECT_RATIO_ERROR_MARGIN);
290         }
291     }
292 
293     @Test
getAdjustedDestinationBounds_returnBoundsMatchesAspectRatio()294     public void  getAdjustedDestinationBounds_returnBoundsMatchesAspectRatio() {
295         final float aspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
296         final Rect currentBounds = new Rect(0, 0, 0, 100);
297         currentBounds.right = (int) (currentBounds.height() * aspectRatio) + currentBounds.left;
298 
299         mPipBoundsState.setAspectRatio(aspectRatio);
300         final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds(
301                 currentBounds, aspectRatio);
302 
303         final float actualAspectRatio =
304                 destinationBounds.width() / (destinationBounds.height() * 1f);
305         assertEquals("Destination bounds matches the given aspect ratio",
306                 aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
307     }
308 
309     @Test
getEntryDestinationBounds_withMinSize_returnMinBounds()310     public void getEntryDestinationBounds_withMinSize_returnMinBounds() {
311         final float[] aspectRatios = new float[] {
312                 (MIN_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2,
313                 DEFAULT_ASPECT_RATIO,
314                 (MAX_ASPECT_RATIO + DEFAULT_ASPECT_RATIO) / 2
315         };
316         final Size[] minimalSizes = new Size[] {
317                 new Size((int) (200 * aspectRatios[0]), 200),
318                 new Size((int) (200 * aspectRatios[1]), 200),
319                 new Size((int) (200 * aspectRatios[2]), 200)
320         };
321         for (int i = 0; i < aspectRatios.length; i++) {
322             final float aspectRatio = aspectRatios[i];
323             final Size minimalSize = minimalSizes[i];
324             mPipBoundsState.setAspectRatio(aspectRatio);
325             mPipBoundsState.setOverrideMinSize(minimalSize);
326             final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
327             assertTrue("Destination bounds is no smaller than minimal requirement",
328                     (destinationBounds.width() == minimalSize.getWidth()
329                             && destinationBounds.height() >= minimalSize.getHeight())
330                             || (destinationBounds.height() == minimalSize.getHeight()
331                             && destinationBounds.width() >= minimalSize.getWidth()));
332             final float actualAspectRatio =
333                     destinationBounds.width() / (destinationBounds.height() * 1f);
334             assertEquals("Destination bounds matches the given aspect ratio",
335                     aspectRatio, actualAspectRatio, ASPECT_RATIO_ERROR_MARGIN);
336         }
337     }
338 
339     @Test
getAdjustedDestinationBounds_ignoreMinBounds()340     public void getAdjustedDestinationBounds_ignoreMinBounds() {
341         final float aspectRatio = (DEFAULT_ASPECT_RATIO + MAX_ASPECT_RATIO) / 2;
342         final Rect currentBounds = new Rect(0, 0, 0, 100);
343         currentBounds.right = (int) (currentBounds.height() * aspectRatio) + currentBounds.left;
344         final Size minSize = new Size(currentBounds.width() / 2, currentBounds.height() / 2);
345 
346         mPipBoundsState.setAspectRatio(aspectRatio);
347         mPipBoundsState.setOverrideMinSize(minSize);
348         final Rect destinationBounds = mPipBoundsAlgorithm.getAdjustedDestinationBounds(
349                 currentBounds, aspectRatio);
350 
351         assertTrue("Destination bounds ignores minimal size",
352                 destinationBounds.width() > minSize.getWidth()
353                         && destinationBounds.height() > minSize.getHeight());
354     }
355 
356     @Test
getEntryDestinationBounds_reentryStateExists_restoreProportionalSize()357     public void getEntryDestinationBounds_reentryStateExists_restoreProportionalSize() {
358         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
359         final Size maxSize = mSizeSpecSource.getMaxSize(DEFAULT_ASPECT_RATIO);
360         mPipBoundsState.setMaxSize(maxSize.getWidth(), maxSize.getHeight());
361         final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
362         reentryBounds.scale(1.25f);
363         mPipBoundsState.setBounds(reentryBounds); // this updates the bounds scale used in reentry
364 
365         final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds);
366 
367         mPipBoundsState.saveReentryState(reentrySnapFraction);
368         final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
369 
370         assertEquals(reentryBounds.width(), destinationBounds.width());
371         assertEquals(reentryBounds.height(), destinationBounds.height());
372     }
373 
374     @Test
getEntryDestinationBounds_reentryStateExists_restoreLastPosition()375     public void getEntryDestinationBounds_reentryStateExists_restoreLastPosition() {
376         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
377         final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
378         reentryBounds.offset(0, -100);
379         final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds);
380 
381         mPipBoundsState.saveReentryState(reentrySnapFraction);
382 
383         final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
384 
385         assertBoundsInclusionWithMargin("restoreLastPosition", reentryBounds, destinationBounds);
386     }
387 
388     @Test
setShelfHeight_offsetBounds()389     public void setShelfHeight_offsetBounds() {
390         final int shelfHeight = 100;
391         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
392         final Rect oldPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
393 
394         mPipBoundsState.setShelfVisibility(true, shelfHeight);
395         final Rect newPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
396 
397         oldPosition.offset(0, -shelfHeight);
398         assertBoundsInclusionWithMargin("offsetBounds by shelf", oldPosition, newPosition);
399     }
400 
401     @Test
onImeVisibilityChanged_offsetBounds()402     public void onImeVisibilityChanged_offsetBounds() {
403         final int imeHeight = 100;
404         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
405         final Rect oldPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
406 
407         mPipBoundsState.setImeVisibility(true, imeHeight);
408         final Rect newPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
409 
410         oldPosition.offset(0, -imeHeight);
411         assertBoundsInclusionWithMargin("offsetBounds by IME", oldPosition, newPosition);
412     }
413 
414     @Test
getEntryDestinationBounds_noReentryState_useDefaultBounds()415     public void getEntryDestinationBounds_noReentryState_useDefaultBounds() {
416         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
417         final Rect defaultBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
418 
419         mPipBoundsState.clearReentryState();
420 
421         final Rect actualBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
422 
423         assertBoundsInclusionWithMargin("useDefaultBounds", defaultBounds, actualBounds);
424     }
425 
426     @Test
adjustNormalBoundsToFitMenu_alreadyFits()427     public void adjustNormalBoundsToFitMenu_alreadyFits() {
428         final Rect normalBounds = new Rect(0, 0, 400, 711);
429         final Size minMenuSize = new Size(396, 292);
430         mPipBoundsState.setAspectRatio(
431                 ((float) normalBounds.width()) / ((float) normalBounds.height()));
432 
433         final Rect bounds =
434                 mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
435 
436         assertEquals(normalBounds, bounds);
437     }
438 
439     @Test
adjustNormalBoundsToFitMenu_widthTooSmall()440     public void adjustNormalBoundsToFitMenu_widthTooSmall() {
441         final Rect normalBounds = new Rect(0, 0, 297, 528);
442         final Size minMenuSize = new Size(396, 292);
443         mPipBoundsState.setAspectRatio(
444                 ((float) normalBounds.width()) / ((float) normalBounds.height()));
445 
446         final Rect bounds =
447                 mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
448 
449         assertEquals(minMenuSize.getWidth(), bounds.width());
450         assertEquals(minMenuSize.getWidth() / mPipBoundsState.getAspectRatio(),
451                 bounds.height(), 0.3f);
452     }
453 
454     @Test
adjustNormalBoundsToFitMenu_heightTooSmall()455     public void adjustNormalBoundsToFitMenu_heightTooSmall() {
456         final Rect normalBounds = new Rect(0, 0, 400, 280);
457         final Size minMenuSize = new Size(396, 292);
458         mPipBoundsState.setAspectRatio(
459                 ((float) normalBounds.width()) / ((float) normalBounds.height()));
460 
461         final Rect bounds =
462                 mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
463 
464         assertEquals(minMenuSize.getHeight(), bounds.height());
465         assertEquals(minMenuSize.getHeight() * mPipBoundsState.getAspectRatio(),
466                 bounds.width(), 0.3f);
467     }
468 
469     @Test
adjustNormalBoundsToFitMenu_widthAndHeightTooSmall()470     public void adjustNormalBoundsToFitMenu_widthAndHeightTooSmall() {
471         final Rect normalBounds = new Rect(0, 0, 350, 280);
472         final Size minMenuSize = new Size(396, 292);
473         mPipBoundsState.setAspectRatio(
474                 ((float) normalBounds.width()) / ((float) normalBounds.height()));
475 
476         final Rect bounds =
477                 mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
478 
479         assertEquals(minMenuSize.getWidth(), bounds.width());
480         assertEquals(minMenuSize.getWidth() / mPipBoundsState.getAspectRatio(),
481                 bounds.height(), 0.3f);
482     }
483 
overrideDefaultAspectRatio(float aspectRatio)484     private void overrideDefaultAspectRatio(float aspectRatio) {
485         final TestableResources res = mContext.getOrCreateTestableResources();
486         res.addOverride(
487                 R.dimen.config_pictureInPictureDefaultAspectRatio,
488                 aspectRatio);
489         mPipBoundsAlgorithm.onConfigurationChanged(mContext);
490     }
491 
overrideDefaultStackGravity(int stackGravity)492     private void overrideDefaultStackGravity(int stackGravity) {
493         final TestableResources res = mContext.getOrCreateTestableResources();
494         res.addOverride(
495                 R.integer.config_defaultPictureInPictureGravity,
496                 stackGravity);
497         mPipBoundsAlgorithm.onConfigurationChanged(mContext);
498     }
499 
assertBoundsInclusionWithMargin(String from, Rect expected, Rect actual)500     private void assertBoundsInclusionWithMargin(String from, Rect expected, Rect actual) {
501         final Rect expectedWithMargin = new Rect(expected);
502         expectedWithMargin.inset(-ROUNDING_ERROR_MARGIN, -ROUNDING_ERROR_MARGIN);
503         assertTrue(from + ": expect " + expected
504                 + " contains " + actual
505                 + " with error margin " + ROUNDING_ERROR_MARGIN,
506                 expectedWithMargin.contains(actual));
507     }
508 
getRectAspectRatio(Rect rect)509     private static float getRectAspectRatio(Rect rect) {
510         return rect.width() / (rect.height() * 1f);
511     }
512 }
513