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