• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2023 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.common.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.Rational;
28 import android.util.Size;
29 import android.view.Gravity;
30 
31 import com.android.internal.protolog.ProtoLog;
32 import com.android.wm.shell.R;
33 import com.android.wm.shell.protolog.ShellProtoLogGroup;
34 
35 import java.io.PrintWriter;
36 
37 /**
38  * Calculates the default, normal, entry, inset and movement bounds of the PIP.
39  */
40 public class PipBoundsAlgorithm {
41 
42     private static final String TAG = PipBoundsAlgorithm.class.getSimpleName();
43     private static final float INVALID_SNAP_FRACTION = -1f;
44 
45     @NonNull private final PipBoundsState mPipBoundsState;
46     @NonNull protected final PipDisplayLayoutState mPipDisplayLayoutState;
47     @NonNull protected final SizeSpecSource mSizeSpecSource;
48     private final PipSnapAlgorithm mSnapAlgorithm;
49     private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
50 
51     private float mDefaultAspectRatio;
52     private float mMinAspectRatio;
53     private float mMaxAspectRatio;
54     private int mDefaultStackGravity;
55 
PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState, @NonNull PipSnapAlgorithm pipSnapAlgorithm, @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm, @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull SizeSpecSource sizeSpecSource)56     public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
57             @NonNull PipSnapAlgorithm pipSnapAlgorithm,
58             @NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
59             @NonNull PipDisplayLayoutState pipDisplayLayoutState,
60             @NonNull SizeSpecSource sizeSpecSource) {
61         mPipBoundsState = pipBoundsState;
62         mSnapAlgorithm = pipSnapAlgorithm;
63         mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
64         mPipDisplayLayoutState = pipDisplayLayoutState;
65         mSizeSpecSource = sizeSpecSource;
66         reloadResources(context);
67         // Initialize the aspect ratio to the default aspect ratio.  Don't do this in reload
68         // resources as it would clobber mAspectRatio when entering PiP from fullscreen which
69         // triggers a configuration change and the resources to be reloaded.
70         mPipBoundsState.setAspectRatio(mDefaultAspectRatio);
71     }
72 
73     /**
74      * TODO: move the resources to SysUI package.
75      */
reloadResources(Context context)76     private void reloadResources(Context context) {
77         final Resources res = context.getResources();
78         mDefaultAspectRatio = res.getFloat(
79                 R.dimen.config_pictureInPictureDefaultAspectRatio);
80         mDefaultStackGravity = res.getInteger(
81                 R.integer.config_defaultPictureInPictureGravity);
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 = getDefaultBounds();
132         if (reentryState != null) {
133             final Size scaledBounds = new Size(
134                     Math.round(mPipBoundsState.getMaxSize().x * reentryState.getBoundsScale()),
135                     Math.round(mPipBoundsState.getMaxSize().y * reentryState.getBoundsScale()));
136             destinationBounds.set(getDefaultBounds(reentryState.getSnapFraction(), scaledBounds));
137         }
138 
139         final boolean useCurrentSize = reentryState != null;
140         Rect aspectRatioBounds = transformBoundsToAspectRatioIfValid(destinationBounds,
141                 mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
142                 useCurrentSize);
143         return aspectRatioBounds;
144     }
145 
146     /** Returns the current bounds adjusted to the new aspect ratio, if valid. */
getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio)147     public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
148         return transformBoundsToAspectRatioIfValid(currentBounds, newAspectRatio,
149                 true /* useCurrentMinEdgeSize */, false /* useCurrentSize */);
150     }
151 
152     /**
153      *
154      * Get the smallest/most minimal size allowed.
155      */
getMinimalSize(ActivityInfo activityInfo)156     public Size getMinimalSize(ActivityInfo activityInfo) {
157         if (activityInfo == null || activityInfo.windowLayout == null) {
158             return null;
159         }
160         final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout;
161         // -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout>
162         // without minWidth/minHeight
163         if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) {
164             // If either dimension is smaller than the allowed minimum, adjust them
165             // according to mOverridableMinSize
166             return new Size(
167                     Math.max(windowLayout.minWidth, getOverrideMinEdgeSize()),
168                     Math.max(windowLayout.minHeight, getOverrideMinEdgeSize()));
169         }
170         return null;
171     }
172 
173     /**
174      * Returns the source hint rect if it is valid (if provided and is contained by the current
175      * task bounds).
176      */
getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds)177     public static Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds) {
178         final Rect sourceHintRect = params != null && params.hasSourceBoundsHint()
179                 ? params.getSourceRectHint()
180                 : null;
181         if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) {
182             return sourceHintRect;
183         }
184         return null;
185     }
186 
187 
188     /**
189      * Returns the source hint rect if it is valid (if provided and is contained by the current
190      * task bounds, while not smaller than the destination bounds).
191      */
192     @Nullable
getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds, Rect destinationBounds)193     public static Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds,
194             Rect destinationBounds) {
195         Rect sourceRectHint = getValidSourceHintRect(params, sourceBounds);
196         if (!isSourceRectHintValidForEnterPip(sourceRectHint, destinationBounds)) {
197             sourceRectHint = null;
198         }
199         return sourceRectHint;
200     }
201 
202     /**
203      * This is a situation in which the source rect hint on at least one axis is smaller
204      * than the destination bounds, which represents a problem because we would have to scale
205      * up that axis to fit the bounds. So instead, just fallback to the non-source hint
206      * animation in this case.
207      *
208      * @return {@code false} if the given source is too small to use for the entering animation.
209      */
isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds)210     public static boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint,
211             Rect destinationBounds) {
212         if (sourceRectHint == null || sourceRectHint.isEmpty()) {
213             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
214                     "isSourceRectHintValidForEnterPip=false, empty hint");
215             return false;
216         }
217         if (sourceRectHint.width() <= destinationBounds.width()
218                 || sourceRectHint.height() <= destinationBounds.height()) {
219             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
220                     "isSourceRectHintValidForEnterPip=false, hint(%s) is smaller"
221                             + " than destination(%s)", sourceRectHint, destinationBounds);
222             return false;
223         }
224         // We use the aspect ratio of source rect hint to check against destination bounds
225         // here to avoid upscaling error.
226         final Rational srcAspectRatio = new Rational(
227                 sourceRectHint.width(), sourceRectHint.height());
228         if (!PictureInPictureParams.isSameAspectRatio(destinationBounds, srcAspectRatio)) {
229             ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
230                     "isSourceRectHintValidForEnterPip=false, hint(%s) does not match"
231                             + " destination(%s) aspect ratio", sourceRectHint, destinationBounds);
232             return false;
233         }
234         return true;
235     }
236 
getDefaultAspectRatio()237     public float getDefaultAspectRatio() {
238         return mDefaultAspectRatio;
239     }
240 
241     /**
242      *
243      * Give the aspect ratio if the supplied PiP params have one, or else return default.
244      */
getAspectRatioOrDefault( @ndroid.annotation.Nullable PictureInPictureParams params)245     public float getAspectRatioOrDefault(
246             @android.annotation.Nullable PictureInPictureParams params) {
247         return params != null && params.hasSetAspectRatio()
248                 ? params.getAspectRatioFloat()
249                 : getDefaultAspectRatio();
250     }
251 
252     /**
253      * @return whether the given aspectRatio is valid.
254      */
isValidPictureInPictureAspectRatio(float aspectRatio)255     public boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
256         return Float.compare(mMinAspectRatio, aspectRatio) <= 0
257                 && Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
258     }
259 
transformBoundsToAspectRatioIfValid(Rect bounds, float aspectRatio, boolean useCurrentMinEdgeSize, boolean useCurrentSize)260     private Rect transformBoundsToAspectRatioIfValid(Rect bounds, float aspectRatio,
261             boolean useCurrentMinEdgeSize, boolean useCurrentSize) {
262         final Rect destinationBounds = new Rect(bounds);
263         if (isValidPictureInPictureAspectRatio(aspectRatio)) {
264             transformBoundsToAspectRatio(destinationBounds, aspectRatio,
265                     useCurrentMinEdgeSize, useCurrentSize);
266         }
267         return destinationBounds;
268     }
269 
270     /**
271      * Set the current bounds (or the default bounds if there are no current bounds) with the
272      * specified aspect ratio.
273      */
transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio, boolean useCurrentMinEdgeSize, boolean useCurrentSize)274     public void transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio,
275             boolean useCurrentMinEdgeSize, boolean useCurrentSize) {
276         // Save the snap fraction and adjust the size based on the new aspect ratio.
277         final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
278                 getMovementBounds(stackBounds), mPipBoundsState.getStashedState());
279 
280         final Size size;
281         if (useCurrentMinEdgeSize || useCurrentSize) {
282             // Use the existing size but adjusted to the new aspect ratio.
283             size = mSizeSpecSource.getSizeForAspectRatio(
284                     new Size(stackBounds.width(), stackBounds.height()), aspectRatio);
285         } else {
286             size = mSizeSpecSource.getDefaultSize(aspectRatio);
287         }
288 
289         final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
290         final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
291         stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
292         mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
293     }
294 
295     /**
296      * @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are
297      * provided, then it will apply the default bounds to the provided snap fraction and size.
298      */
getDefaultBounds(float snapFraction, Size size)299     private Rect getDefaultBounds(float snapFraction, Size size) {
300         final Rect defaultBounds = new Rect();
301         if (snapFraction != INVALID_SNAP_FRACTION && size != null) {
302             // The default bounds are the given size positioned at the given snap fraction.
303             defaultBounds.set(0, 0, size.getWidth(), size.getHeight());
304             final Rect movementBounds = getMovementBounds(defaultBounds);
305             mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
306             return defaultBounds;
307         }
308 
309         // Calculate the default size.
310         final Size defaultSize;
311         final Rect insetBounds = new Rect();
312         getInsetBounds(insetBounds);
313 
314         // Calculate the default size
315         defaultSize = mSizeSpecSource.getDefaultSize(mDefaultAspectRatio);
316 
317         // Now that we have the default size, apply the snap fraction if valid or position the
318         // bounds using the default gravity.
319         if (snapFraction != INVALID_SNAP_FRACTION) {
320             defaultBounds.set(0, 0, defaultSize.getWidth(), defaultSize.getHeight());
321             final Rect movementBounds = getMovementBounds(defaultBounds);
322             mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
323         } else {
324             Gravity.apply(mDefaultStackGravity, defaultSize.getWidth(), defaultSize.getHeight(),
325                     insetBounds, 0, Math.max(
326                             mPipBoundsState.isImeShowing() ? mPipBoundsState.getImeHeight() : 0,
327                             mPipBoundsState.isShelfShowing()
328                                     ? mPipBoundsState.getShelfHeight() : 0), defaultBounds);
329         }
330         return defaultBounds;
331     }
332 
333     /**
334      * Populates the bounds on the screen that the PIP can be visible in.
335      */
getInsetBounds(Rect outRect)336     public void getInsetBounds(Rect outRect) {
337         outRect.set(mPipDisplayLayoutState.getInsetBounds());
338     }
339 
getOverrideMinEdgeSize()340     private int getOverrideMinEdgeSize() {
341         return mSizeSpecSource.getOverrideMinEdgeSize();
342     }
343 
344     /**
345      * @return the movement bounds for the given stackBounds and the current state of the
346      *         controller.
347      */
getMovementBounds(Rect stackBounds)348     public Rect getMovementBounds(Rect stackBounds) {
349         return getMovementBounds(stackBounds, true /* adjustForIme */);
350     }
351 
352     /**
353      * @return the movement bounds for the given stackBounds and the current state of the
354      *         controller.
355      */
getMovementBounds(Rect stackBounds, boolean adjustForIme)356     public Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
357         final Rect movementBounds = new Rect();
358         getInsetBounds(movementBounds);
359 
360         // Apply the movement bounds adjustments based on the current state.
361         getMovementBounds(stackBounds, movementBounds, movementBounds,
362                 (adjustForIme && mPipBoundsState.isImeShowing())
363                         ? mPipBoundsState.getImeHeight() : 0);
364 
365         return movementBounds;
366     }
367 
368     /**
369      * Adjusts movementBoundsOut so that it is the movement bounds for the given stackBounds.
370      */
getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut, int bottomOffset)371     public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
372             int bottomOffset) {
373         // Adjust the right/bottom to ensure the stack bounds never goes offscreen
374         movementBoundsOut.set(insetBounds);
375         movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right
376                 - stackBounds.width());
377         movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom
378                 - stackBounds.height());
379         movementBoundsOut.bottom -= bottomOffset;
380     }
381 
382     /**
383      * @return the default snap fraction to apply instead of the default gravity when calculating
384      *         the default stack bounds when first entering PiP.
385      */
getSnapFraction(Rect stackBounds)386     public float getSnapFraction(Rect stackBounds) {
387         return getSnapFraction(stackBounds, getMovementBounds(stackBounds));
388     }
389 
390     /**
391      * @return the default snap fraction to apply instead of the default gravity when calculating
392      *         the default stack bounds when first entering PiP.
393      */
getSnapFraction(Rect stackBounds, Rect movementBounds)394     public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
395         return mSnapAlgorithm.getSnapFraction(stackBounds, movementBounds);
396     }
397 
398     /**
399      * Applies the given snap fraction to the given stack bounds.
400      */
applySnapFraction(Rect stackBounds, float snapFraction)401     public void applySnapFraction(Rect stackBounds, float snapFraction) {
402         final Rect movementBounds = getMovementBounds(stackBounds);
403         mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction);
404     }
405 
406     /**
407      * @return the pixels for a given dp value.
408      */
dpToPx(float dpValue, DisplayMetrics dm)409     private int dpToPx(float dpValue, DisplayMetrics dm) {
410         return PipUtils.dpToPx(dpValue, dm);
411     }
412 
413     /**
414      * @return the normal bounds adjusted so that they fit the menu actions.
415      */
adjustNormalBoundsToFitMenu(@onNull Rect normalBounds, @Nullable Size minMenuSize)416     public Rect adjustNormalBoundsToFitMenu(@NonNull Rect normalBounds,
417             @Nullable Size minMenuSize) {
418         if (minMenuSize == null) {
419             return normalBounds;
420         }
421         if (normalBounds.width() >= minMenuSize.getWidth()
422                 && normalBounds.height() >= minMenuSize.getHeight()) {
423             // The normal bounds can fit the menu as is, no need to adjust the bounds.
424             return normalBounds;
425         }
426         final Rect adjustedNormalBounds = new Rect();
427         final boolean needsWidthAdj = minMenuSize.getWidth() > normalBounds.width();
428         final boolean needsHeightAdj = minMenuSize.getHeight() > normalBounds.height();
429         final int adjWidth;
430         final int adjHeight;
431         if (needsWidthAdj && needsHeightAdj) {
432             // Both the width and the height are too small - find the edge that needs the larger
433             // adjustment and scale that edge. The other edge will scale beyond the minMenuSize
434             // when the aspect ratio is applied.
435             final float widthScaleFactor =
436                     ((float) (minMenuSize.getWidth())) / ((float) (normalBounds.width()));
437             final float heightScaleFactor =
438                     ((float) (minMenuSize.getHeight())) / ((float) (normalBounds.height()));
439             if (widthScaleFactor > heightScaleFactor) {
440                 adjWidth = minMenuSize.getWidth();
441                 adjHeight = Math.round(adjWidth / mPipBoundsState.getAspectRatio());
442             } else {
443                 adjHeight = minMenuSize.getHeight();
444                 adjWidth = Math.round(adjHeight * mPipBoundsState.getAspectRatio());
445             }
446         } else if (needsWidthAdj) {
447             // Width is too small - use the min menu size width instead.
448             adjWidth = minMenuSize.getWidth();
449             adjHeight = Math.round(adjWidth / mPipBoundsState.getAspectRatio());
450         } else {
451             // Height is too small - use the min menu size height instead.
452             adjHeight = minMenuSize.getHeight();
453             adjWidth = Math.round(adjHeight * mPipBoundsState.getAspectRatio());
454         }
455         adjustedNormalBounds.set(0, 0, adjWidth, adjHeight);
456         // Make sure the bounds conform to the aspect ratio and min edge size.
457         transformBoundsToAspectRatio(adjustedNormalBounds,
458                 mPipBoundsState.getAspectRatio(), true /* useCurrentMinEdgeSize */,
459                 true /* useCurrentSize */);
460         return adjustedNormalBounds;
461     }
462 
463     /**
464      * Dumps internal states.
465      */
dump(PrintWriter pw, String prefix)466     public void dump(PrintWriter pw, String prefix) {
467         final String innerPrefix = prefix + "  ";
468         pw.println(prefix + TAG);
469         pw.println(innerPrefix + "mDefaultAspectRatio=" + mDefaultAspectRatio);
470         pw.println(innerPrefix + "mMinAspectRatio=" + mMinAspectRatio);
471         pw.println(innerPrefix + "mMaxAspectRatio=" + mMaxAspectRatio);
472         pw.println(innerPrefix + "mDefaultStackGravity=" + mDefaultStackGravity);
473         pw.println(innerPrefix + "mSnapAlgorithm" + mSnapAlgorithm);
474     }
475 }
476