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.legacysplitscreen;
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_CLOSE;
22 import static android.view.WindowManager.TRANSIT_FIRST_CUSTOM;
23 import static android.view.WindowManager.TRANSIT_OPEN;
24 import static android.view.WindowManager.TRANSIT_TO_BACK;
25 import static android.view.WindowManager.TRANSIT_TO_FRONT;
26 
27 import android.animation.Animator;
28 import android.animation.AnimatorListenerAdapter;
29 import android.animation.ValueAnimator;
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.app.ActivityManager;
33 import android.app.WindowConfiguration;
34 import android.graphics.Rect;
35 import android.os.IBinder;
36 import android.view.SurfaceControl;
37 import android.view.WindowManager;
38 import android.window.TransitionInfo;
39 import android.window.TransitionRequestInfo;
40 import android.window.WindowContainerTransaction;
41 
42 import com.android.wm.shell.common.TransactionPool;
43 import com.android.wm.shell.common.annotations.ExternalThread;
44 import com.android.wm.shell.transition.Transitions;
45 
46 import java.util.ArrayList;
47 
48 /** Plays transition animations for split-screen */
49 public class LegacySplitScreenTransitions implements Transitions.TransitionHandler {
50     private static final String TAG = "SplitScreenTransitions";
51 
52     public static final int TRANSIT_SPLIT_DISMISS_SNAP = TRANSIT_FIRST_CUSTOM + 10;
53 
54     private final TransactionPool mTransactionPool;
55     private final Transitions mTransitions;
56     private final LegacySplitScreenController mSplitScreen;
57     private final LegacySplitScreenTaskListener mListener;
58 
59     private IBinder mPendingDismiss = null;
60     private boolean mDismissFromSnap = false;
61     private IBinder mPendingEnter = null;
62     private IBinder mAnimatingTransition = null;
63 
64     /** Keeps track of currently running animations */
65     private final ArrayList<Animator> mAnimations = new ArrayList<>();
66 
67     private Transitions.TransitionFinishCallback mFinishCallback = null;
68     private SurfaceControl.Transaction mFinishTransaction;
69 
LegacySplitScreenTransitions(@onNull TransactionPool pool, @NonNull Transitions transitions, @NonNull LegacySplitScreenController splitScreen, @NonNull LegacySplitScreenTaskListener listener)70     LegacySplitScreenTransitions(@NonNull TransactionPool pool, @NonNull Transitions transitions,
71             @NonNull LegacySplitScreenController splitScreen,
72             @NonNull LegacySplitScreenTaskListener listener) {
73         mTransactionPool = pool;
74         mTransitions = transitions;
75         mSplitScreen = splitScreen;
76         mListener = listener;
77     }
78 
79     @Override
handleRequest(@onNull IBinder transition, @Nullable TransitionRequestInfo request)80     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
81             @Nullable TransitionRequestInfo request) {
82         WindowContainerTransaction out = null;
83         final ActivityManager.RunningTaskInfo triggerTask = request.getTriggerTask();
84         final @WindowManager.TransitionType int type = request.getType();
85         if (mSplitScreen.isDividerVisible()) {
86             // try to handle everything while in split-screen
87             out = new WindowContainerTransaction();
88             if (triggerTask != null) {
89                 final boolean shouldDismiss =
90                         // if we close the primary-docked task, then leave split-screen since there
91                         // is nothing behind it.
92                         ((type == TRANSIT_CLOSE || type == TRANSIT_TO_BACK)
93                                 && triggerTask.parentTaskId == mListener.mPrimary.taskId)
94                         // if an activity that is not supported in multi window mode is launched,
95                         // we also need to leave split-screen.
96                         || ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)
97                                 && !triggerTask.supportsMultiWindow);
98                 // In both cases, dismiss the primary
99                 if (shouldDismiss) {
100                     WindowManagerProxy.buildDismissSplit(out, mListener,
101                             mSplitScreen.getSplitLayout(), true /* dismiss */);
102                     if (type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT) {
103                         out.reorder(triggerTask.token, true /* onTop */);
104                     }
105                     mPendingDismiss = transition;
106                 }
107             }
108         } else if (triggerTask != null) {
109             // Not in split mode, so look for an open with a trigger task.
110             if ((type == TRANSIT_OPEN || type == TRANSIT_TO_FRONT)
111                     && triggerTask.configuration.windowConfiguration.getWindowingMode()
112                         == WindowConfiguration.WINDOWING_MODE_SPLIT_SCREEN_PRIMARY) {
113                 out = new WindowContainerTransaction();
114                 mSplitScreen.prepareEnterSplitTransition(out);
115                 mPendingEnter = transition;
116             }
117         }
118         return out;
119     }
120 
121     // TODO(shell-transitions): real animations
startExampleAnimation(@onNull SurfaceControl leash, boolean show)122     private void startExampleAnimation(@NonNull SurfaceControl leash, boolean show) {
123         final float end = show ? 1.f : 0.f;
124         final float start = 1.f - end;
125         final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
126         final ValueAnimator va = ValueAnimator.ofFloat(start, end);
127         va.setDuration(500);
128         va.addUpdateListener(animation -> {
129             float fraction = animation.getAnimatedFraction();
130             transaction.setAlpha(leash, start * (1.f - fraction) + end * fraction);
131             transaction.apply();
132         });
133         final Runnable finisher = () -> {
134             transaction.setAlpha(leash, end);
135             transaction.apply();
136             mTransactionPool.release(transaction);
137             mTransitions.getMainExecutor().execute(() -> {
138                 mAnimations.remove(va);
139                 onFinish();
140             });
141         };
142         va.addListener(new Animator.AnimatorListener() {
143             @Override
144             public void onAnimationStart(Animator animation) { }
145 
146             @Override
147             public void onAnimationEnd(Animator animation) {
148                 finisher.run();
149             }
150 
151             @Override
152             public void onAnimationCancel(Animator animation) {
153                 finisher.run();
154             }
155 
156             @Override
157             public void onAnimationRepeat(Animator animation) { }
158         });
159         mAnimations.add(va);
160         mTransitions.getAnimExecutor().execute(va::start);
161     }
162 
163     // TODO(shell-transitions): real animations
startExampleResizeAnimation(@onNull SurfaceControl leash, @NonNull Rect startBounds, @NonNull Rect endBounds)164     private void startExampleResizeAnimation(@NonNull SurfaceControl leash,
165             @NonNull Rect startBounds, @NonNull Rect endBounds) {
166         final SurfaceControl.Transaction transaction = mTransactionPool.acquire();
167         final ValueAnimator va = ValueAnimator.ofFloat(0.f, 1.f);
168         va.setDuration(500);
169         va.addUpdateListener(animation -> {
170             float fraction = animation.getAnimatedFraction();
171             transaction.setWindowCrop(leash,
172                     (int) (startBounds.width() * (1.f - fraction) + endBounds.width() * fraction),
173                     (int) (startBounds.height() * (1.f - fraction)
174                             + endBounds.height() * fraction));
175             transaction.setPosition(leash,
176                     startBounds.left * (1.f - fraction) + endBounds.left * fraction,
177                     startBounds.top * (1.f - fraction) + endBounds.top * fraction);
178             transaction.apply();
179         });
180         final Runnable finisher = () -> {
181             transaction.setWindowCrop(leash, 0, 0);
182             transaction.setPosition(leash, endBounds.left, endBounds.top);
183             transaction.apply();
184             mTransactionPool.release(transaction);
185             mTransitions.getMainExecutor().execute(() -> {
186                 mAnimations.remove(va);
187                 onFinish();
188             });
189         };
190         va.addListener(new AnimatorListenerAdapter() {
191             @Override
192             public void onAnimationEnd(Animator animation) {
193                 finisher.run();
194             }
195 
196             @Override
197             public void onAnimationCancel(Animator animation) {
198                 finisher.run();
199             }
200         });
201         mAnimations.add(va);
202         mTransitions.getAnimExecutor().execute(va::start);
203     }
204 
205     @Override
startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)206     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
207             @NonNull SurfaceControl.Transaction startTransaction,
208             @NonNull SurfaceControl.Transaction finishTransaction,
209             @NonNull Transitions.TransitionFinishCallback finishCallback) {
210         if (transition != mPendingDismiss && transition != mPendingEnter) {
211             // If we're not in split-mode, just abort
212             if (!mSplitScreen.isDividerVisible()) return false;
213             // Check to see if HOME is involved
214             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
215                 final TransitionInfo.Change change = info.getChanges().get(i);
216                 if (change.getTaskInfo() == null
217                         || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) continue;
218                 if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) {
219                     mSplitScreen.ensureMinimizedSplit();
220                 } else if (change.getMode() == TRANSIT_CLOSE
221                         || change.getMode() == TRANSIT_TO_BACK) {
222                     mSplitScreen.ensureNormalSplit();
223                 }
224             }
225             // Use normal animations.
226             return false;
227         }
228 
229         mFinishCallback = finishCallback;
230         mFinishTransaction = mTransactionPool.acquire();
231         mAnimatingTransition = transition;
232 
233         // Play fade animations
234         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
235             final TransitionInfo.Change change = info.getChanges().get(i);
236             final SurfaceControl leash = change.getLeash();
237             final int mode = info.getChanges().get(i).getMode();
238 
239             if (mode == TRANSIT_CHANGE) {
240                 if (change.getParent() != null) {
241                     // This is probably reparented, so we want the parent to be immediately visible
242                     final TransitionInfo.Change parentChange = info.getChange(change.getParent());
243                     startTransaction.show(parentChange.getLeash());
244                     startTransaction.setAlpha(parentChange.getLeash(), 1.f);
245                     // and then animate this layer outside the parent (since, for example, this is
246                     // the home task animating from fullscreen to part-screen).
247                     startTransaction.reparent(leash, info.getRootLeash());
248                     startTransaction.setLayer(leash, info.getChanges().size() - i);
249                     // build the finish reparent/reposition
250                     mFinishTransaction.reparent(leash, parentChange.getLeash());
251                     mFinishTransaction.setPosition(leash,
252                             change.getEndRelOffset().x, change.getEndRelOffset().y);
253                 }
254                 // TODO(shell-transitions): screenshot here
255                 final Rect startBounds = new Rect(change.getStartAbsBounds());
256                 final boolean isHome = change.getTaskInfo() != null
257                         && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
258                 if (mPendingDismiss == transition && mDismissFromSnap && !isHome) {
259                     // Home is special since it doesn't move during fling. Everything else, though,
260                     // when dismissing from snap, the top/left is at 0,0.
261                     startBounds.offsetTo(0, 0);
262                 }
263                 final Rect endBounds = new Rect(change.getEndAbsBounds());
264                 startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
265                 endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
266                 startExampleResizeAnimation(leash, startBounds, endBounds);
267             }
268             if (change.getParent() != null) {
269                 continue;
270             }
271 
272             if (transition == mPendingEnter
273                     && mListener.mPrimary.token.equals(change.getContainer())
274                     || mListener.mSecondary.token.equals(change.getContainer())) {
275                 startTransaction.setWindowCrop(leash, change.getStartAbsBounds().width(),
276                         change.getStartAbsBounds().height());
277                 if (mListener.mPrimary.token.equals(change.getContainer())) {
278                     // Move layer to top since we want it above the oversized home task during
279                     // animation even though home task is on top in hierarchy.
280                     startTransaction.setLayer(leash, info.getChanges().size() + 1);
281                 }
282             }
283             boolean isOpening = Transitions.isOpeningType(info.getType());
284             if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
285                 // fade in
286                 startExampleAnimation(leash, true /* show */);
287             } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
288                 // fade out
289                 if (transition == mPendingDismiss && mDismissFromSnap) {
290                     // Dismissing via snap-to-top/bottom means that the dismissed task is already
291                     // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
292                     // and don't animate it so it doesn't pop-in when reparented.
293                     startTransaction.setAlpha(leash, 0.f);
294                 } else {
295                     startExampleAnimation(leash, false /* show */);
296                 }
297             }
298         }
299         if (transition == mPendingEnter) {
300             // If entering, check if we should enter into minimized or normal split
301             boolean homeIsVisible = false;
302             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
303                 final TransitionInfo.Change change = info.getChanges().get(i);
304                 if (change.getTaskInfo() == null
305                         || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) {
306                     continue;
307                 }
308                 homeIsVisible = change.getMode() == TRANSIT_OPEN
309                         || change.getMode() == TRANSIT_TO_FRONT
310                         || change.getMode() == TRANSIT_CHANGE;
311                 break;
312             }
313             mSplitScreen.finishEnterSplitTransition(homeIsVisible);
314         }
315         startTransaction.apply();
316         onFinish();
317         return true;
318     }
319 
320     @ExternalThread
dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize, boolean snapped)321     void dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout,
322             boolean dismissOrMaximize, boolean snapped) {
323         final WindowContainerTransaction wct = new WindowContainerTransaction();
324         WindowManagerProxy.buildDismissSplit(wct, tiles, layout, dismissOrMaximize);
325         mTransitions.getMainExecutor().execute(() -> {
326             mDismissFromSnap = snapped;
327             mPendingDismiss = mTransitions.startTransition(TRANSIT_SPLIT_DISMISS_SNAP, wct, this);
328         });
329     }
330 
onFinish()331     private void onFinish() {
332         if (!mAnimations.isEmpty()) return;
333         mFinishTransaction.apply();
334         mTransactionPool.release(mFinishTransaction);
335         mFinishTransaction = null;
336         mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
337         mFinishCallback = null;
338         if (mAnimatingTransition == mPendingEnter) {
339             mPendingEnter = null;
340         }
341         if (mAnimatingTransition == mPendingDismiss) {
342             mSplitScreen.onDismissSplit();
343             mPendingDismiss = null;
344         }
345         mDismissFromSnap = false;
346         mAnimatingTransition = null;
347     }
348 }
349