1 /*
2  * Copyright (C) 2022 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.tv;
18 
19 import static com.android.wm.shell.common.pip.PipBoundsState.STASH_TYPE_NONE;
20 
21 import android.annotation.NonNull;
22 import android.annotation.Nullable;
23 import android.content.Context;
24 import android.content.res.Resources;
25 import android.graphics.Rect;
26 import android.os.Handler;
27 
28 import com.android.internal.annotations.VisibleForTesting;
29 import com.android.internal.protolog.common.ProtoLog;
30 import com.android.wm.shell.R;
31 import com.android.wm.shell.pip.tv.TvPipKeepClearAlgorithm.Placement;
32 import com.android.wm.shell.protolog.ShellProtoLogGroup;
33 
34 import java.util.Objects;
35 import java.util.function.Supplier;
36 
37 /**
38  * Controller managing the PiP's position.
39  * Manages debouncing of PiP movements and scheduling of unstashing.
40  */
41 public class TvPipBoundsController {
42     private static final String TAG = "TvPipBoundsController";
43 
44     /**
45      * Time the calculated PiP position needs to be stable before PiP is moved there,
46      * to avoid erratic movement.
47      * Some changes will cause the PiP to be repositioned immediately, such as changes to
48      * unrestricted keep clear areas.
49      */
50     @VisibleForTesting
51     static final long POSITION_DEBOUNCE_TIMEOUT_MILLIS = 300L;
52 
53     private final Context mContext;
54     private final Supplier<Long> mClock;
55     private final Handler mMainHandler;
56     private final TvPipBoundsState mTvPipBoundsState;
57     private final TvPipBoundsAlgorithm mTvPipBoundsAlgorithm;
58 
59     @Nullable
60     private PipBoundsListener mListener;
61 
62     private int mResizeAnimationDuration;
63     private int mStashDurationMs;
64     private Rect mCurrentPlacementBounds;
65     private Rect mPipTargetBounds;
66 
67     private final Runnable mApplyPendingPlacementRunnable = this::applyPendingPlacement;
68     private boolean mPendingStash;
69     private Placement mPendingPlacement;
70     private int mPendingPlacementAnimationDuration;
71     private Runnable mUnstashRunnable;
72 
TvPipBoundsController( Context context, Supplier<Long> clock, Handler mainHandler, TvPipBoundsState tvPipBoundsState, TvPipBoundsAlgorithm tvPipBoundsAlgorithm)73     public TvPipBoundsController(
74             Context context,
75             Supplier<Long> clock,
76             Handler mainHandler,
77             TvPipBoundsState tvPipBoundsState,
78             TvPipBoundsAlgorithm tvPipBoundsAlgorithm) {
79         mContext = context;
80         mClock = clock;
81         mMainHandler = mainHandler;
82         mTvPipBoundsState = tvPipBoundsState;
83         mTvPipBoundsAlgorithm = tvPipBoundsAlgorithm;
84 
85         loadConfigurations();
86     }
87 
loadConfigurations()88     private void loadConfigurations() {
89         final Resources res = mContext.getResources();
90         mResizeAnimationDuration = res.getInteger(R.integer.config_pipResizeAnimationDuration);
91         mStashDurationMs = res.getInteger(R.integer.config_pipStashDuration);
92     }
93 
setListener(PipBoundsListener listener)94     void setListener(PipBoundsListener listener) {
95         mListener = listener;
96     }
97 
98     /**
99      * Update the PiP bounds based on the state of the PiP, decors, and keep clear areas.
100      * Unless {@code immediate} is {@code true}, the PiP does not move immediately to avoid
101      * keep clear areas, but waits for a new position to stay uncontested for
102      * {@link #POSITION_DEBOUNCE_TIMEOUT_MILLIS} before moving to it.
103      * Temporary decor changes are applied immediately.
104      *
105      * @param stayAtAnchorPosition If true, PiP will be placed at the anchor position
106      * @param disallowStashing     If true, PiP will not be placed off-screen in a stashed position
107      * @param animationDuration    Duration of the animation to the new position
108      * @param immediate            If true, PiP will move immediately to avoid keep clear areas
109      */
110     @VisibleForTesting
recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing, int animationDuration, boolean immediate)111     void recalculatePipBounds(boolean stayAtAnchorPosition, boolean disallowStashing,
112             int animationDuration, boolean immediate) {
113         final Placement placement = mTvPipBoundsAlgorithm.getTvPipPlacement();
114 
115         final int stashType = disallowStashing ? STASH_TYPE_NONE : placement.getStashType();
116         mTvPipBoundsState.setStashed(stashType);
117         if (stayAtAnchorPosition) {
118             cancelScheduledPlacement();
119             applyPlacementBounds(placement.getAnchorBounds(), animationDuration);
120         } else if (disallowStashing) {
121             cancelScheduledPlacement();
122             applyPlacementBounds(placement.getUnstashedBounds(), animationDuration);
123         } else if (immediate) {
124             boolean shouldStash = mUnstashRunnable != null || placement.getTriggerStash();
125             cancelScheduledPlacement();
126             applyPlacement(placement, shouldStash, animationDuration);
127         } else {
128             if (mCurrentPlacementBounds != null) {
129                 applyPlacementBounds(mCurrentPlacementBounds, animationDuration);
130             }
131             schedulePinnedStackPlacement(placement, animationDuration);
132         }
133     }
134 
schedulePinnedStackPlacement(@onNull final Placement placement, int animationDuration)135     private void schedulePinnedStackPlacement(@NonNull final Placement placement,
136             int animationDuration) {
137         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
138                 "%s: schedulePinnedStackPlacement() - pip bounds: %s",
139                 TAG, placement.getBounds().toShortString());
140 
141         if (mPendingPlacement != null && Objects.equals(mPendingPlacement.getBounds(),
142                 placement.getBounds())) {
143             mPendingStash = mPendingStash || placement.getTriggerStash();
144             return;
145         }
146 
147         mPendingStash = placement.getStashType() != STASH_TYPE_NONE
148                 && (mPendingStash || placement.getTriggerStash());
149 
150         mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable);
151         mPendingPlacement = placement;
152         mPendingPlacementAnimationDuration = animationDuration;
153         mMainHandler.postAtTime(mApplyPendingPlacementRunnable,
154                 mClock.get() + POSITION_DEBOUNCE_TIMEOUT_MILLIS);
155     }
156 
scheduleUnstashIfNeeded(final Placement placement)157     private void scheduleUnstashIfNeeded(final Placement placement) {
158         if (mUnstashRunnable != null) {
159             mMainHandler.removeCallbacks(mUnstashRunnable);
160             mUnstashRunnable = null;
161         }
162         if (placement.getUnstashDestinationBounds() != null) {
163             mUnstashRunnable = () -> {
164                 applyPlacementBounds(placement.getUnstashDestinationBounds(),
165                         mResizeAnimationDuration);
166                 mUnstashRunnable = null;
167             };
168             mMainHandler.postAtTime(mUnstashRunnable, mClock.get() + mStashDurationMs);
169         }
170     }
171 
applyPendingPlacement()172     private void applyPendingPlacement() {
173         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
174                 "%s: applyPendingPlacement()", TAG);
175         if (mPendingPlacement != null) {
176             applyPlacement(mPendingPlacement, mPendingStash, mPendingPlacementAnimationDuration);
177             mPendingStash = false;
178             mPendingPlacement = null;
179         }
180     }
181 
applyPlacement(@onNull final Placement placement, boolean shouldStash, int animationDuration)182     private void applyPlacement(@NonNull final Placement placement, boolean shouldStash,
183             int animationDuration) {
184         if (placement.getStashType() != STASH_TYPE_NONE && shouldStash) {
185             scheduleUnstashIfNeeded(placement);
186         }
187 
188         Rect bounds =
189                 mUnstashRunnable != null ? placement.getBounds() : placement.getUnstashedBounds();
190         applyPlacementBounds(bounds, animationDuration);
191     }
192 
reset()193     void reset() {
194         mCurrentPlacementBounds = null;
195         mPipTargetBounds = null;
196         cancelScheduledPlacement();
197     }
198 
cancelScheduledPlacement()199     private void cancelScheduledPlacement() {
200         mMainHandler.removeCallbacks(mApplyPendingPlacementRunnable);
201         mPendingPlacement = null;
202 
203         if (mUnstashRunnable != null) {
204             mMainHandler.removeCallbacks(mUnstashRunnable);
205             mUnstashRunnable = null;
206         }
207     }
208 
applyPlacementBounds(Rect bounds, int animationDuration)209     private void applyPlacementBounds(Rect bounds, int animationDuration) {
210         if (bounds == null) {
211             return;
212         }
213 
214         mCurrentPlacementBounds = bounds;
215         Rect adjustedBounds = mTvPipBoundsAlgorithm.adjustBoundsForTemporaryDecor(bounds);
216         movePipTo(adjustedBounds, animationDuration);
217     }
218 
219     /** Animates the PiP to the given bounds with the given animation duration. */
movePipTo(Rect bounds, int animationDuration)220     private void movePipTo(Rect bounds, int animationDuration) {
221         if (Objects.equals(mPipTargetBounds, bounds)) {
222             return;
223         }
224 
225         mPipTargetBounds = bounds;
226         ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
227                 "%s: movePipTo() - new pip bounds: %s", TAG, bounds.toShortString());
228 
229         if (mListener != null) {
230             mListener.onPipTargetBoundsChange(bounds, animationDuration);
231         }
232     }
233 
234     /**
235      * Interface being notified of changes to the PiP bounds as calculated by
236      * @link TvPipBoundsController}.
237      */
238     public interface PipBoundsListener {
239         /**
240          * Called when the calculated PiP bounds are changing.
241          *
242          * @param newTargetBounds The new bounds of the PiP.
243          * @param animationDuration The animation duration for the PiP movement.
244          */
onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration)245         void onPipTargetBoundsChange(Rect newTargetBounds, int animationDuration);
246     }
247 }
248