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