• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.app.PictureInPictureParams;
22 import android.content.Context;
23 import android.content.pm.ActivityInfo;
24 import android.content.res.Resources;
25 import android.graphics.Rect;
26 import android.util.DisplayMetrics;
27 import android.util.Size;
28 import android.view.Gravity;
29 
30 import com.android.wm.shell.R;
31 import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
32 
33 import java.io.PrintWriter;
34 
35 /**
36  * Calculates the default, normal, entry, inset and movement bounds of the PIP.
37  */
38 public class PipBoundsAlgorithm {
39 
40     private static final String TAG = PipBoundsAlgorithm.class.getSimpleName();
41     private static final float INVALID_SNAP_FRACTION = -1f;
42 
43     @NonNull private final PipBoundsState mPipBoundsState;
44     @NonNull protected final PipSizeSpecHandler mPipSizeSpecHandler;
45     private final PipSnapAlgorithm mSnapAlgorithm;
46     private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
47 
48     private float mDefaultAspectRatio;
49     private float mMinAspectRatio;
50     private float mMaxAspectRatio;
51     private int mDefaultStackGravity;
52 
PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState, @NonNull PipSnapAlgorithm pipSnapAlgorithm, @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, @NonNull PipSizeSpecHandler pipSizeSpecHandler)53     public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
54             @NonNull PipSnapAlgorithm pipSnapAlgorithm,
55             @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
56             @NonNull PipSizeSpecHandler pipSizeSpecHandler) {
57         mPipBoundsState = pipBoundsState;
58         mSnapAlgorithm = pipSnapAlgorithm;
59         mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
60         mPipSizeSpecHandler = pipSizeSpecHandler;
61         reloadResources(context);
62         // Initialize the aspect ratio to the default aspect ratio.  Don't do this in reload
63         // resources as it would clobber mAspectRatio when entering PiP from fullscreen which
64         // triggers a configuration change and the resources to be reloaded.
65         mPipBoundsState.setAspectRatio(mDefaultAspectRatio);
66     }
67 
68     /**
69      * TODO: move the resources to SysUI package.
70      */
reloadResources(Context context)71     private void reloadResources(Context context) {
72         final Resources res = context.getResources();
73         mDefaultAspectRatio = res.getFloat(
74                 R.dimen.config_pictureInPictureDefaultAspectRatio);
75         mDefaultStackGravity = res.getInteger(
76                 R.integer.config_defaultPictureInPictureGravity);
77         final String screenEdgeInsetsDpString = res.getString(
78                 R.string.config_defaultPictureInPictureScreenEdgeInsets);
79         final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
80                 ? Size.parseSize(screenEdgeInsetsDpString)
81                 : null;
82         mMinAspectRatio = res.getFloat(
83                 com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
84         mMaxAspectRatio = res.getFloat(
85                 com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
86     }
87 
88     /**
89      * The {@link PipSnapAlgorithm} is couple on display bounds
90      * @return {@link PipSnapAlgorithm}.
91      */
getSnapAlgorithm()92     public PipSnapAlgorithm getSnapAlgorithm() {
93         return mSnapAlgorithm;
94     }
95 
96     /** Responds to configuration change. */
onConfigurationChanged(Context context)97     public void onConfigurationChanged(Context context) {
98         reloadResources(context);
99     }
100 
101     /** Returns the normal bounds (i.e. the default entry bounds). */
getNormalBounds()102     public Rect getNormalBounds() {
103         // The normal bounds are the default bounds adjusted to the current aspect ratio.
104         return transformBoundsToAspectRatioIfValid(getDefaultBounds(),
105                 mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
106                 false /* useCurrentSize */);
107     }
108 
109     /** Returns the default bounds. */
getDefaultBounds()110     public Rect getDefaultBounds() {
111         return getDefaultBounds(INVALID_SNAP_FRACTION, null /* size */);
112     }
113 
114     /**
115      * Returns the destination bounds to place the PIP window on entry.
116      * If there are any keep clear areas registered, the position will try to avoid occluding them.
117      */
getEntryDestinationBounds()118     public Rect getEntryDestinationBounds() {
119         Rect entryBounds = getEntryDestinationBoundsIgnoringKeepClearAreas();
120         Rect insets = new Rect();
121         getInsetBounds(insets);
122         return mPipKeepClearAlgorithm.findUnoccludedPosition(entryBounds,
123                 mPipBoundsState.getRestrictedKeepClearAreas(),
124                 mPipBoundsState.getUnrestrictedKeepClearAreas(), insets);
125     }
126 
127     /** Returns the destination bounds to place the PIP window on entry. */
getEntryDestinationBoundsIgnoringKeepClearAreas()128     public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() {
129         final PipBoundsState.PipReentryState reentryState = mPipBoundsState.getReentryState();
130 
131         final Rect destinationBounds = reentryState != null
132                 ? getDefaultBounds(reentryState.getSnapFraction(), reentryState.getSize())
133                 : getDefaultBounds();
134 
135         final boolean useCurrentSize = reentryState != null && reentryState.getSize() != null;
136         Rect aspectRatioBounds = transformBoundsToAspectRatioIfValid(destinationBounds,
137                 mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
138                 useCurrentSize);
139         return aspectRatioBounds;
140     }
141 
142     /** Returns the current bounds adjusted to the new aspect ratio, if valid. */
getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio)143     public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
144         return transformBoundsToAspectRatioIfValid(currentBounds, newAspectRatio,
145                 true /* useCurrentMinEdgeSize */, false /* useCurrentSize */);
146     }
147 
148     /**
149      *
150      * Get the smallest/most minimal size allowed.
151      */
getMinimalSize(ActivityInfo activityInfo)152     public Size getMinimalSize(ActivityInfo activityInfo) {
153         if (activityInfo == null || activityInfo.windowLayout == null) {
154             return null;
155         }
156         final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout;
157         // -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout>
158         // without minWidth/minHeight
159         if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) {
160             // If either dimension is smaller than the allowed minimum, adjust them
161             // according to mOverridableMinSize
162             return new Size(
163                     Math.max(windowLayout.minWidth, mPipSizeSpecHandler.getOverrideMinEdgeSize()),
164                     Math.max(windowLayout.minHeight, mPipSizeSpecHandler.getOverrideMinEdgeSize()));
165         }
166         return null;
167     }
168 
169     /**
170      * Returns the source hint rect if it is valid (if provided and is contained by the current
171      * task bounds).
172      */
getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds)173     public static Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds) {
174         final Rect sourceHintRect = params != null && params.hasSourceBoundsHint()
175                 ? params.getSourceRectHint()
176                 : null;
177         if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) {
178             return sourceHintRect;
179         }
180         return null;
181     }
182 
getDefaultAspectRatio()183     public float getDefaultAspectRatio() {
184         return mDefaultAspectRatio;
185     }
186 
187     /**
188      *
189      * Give the aspect ratio if the supplied PiP params have one, or else return default.
190      */
getAspectRatioOrDefault( @ndroid.annotation.Nullable PictureInPictureParams params)191     public float getAspectRatioOrDefault(
192             @android.annotation.Nullable PictureInPictureParams params) {
193         return params != null && params.hasSetAspectRatio()
194                 ? params.getAspectRatioFloat()
195                 : getDefaultAspectRatio();
196     }
197 
198     /**
199      * @return whether the given {@param aspectRatio} is valid.
200      */
isValidPictureInPictureAspectRatio(float aspectRatio)201     public boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
202         return Float.compare(mMinAspectRatio, aspectRatio) <= 0
203                 && Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
204     }
205 
transformBoundsToAspectRatioIfValid(Rect bounds, float aspectRatio, boolean useCurrentMinEdgeSize, boolean useCurrentSize)206     private Rect transformBoundsToAspectRatioIfValid(Rect bounds, float aspectRatio,
207             boolean useCurrentMinEdgeSize, boolean useCurrentSize) {
208         final Rect destinationBounds = new Rect(bounds);
209         if (isValidPictureInPictureAspectRatio(aspectRatio)) {
210             transformBoundsToAspectRatio(destinationBounds, aspectRatio,
211                     useCurrentMinEdgeSize, useCurrentSize);
212         }
213         return destinationBounds;
214     }
215 
216     /**
217      * Set the current bounds (or the default bounds if there are no current bounds) with the
218      * specified aspect ratio.
219      */
transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio, boolean useCurrentMinEdgeSize, boolean useCurrentSize)220     public void transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio,
221             boolean useCurrentMinEdgeSize, boolean useCurrentSize) {
222         // Save the snap fraction and adjust the size based on the new aspect ratio.
223         final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
224                 getMovementBounds(stackBounds), mPipBoundsState.getStashedState());
225 
226         final Size size;
227         if (useCurrentMinEdgeSize || useCurrentSize) {
228             // Use the existing size but adjusted to the new aspect ratio.
229             size = mPipSizeSpecHandler.getSizeForAspectRatio(
230                     new Size(stackBounds.width(), stackBounds.height()), aspectRatio);
231         } else {
232             size = mPipSizeSpecHandler.getDefaultSize(aspectRatio);
233         }
234 
235         final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
236         final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
237         stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
238         mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
239     }
240 
241     /**
242      * @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are
243      * provided, then it will apply the default bounds to the provided snap fraction and size.
244      */
getDefaultBounds(float snapFraction, Size size)245     private Rect getDefaultBounds(float snapFraction, Size size) {
246         final Rect defaultBounds = new Rect();
247         if (snapFraction != INVALID_SNAP_FRACTION && size != null) {
248             // The default bounds are the given size positioned at the given snap fraction.
249             defaultBounds.set(0, 0, size.getWidth(), size.getHeight());
250             final Rect movementBounds = getMovementBounds(defaultBounds);
251             mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
252             return defaultBounds;
253         }
254 
255         // Calculate the default size.
256         final Size defaultSize;
257         final Rect insetBounds = new Rect();
258         getInsetBounds(insetBounds);
259 
260         // Calculate the default size
261         defaultSize = mPipSizeSpecHandler.getDefaultSize(mDefaultAspectRatio);
262 
263         // Now that we have the default size, apply the snap fraction if valid or position the
264         // bounds using the default gravity.
265         if (snapFraction != INVALID_SNAP_FRACTION) {
266             defaultBounds.set(0, 0, defaultSize.getWidth(), defaultSize.getHeight());
267             final Rect movementBounds = getMovementBounds(defaultBounds);
268             mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
269         } else {
270             Gravity.apply(mDefaultStackGravity, defaultSize.getWidth(), defaultSize.getHeight(),
271                     insetBounds, 0, Math.max(
272                             mPipBoundsState.isImeShowing() ? mPipBoundsState.getImeHeight() : 0,
273                             mPipBoundsState.isShelfShowing()
274                                     ? mPipBoundsState.getShelfHeight() : 0), defaultBounds);
275         }
276         return defaultBounds;
277     }
278 
279     /**
280      * Populates the bounds on the screen that the PIP can be visible in.
281      */
getInsetBounds(Rect outRect)282     public void getInsetBounds(Rect outRect) {
283         outRect.set(mPipSizeSpecHandler.getInsetBounds());
284     }
285 
286     /**
287      * @return the movement bounds for the given stackBounds and the current state of the
288      *         controller.
289      */
getMovementBounds(Rect stackBounds)290     public Rect getMovementBounds(Rect stackBounds) {
291         return getMovementBounds(stackBounds, true /* adjustForIme */);
292     }
293 
294     /**
295      * @return the movement bounds for the given stackBounds and the current state of the
296      *         controller.
297      */
getMovementBounds(Rect stackBounds, boolean adjustForIme)298     public Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
299         final Rect movementBounds = new Rect();
300         getInsetBounds(movementBounds);
301 
302         // Apply the movement bounds adjustments based on the current state.
303         getMovementBounds(stackBounds, movementBounds, movementBounds,
304                 (adjustForIme && mPipBoundsState.isImeShowing())
305                         ? mPipBoundsState.getImeHeight() : 0);
306 
307         return movementBounds;
308     }
309 
310     /**
311      * Adjusts movementBoundsOut so that it is the movement bounds for the given stackBounds.
312      */
getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut, int bottomOffset)313     public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
314             int bottomOffset) {
315         // Adjust the right/bottom to ensure the stack bounds never goes offscreen
316         movementBoundsOut.set(insetBounds);
317         movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right
318                 - stackBounds.width());
319         movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom
320                 - stackBounds.height());
321         movementBoundsOut.bottom -= bottomOffset;
322     }
323 
324     /**
325      * @return the default snap fraction to apply instead of the default gravity when calculating
326      *         the default stack bounds when first entering PiP.
327      */
getSnapFraction(Rect stackBounds)328     public float getSnapFraction(Rect stackBounds) {
329         return getSnapFraction(stackBounds, getMovementBounds(stackBounds));
330     }
331 
332     /**
333      * @return the default snap fraction to apply instead of the default gravity when calculating
334      *         the default stack bounds when first entering PiP.
335      */
getSnapFraction(Rect stackBounds, Rect movementBounds)336     public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
337         return mSnapAlgorithm.getSnapFraction(stackBounds, movementBounds);
338     }
339 
340     /**
341      * Applies the given snap fraction to the given stack bounds.
342      */
applySnapFraction(Rect stackBounds, float snapFraction)343     public void applySnapFraction(Rect stackBounds, float snapFraction) {
344         final Rect movementBounds = getMovementBounds(stackBounds);
345         mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction);
346     }
347 
348     /**
349      * @return the pixels for a given dp value.
350      */
dpToPx(float dpValue, DisplayMetrics dm)351     private int dpToPx(float dpValue, DisplayMetrics dm) {
352         return PipUtils.dpToPx(dpValue, dm);
353     }
354 
355     /**
356      * @return the normal bounds adjusted so that they fit the menu actions.
357      */
adjustNormalBoundsToFitMenu(@onNull Rect normalBounds, @Nullable Size minMenuSize)358     public Rect adjustNormalBoundsToFitMenu(@NonNull Rect normalBounds,
359             @Nullable Size minMenuSize) {
360         if (minMenuSize == null) {
361             return normalBounds;
362         }
363         if (normalBounds.width() >= minMenuSize.getWidth()
364                 && normalBounds.height() >= minMenuSize.getHeight()) {
365             // The normal bounds can fit the menu as is, no need to adjust the bounds.
366             return normalBounds;
367         }
368         final Rect adjustedNormalBounds = new Rect();
369         final boolean needsWidthAdj = minMenuSize.getWidth() > normalBounds.width();
370         final boolean needsHeightAdj = minMenuSize.getHeight() > normalBounds.height();
371         final int adjWidth;
372         final int adjHeight;
373         if (needsWidthAdj && needsHeightAdj) {
374             // Both the width and the height are too small - find the edge that needs the larger
375             // adjustment and scale that edge. The other edge will scale beyond the minMenuSize
376             // when the aspect ratio is applied.
377             final float widthScaleFactor =
378                     ((float) (minMenuSize.getWidth())) / ((float) (normalBounds.width()));
379             final float heightScaleFactor =
380                     ((float) (minMenuSize.getHeight())) / ((float) (normalBounds.height()));
381             if (widthScaleFactor > heightScaleFactor) {
382                 adjWidth = minMenuSize.getWidth();
383                 adjHeight = Math.round(adjWidth / mPipBoundsState.getAspectRatio());
384             } else {
385                 adjHeight = minMenuSize.getHeight();
386                 adjWidth = Math.round(adjHeight * mPipBoundsState.getAspectRatio());
387             }
388         } else if (needsWidthAdj) {
389             // Width is too small - use the min menu size width instead.
390             adjWidth = minMenuSize.getWidth();
391             adjHeight = Math.round(adjWidth / mPipBoundsState.getAspectRatio());
392         } else {
393             // Height is too small - use the min menu size height instead.
394             adjHeight = minMenuSize.getHeight();
395             adjWidth = Math.round(adjHeight * mPipBoundsState.getAspectRatio());
396         }
397         adjustedNormalBounds.set(0, 0, adjWidth, adjHeight);
398         // Make sure the bounds conform to the aspect ratio and min edge size.
399         transformBoundsToAspectRatio(adjustedNormalBounds,
400                 mPipBoundsState.getAspectRatio(), true /* useCurrentMinEdgeSize */,
401                 true /* useCurrentSize */);
402         return adjustedNormalBounds;
403     }
404 
405     /**
406      * Dumps internal states.
407      */
dump(PrintWriter pw, String prefix)408     public void dump(PrintWriter pw, String prefix) {
409         final String innerPrefix = prefix + "  ";
410         pw.println(prefix + TAG);
411         pw.println(innerPrefix + "mDefaultAspectRatio=" + mDefaultAspectRatio);
412         pw.println(innerPrefix + "mMinAspectRatio=" + mMinAspectRatio);
413         pw.println(innerPrefix + "mMaxAspectRatio=" + mMaxAspectRatio);
414         pw.println(innerPrefix + "mDefaultStackGravity=" + mDefaultStackGravity);
415         pw.println(innerPrefix + "mSnapAlgorithm" + mSnapAlgorithm);
416     }
417 }
418