1 /*
2  * Copyright (C) 2023 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.desktopmode;
18 
19 import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.RectEvaluator;
24 import android.animation.ValueAnimator;
25 import android.app.ActivityManager;
26 import android.graphics.PointF;
27 import android.graphics.Rect;
28 import android.os.IBinder;
29 import android.util.Slog;
30 import android.view.SurfaceControl;
31 import android.view.WindowManager;
32 import android.window.TransitionInfo;
33 import android.window.TransitionRequestInfo;
34 import android.window.WindowContainerTransaction;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import com.android.wm.shell.transition.Transitions;
40 import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration;
41 import com.android.wm.shell.windowdecor.MoveToDesktopAnimator;
42 
43 import java.util.ArrayList;
44 import java.util.List;
45 import java.util.function.Consumer;
46 import java.util.function.Supplier;
47 
48 /**
49  * The {@link Transitions.TransitionHandler} that handles transitions for desktop mode tasks
50  * entering and exiting freeform.
51  */
52 public class EnterDesktopTaskTransitionHandler implements Transitions.TransitionHandler {
53 
54     private static final String TAG = "EnterDesktopTaskTransitionHandler";
55     private final Transitions mTransitions;
56     private final Supplier<SurfaceControl.Transaction> mTransactionSupplier;
57 
58     // The size of the screen after drag relative to the fullscreen size
59     public static final float FINAL_FREEFORM_SCALE = 0.6f;
60     public static final int FREEFORM_ANIMATION_DURATION = 336;
61 
62     private final List<IBinder> mPendingTransitionTokens = new ArrayList<>();
63     private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback;
64     private MoveToDesktopAnimator mMoveToDesktopAnimator;
65     private DesktopModeWindowDecoration mDesktopModeWindowDecoration;
66 
EnterDesktopTaskTransitionHandler( Transitions transitions)67     public EnterDesktopTaskTransitionHandler(
68             Transitions transitions) {
69         this(transitions, SurfaceControl.Transaction::new);
70     }
71 
EnterDesktopTaskTransitionHandler( Transitions transitions, Supplier<SurfaceControl.Transaction> supplier)72     public EnterDesktopTaskTransitionHandler(
73             Transitions transitions,
74             Supplier<SurfaceControl.Transaction> supplier) {
75         mTransitions = transitions;
76         mTransactionSupplier = supplier;
77     }
78 
79     /**
80      * Starts Transition of a given type
81      * @param type Transition type
82      * @param wct WindowContainerTransaction for transition
83      * @param onAnimationEndCallback to be called after animation
84      */
startTransition(@indowManager.TransitionType int type, @NonNull WindowContainerTransaction wct, Consumer<SurfaceControl.Transaction> onAnimationEndCallback)85     private void startTransition(@WindowManager.TransitionType int type,
86             @NonNull WindowContainerTransaction wct,
87             Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
88         mOnAnimationFinishedCallback = onAnimationEndCallback;
89         final IBinder token = mTransitions.startTransition(type, wct, this);
90         mPendingTransitionTokens.add(token);
91     }
92 
93     /**
94      * Starts Transition of type TRANSIT_START_DRAG_TO_DESKTOP_MODE
95      * @param wct WindowContainerTransaction for transition
96      * @param moveToDesktopAnimator Animator that shrinks and positions task during two part move
97      *                              to desktop animation
98      * @param onAnimationEndCallback to be called after animation
99      */
startMoveToDesktop(@onNull WindowContainerTransaction wct, @NonNull MoveToDesktopAnimator moveToDesktopAnimator, Consumer<SurfaceControl.Transaction> onAnimationEndCallback)100     public void startMoveToDesktop(@NonNull WindowContainerTransaction wct,
101             @NonNull MoveToDesktopAnimator moveToDesktopAnimator,
102             Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
103         mMoveToDesktopAnimator = moveToDesktopAnimator;
104         startTransition(Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE, wct,
105                 onAnimationEndCallback);
106     }
107 
108     /**
109      * Starts Transition of type TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
110      * @param wct WindowContainerTransaction for transition
111      * @param onAnimationEndCallback to be called after animation
112      */
finalizeMoveToDesktop(@onNull WindowContainerTransaction wct, Consumer<SurfaceControl.Transaction> onAnimationEndCallback)113     public void finalizeMoveToDesktop(@NonNull WindowContainerTransaction wct,
114             Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
115         startTransition(Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE, wct,
116                 onAnimationEndCallback);
117     }
118 
119     /**
120      * Starts Transition of type TRANSIT_CANCEL_ENTERING_DESKTOP_MODE
121      * @param wct WindowContainerTransaction for transition
122      * @param moveToDesktopAnimator Animator that shrinks and positions task during two part move
123      *                              to desktop animation
124      * @param onAnimationEndCallback to be called after animation
125      */
startCancelMoveToDesktopMode(@onNull WindowContainerTransaction wct, MoveToDesktopAnimator moveToDesktopAnimator, Consumer<SurfaceControl.Transaction> onAnimationEndCallback)126     public void startCancelMoveToDesktopMode(@NonNull WindowContainerTransaction wct,
127             MoveToDesktopAnimator moveToDesktopAnimator,
128             Consumer<SurfaceControl.Transaction> onAnimationEndCallback) {
129         mMoveToDesktopAnimator = moveToDesktopAnimator;
130         startTransition(Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE, wct,
131                 onAnimationEndCallback);
132     }
133 
134     /**
135      * Starts Transition of type TRANSIT_MOVE_TO_DESKTOP
136      * @param wct WindowContainerTransaction for transition
137      * @param decor {@link DesktopModeWindowDecoration} of task being animated
138      */
moveToDesktop(@onNull WindowContainerTransaction wct, DesktopModeWindowDecoration decor)139     public void moveToDesktop(@NonNull WindowContainerTransaction wct,
140             DesktopModeWindowDecoration decor) {
141         mDesktopModeWindowDecoration = decor;
142         startTransition(Transitions.TRANSIT_MOVE_TO_DESKTOP, wct,
143                 null /* onAnimationEndCallback */);
144     }
145 
146     @Override
startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback)147     public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
148             @NonNull SurfaceControl.Transaction startT,
149             @NonNull SurfaceControl.Transaction finishT,
150             @NonNull Transitions.TransitionFinishCallback finishCallback) {
151         boolean transitionHandled = false;
152         for (TransitionInfo.Change change : info.getChanges()) {
153             if ((change.getFlags() & TransitionInfo.FLAG_IS_WALLPAPER) != 0) {
154                 continue;
155             }
156 
157             final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
158             if (taskInfo == null || taskInfo.taskId == -1) {
159                 continue;
160             }
161 
162             if (change.getMode() == WindowManager.TRANSIT_CHANGE) {
163                 transitionHandled |= startChangeTransition(
164                         transition, info.getType(), change, startT, finishT, finishCallback);
165             }
166         }
167 
168         mPendingTransitionTokens.remove(transition);
169 
170         return transitionHandled;
171     }
172 
startChangeTransition( @onNull IBinder transition, @WindowManager.TransitionType int type, @NonNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback)173     private boolean startChangeTransition(
174             @NonNull IBinder transition,
175             @WindowManager.TransitionType int type,
176             @NonNull TransitionInfo.Change change,
177             @NonNull SurfaceControl.Transaction startT,
178             @NonNull SurfaceControl.Transaction finishT,
179             @NonNull Transitions.TransitionFinishCallback finishCallback) {
180         if (!mPendingTransitionTokens.contains(transition)) {
181             return false;
182         }
183 
184         final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo();
185         if (type == Transitions.TRANSIT_MOVE_TO_DESKTOP
186                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
187             return animateMoveToDesktop(change, startT, finishCallback);
188         }
189 
190         if (type == Transitions.TRANSIT_START_DRAG_TO_DESKTOP_MODE
191                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
192             return animateStartDragToDesktopMode(change, startT, finishT, finishCallback);
193         }
194 
195         final Rect endBounds = change.getEndAbsBounds();
196         if (type == Transitions.TRANSIT_FINALIZE_DRAG_TO_DESKTOP_MODE
197                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM
198                 && !endBounds.isEmpty()) {
199             return animateFinalizeDragToDesktopMode(change, startT, finishT, finishCallback,
200                     endBounds);
201         }
202 
203         if (type == Transitions.TRANSIT_CANCEL_DRAG_TO_DESKTOP_MODE
204                 && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) {
205             return animateCancelDragToDesktopMode(change, startT, finishT, finishCallback,
206                     endBounds);
207         }
208 
209         return false;
210     }
211 
animateMoveToDesktop( @onNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull Transitions.TransitionFinishCallback finishCallback)212     private boolean animateMoveToDesktop(
213             @NonNull TransitionInfo.Change change,
214             @NonNull SurfaceControl.Transaction startT,
215             @NonNull Transitions.TransitionFinishCallback finishCallback) {
216         if (mDesktopModeWindowDecoration == null) {
217             Slog.e(TAG, "Window Decoration is not available for this transition");
218             return false;
219         }
220 
221         final SurfaceControl leash = change.getLeash();
222         final Rect startBounds = change.getStartAbsBounds();
223         startT.setPosition(leash, startBounds.left, startBounds.right)
224                 .setWindowCrop(leash, startBounds.width(), startBounds.height())
225                 .show(leash);
226         mDesktopModeWindowDecoration.showResizeVeil(startT, startBounds);
227 
228         final ValueAnimator animator = ValueAnimator.ofObject(new RectEvaluator(),
229                 change.getStartAbsBounds(), change.getEndAbsBounds());
230         animator.setDuration(FREEFORM_ANIMATION_DURATION);
231         SurfaceControl.Transaction t = mTransactionSupplier.get();
232         animator.addUpdateListener(animation -> {
233             final Rect animationValue = (Rect) animator.getAnimatedValue();
234             t.setPosition(leash, animationValue.left, animationValue.right)
235                     .setWindowCrop(leash, animationValue.width(), animationValue.height())
236                     .show(leash);
237             mDesktopModeWindowDecoration.updateResizeVeil(t, animationValue);
238         });
239         animator.addListener(new AnimatorListenerAdapter() {
240             @Override
241             public void onAnimationEnd(Animator animation) {
242                 mDesktopModeWindowDecoration.hideResizeVeil();
243                 mTransitions.getMainExecutor().execute(
244                         () -> finishCallback.onTransitionFinished(null));
245             }
246         });
247         animator.start();
248         return true;
249     }
250 
animateStartDragToDesktopMode( @onNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback)251     private boolean animateStartDragToDesktopMode(
252             @NonNull TransitionInfo.Change change,
253             @NonNull SurfaceControl.Transaction startT,
254             @NonNull SurfaceControl.Transaction finishT,
255             @NonNull Transitions.TransitionFinishCallback finishCallback) {
256         // Transitioning to freeform but keeping fullscreen bounds, so the crop is set
257         // to null and we don't require an animation
258         final SurfaceControl sc = change.getLeash();
259         startT.setWindowCrop(sc, null);
260 
261         if (mMoveToDesktopAnimator == null
262                 || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) {
263             Slog.e(TAG, "No animator available for this transition");
264             return false;
265         }
266 
267         // Calculate and set position of the task
268         final PointF position = mMoveToDesktopAnimator.getPosition();
269         startT.setPosition(sc, position.x, position.y);
270         finishT.setPosition(sc, position.x, position.y);
271 
272         startT.apply();
273 
274         mTransitions.getMainExecutor().execute(
275                 () -> finishCallback.onTransitionFinished(null));
276 
277         return true;
278     }
279 
animateFinalizeDragToDesktopMode( @onNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull Rect endBounds)280     private boolean animateFinalizeDragToDesktopMode(
281             @NonNull TransitionInfo.Change change,
282             @NonNull SurfaceControl.Transaction startT,
283             @NonNull SurfaceControl.Transaction finishT,
284             @NonNull Transitions.TransitionFinishCallback finishCallback,
285             @NonNull Rect endBounds) {
286         // This Transition animates a task to freeform bounds after being dragged into freeform
287         // mode and brings the remaining freeform tasks to front
288         final SurfaceControl sc = change.getLeash();
289         startT.setWindowCrop(sc, endBounds.width(),
290                 endBounds.height());
291         startT.apply();
292 
293         // End the animation that shrinks the window when task is first dragged from fullscreen
294         if (mMoveToDesktopAnimator != null) {
295             mMoveToDesktopAnimator.endAnimator();
296         }
297 
298         // We want to find the scale of the current bounds relative to the end bounds. The
299         // task is currently scaled to DRAG_FREEFORM_SCALE and the final bounds will be
300         // scaled to FINAL_FREEFORM_SCALE. So, it is scaled to
301         // DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE relative to the freeform bounds
302         final ValueAnimator animator =
303                 ValueAnimator.ofFloat(
304                         MoveToDesktopAnimator.DRAG_FREEFORM_SCALE / FINAL_FREEFORM_SCALE, 1f);
305         animator.setDuration(FREEFORM_ANIMATION_DURATION);
306         final SurfaceControl.Transaction t = mTransactionSupplier.get();
307         animator.addUpdateListener(animation -> {
308             final float animationValue = (float) animation.getAnimatedValue();
309             t.setScale(sc, animationValue, animationValue);
310 
311             final float animationWidth = endBounds.width() * animationValue;
312             final float animationHeight = endBounds.height() * animationValue;
313             final int animationX = endBounds.centerX() - (int) (animationWidth / 2);
314             final int animationY = endBounds.centerY() - (int) (animationHeight / 2);
315 
316             t.setPosition(sc, animationX, animationY);
317             t.apply();
318         });
319 
320         animator.addListener(new AnimatorListenerAdapter() {
321             @Override
322             public void onAnimationEnd(Animator animation) {
323                 if (mOnAnimationFinishedCallback != null) {
324                     mOnAnimationFinishedCallback.accept(finishT);
325                 }
326                 mTransitions.getMainExecutor().execute(
327                         () -> finishCallback.onTransitionFinished(null));
328             }
329         });
330 
331         animator.start();
332         return true;
333     }
animateCancelDragToDesktopMode( @onNull TransitionInfo.Change change, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull Transitions.TransitionFinishCallback finishCallback, @NonNull Rect endBounds)334     private boolean animateCancelDragToDesktopMode(
335             @NonNull TransitionInfo.Change change,
336             @NonNull SurfaceControl.Transaction startT,
337             @NonNull SurfaceControl.Transaction finishT,
338             @NonNull Transitions.TransitionFinishCallback finishCallback,
339             @NonNull Rect endBounds) {
340         // This Transition animates a task to fullscreen after being dragged from the status
341         // bar and then released back into the status bar area
342         final SurfaceControl sc = change.getLeash();
343         // Hide the first (fullscreen) frame because the animation will start from the smaller
344         // scale size.
345         startT.hide(sc)
346                 .setWindowCrop(sc, endBounds.width(), endBounds.height())
347                 .apply();
348 
349         if (mMoveToDesktopAnimator == null
350                 || mMoveToDesktopAnimator.getTaskId() != change.getTaskInfo().taskId) {
351             Slog.e(TAG, "No animator available for this transition");
352             return false;
353         }
354 
355         // End the animation that shrinks the window when task is first dragged from fullscreen
356         mMoveToDesktopAnimator.endAnimator();
357 
358         final ValueAnimator animator = new ValueAnimator();
359         animator.setFloatValues(MoveToDesktopAnimator.DRAG_FREEFORM_SCALE, 1f);
360         animator.setDuration(FREEFORM_ANIMATION_DURATION);
361         final SurfaceControl.Transaction t = mTransactionSupplier.get();
362 
363         // Get position of the task
364         final float x = mMoveToDesktopAnimator.getPosition().x;
365         final float y = mMoveToDesktopAnimator.getPosition().y;
366 
367         animator.addUpdateListener(animation -> {
368             final float scale = (float) animation.getAnimatedValue();
369             t.setPosition(sc, x * (1 - scale), y * (1 - scale))
370                     .setScale(sc, scale, scale)
371                     .show(sc)
372                     .apply();
373         });
374         animator.addListener(new AnimatorListenerAdapter() {
375             @Override
376             public void onAnimationEnd(Animator animation) {
377                 if (mOnAnimationFinishedCallback != null) {
378                     mOnAnimationFinishedCallback.accept(finishT);
379                 }
380                 mTransitions.getMainExecutor().execute(
381                         () -> finishCallback.onTransitionFinished(null));
382             }
383         });
384         animator.start();
385         return true;
386     }
387 
388     @Nullable
389     @Override
handleRequest(@onNull IBinder transition, @NonNull TransitionRequestInfo request)390     public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
391             @NonNull TransitionRequestInfo request) {
392         return null;
393     }
394 }
395