• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2024 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.pip2.phone;
18 
19 import android.annotation.IntDef;
20 import android.app.TaskInfo;
21 import android.graphics.Rect;
22 import android.os.Bundle;
23 import android.os.Handler;
24 import android.view.SurfaceControl;
25 import android.window.WindowContainerToken;
26 
27 import androidx.annotation.NonNull;
28 import androidx.annotation.Nullable;
29 import androidx.annotation.VisibleForTesting;
30 
31 import com.android.internal.protolog.ProtoLog;
32 import com.android.internal.util.Preconditions;
33 import com.android.wm.shell.common.pip.PipDesktopState;
34 import com.android.wm.shell.protolog.ShellProtoLogGroup;
35 import com.android.wm.shell.shared.annotations.ShellMainThread;
36 
37 import java.io.PrintWriter;
38 import java.lang.annotation.Retention;
39 import java.lang.annotation.RetentionPolicy;
40 import java.util.ArrayList;
41 import java.util.List;
42 
43 /**
44  * Contains the state relevant to carry out or probe the status of PiP transitions.
45  *
46  * <p>Existing and new PiP components can subscribe to PiP transition related state changes
47  * via <code>PipTransitionStateChangedListener</code>.</p>
48  *
49  * <p><code>PipTransitionState</code> users shouldn't rely on listener execution ordering.
50  * For example, if a class <code>Foo</code> wants to change some arbitrary state A that belongs
51  * to some other class <code>Bar</code>, a special care must be given when manipulating state A in
52  * <code>Foo#onPipTransitionStateChanged()</code>, since that's the responsibility of
53  * the class <code>Bar</code>.</p>
54  *
55  * <p>Hence, the recommended usage for classes who want to subscribe to
56  * <code>PipTransitionState</code> changes is to manipulate only their own internal state or
57  * <code>PipTransitionState</code> state.</p>
58  *
59  * <p>If there is some state that must be manipulated in another class <code>Bar</code>, it should
60  * just be moved to <code>PipTransitionState</code> and become a shared state
61  * between Foo and Bar.</p>
62  *
63  * <p>Moreover, <code>onPipTransitionStateChanged(oldState, newState, extra)</code>
64  * receives a <code>Bundle</code> extra object that can be optionally set via
65  * <code>setState(state, extra)</code>. This can be used to resolve extra information to update
66  * relevant internal or <code>PipTransitionState</code> state. However, each listener
67  * needs to check for whether the extra passed is correct for a particular state,
68  * and throw an <code>IllegalStateException</code> otherwise.</p>
69  */
70 public class PipTransitionState {
71     private static final String TAG = PipTransitionState.class.getSimpleName();
72 
73     public static final int UNDEFINED = 0;
74 
75     // State for Launcher animating the swipe PiP to home animation.
76     public static final int SWIPING_TO_PIP = 1;
77 
78     // State for Shell animating enter PiP or jump-cutting to PiP mode after Launcher animation.
79     public static final int ENTERING_PIP = 2;
80 
81     // State for app finishing drawing in PiP mode as a final step in enter PiP flow.
82     public static final int ENTERED_PIP = 3;
83 
84     // State to indicate we have scheduled a PiP bounds change transition.
85     public static final int SCHEDULED_BOUNDS_CHANGE = 4;
86 
87     // State for the start of playing a transition to change PiP bounds. At this point, WM Core
88     // is aware of the new PiP bounds, but Shell might still be continuing animating.
89     public static final int CHANGING_PIP_BOUNDS = 5;
90 
91     // State for finishing animating into new PiP bounds after resize is complete.
92     public static final int CHANGED_PIP_BOUNDS = 6;
93 
94     // State for starting exiting PiP.
95     public static final int EXITING_PIP = 7;
96 
97     // State for finishing exit PiP flow.
98     public static final int EXITED_PIP = 8;
99 
100     private static final int FIRST_CUSTOM_STATE = 1000;
101 
102     private int mPrevCustomState = FIRST_CUSTOM_STATE;
103 
104     @IntDef(prefix = { "TRANSITION_STATE_" }, value =  {
105             UNDEFINED,
106             SWIPING_TO_PIP,
107             ENTERING_PIP,
108             ENTERED_PIP,
109             SCHEDULED_BOUNDS_CHANGE,
110             CHANGING_PIP_BOUNDS,
111             CHANGED_PIP_BOUNDS,
112             EXITING_PIP,
113             EXITED_PIP,
114     })
115     @Retention(RetentionPolicy.SOURCE)
116     public @interface TransitionState {}
117 
118     @TransitionState
119     private int mState;
120 
121     //
122     // Dependencies
123     //
124 
125     @ShellMainThread
126     private final Handler mMainHandler;
127 
128     private final PipDesktopState mPipDesktopState;
129 
130     //
131     // Swipe up to enter PiP related state
132     //
133 
134     // true if Launcher has started swipe PiP to home animation
135     private boolean mInSwipePipToHomeTransition;
136 
137     // App bounds used when as a starting point to swipe PiP to home animation in Launcher;
138     // these are also used to calculate the app icon overlay buffer size.
139     @NonNull
140     private final Rect mSwipePipToHomeAppBounds = new Rect();
141 
142     //
143     // Task related caches
144     //
145 
146     // pinned PiP task's leash
147     @Nullable
148     private SurfaceControl mPinnedTaskLeash;
149 
150     // pinned PiP task info
151     @Nullable
152     private TaskInfo mPipTaskInfo;
153 
154     // Overlay leash potentially used during swipe PiP to home transition;
155     // if null while mInSwipePipToHomeTransition is true, then srcRectHint was invalid.
156     @Nullable
157     private SurfaceControl mSwipePipToHomeOverlay;
158 
159     //
160     // Scheduling-related state
161     //
162     @Nullable
163     private Runnable mOnIdlePipTransitionStateRunnable;
164 
165     private boolean mInFixedRotation = false;
166 
167     /**
168      * An interface to track state updates as we progress through PiP transitions.
169      */
170     public interface PipTransitionStateChangedListener {
171 
172         /** Reports changes in PiP transition state. */
onPipTransitionStateChanged(@ransitionState int oldState, @TransitionState int newState, @Nullable Bundle extra)173         void onPipTransitionStateChanged(@TransitionState int oldState,
174                 @TransitionState int newState, @Nullable Bundle extra);
175     }
176 
177     private final List<PipTransitionStateChangedListener> mCallbacks = new ArrayList<>();
178 
PipTransitionState(@hellMainThread Handler handler, PipDesktopState pipDesktopState)179     public PipTransitionState(@ShellMainThread Handler handler, PipDesktopState pipDesktopState) {
180         mMainHandler = handler;
181         mPipDesktopState = pipDesktopState;
182     }
183 
184     /**
185      * @return the state of PiP in the context of transitions.
186      */
187     @TransitionState
getState()188     public int getState() {
189         return mState;
190     }
191 
192     /**
193      * Sets the state of PiP in the context of transitions.
194      */
setState(@ransitionState int state)195     public void setState(@TransitionState int state) {
196         setState(state, null /* extra */);
197     }
198 
199     /**
200      * Sets the state of PiP in the context of transitions
201      *
202      * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info.
203      */
setState(@ransitionState int state, @Nullable Bundle extra)204     public void setState(@TransitionState int state, @Nullable Bundle extra) {
205         if (state == ENTERING_PIP || state == SWIPING_TO_PIP
206                 || state == SCHEDULED_BOUNDS_CHANGE || state == CHANGING_PIP_BOUNDS) {
207             // States listed above require extra bundles to be provided.
208             Preconditions.checkArgument(extra != null && !extra.isEmpty(),
209                     "No extra bundle for " + stateToString(state) + " state.");
210         }
211         if (!shouldTransitionToState(state)) {
212             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
213                     "%s: Attempted to transition to an invalid state=%s, while in %s",
214                     TAG, stateToString(state), this);
215             return;
216         }
217 
218         if (mState != state) {
219             final int prevState = mState;
220             mState = state;
221             dispatchPipTransitionStateChanged(prevState, mState, extra);
222         }
223 
224         maybeRunOnIdlePipTransitionStateCallback();
225     }
226 
227     /**
228      * Posts the state update for PiP in the context of transitions onto the main handler.
229      *
230      * <p>This is done to guarantee that any callback dispatches for the present state are
231      * complete. This is relevant for states that have multiple listeners, such as
232      * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with
233      * the actual transition scheduling.</p>
234      */
postState(@ransitionState int state)235     public void postState(@TransitionState int state) {
236         postState(state, null /* extra */);
237     }
238 
239     /**
240      * Posts the state update for PiP in the context of transitions onto the main handler.
241      *
242      * <p>This is done to guarantee that any callback dispatches for the present state are
243      * complete. This is relevant for states that have multiple listeners, such as
244      * <code>SCHEDULED_BOUNDS_CHANGE</code> that helps turn off touch interactions along with
245      * the actual transition scheduling.</p>
246      *
247      * @param extra a bundle passed to the subscribed listeners to resolve/cache extra info.
248      */
postState(@ransitionState int state, @Nullable Bundle extra)249     public void postState(@TransitionState int state, @Nullable Bundle extra) {
250         mMainHandler.post(() -> setState(state, extra));
251     }
252 
dispatchPipTransitionStateChanged(@ransitionState int oldState, @TransitionState int newState, @Nullable Bundle extra)253     private void dispatchPipTransitionStateChanged(@TransitionState int oldState,
254             @TransitionState int newState, @Nullable Bundle extra) {
255         mCallbacks.forEach(l -> l.onPipTransitionStateChanged(oldState, newState, extra));
256     }
257 
258     /**
259      * Schedule a callback to run when in a valid idle PiP state.
260      *
261      * <p>We only allow for one callback to be scheduled to avoid cases with multiple transitions
262      * being scheduled. For instance, if user double taps and IME shows, this would
263      * schedule a bounds change transition for IME appearing. But if some other transition would
264      * want to animate PiP before the scheduled callback executes, we would rather want to replace
265      * the existing callback with a new one, to avoid multiple animations
266      * as soon as we are idle.</p>
267      */
setOnIdlePipTransitionStateRunnable( @ullable Runnable onIdlePipTransitionStateRunnable)268     public void setOnIdlePipTransitionStateRunnable(
269             @Nullable Runnable onIdlePipTransitionStateRunnable) {
270         mOnIdlePipTransitionStateRunnable = onIdlePipTransitionStateRunnable;
271         maybeRunOnIdlePipTransitionStateCallback();
272     }
273 
maybeRunOnIdlePipTransitionStateCallback()274     private void maybeRunOnIdlePipTransitionStateCallback() {
275         if (mOnIdlePipTransitionStateRunnable != null && isPipStateIdle()) {
276             mMainHandler.post(mOnIdlePipTransitionStateRunnable);
277             mOnIdlePipTransitionStateRunnable = null;
278         }
279     }
280 
281     /**
282      * Adds a {@link PipTransitionStateChangedListener} for future PiP transition state updates.
283      */
addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener)284     public void addPipTransitionStateChangedListener(PipTransitionStateChangedListener listener) {
285         if (mCallbacks.contains(listener)) {
286             return;
287         }
288         mCallbacks.add(listener);
289     }
290 
291     /**
292      * @return true if provided {@link PipTransitionStateChangedListener}
293      * is registered before removing it.
294      */
removePipTransitionStateChangedListener( PipTransitionStateChangedListener listener)295     public boolean removePipTransitionStateChangedListener(
296             PipTransitionStateChangedListener listener) {
297         return mCallbacks.remove(listener);
298     }
299 
300     /**
301      * @return true if we have fully entered PiP.
302      */
isInPip()303     public boolean isInPip() {
304         return mState > ENTERING_PIP && mState < EXITING_PIP;
305     }
306 
setSwipePipToHomeState(@ullable SurfaceControl overlayLeash, @NonNull Rect appBounds)307     void setSwipePipToHomeState(@Nullable SurfaceControl overlayLeash,
308             @NonNull Rect appBounds) {
309         mInSwipePipToHomeTransition = true;
310         if (overlayLeash != null && !appBounds.isEmpty()) {
311             mSwipePipToHomeOverlay = overlayLeash;
312             mSwipePipToHomeAppBounds.set(appBounds);
313         }
314     }
315 
resetSwipePipToHomeState()316     void resetSwipePipToHomeState() {
317         mInSwipePipToHomeTransition = false;
318         mSwipePipToHomeOverlay = null;
319         mSwipePipToHomeAppBounds.setEmpty();
320     }
321 
322     @Nullable
getPipTaskToken()323     public WindowContainerToken getPipTaskToken() {
324         return mPipTaskInfo != null ? mPipTaskInfo.getToken() : null;
325     }
326 
getPinnedTaskLeash()327     @Nullable SurfaceControl getPinnedTaskLeash() {
328         return mPinnedTaskLeash;
329     }
330 
setPinnedTaskLeash(@ullable SurfaceControl leash)331     void setPinnedTaskLeash(@Nullable SurfaceControl leash) {
332         mPinnedTaskLeash = leash;
333     }
334 
getPipTaskInfo()335     @Nullable TaskInfo getPipTaskInfo() {
336         return mPipTaskInfo;
337     }
338 
setPipTaskInfo(@ullable TaskInfo pipTaskInfo)339     void setPipTaskInfo(@Nullable TaskInfo pipTaskInfo) {
340         mPipTaskInfo = pipTaskInfo;
341     }
342 
343     /**
344      * @return true if either in swipe or button-nav fixed rotation.
345      */
isInFixedRotation()346     public boolean isInFixedRotation() {
347         return mInFixedRotation;
348     }
349 
350     /**
351      * Sets the fixed rotation flag.
352      */
setInFixedRotation(boolean inFixedRotation)353     public void setInFixedRotation(boolean inFixedRotation) {
354         mInFixedRotation = inFixedRotation;
355         if (!inFixedRotation) {
356             maybeRunOnIdlePipTransitionStateCallback();
357         }
358     }
359 
360     /**
361      * @return true if in swipe PiP to home. Note that this is true until overlay fades if used too.
362      */
isInSwipePipToHomeTransition()363     public boolean isInSwipePipToHomeTransition() {
364         return mInSwipePipToHomeTransition;
365     }
366 
367     /**
368      * @return the overlay used during swipe PiP to home for invalid srcRectHints in auto-enter PiP;
369      * null if srcRectHint provided is valid.
370      */
371     @Nullable
getSwipePipToHomeOverlay()372     public SurfaceControl getSwipePipToHomeOverlay() {
373         return mSwipePipToHomeOverlay;
374     }
375 
376     /**
377      * @return app bounds used to calculate
378      */
379     @NonNull
getSwipePipToHomeAppBounds()380     public Rect getSwipePipToHomeAppBounds() {
381         return mSwipePipToHomeAppBounds;
382     }
383 
384     /**
385      * @return a custom state solely for internal use by the caller.
386      */
387     @TransitionState
getCustomState()388     public int getCustomState() {
389         return ++mPrevCustomState;
390     }
391 
392     @VisibleForTesting
shouldTransitionToState(@ransitionState int newState)393     boolean shouldTransitionToState(@TransitionState int newState) {
394         switch (newState) {
395             case SCHEDULED_BOUNDS_CHANGE:
396                 // Allow scheduling bounds change only when both of these are true:
397                 // - while in PiP, except for if another bounds change was scheduled but hasn't
398                 //   started playing yet
399                 // - there is no drag-to-desktop gesture in progress; otherwise the PiP resize
400                 //   transition will block the drag-to-desktop transitions from finishing
401                 return isInPip() && !mPipDesktopState.isDragToDesktopInProgress();
402             default:
403                 return true;
404         }
405     }
406 
stateToString(int state)407     private static String stateToString(int state) {
408         switch (state) {
409             case UNDEFINED: return "undefined";
410             case SWIPING_TO_PIP: return "swiping_to_pip";
411             case ENTERING_PIP: return "entering-pip";
412             case ENTERED_PIP: return "entered-pip";
413             case SCHEDULED_BOUNDS_CHANGE: return "scheduled_bounds_change";
414             case CHANGING_PIP_BOUNDS: return "changing-bounds";
415             case CHANGED_PIP_BOUNDS: return "changed-bounds";
416             case EXITING_PIP: return "exiting-pip";
417             case EXITED_PIP: return "exited-pip";
418         }
419         throw new IllegalStateException("Unknown state: " + state);
420     }
421 
isPipStateIdle()422     public boolean isPipStateIdle() {
423         // This needs to be a valid in-PiP state that isn't a transient state.
424         return (mState == ENTERED_PIP || mState == CHANGED_PIP_BOUNDS) && !isInFixedRotation();
425     }
426 
427     @Override
toString()428     public String toString() {
429         return String.format("PipTransitionState(mState=%s, mInSwipePipToHomeTransition=%b)",
430                 stateToString(mState), mInSwipePipToHomeTransition);
431     }
432 
433     /** Dumps internal state. */
dump(PrintWriter pw, String prefix)434     public void dump(PrintWriter pw, String prefix) {
435         final String innerPrefix = prefix + "  ";
436         pw.println(prefix + TAG);
437         pw.println(innerPrefix + "mState=" + stateToString(mState));
438     }
439 }
440