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_restoreLastSize()357     public void getEntryDestinationBounds_reentryStateExists_restoreLastSize() {
358         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
359         final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
360         reentryBounds.scale(1.25f);
361         final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds);
362 
363         mPipBoundsState.saveReentryState(
364                 new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction);
365         final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
366 
367         assertEquals(reentryBounds.width(), destinationBounds.width());
368         assertEquals(reentryBounds.height(), destinationBounds.height());
369     }
370 
371     @Test
getEntryDestinationBounds_reentryStateExists_restoreLastPosition()372     public void getEntryDestinationBounds_reentryStateExists_restoreLastPosition() {
373         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
374         final Rect reentryBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
375         reentryBounds.offset(0, -100);
376         final float reentrySnapFraction = mPipBoundsAlgorithm.getSnapFraction(reentryBounds);
377 
378         mPipBoundsState.saveReentryState(
379                 new Size(reentryBounds.width(), reentryBounds.height()), reentrySnapFraction);
380 
381         final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
382 
383         assertBoundsInclusionWithMargin("restoreLastPosition", reentryBounds, destinationBounds);
384     }
385 
386     @Test
setShelfHeight_offsetBounds()387     public void setShelfHeight_offsetBounds() {
388         final int shelfHeight = 100;
389         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
390         final Rect oldPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
391 
392         mPipBoundsState.setShelfVisibility(true, shelfHeight);
393         final Rect newPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
394 
395         oldPosition.offset(0, -shelfHeight);
396         assertBoundsInclusionWithMargin("offsetBounds by shelf", oldPosition, newPosition);
397     }
398 
399     @Test
onImeVisibilityChanged_offsetBounds()400     public void onImeVisibilityChanged_offsetBounds() {
401         final int imeHeight = 100;
402         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
403         final Rect oldPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
404 
405         mPipBoundsState.setImeVisibility(true, imeHeight);
406         final Rect newPosition = mPipBoundsAlgorithm.getEntryDestinationBounds();
407 
408         oldPosition.offset(0, -imeHeight);
409         assertBoundsInclusionWithMargin("offsetBounds by IME", oldPosition, newPosition);
410     }
411 
412     @Test
getEntryDestinationBounds_noReentryState_useDefaultBounds()413     public void getEntryDestinationBounds_noReentryState_useDefaultBounds() {
414         mPipBoundsState.setAspectRatio(DEFAULT_ASPECT_RATIO);
415         final Rect defaultBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
416 
417         mPipBoundsState.clearReentryState();
418 
419         final Rect actualBounds = mPipBoundsAlgorithm.getEntryDestinationBounds();
420 
421         assertBoundsInclusionWithMargin("useDefaultBounds", defaultBounds, actualBounds);
422     }
423 
424     @Test
adjustNormalBoundsToFitMenu_alreadyFits()425     public void adjustNormalBoundsToFitMenu_alreadyFits() {
426         final Rect normalBounds = new Rect(0, 0, 400, 711);
427         final Size minMenuSize = new Size(396, 292);
428         mPipBoundsState.setAspectRatio(
429                 ((float) normalBounds.width()) / ((float) normalBounds.height()));
430 
431         final Rect bounds =
432                 mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
433 
434         assertEquals(normalBounds, bounds);
435     }
436 
437     @Test
adjustNormalBoundsToFitMenu_widthTooSmall()438     public void adjustNormalBoundsToFitMenu_widthTooSmall() {
439         final Rect normalBounds = new Rect(0, 0, 297, 528);
440         final Size minMenuSize = new Size(396, 292);
441         mPipBoundsState.setAspectRatio(
442                 ((float) normalBounds.width()) / ((float) normalBounds.height()));
443 
444         final Rect bounds =
445                 mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
446 
447         assertEquals(minMenuSize.getWidth(), bounds.width());
448         assertEquals(minMenuSize.getWidth() / mPipBoundsState.getAspectRatio(),
449                 bounds.height(), 0.3f);
450     }
451 
452     @Test
adjustNormalBoundsToFitMenu_heightTooSmall()453     public void adjustNormalBoundsToFitMenu_heightTooSmall() {
454         final Rect normalBounds = new Rect(0, 0, 400, 280);
455         final Size minMenuSize = new Size(396, 292);
456         mPipBoundsState.setAspectRatio(
457                 ((float) normalBounds.width()) / ((float) normalBounds.height()));
458 
459         final Rect bounds =
460                 mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
461 
462         assertEquals(minMenuSize.getHeight(), bounds.height());
463         assertEquals(minMenuSize.getHeight() * mPipBoundsState.getAspectRatio(),
464                 bounds.width(), 0.3f);
465     }
466 
467     @Test
adjustNormalBoundsToFitMenu_widthAndHeightTooSmall()468     public void adjustNormalBoundsToFitMenu_widthAndHeightTooSmall() {
469         final Rect normalBounds = new Rect(0, 0, 350, 280);
470         final Size minMenuSize = new Size(396, 292);
471         mPipBoundsState.setAspectRatio(
472                 ((float) normalBounds.width()) / ((float) normalBounds.height()));
473 
474         final Rect bounds =
475                 mPipBoundsAlgorithm.adjustNormalBoundsToFitMenu(normalBounds, minMenuSize);
476 
477         assertEquals(minMenuSize.getWidth(), bounds.width());
478         assertEquals(minMenuSize.getWidth() / mPipBoundsState.getAspectRatio(),
479                 bounds.height(), 0.3f);
480     }
481 
overrideDefaultAspectRatio(float aspectRatio)482     private void overrideDefaultAspectRatio(float aspectRatio) {
483         final TestableResources res = mContext.getOrCreateTestableResources();
484         res.addOverride(
485                 R.dimen.config_pictureInPictureDefaultAspectRatio,
486                 aspectRatio);
487         mPipBoundsAlgorithm.onConfigurationChanged(mContext);
488     }
489 
overrideDefaultStackGravity(int stackGravity)490     private void overrideDefaultStackGravity(int stackGravity) {
491         final TestableResources res = mContext.getOrCreateTestableResources();
492         res.addOverride(
493                 R.integer.config_defaultPictureInPictureGravity,
494                 stackGravity);
495         mPipBoundsAlgorithm.onConfigurationChanged(mContext);
496     }
497 
assertBoundsInclusionWithMargin(String from, Rect expected, Rect actual)498     private void assertBoundsInclusionWithMargin(String from, Rect expected, Rect actual) {
499         final Rect expectedWithMargin = new Rect(expected);
500         expectedWithMargin.inset(-ROUNDING_ERROR_MARGIN, -ROUNDING_ERROR_MARGIN);
501         assertTrue(from + ": expect " + expected
502                 + " contains " + actual
503                 + " with error margin " + ROUNDING_ERROR_MARGIN,
504                 expectedWithMargin.contains(actual));
505     }
506 
getRectAspectRatio(Rect rect)507     private static float getRectAspectRatio(Rect rect) {
508         return rect.width() / (rect.height() * 1f);
509     }
510 }
511