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