• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.transition;
18 
19 import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME;
20 import static android.view.WindowManager.TRANSIT_CHANGE;
21 import static android.view.WindowManager.TRANSIT_TO_BACK;
22 import static android.window.TransitionInfo.FLAG_IS_WALLPAPER;
23 
24 import static com.android.wm.shell.common.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR;
25 import static com.android.wm.shell.splitscreen.SplitScreen.STAGE_TYPE_UNDEFINED;
26 import static com.android.wm.shell.splitscreen.SplitScreenController.EXIT_REASON_CHILD_TASK_ENTER_PIP;
27 
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.os.IBinder;
31 import android.view.SurfaceControl;
32 import android.view.WindowManager;
33 import android.window.TransitionInfo;
34 import android.window.TransitionRequestInfo;
35 import android.window.WindowContainerTransaction;
36 
37 import com.android.internal.protolog.common.ProtoLog;
38 import com.android.wm.shell.pip.PipTransitionController;
39 import com.android.wm.shell.pip.phone.PipTouchHandler;
40 import com.android.wm.shell.protolog.ShellProtoLogGroup;
41 import com.android.wm.shell.splitscreen.SplitScreenController;
42 import com.android.wm.shell.splitscreen.StageCoordinator;
43 import com.android.wm.shell.sysui.ShellInit;
44 
45 import java.util.ArrayList;
46 import java.util.Optional;
47 
48 /**
49  * A handler for dealing with transitions involving multiple other handlers. For example: an
50  * activity in split-screen going into PiP.
51  */
52 public class DefaultMixedHandler implements Transitions.TransitionHandler {
53 
54     private final Transitions mPlayer;
55     private PipTransitionController mPipHandler;
56     private StageCoordinator mSplitHandler;
57 
58     private static class MixedTransition {
59         static final int TYPE_ENTER_PIP_FROM_SPLIT = 1;
60 
61         /** Both the display and split-state (enter/exit) is changing */
62         static final int TYPE_DISPLAY_AND_SPLIT_CHANGE = 2;
63 
64         /** The default animation for this mixed transition. */
65         static final int ANIM_TYPE_DEFAULT = 0;
66 
67         /** For ENTER_PIP_FROM_SPLIT, indicates that this is a to-home animation. */
68         static final int ANIM_TYPE_GOING_HOME = 1;
69 
70         final int mType;
71         int mAnimType = 0;
72         final IBinder mTransition;
73 
74         Transitions.TransitionFinishCallback mFinishCallback = null;
75         Transitions.TransitionHandler mLeftoversHandler = null;
76         WindowContainerTransaction mFinishWCT = null;
77 
78         /**
79          * Mixed transitions are made up of multiple "parts". This keeps track of how many
80          * parts are currently animating.
81          */
82         int mInFlightSubAnimations = 0;
83 
MixedTransition(int type, IBinder transition)84         MixedTransition(int type, IBinder transition) {
85             mType = type;
86             mTransition = transition;
87         }
88     }
89 
90     private final ArrayList<MixedTransition> mActiveTransitions = new ArrayList<>();
91 
DefaultMixedHandler(@onNull ShellInit shellInit, @NonNull Transitions player, Optional<SplitScreenController> splitScreenControllerOptional, Optional<PipTouchHandler> pipTouchHandlerOptional)92     public DefaultMixedHandler(@NonNull ShellInit shellInit, @NonNull Transitions player,
93             Optional<SplitScreenController> splitScreenControllerOptional,
94             Optional<PipTouchHandler> pipTouchHandlerOptional) {
95         mPlayer = player;
96         if (Transitions.ENABLE_SHELL_TRANSITIONS && pipTouchHandlerOptional.isPresent()
97                 && splitScreenControllerOptional.isPresent()) {
98             // Add after dependencies because it is higher priority
99             shellInit.addInitCallback(() -> {
100                 mPipHandler = pipTouchHandlerOptional.get().getTransitionHandler();
101                 mSplitHandler = splitScreenControllerOptional.get().getTransitionHandler();
102                 mPlayer.addHandler(this);
103                 if (mSplitHandler != null) {
104                     mSplitHandler.setMixedHandler(this);
105                 }
106             }, this);
107         }
108     }
109 
110     @Nullable
111     @Override
handleRequest(@onNull IBinder transition, @NonNull TransitionRequestInfo request)112     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
113             @NonNull TransitionRequestInfo request) {
114         if (mPipHandler.requestHasPipEnter(request) && mSplitHandler.isSplitScreenVisible()) {
115             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Got a PiP-enter request while "
116                     + "Split-Screen is active, so treat it as Mixed.");
117             if (request.getRemoteTransition() != null) {
118                 throw new IllegalStateException("Unexpected remote transition in"
119                         + "pip-enter-from-split request");
120             }
121             mActiveTransitions.add(new MixedTransition(MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT,
122                     transition));
123 
124             WindowContainerTransaction out = new WindowContainerTransaction();
125             mPipHandler.augmentRequest(transition, request, out);
126             mSplitHandler.addEnterOrExitIfNeeded(request, out);
127             return out;
128         }
129         return null;
130     }
131 
subCopy(@onNull TransitionInfo info, @WindowManager.TransitionType int newType, boolean withChanges)132     private TransitionInfo subCopy(@NonNull TransitionInfo info,
133             @WindowManager.TransitionType int newType, boolean withChanges) {
134         final TransitionInfo out = new TransitionInfo(newType, withChanges ? info.getFlags() : 0);
135         if (withChanges) {
136             for (int i = 0; i < info.getChanges().size(); ++i) {
137                 out.getChanges().add(info.getChanges().get(i));
138             }
139         }
140         out.setRootLeash(info.getRootLeash(), info.getRootOffset().x, info.getRootOffset().y);
141         out.setAnimationOptions(info.getAnimationOptions());
142         return out;
143     }
144 
isHomeOpening(@onNull TransitionInfo.Change change)145     private boolean isHomeOpening(@NonNull TransitionInfo.Change change) {
146         return change.getTaskInfo() != null
147                 && change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME;
148     }
149 
isWallpaper(@onNull TransitionInfo.Change change)150     private boolean isWallpaper(@NonNull TransitionInfo.Change change) {
151         return (change.getFlags() & FLAG_IS_WALLPAPER) != 0;
152     }
153 
154     @Override
startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)155     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
156             @NonNull SurfaceControl.Transaction startTransaction,
157             @NonNull SurfaceControl.Transaction finishTransaction,
158             @NonNull Transitions.TransitionFinishCallback finishCallback) {
159         MixedTransition mixed = null;
160         for (int i = mActiveTransitions.size() - 1; i >= 0; --i) {
161             if (mActiveTransitions.get(i).mTransition != transition) continue;
162             mixed = mActiveTransitions.get(i);
163             break;
164         }
165         if (mixed == null) return false;
166 
167         if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
168             return animateEnterPipFromSplit(mixed, info, startTransaction, finishTransaction,
169                     finishCallback);
170         } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
171             return false;
172         } else {
173             mActiveTransitions.remove(mixed);
174             throw new IllegalStateException("Starting mixed animation without a known mixed type? "
175                     + mixed.mType);
176         }
177     }
178 
animateEnterPipFromSplit(@onNull final MixedTransition mixed, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)179     private boolean animateEnterPipFromSplit(@NonNull final MixedTransition mixed,
180             @NonNull TransitionInfo info,
181             @NonNull SurfaceControl.Transaction startTransaction,
182             @NonNull SurfaceControl.Transaction finishTransaction,
183             @NonNull Transitions.TransitionFinishCallback finishCallback) {
184         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animating a mixed transition for "
185                 + "entering PIP while Split-Screen is active.");
186         TransitionInfo.Change pipChange = null;
187         TransitionInfo.Change wallpaper = null;
188         final TransitionInfo everythingElse = subCopy(info, TRANSIT_TO_BACK, true /* changes */);
189         boolean homeIsOpening = false;
190         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
191             TransitionInfo.Change change = info.getChanges().get(i);
192             if (mPipHandler.isEnteringPip(change, info.getType())) {
193                 if (pipChange != null) {
194                     throw new IllegalStateException("More than 1 pip-entering changes in one"
195                             + " transition? " + info);
196                 }
197                 pipChange = change;
198                 // going backwards, so remove-by-index is fine.
199                 everythingElse.getChanges().remove(i);
200             } else if (isHomeOpening(change)) {
201                 homeIsOpening = true;
202             } else if (isWallpaper(change)) {
203                 wallpaper = change;
204             }
205         }
206         if (pipChange == null) {
207             // um, something probably went wrong.
208             return false;
209         }
210         final boolean isGoingHome = homeIsOpening;
211         mixed.mFinishCallback = finishCallback;
212         Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
213             --mixed.mInFlightSubAnimations;
214             if (mixed.mInFlightSubAnimations > 0) return;
215             mActiveTransitions.remove(mixed);
216             if (isGoingHome) {
217                 mSplitHandler.onTransitionAnimationComplete();
218             }
219             mixed.mFinishCallback.onTransitionFinished(wct, wctCB);
220         };
221         if (isGoingHome) {
222             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is actually mixed "
223                     + "since entering-PiP caused us to leave split and return home.");
224             // We need to split the transition into 2 parts: the pip part (animated by pip)
225             // and the dismiss-part (animated by launcher).
226             mixed.mInFlightSubAnimations = 2;
227             // immediately make the wallpaper visible (so that we don't see it pop-in during
228             // the time it takes to start recents animation (which is remote).
229             if (wallpaper != null) {
230                 startTransaction.show(wallpaper.getLeash()).setAlpha(wallpaper.getLeash(), 1.f);
231             }
232             // make a new startTransaction because pip's startEnterAnimation "consumes" it so
233             // we need a separate one to send over to launcher.
234             SurfaceControl.Transaction otherStartT = new SurfaceControl.Transaction();
235             // Let split update internal state for dismiss.
236             mSplitHandler.prepareDismissAnimation(STAGE_TYPE_UNDEFINED,
237                     EXIT_REASON_CHILD_TASK_ENTER_PIP, everythingElse, otherStartT,
238                     finishTransaction);
239 
240             // We are trying to accommodate launcher's close animation which can't handle the
241             // divider-bar, so if split-handler is closing the divider-bar, just hide it and remove
242             // from transition info.
243             for (int i = everythingElse.getChanges().size() - 1; i >= 0; --i) {
244                 if ((everythingElse.getChanges().get(i).getFlags() & FLAG_IS_DIVIDER_BAR) != 0) {
245                     everythingElse.getChanges().remove(i);
246                     break;
247                 }
248             }
249 
250             mPipHandler.startEnterAnimation(pipChange, startTransaction, finishTransaction,
251                     finishCB);
252             // Dispatch the rest of the transition normally. This will most-likely be taken by
253             // recents or default handler.
254             mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, everythingElse,
255                     otherStartT, finishTransaction, finishCB, this);
256         } else {
257             ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "  Not leaving split, so just "
258                     + "forward animation to Pip-Handler.");
259             // This happens if the pip-ing activity is in a multi-activity task (and thus a
260             // new pip task is spawned). In this case, we don't actually exit split so we can
261             // just let pip transition handle the animation verbatim.
262             mixed.mInFlightSubAnimations = 1;
263             mPipHandler.startAnimation(mixed.mTransition, info, startTransaction, finishTransaction,
264                     finishCB);
265         }
266         return true;
267     }
268 
unlinkMissingParents(TransitionInfo from)269     private void unlinkMissingParents(TransitionInfo from) {
270         for (int i = 0; i < from.getChanges().size(); ++i) {
271             final TransitionInfo.Change chg = from.getChanges().get(i);
272             if (chg.getParent() == null) continue;
273             if (from.getChange(chg.getParent()) == null) {
274                 from.getChanges().get(i).setParent(null);
275             }
276         }
277     }
278 
isWithinTask(TransitionInfo info, TransitionInfo.Change chg)279     private boolean isWithinTask(TransitionInfo info, TransitionInfo.Change chg) {
280         TransitionInfo.Change curr = chg;
281         while (curr != null) {
282             if (curr.getTaskInfo() != null) return true;
283             if (curr.getParent() == null) break;
284             curr = info.getChange(curr.getParent());
285         }
286         return false;
287     }
288 
289     /**
290      * This is intended to be called by SplitCoordinator as a helper to mix an already-pending
291      * split transition with a display-change. The use-case for this is when a display
292      * change/rotation gets collected into a split-screen enter/exit transition which has already
293      * been claimed by StageCoordinator.handleRequest . This happens during launcher tests.
294      */
animatePendingSplitWithDisplayChange(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback)295     public boolean animatePendingSplitWithDisplayChange(@NonNull IBinder transition,
296             @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT,
297             @NonNull SurfaceControl.Transaction finishT,
298             @NonNull Transitions.TransitionFinishCallback finishCallback) {
299         final TransitionInfo everythingElse = subCopy(info, info.getType(), true /* withChanges */);
300         final TransitionInfo displayPart = subCopy(info, TRANSIT_CHANGE, false /* withChanges */);
301         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
302             TransitionInfo.Change change = info.getChanges().get(i);
303             if (isWithinTask(info, change)) continue;
304             displayPart.addChange(change);
305             everythingElse.getChanges().remove(i);
306         }
307         if (displayPart.getChanges().isEmpty()) return false;
308         unlinkMissingParents(everythingElse);
309         final MixedTransition mixed = new MixedTransition(
310                 MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE, transition);
311         mixed.mFinishCallback = finishCallback;
312         mActiveTransitions.add(mixed);
313         ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Animation is a mix of display change "
314                 + "and split change.");
315         // We need to split the transition into 2 parts: the split part and the display part.
316         mixed.mInFlightSubAnimations = 2;
317 
318         Transitions.TransitionFinishCallback finishCB = (wct, wctCB) -> {
319             --mixed.mInFlightSubAnimations;
320             if (wctCB != null) {
321                 throw new IllegalArgumentException("Can't mix transitions that require finish"
322                         + " sync callback");
323             }
324             if (wct != null) {
325                 if (mixed.mFinishWCT == null) {
326                     mixed.mFinishWCT = wct;
327                 } else {
328                     mixed.mFinishWCT.merge(wct, true /* transfer */);
329                 }
330             }
331             if (mixed.mInFlightSubAnimations > 0) return;
332             mActiveTransitions.remove(mixed);
333             mixed.mFinishCallback.onTransitionFinished(mixed.mFinishWCT, null /* wctCB */);
334         };
335 
336         // Dispatch the display change. This will most-likely be taken by the default handler.
337         // Do this first since the first handler used will apply the startT; the display change
338         // needs to take a screenshot before that happens so we need it to be the first handler.
339         mixed.mLeftoversHandler = mPlayer.dispatchTransition(mixed.mTransition, displayPart,
340                 startT, finishT, finishCB, mSplitHandler);
341 
342         // Note: at this point, startT has probably already been applied, so we are basically
343         // giving splitHandler an empty startT. This is currently OK because display-change will
344         // grab a screenshot and paste it on top anyways.
345         mSplitHandler.startPendingAnimation(
346                 transition, everythingElse, startT, finishT, finishCB);
347         return true;
348     }
349 
350     @Override
mergeAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback)351     public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
352             @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget,
353             @NonNull Transitions.TransitionFinishCallback finishCallback) {
354         for (int i = 0; i < mActiveTransitions.size(); ++i) {
355             if (mActiveTransitions.get(i) != mergeTarget) continue;
356             MixedTransition mixed = mActiveTransitions.get(i);
357             if (mixed.mInFlightSubAnimations <= 0) {
358                 // Already done, so no need to end it.
359                 return;
360             }
361             if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
362                 if (mixed.mAnimType == MixedTransition.ANIM_TYPE_GOING_HOME) {
363                     boolean ended = mSplitHandler.end();
364                     // If split couldn't end (because it is remote), then don't end everything else
365                     // since we have to play out the animation anyways.
366                     if (!ended) return;
367                     mPipHandler.end();
368                     if (mixed.mLeftoversHandler != null) {
369                         mixed.mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget,
370                                 finishCallback);
371                     }
372                 } else {
373                     mPipHandler.end();
374                 }
375             } else if (mixed.mType == MixedTransition.TYPE_DISPLAY_AND_SPLIT_CHANGE) {
376                 // queue
377             } else {
378                 throw new IllegalStateException("Playing a mixed transition with unknown type? "
379                         + mixed.mType);
380             }
381         }
382     }
383 
384     @Override
onTransitionConsumed(@onNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishT)385     public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
386             @Nullable SurfaceControl.Transaction finishT) {
387         MixedTransition mixed = null;
388         for (int i = mActiveTransitions.size() - 1; i >= 0; --i) {
389             if (mActiveTransitions.get(i).mTransition != transition) continue;
390             mixed = mActiveTransitions.remove(i);
391             break;
392         }
393         if (mixed == null) return;
394         if (mixed.mType == MixedTransition.TYPE_ENTER_PIP_FROM_SPLIT) {
395             mPipHandler.onTransitionConsumed(transition, aborted, finishT);
396         }
397     }
398 }
399