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