• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 t, @NonNull Transitions.TransitionFinishCallback finishCallback)206     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
207             @NonNull SurfaceControl.Transaction t,
208             @NonNull Transitions.TransitionFinishCallback finishCallback) {
209         if (transition != mPendingDismiss && transition != mPendingEnter) {
210             // If we're not in split-mode, just abort
211             if (!mSplitScreen.isDividerVisible()) return false;
212             // Check to see if HOME is involved
213             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
214                 final TransitionInfo.Change change = info.getChanges().get(i);
215                 if (change.getTaskInfo() == null
216                         || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) continue;
217                 if (change.getMode() == TRANSIT_OPEN || change.getMode() == TRANSIT_TO_FRONT) {
218                     mSplitScreen.ensureMinimizedSplit();
219                 } else if (change.getMode() == TRANSIT_CLOSE
220                         || change.getMode() == TRANSIT_TO_BACK) {
221                     mSplitScreen.ensureNormalSplit();
222                 }
223             }
224             // Use normal animations.
225             return false;
226         }
227 
228         mFinishCallback = finishCallback;
229         mFinishTransaction = mTransactionPool.acquire();
230         mAnimatingTransition = transition;
231 
232         // Play fade animations
233         for (int i = info.getChanges().size() - 1; i >= 0; --i) {
234             final TransitionInfo.Change change = info.getChanges().get(i);
235             final SurfaceControl leash = change.getLeash();
236             final int mode = info.getChanges().get(i).getMode();
237 
238             if (mode == TRANSIT_CHANGE) {
239                 if (change.getParent() != null) {
240                     // This is probably reparented, so we want the parent to be immediately visible
241                     final TransitionInfo.Change parentChange = info.getChange(change.getParent());
242                     t.show(parentChange.getLeash());
243                     t.setAlpha(parentChange.getLeash(), 1.f);
244                     // and then animate this layer outside the parent (since, for example, this is
245                     // the home task animating from fullscreen to part-screen).
246                     t.reparent(leash, info.getRootLeash());
247                     t.setLayer(leash, info.getChanges().size() - i);
248                     // build the finish reparent/reposition
249                     mFinishTransaction.reparent(leash, parentChange.getLeash());
250                     mFinishTransaction.setPosition(leash,
251                             change.getEndRelOffset().x, change.getEndRelOffset().y);
252                 }
253                 // TODO(shell-transitions): screenshot here
254                 final Rect startBounds = new Rect(change.getStartAbsBounds());
255                 final boolean isHome = change.getTaskInfo() != null
256                         && change.getTaskInfo().getActivityType() == ACTIVITY_TYPE_HOME;
257                 if (mPendingDismiss == transition && mDismissFromSnap && !isHome) {
258                     // Home is special since it doesn't move during fling. Everything else, though,
259                     // when dismissing from snap, the top/left is at 0,0.
260                     startBounds.offsetTo(0, 0);
261                 }
262                 final Rect endBounds = new Rect(change.getEndAbsBounds());
263                 startBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
264                 endBounds.offset(-info.getRootOffset().x, -info.getRootOffset().y);
265                 startExampleResizeAnimation(leash, startBounds, endBounds);
266             }
267             if (change.getParent() != null) {
268                 continue;
269             }
270 
271             if (transition == mPendingEnter
272                     && mListener.mPrimary.token.equals(change.getContainer())
273                     || mListener.mSecondary.token.equals(change.getContainer())) {
274                 t.setWindowCrop(leash, change.getStartAbsBounds().width(),
275                         change.getStartAbsBounds().height());
276                 if (mListener.mPrimary.token.equals(change.getContainer())) {
277                     // Move layer to top since we want it above the oversized home task during
278                     // animation even though home task is on top in hierarchy.
279                     t.setLayer(leash, info.getChanges().size() + 1);
280                 }
281             }
282             boolean isOpening = Transitions.isOpeningType(info.getType());
283             if (isOpening && (mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT)) {
284                 // fade in
285                 startExampleAnimation(leash, true /* show */);
286             } else if (!isOpening && (mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK)) {
287                 // fade out
288                 if (transition == mPendingDismiss && mDismissFromSnap) {
289                     // Dismissing via snap-to-top/bottom means that the dismissed task is already
290                     // not-visible (usually cropped to oblivion) so immediately set its alpha to 0
291                     // and don't animate it so it doesn't pop-in when reparented.
292                     t.setAlpha(leash, 0.f);
293                 } else {
294                     startExampleAnimation(leash, false /* show */);
295                 }
296             }
297         }
298         if (transition == mPendingEnter) {
299             // If entering, check if we should enter into minimized or normal split
300             boolean homeIsVisible = false;
301             for (int i = info.getChanges().size() - 1; i >= 0; --i) {
302                 final TransitionInfo.Change change = info.getChanges().get(i);
303                 if (change.getTaskInfo() == null
304                         || change.getTaskInfo().getActivityType() != ACTIVITY_TYPE_HOME) {
305                     continue;
306                 }
307                 homeIsVisible = change.getMode() == TRANSIT_OPEN
308                         || change.getMode() == TRANSIT_TO_FRONT
309                         || change.getMode() == TRANSIT_CHANGE;
310                 break;
311             }
312             mSplitScreen.finishEnterSplitTransition(homeIsVisible);
313         }
314         t.apply();
315         onFinish();
316         return true;
317     }
318 
319     @ExternalThread
dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout, boolean dismissOrMaximize, boolean snapped)320     void dismissSplit(LegacySplitScreenTaskListener tiles, LegacySplitDisplayLayout layout,
321             boolean dismissOrMaximize, boolean snapped) {
322         final WindowContainerTransaction wct = new WindowContainerTransaction();
323         WindowManagerProxy.buildDismissSplit(wct, tiles, layout, dismissOrMaximize);
324         mTransitions.getMainExecutor().execute(() -> {
325             mDismissFromSnap = snapped;
326             mPendingDismiss = mTransitions.startTransition(TRANSIT_SPLIT_DISMISS_SNAP, wct, this);
327         });
328     }
329 
onFinish()330     private void onFinish() {
331         if (!mAnimations.isEmpty()) return;
332         mFinishTransaction.apply();
333         mTransactionPool.release(mFinishTransaction);
334         mFinishTransaction = null;
335         mFinishCallback.onTransitionFinished(null /* wct */, null /* wctCB */);
336         mFinishCallback = null;
337         if (mAnimatingTransition == mPendingEnter) {
338             mPendingEnter = null;
339         }
340         if (mAnimatingTransition == mPendingDismiss) {
341             mSplitScreen.onDismissSplit();
342             mPendingDismiss = null;
343         }
344         mDismissFromSnap = false;
345         mAnimatingTransition = null;
346     }
347 }
348