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.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.app.ActivityTaskManager; 23 import android.app.PictureInPictureUiState; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.graphics.Point; 27 import android.graphics.Rect; 28 import android.os.RemoteException; 29 import android.util.Log; 30 import android.util.Size; 31 import android.view.Display; 32 33 import com.android.internal.annotations.VisibleForTesting; 34 import com.android.internal.util.function.TriConsumer; 35 import com.android.wm.shell.R; 36 import com.android.wm.shell.common.DisplayLayout; 37 38 import java.io.PrintWriter; 39 import java.lang.annotation.Retention; 40 import java.lang.annotation.RetentionPolicy; 41 import java.util.Objects; 42 import java.util.function.Consumer; 43 44 /** 45 * Singleton source of truth for the current state of PIP bounds. 46 */ 47 public final class PipBoundsState { 48 public static final int STASH_TYPE_NONE = 0; 49 public static final int STASH_TYPE_LEFT = 1; 50 public static final int STASH_TYPE_RIGHT = 2; 51 52 @IntDef(prefix = { "STASH_TYPE_" }, value = { 53 STASH_TYPE_NONE, 54 STASH_TYPE_LEFT, 55 STASH_TYPE_RIGHT 56 }) 57 @Retention(RetentionPolicy.SOURCE) 58 public @interface StashType {} 59 60 private static final String TAG = PipBoundsState.class.getSimpleName(); 61 62 private final @NonNull Rect mBounds = new Rect(); 63 private final @NonNull Rect mMovementBounds = new Rect(); 64 private final @NonNull Rect mNormalBounds = new Rect(); 65 private final @NonNull Rect mExpandedBounds = new Rect(); 66 private final @NonNull Rect mNormalMovementBounds = new Rect(); 67 private final @NonNull Rect mExpandedMovementBounds = new Rect(); 68 private final Point mMaxSize = new Point(); 69 private final Point mMinSize = new Point(); 70 private final @NonNull Context mContext; 71 private float mAspectRatio; 72 private int mStashedState = STASH_TYPE_NONE; 73 private int mStashOffset; 74 private @Nullable PipReentryState mPipReentryState; 75 private @Nullable ComponentName mLastPipComponentName; 76 private int mDisplayId = Display.DEFAULT_DISPLAY; 77 private final @NonNull DisplayLayout mDisplayLayout = new DisplayLayout(); 78 /** The current minimum edge size of PIP. */ 79 private int mMinEdgeSize; 80 /** The preferred minimum (and default) size specified by apps. */ 81 private @Nullable Size mOverrideMinSize; 82 private final @NonNull MotionBoundsState mMotionBoundsState = new MotionBoundsState(); 83 private boolean mIsImeShowing; 84 private int mImeHeight; 85 private boolean mIsShelfShowing; 86 private int mShelfHeight; 87 /** Whether the user has resized the PIP manually. */ 88 private boolean mHasUserResizedPip; 89 90 private @Nullable Runnable mOnMinimalSizeChangeCallback; 91 private @Nullable TriConsumer<Boolean, Integer, Boolean> mOnShelfVisibilityChangeCallback; 92 private @Nullable Consumer<Rect> mOnPipExclusionBoundsChangeCallback; 93 PipBoundsState(@onNull Context context)94 public PipBoundsState(@NonNull Context context) { 95 mContext = context; 96 reloadResources(); 97 } 98 99 /** Reloads the resources. */ onConfigurationChanged()100 public void onConfigurationChanged() { 101 reloadResources(); 102 } 103 reloadResources()104 private void reloadResources() { 105 mStashOffset = mContext.getResources().getDimensionPixelSize(R.dimen.pip_stash_offset); 106 } 107 108 /** Set the current PIP bounds. */ setBounds(@onNull Rect bounds)109 public void setBounds(@NonNull Rect bounds) { 110 mBounds.set(bounds); 111 if (mOnPipExclusionBoundsChangeCallback != null) { 112 mOnPipExclusionBoundsChangeCallback.accept(bounds); 113 } 114 } 115 116 /** Get the current PIP bounds. */ 117 @NonNull getBounds()118 public Rect getBounds() { 119 return new Rect(mBounds); 120 } 121 122 /** Returns the current movement bounds. */ 123 @NonNull getMovementBounds()124 public Rect getMovementBounds() { 125 return mMovementBounds; 126 } 127 128 /** Set the current normal PIP bounds. */ setNormalBounds(@onNull Rect bounds)129 public void setNormalBounds(@NonNull Rect bounds) { 130 mNormalBounds.set(bounds); 131 } 132 133 /** Get the current normal PIP bounds. */ 134 @NonNull getNormalBounds()135 public Rect getNormalBounds() { 136 return mNormalBounds; 137 } 138 139 /** Set the expanded bounds of PIP. */ setExpandedBounds(@onNull Rect bounds)140 public void setExpandedBounds(@NonNull Rect bounds) { 141 mExpandedBounds.set(bounds); 142 } 143 144 /** Get the PIP expanded bounds. */ 145 @NonNull getExpandedBounds()146 public Rect getExpandedBounds() { 147 return mExpandedBounds; 148 } 149 150 /** Set the normal movement bounds. */ setNormalMovementBounds(@onNull Rect bounds)151 public void setNormalMovementBounds(@NonNull Rect bounds) { 152 mNormalMovementBounds.set(bounds); 153 } 154 155 /** Returns the normal movement bounds. */ 156 @NonNull getNormalMovementBounds()157 public Rect getNormalMovementBounds() { 158 return mNormalMovementBounds; 159 } 160 161 /** Set the expanded movement bounds. */ setExpandedMovementBounds(@onNull Rect bounds)162 public void setExpandedMovementBounds(@NonNull Rect bounds) { 163 mExpandedMovementBounds.set(bounds); 164 } 165 166 /** Sets the max possible size for resize. */ setMaxSize(int width, int height)167 public void setMaxSize(int width, int height) { 168 mMaxSize.set(width, height); 169 } 170 171 /** Sets the min possible size for resize. */ setMinSize(int width, int height)172 public void setMinSize(int width, int height) { 173 mMinSize.set(width, height); 174 } 175 getMaxSize()176 public Point getMaxSize() { 177 return mMaxSize; 178 } 179 getMinSize()180 public Point getMinSize() { 181 return mMinSize; 182 } 183 184 /** Returns the expanded movement bounds. */ 185 @NonNull getExpandedMovementBounds()186 public Rect getExpandedMovementBounds() { 187 return mExpandedMovementBounds; 188 } 189 190 /** Dictate where PiP currently should be stashed, if at all. */ setStashed(@tashType int stashedState)191 public void setStashed(@StashType int stashedState) { 192 if (mStashedState == stashedState) { 193 return; 194 } 195 196 mStashedState = stashedState; 197 try { 198 ActivityTaskManager.getService().onPictureInPictureStateChanged( 199 new PictureInPictureUiState(stashedState != STASH_TYPE_NONE /* isStashed */) 200 ); 201 } catch (RemoteException e) { 202 Log.e(TAG, "Unable to set alert PiP state change."); 203 } 204 } 205 206 /** 207 * Return where the PiP is stashed, if at all. 208 * @return {@code STASH_NONE}, {@code STASH_LEFT} or {@code STASH_RIGHT}. 209 */ getStashedState()210 public @StashType int getStashedState() { 211 return mStashedState; 212 } 213 214 /** Whether PiP is stashed or not. */ isStashed()215 public boolean isStashed() { 216 return mStashedState != STASH_TYPE_NONE; 217 } 218 219 /** Returns the offset from the edge of the screen for PiP stash. */ getStashOffset()220 public int getStashOffset() { 221 return mStashOffset; 222 } 223 224 /** Set the PIP aspect ratio. */ setAspectRatio(float aspectRatio)225 public void setAspectRatio(float aspectRatio) { 226 mAspectRatio = aspectRatio; 227 } 228 229 /** Get the PIP aspect ratio. */ getAspectRatio()230 public float getAspectRatio() { 231 return mAspectRatio; 232 } 233 234 /** Save the reentry state to restore to when re-entering PIP mode. */ saveReentryState(Size size, float fraction)235 public void saveReentryState(Size size, float fraction) { 236 mPipReentryState = new PipReentryState(size, fraction); 237 } 238 239 /** Returns the saved reentry state. */ 240 @Nullable getReentryState()241 public PipReentryState getReentryState() { 242 return mPipReentryState; 243 } 244 245 /** Set the last {@link ComponentName} to enter PIP mode. */ setLastPipComponentName(@ullable ComponentName lastPipComponentName)246 public void setLastPipComponentName(@Nullable ComponentName lastPipComponentName) { 247 final boolean changed = !Objects.equals(mLastPipComponentName, lastPipComponentName); 248 mLastPipComponentName = lastPipComponentName; 249 if (changed) { 250 clearReentryState(); 251 setHasUserResizedPip(false); 252 } 253 } 254 255 /** Get the last PIP component name, if any. */ 256 @Nullable getLastPipComponentName()257 public ComponentName getLastPipComponentName() { 258 return mLastPipComponentName; 259 } 260 261 /** Get the current display id. */ getDisplayId()262 public int getDisplayId() { 263 return mDisplayId; 264 } 265 266 /** Set the current display id for the associated display layout. */ setDisplayId(int displayId)267 public void setDisplayId(int displayId) { 268 mDisplayId = displayId; 269 } 270 271 /** Returns the display's bounds. */ 272 @NonNull getDisplayBounds()273 public Rect getDisplayBounds() { 274 return new Rect(0, 0, mDisplayLayout.width(), mDisplayLayout.height()); 275 } 276 277 /** Update the display layout. */ setDisplayLayout(@onNull DisplayLayout displayLayout)278 public void setDisplayLayout(@NonNull DisplayLayout displayLayout) { 279 mDisplayLayout.set(displayLayout); 280 } 281 282 /** Get the display layout. */ 283 @NonNull getDisplayLayout()284 public DisplayLayout getDisplayLayout() { 285 return mDisplayLayout; 286 } 287 288 @VisibleForTesting clearReentryState()289 void clearReentryState() { 290 mPipReentryState = null; 291 } 292 293 /** Set the PIP minimum edge size. */ setMinEdgeSize(int minEdgeSize)294 public void setMinEdgeSize(int minEdgeSize) { 295 mMinEdgeSize = minEdgeSize; 296 } 297 298 /** Returns the PIP's current minimum edge size. */ getMinEdgeSize()299 public int getMinEdgeSize() { 300 return mMinEdgeSize; 301 } 302 303 /** Sets the preferred size of PIP as specified by the activity in PIP mode. */ setOverrideMinSize(@ullable Size overrideMinSize)304 public void setOverrideMinSize(@Nullable Size overrideMinSize) { 305 final boolean changed = !Objects.equals(overrideMinSize, mOverrideMinSize); 306 mOverrideMinSize = overrideMinSize; 307 if (changed && mOnMinimalSizeChangeCallback != null) { 308 mOnMinimalSizeChangeCallback.run(); 309 } 310 } 311 312 /** Returns the preferred minimal size specified by the activity in PIP. */ 313 @Nullable getOverrideMinSize()314 public Size getOverrideMinSize() { 315 return mOverrideMinSize; 316 } 317 318 /** Returns the minimum edge size of the override minimum size, or 0 if not set. */ getOverrideMinEdgeSize()319 public int getOverrideMinEdgeSize() { 320 if (mOverrideMinSize == null) return 0; 321 return Math.min(mOverrideMinSize.getWidth(), mOverrideMinSize.getHeight()); 322 } 323 324 /** Get the state of the bounds in motion. */ 325 @NonNull getMotionBoundsState()326 public MotionBoundsState getMotionBoundsState() { 327 return mMotionBoundsState; 328 } 329 330 /** Set whether the IME is currently showing and its height. */ setImeVisibility(boolean imeShowing, int imeHeight)331 public void setImeVisibility(boolean imeShowing, int imeHeight) { 332 mIsImeShowing = imeShowing; 333 mImeHeight = imeHeight; 334 } 335 336 /** Returns whether the IME is currently showing. */ isImeShowing()337 public boolean isImeShowing() { 338 return mIsImeShowing; 339 } 340 341 /** Returns the IME height. */ getImeHeight()342 public int getImeHeight() { 343 return mImeHeight; 344 } 345 346 /** Set whether the shelf is showing and its height. */ setShelfVisibility(boolean showing, int height)347 public void setShelfVisibility(boolean showing, int height) { 348 setShelfVisibility(showing, height, true); 349 } 350 351 /** Set whether the shelf is showing and its height. */ setShelfVisibility(boolean showing, int height, boolean updateMovementBounds)352 public void setShelfVisibility(boolean showing, int height, boolean updateMovementBounds) { 353 final boolean shelfShowing = showing && height > 0; 354 if (shelfShowing == mIsShelfShowing && height == mShelfHeight) { 355 return; 356 } 357 358 mIsShelfShowing = showing; 359 mShelfHeight = height; 360 if (mOnShelfVisibilityChangeCallback != null) { 361 mOnShelfVisibilityChangeCallback.accept(mIsShelfShowing, mShelfHeight, 362 updateMovementBounds); 363 } 364 } 365 366 /** 367 * Initialize states when first entering PiP. 368 */ setBoundsStateForEntry(ComponentName componentName, float aspectRatio, Size overrideMinSize)369 public void setBoundsStateForEntry(ComponentName componentName, float aspectRatio, 370 Size overrideMinSize) { 371 setLastPipComponentName(componentName); 372 setAspectRatio(aspectRatio); 373 setOverrideMinSize(overrideMinSize); 374 } 375 376 /** Returns whether the shelf is currently showing. */ isShelfShowing()377 public boolean isShelfShowing() { 378 return mIsShelfShowing; 379 } 380 381 /** Returns the shelf height. */ getShelfHeight()382 public int getShelfHeight() { 383 return mShelfHeight; 384 } 385 386 /** Returns whether the user has resized the PIP. */ hasUserResizedPip()387 public boolean hasUserResizedPip() { 388 return mHasUserResizedPip; 389 } 390 391 /** Set whether the user has resized the PIP. */ setHasUserResizedPip(boolean hasUserResizedPip)392 public void setHasUserResizedPip(boolean hasUserResizedPip) { 393 mHasUserResizedPip = hasUserResizedPip; 394 } 395 396 /** 397 * Registers a callback when the minimal size of PIP that is set by the app changes. 398 */ setOnMinimalSizeChangeCallback(@ullable Runnable onMinimalSizeChangeCallback)399 public void setOnMinimalSizeChangeCallback(@Nullable Runnable onMinimalSizeChangeCallback) { 400 mOnMinimalSizeChangeCallback = onMinimalSizeChangeCallback; 401 } 402 403 /** Set a callback to be notified when the shelf visibility changes. */ setOnShelfVisibilityChangeCallback( @ullable TriConsumer<Boolean, Integer, Boolean> onShelfVisibilityChangeCallback)404 public void setOnShelfVisibilityChangeCallback( 405 @Nullable TriConsumer<Boolean, Integer, Boolean> onShelfVisibilityChangeCallback) { 406 mOnShelfVisibilityChangeCallback = onShelfVisibilityChangeCallback; 407 } 408 409 /** 410 * Set a callback to watch out for PiP bounds. This is mostly used by SystemUI's 411 * Back-gesture handler, to avoid conflicting with PiP when it's stashed. 412 */ setPipExclusionBoundsChangeCallback( @ullable Consumer<Rect> onPipExclusionBoundsChangeCallback)413 public void setPipExclusionBoundsChangeCallback( 414 @Nullable Consumer<Rect> onPipExclusionBoundsChangeCallback) { 415 mOnPipExclusionBoundsChangeCallback = onPipExclusionBoundsChangeCallback; 416 if (mOnPipExclusionBoundsChangeCallback != null) { 417 mOnPipExclusionBoundsChangeCallback.accept(getBounds()); 418 } 419 } 420 421 /** Source of truth for the current bounds of PIP that may be in motion. */ 422 public static class MotionBoundsState { 423 /** The bounds used when PIP is in motion (e.g. during a drag or animation) */ 424 private final @NonNull Rect mBoundsInMotion = new Rect(); 425 /** The destination bounds to which PIP is animating. */ 426 private final @NonNull Rect mAnimatingToBounds = new Rect(); 427 428 /** Whether PIP is being dragged or animated (e.g. resizing, in fling, etc). */ isInMotion()429 public boolean isInMotion() { 430 return !mBoundsInMotion.isEmpty(); 431 } 432 433 /** Set the temporary bounds used to represent the drag or animation bounds of PIP. */ setBoundsInMotion(@onNull Rect bounds)434 public void setBoundsInMotion(@NonNull Rect bounds) { 435 mBoundsInMotion.set(bounds); 436 } 437 438 /** Set the bounds to which PIP is animating. */ setAnimatingToBounds(@onNull Rect bounds)439 public void setAnimatingToBounds(@NonNull Rect bounds) { 440 mAnimatingToBounds.set(bounds); 441 } 442 443 /** Called when all ongoing motion operations have ended. */ onAllAnimationsEnded()444 public void onAllAnimationsEnded() { 445 mBoundsInMotion.setEmpty(); 446 } 447 448 /** Called when an ongoing physics animation has ended. */ onPhysicsAnimationEnded()449 public void onPhysicsAnimationEnded() { 450 mAnimatingToBounds.setEmpty(); 451 } 452 453 /** Returns the motion bounds. */ 454 @NonNull getBoundsInMotion()455 public Rect getBoundsInMotion() { 456 return mBoundsInMotion; 457 } 458 459 /** Returns the destination bounds to which PIP is currently animating. */ 460 @NonNull getAnimatingToBounds()461 public Rect getAnimatingToBounds() { 462 return mAnimatingToBounds; 463 } 464 dump(PrintWriter pw, String prefix)465 void dump(PrintWriter pw, String prefix) { 466 final String innerPrefix = prefix + " "; 467 pw.println(prefix + MotionBoundsState.class.getSimpleName()); 468 pw.println(innerPrefix + "mBoundsInMotion=" + mBoundsInMotion); 469 pw.println(innerPrefix + "mAnimatingToBounds=" + mAnimatingToBounds); 470 } 471 } 472 473 static final class PipReentryState { 474 private static final String TAG = PipReentryState.class.getSimpleName(); 475 476 private final @Nullable Size mSize; 477 private final float mSnapFraction; 478 PipReentryState(@ullable Size size, float snapFraction)479 PipReentryState(@Nullable Size size, float snapFraction) { 480 mSize = size; 481 mSnapFraction = snapFraction; 482 } 483 484 @Nullable getSize()485 Size getSize() { 486 return mSize; 487 } 488 getSnapFraction()489 float getSnapFraction() { 490 return mSnapFraction; 491 } 492 dump(PrintWriter pw, String prefix)493 void dump(PrintWriter pw, String prefix) { 494 final String innerPrefix = prefix + " "; 495 pw.println(prefix + TAG); 496 pw.println(innerPrefix + "mSize=" + mSize); 497 pw.println(innerPrefix + "mSnapFraction=" + mSnapFraction); 498 } 499 } 500 501 /** Dumps internal state. */ dump(PrintWriter pw, String prefix)502 public void dump(PrintWriter pw, String prefix) { 503 final String innerPrefix = prefix + " "; 504 pw.println(prefix + TAG); 505 pw.println(innerPrefix + "mBounds=" + mBounds); 506 pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds); 507 pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds); 508 pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds); 509 pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds); 510 pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds); 511 pw.println(innerPrefix + "mLastPipComponentName=" + mLastPipComponentName); 512 pw.println(innerPrefix + "mAspectRatio=" + mAspectRatio); 513 pw.println(innerPrefix + "mDisplayId=" + mDisplayId); 514 pw.println(innerPrefix + "mDisplayLayout=" + mDisplayLayout); 515 pw.println(innerPrefix + "mStashedState=" + mStashedState); 516 pw.println(innerPrefix + "mStashOffset=" + mStashOffset); 517 pw.println(innerPrefix + "mMinEdgeSize=" + mMinEdgeSize); 518 pw.println(innerPrefix + "mOverrideMinSize=" + mOverrideMinSize); 519 pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing); 520 pw.println(innerPrefix + "mImeHeight=" + mImeHeight); 521 pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing); 522 pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight); 523 if (mPipReentryState == null) { 524 pw.println(innerPrefix + "mPipReentryState=null"); 525 } else { 526 mPipReentryState.dump(pw, innerPrefix); 527 } 528 mMotionBoundsState.dump(pw, innerPrefix); 529 } 530 } 531