blob: f51eb5299dd9656586395b53247c3d2172cb7387 [file] [log] [blame]
/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.wm.shell.pip;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.PictureInPictureParams;
import android.content.Context;
import android.content.pm.ActivityInfo;
import android.content.res.Resources;
import android.graphics.Rect;
import android.util.DisplayMetrics;
import android.util.Size;
import android.view.Gravity;
import com.android.wm.shell.R;
import com.android.wm.shell.pip.phone.PipSizeSpecHandler;
import java.io.PrintWriter;
/**
* Calculates the default, normal, entry, inset and movement bounds of the PIP.
*/
public class PipBoundsAlgorithm {
private static final String TAG = PipBoundsAlgorithm.class.getSimpleName();
private static final float INVALID_SNAP_FRACTION = -1f;
@NonNull private final PipBoundsState mPipBoundsState;
@NonNull protected final PipSizeSpecHandler mPipSizeSpecHandler;
private final PipSnapAlgorithm mSnapAlgorithm;
private final PipKeepClearAlgorithmInterface mPipKeepClearAlgorithm;
private float mDefaultAspectRatio;
private float mMinAspectRatio;
private float mMaxAspectRatio;
private int mDefaultStackGravity;
public PipBoundsAlgorithm(Context context, @NonNull PipBoundsState pipBoundsState,
@NonNull PipSnapAlgorithm pipSnapAlgorithm,
@NonNull PipKeepClearAlgorithmInterface pipKeepClearAlgorithm,
@NonNull PipSizeSpecHandler pipSizeSpecHandler) {
mPipBoundsState = pipBoundsState;
mSnapAlgorithm = pipSnapAlgorithm;
mPipKeepClearAlgorithm = pipKeepClearAlgorithm;
mPipSizeSpecHandler = pipSizeSpecHandler;
reloadResources(context);
// Initialize the aspect ratio to the default aspect ratio. Don't do this in reload
// resources as it would clobber mAspectRatio when entering PiP from fullscreen which
// triggers a configuration change and the resources to be reloaded.
mPipBoundsState.setAspectRatio(mDefaultAspectRatio);
}
/**
* TODO: move the resources to SysUI package.
*/
private void reloadResources(Context context) {
final Resources res = context.getResources();
mDefaultAspectRatio = res.getFloat(
R.dimen.config_pictureInPictureDefaultAspectRatio);
mDefaultStackGravity = res.getInteger(
R.integer.config_defaultPictureInPictureGravity);
final String screenEdgeInsetsDpString = res.getString(
R.string.config_defaultPictureInPictureScreenEdgeInsets);
final Size screenEdgeInsetsDp = !screenEdgeInsetsDpString.isEmpty()
? Size.parseSize(screenEdgeInsetsDpString)
: null;
mMinAspectRatio = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio);
mMaxAspectRatio = res.getFloat(
com.android.internal.R.dimen.config_pictureInPictureMaxAspectRatio);
}
/**
* The {@link PipSnapAlgorithm} is couple on display bounds
* @return {@link PipSnapAlgorithm}.
*/
public PipSnapAlgorithm getSnapAlgorithm() {
return mSnapAlgorithm;
}
/** Responds to configuration change. */
public void onConfigurationChanged(Context context) {
reloadResources(context);
}
/** Returns the normal bounds (i.e. the default entry bounds). */
public Rect getNormalBounds() {
// The normal bounds are the default bounds adjusted to the current aspect ratio.
return transformBoundsToAspectRatioIfValid(getDefaultBounds(),
mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
false /* useCurrentSize */);
}
/** Returns the default bounds. */
public Rect getDefaultBounds() {
return getDefaultBounds(INVALID_SNAP_FRACTION, null /* size */);
}
/**
* Returns the destination bounds to place the PIP window on entry.
* If there are any keep clear areas registered, the position will try to avoid occluding them.
*/
public Rect getEntryDestinationBounds() {
Rect entryBounds = getEntryDestinationBoundsIgnoringKeepClearAreas();
Rect insets = new Rect();
getInsetBounds(insets);
return mPipKeepClearAlgorithm.findUnoccludedPosition(entryBounds,
mPipBoundsState.getRestrictedKeepClearAreas(),
mPipBoundsState.getUnrestrictedKeepClearAreas(), insets);
}
/** Returns the destination bounds to place the PIP window on entry. */
public Rect getEntryDestinationBoundsIgnoringKeepClearAreas() {
final PipBoundsState.PipReentryState reentryState = mPipBoundsState.getReentryState();
final Rect destinationBounds = reentryState != null
? getDefaultBounds(reentryState.getSnapFraction(), reentryState.getSize())
: getDefaultBounds();
final boolean useCurrentSize = reentryState != null && reentryState.getSize() != null;
Rect aspectRatioBounds = transformBoundsToAspectRatioIfValid(destinationBounds,
mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */,
useCurrentSize);
return aspectRatioBounds;
}
/** Returns the current bounds adjusted to the new aspect ratio, if valid. */
public Rect getAdjustedDestinationBounds(Rect currentBounds, float newAspectRatio) {
return transformBoundsToAspectRatioIfValid(currentBounds, newAspectRatio,
true /* useCurrentMinEdgeSize */, false /* useCurrentSize */);
}
/**
*
* Get the smallest/most minimal size allowed.
*/
public Size getMinimalSize(ActivityInfo activityInfo) {
if (activityInfo == null || activityInfo.windowLayout == null) {
return null;
}
final ActivityInfo.WindowLayout windowLayout = activityInfo.windowLayout;
// -1 will be populated if an activity specifies defaultWidth/defaultHeight in <layout>
// without minWidth/minHeight
if (windowLayout.minWidth > 0 && windowLayout.minHeight > 0) {
// If either dimension is smaller than the allowed minimum, adjust them
// according to mOverridableMinSize
return new Size(
Math.max(windowLayout.minWidth, mPipSizeSpecHandler.getOverrideMinEdgeSize()),
Math.max(windowLayout.minHeight, mPipSizeSpecHandler.getOverrideMinEdgeSize()));
}
return null;
}
/**
* Returns the source hint rect if it is valid (if provided and is contained by the current
* task bounds).
*/
public static Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds) {
final Rect sourceHintRect = params != null && params.hasSourceBoundsHint()
? params.getSourceRectHint()
: null;
if (sourceHintRect != null && sourceBounds.contains(sourceHintRect)) {
return sourceHintRect;
}
return null;
}
/**
* Returns the source hint rect if it is valid (if provided and is contained by the current
* task bounds, while not smaller than the destination bounds).
*/
@Nullable
public static Rect getValidSourceHintRect(PictureInPictureParams params, Rect sourceBounds,
Rect destinationBounds) {
Rect sourceRectHint = getValidSourceHintRect(params, sourceBounds);
if (!isSourceRectHintValidForEnterPip(sourceRectHint, destinationBounds)) {
sourceRectHint = null;
}
return sourceRectHint;
}
/**
* This is a situation in which the source rect hint on at least one axis is smaller
* than the destination bounds, which represents a problem because we would have to scale
* up that axis to fit the bounds. So instead, just fallback to the non-source hint
* animation in this case.
*
* @return {@code false} if the given source is too small to use for the entering animation.
*/
static boolean isSourceRectHintValidForEnterPip(Rect sourceRectHint, Rect destinationBounds) {
return sourceRectHint != null
&& sourceRectHint.width() > destinationBounds.width()
&& sourceRectHint.height() > destinationBounds.height();
}
public float getDefaultAspectRatio() {
return mDefaultAspectRatio;
}
/**
*
* Give the aspect ratio if the supplied PiP params have one, or else return default.
*/
public float getAspectRatioOrDefault(
@android.annotation.Nullable PictureInPictureParams params) {
return params != null && params.hasSetAspectRatio()
? params.getAspectRatioFloat()
: getDefaultAspectRatio();
}
/**
* @return whether the given {@param aspectRatio} is valid.
*/
public boolean isValidPictureInPictureAspectRatio(float aspectRatio) {
return Float.compare(mMinAspectRatio, aspectRatio) <= 0
&& Float.compare(aspectRatio, mMaxAspectRatio) <= 0;
}
private Rect transformBoundsToAspectRatioIfValid(Rect bounds, float aspectRatio,
boolean useCurrentMinEdgeSize, boolean useCurrentSize) {
final Rect destinationBounds = new Rect(bounds);
if (isValidPictureInPictureAspectRatio(aspectRatio)) {
transformBoundsToAspectRatio(destinationBounds, aspectRatio,
useCurrentMinEdgeSize, useCurrentSize);
}
return destinationBounds;
}
/**
* Set the current bounds (or the default bounds if there are no current bounds) with the
* specified aspect ratio.
*/
public void transformBoundsToAspectRatio(Rect stackBounds, float aspectRatio,
boolean useCurrentMinEdgeSize, boolean useCurrentSize) {
// Save the snap fraction and adjust the size based on the new aspect ratio.
final float snapFraction = mSnapAlgorithm.getSnapFraction(stackBounds,
getMovementBounds(stackBounds), mPipBoundsState.getStashedState());
final Size size;
if (useCurrentMinEdgeSize || useCurrentSize) {
// Use the existing size but adjusted to the new aspect ratio.
size = mPipSizeSpecHandler.getSizeForAspectRatio(
new Size(stackBounds.width(), stackBounds.height()), aspectRatio);
} else {
size = mPipSizeSpecHandler.getDefaultSize(aspectRatio);
}
final int left = (int) (stackBounds.centerX() - size.getWidth() / 2f);
final int top = (int) (stackBounds.centerY() - size.getHeight() / 2f);
stackBounds.set(left, top, left + size.getWidth(), top + size.getHeight());
mSnapAlgorithm.applySnapFraction(stackBounds, getMovementBounds(stackBounds), snapFraction);
}
/**
* @return the default bounds to show the PIP, if a {@param snapFraction} and {@param size} are
* provided, then it will apply the default bounds to the provided snap fraction and size.
*/
private Rect getDefaultBounds(float snapFraction, Size size) {
final Rect defaultBounds = new Rect();
if (snapFraction != INVALID_SNAP_FRACTION && size != null) {
// The default bounds are the given size positioned at the given snap fraction.
defaultBounds.set(0, 0, size.getWidth(), size.getHeight());
final Rect movementBounds = getMovementBounds(defaultBounds);
mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
return defaultBounds;
}
// Calculate the default size.
final Size defaultSize;
final Rect insetBounds = new Rect();
getInsetBounds(insetBounds);
// Calculate the default size
defaultSize = mPipSizeSpecHandler.getDefaultSize(mDefaultAspectRatio);
// Now that we have the default size, apply the snap fraction if valid or position the
// bounds using the default gravity.
if (snapFraction != INVALID_SNAP_FRACTION) {
defaultBounds.set(0, 0, defaultSize.getWidth(), defaultSize.getHeight());
final Rect movementBounds = getMovementBounds(defaultBounds);
mSnapAlgorithm.applySnapFraction(defaultBounds, movementBounds, snapFraction);
} else {
Gravity.apply(mDefaultStackGravity, defaultSize.getWidth(), defaultSize.getHeight(),
insetBounds, 0, Math.max(
mPipBoundsState.isImeShowing() ? mPipBoundsState.getImeHeight() : 0,
mPipBoundsState.isShelfShowing()
? mPipBoundsState.getShelfHeight() : 0), defaultBounds);
}
return defaultBounds;
}
/**
* Populates the bounds on the screen that the PIP can be visible in.
*/
public void getInsetBounds(Rect outRect) {
outRect.set(mPipSizeSpecHandler.getInsetBounds());
}
/**
* @return the movement bounds for the given stackBounds and the current state of the
* controller.
*/
public Rect getMovementBounds(Rect stackBounds) {
return getMovementBounds(stackBounds, true /* adjustForIme */);
}
/**
* @return the movement bounds for the given stackBounds and the current state of the
* controller.
*/
public Rect getMovementBounds(Rect stackBounds, boolean adjustForIme) {
final Rect movementBounds = new Rect();
getInsetBounds(movementBounds);
// Apply the movement bounds adjustments based on the current state.
getMovementBounds(stackBounds, movementBounds, movementBounds,
(adjustForIme && mPipBoundsState.isImeShowing())
? mPipBoundsState.getImeHeight() : 0);
return movementBounds;
}
/**
* Adjusts movementBoundsOut so that it is the movement bounds for the given stackBounds.
*/
public void getMovementBounds(Rect stackBounds, Rect insetBounds, Rect movementBoundsOut,
int bottomOffset) {
// Adjust the right/bottom to ensure the stack bounds never goes offscreen
movementBoundsOut.set(insetBounds);
movementBoundsOut.right = Math.max(insetBounds.left, insetBounds.right
- stackBounds.width());
movementBoundsOut.bottom = Math.max(insetBounds.top, insetBounds.bottom
- stackBounds.height());
movementBoundsOut.bottom -= bottomOffset;
}
/**
* @return the default snap fraction to apply instead of the default gravity when calculating
* the default stack bounds when first entering PiP.
*/
public float getSnapFraction(Rect stackBounds) {
return getSnapFraction(stackBounds, getMovementBounds(stackBounds));
}
/**
* @return the default snap fraction to apply instead of the default gravity when calculating
* the default stack bounds when first entering PiP.
*/
public float getSnapFraction(Rect stackBounds, Rect movementBounds) {
return mSnapAlgorithm.getSnapFraction(stackBounds, movementBounds);
}
/**
* Applies the given snap fraction to the given stack bounds.
*/
public void applySnapFraction(Rect stackBounds, float snapFraction) {
final Rect movementBounds = getMovementBounds(stackBounds);
mSnapAlgorithm.applySnapFraction(stackBounds, movementBounds, snapFraction);
}
/**
* @return the pixels for a given dp value.
*/
private int dpToPx(float dpValue, DisplayMetrics dm) {
return PipUtils.dpToPx(dpValue, dm);
}
/**
* @return the normal bounds adjusted so that they fit the menu actions.
*/
public Rect adjustNormalBoundsToFitMenu(@NonNull Rect normalBounds,
@Nullable Size minMenuSize) {
if (minMenuSize == null) {
return normalBounds;
}
if (normalBounds.width() >= minMenuSize.getWidth()
&& normalBounds.height() >= minMenuSize.getHeight()) {
// The normal bounds can fit the menu as is, no need to adjust the bounds.
return normalBounds;
}
final Rect adjustedNormalBounds = new Rect();
final boolean needsWidthAdj = minMenuSize.getWidth() > normalBounds.width();
final boolean needsHeightAdj = minMenuSize.getHeight() > normalBounds.height();
final int adjWidth;
final int adjHeight;
if (needsWidthAdj && needsHeightAdj) {
// Both the width and the height are too small - find the edge that needs the larger
// adjustment and scale that edge. The other edge will scale beyond the minMenuSize
// when the aspect ratio is applied.
final float widthScaleFactor =
((float) (minMenuSize.getWidth())) / ((float) (normalBounds.width()));
final float heightScaleFactor =
((float) (minMenuSize.getHeight())) / ((float) (normalBounds.height()));
if (widthScaleFactor > heightScaleFactor) {
adjWidth = minMenuSize.getWidth();
adjHeight = Math.round(adjWidth / mPipBoundsState.getAspectRatio());
} else {
adjHeight = minMenuSize.getHeight();
adjWidth = Math.round(adjHeight * mPipBoundsState.getAspectRatio());
}
} else if (needsWidthAdj) {
// Width is too small - use the min menu size width instead.
adjWidth = minMenuSize.getWidth();
adjHeight = Math.round(adjWidth / mPipBoundsState.getAspectRatio());
} else {
// Height is too small - use the min menu size height instead.
adjHeight = minMenuSize.getHeight();
adjWidth = Math.round(adjHeight * mPipBoundsState.getAspectRatio());
}
adjustedNormalBounds.set(0, 0, adjWidth, adjHeight);
// Make sure the bounds conform to the aspect ratio and min edge size.
transformBoundsToAspectRatio(adjustedNormalBounds,
mPipBoundsState.getAspectRatio(), true /* useCurrentMinEdgeSize */,
true /* useCurrentSize */);
return adjustedNormalBounds;
}
/**
* Dumps internal states.
*/
public void dump(PrintWriter pw, String prefix) {
final String innerPrefix = prefix + " ";
pw.println(prefix + TAG);
pw.println(innerPrefix + "mDefaultAspectRatio=" + mDefaultAspectRatio);
pw.println(innerPrefix + "mMinAspectRatio=" + mMinAspectRatio);
pw.println(innerPrefix + "mMaxAspectRatio=" + mMaxAspectRatio);
pw.println(innerPrefix + "mDefaultStackGravity=" + mDefaultStackGravity);
pw.println(innerPrefix + "mSnapAlgorithm" + mSnapAlgorithm);
}
}