• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2025 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.bubbles;
18 
19 import static android.app.ActivityTaskManager.INVALID_TASK_ID;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
21 import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
22 import static android.view.View.INVISIBLE;
23 import static android.view.WindowManager.TRANSIT_CHANGE;
24 import static android.view.WindowManager.TRANSIT_TO_FRONT;
25 
26 import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE;
27 
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.app.ActivityManager;
31 import android.app.TaskInfo;
32 import android.content.Context;
33 import android.graphics.Point;
34 import android.graphics.PointF;
35 import android.graphics.Rect;
36 import android.os.IBinder;
37 import android.util.Slog;
38 import android.view.SurfaceControl;
39 import android.view.SurfaceView;
40 import android.view.View;
41 import android.window.TransitionInfo;
42 import android.window.TransitionRequestInfo;
43 import android.window.WindowContainerToken;
44 import android.window.WindowContainerTransaction;
45 
46 import androidx.core.animation.Animator;
47 import androidx.core.animation.Animator.AnimatorUpdateListener;
48 import androidx.core.animation.AnimatorListenerAdapter;
49 import androidx.core.animation.ValueAnimator;
50 
51 import com.android.internal.annotations.VisibleForTesting;
52 import com.android.launcher3.icons.BubbleIconFactory;
53 import com.android.wm.shell.ShellTaskOrganizer;
54 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
55 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
56 import com.android.wm.shell.taskview.TaskView;
57 import com.android.wm.shell.taskview.TaskViewRepository;
58 import com.android.wm.shell.taskview.TaskViewTaskController;
59 import com.android.wm.shell.taskview.TaskViewTransitions;
60 import com.android.wm.shell.transition.Transitions;
61 
62 import java.util.concurrent.Executor;
63 
64 /**
65  * Implements transition coordination for bubble operations.
66  */
67 public class BubbleTransitions {
68     private static final String TAG = "BubbleTransitions";
69 
70     /**
71      * Multiplier used to convert a view elevation to an "equivalent" shadow-radius. This is the
72      * same multiple used by skia and surface-outsets in WMS.
73      */
74     private static final float ELEVATION_TO_RADIUS = 2;
75 
76     @NonNull final Transitions mTransitions;
77     @NonNull final ShellTaskOrganizer mTaskOrganizer;
78     @NonNull final TaskViewRepository mRepository;
79     @NonNull final Executor mMainExecutor;
80     @NonNull final BubbleData mBubbleData;
81     @NonNull final TaskViewTransitions mTaskViewTransitions;
82     @NonNull final Context mContext;
83 
BubbleTransitions(@onNull Transitions transitions, @NonNull ShellTaskOrganizer organizer, @NonNull TaskViewRepository repository, @NonNull BubbleData bubbleData, @NonNull TaskViewTransitions taskViewTransitions, Context context)84     BubbleTransitions(@NonNull Transitions transitions, @NonNull ShellTaskOrganizer organizer,
85             @NonNull TaskViewRepository repository, @NonNull BubbleData bubbleData,
86             @NonNull TaskViewTransitions taskViewTransitions, Context context) {
87         mTransitions = transitions;
88         mTaskOrganizer = organizer;
89         mRepository = repository;
90         mMainExecutor = transitions.getMainExecutor();
91         mBubbleData = bubbleData;
92         mTaskViewTransitions = taskViewTransitions;
93         mContext = context;
94     }
95 
96     /**
97      * Starts a convert-to-bubble transition.
98      *
99      * @see ConvertToBubble
100      */
startConvertToBubble(Bubble bubble, TaskInfo taskInfo, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, BubblePositioner positioner, BubbleStackView stackView, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, DragData dragData, boolean inflateSync)101     public BubbleTransition startConvertToBubble(Bubble bubble, TaskInfo taskInfo,
102             BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory,
103             BubblePositioner positioner, BubbleStackView stackView,
104             BubbleBarLayerView layerView, BubbleIconFactory iconFactory,
105             DragData dragData, boolean inflateSync) {
106         return new ConvertToBubble(bubble, taskInfo, mContext,
107                 expandedViewManager, factory, positioner, stackView, layerView, iconFactory,
108                 dragData, inflateSync);
109     }
110 
111     /**
112      * Starts a convert-from-bubble transition.
113      *
114      * @see ConvertFromBubble
115      */
startConvertFromBubble(Bubble bubble, TaskInfo taskInfo)116     public BubbleTransition startConvertFromBubble(Bubble bubble,
117             TaskInfo taskInfo) {
118         ConvertFromBubble convert = new ConvertFromBubble(bubble, taskInfo);
119         return convert;
120     }
121 
122     /** Starts a transition that converts a dragged bubble icon to a full screen task. */
startDraggedBubbleIconToFullscreen(Bubble bubble, Point dropLocation)123     public BubbleTransition startDraggedBubbleIconToFullscreen(Bubble bubble, Point dropLocation) {
124         return new DraggedBubbleIconToFullscreen(bubble, dropLocation);
125     }
126 
127     /**
128      * Plucks the task-surface out of an ancestor view while making the view invisible. This helper
129      * attempts to do this seamlessly (ie. view becomes invisible in sync with task reparent).
130      */
pluck(SurfaceControl taskLeash, View fromView, SurfaceControl dest, float destX, float destY, float cornerRadius, SurfaceControl.Transaction t, Runnable onPlucked)131     private void pluck(SurfaceControl taskLeash, View fromView, SurfaceControl dest,
132             float destX, float destY, float cornerRadius, SurfaceControl.Transaction t,
133             Runnable onPlucked) {
134         SurfaceControl.Transaction pluckT = new SurfaceControl.Transaction();
135         pluckT.reparent(taskLeash, dest);
136         t.reparent(taskLeash, dest);
137         pluckT.setPosition(taskLeash, destX, destY);
138         t.setPosition(taskLeash, destX, destY);
139         pluckT.show(taskLeash);
140         pluckT.setAlpha(taskLeash, 1.f);
141         float shadowRadius = fromView.getElevation() * ELEVATION_TO_RADIUS;
142         pluckT.setShadowRadius(taskLeash, shadowRadius);
143         pluckT.setCornerRadius(taskLeash, cornerRadius);
144         t.setShadowRadius(taskLeash, shadowRadius);
145         t.setCornerRadius(taskLeash, cornerRadius);
146 
147         // Need to remove the taskview AFTER applying the startTransaction because it isn't
148         // synchronized.
149         pluckT.addTransactionCommittedListener(mMainExecutor, onPlucked::run);
150         fromView.getViewRootImpl().applyTransactionOnDraw(pluckT);
151         fromView.setVisibility(INVISIBLE);
152     }
153 
154     /**
155      * Interface to a bubble-specific transition. Bubble transitions have a multi-step lifecycle
156      * in order to coordinate with the bubble view logic. These steps are communicated on this
157      * interface.
158      */
159     interface BubbleTransition {
surfaceCreated()160         default void surfaceCreated() {}
continueExpand()161         default void continueExpand() {}
skip()162         void skip();
continueCollapse()163         default void continueCollapse() {}
164     }
165 
166     /**
167      * Information about the task when it is being dragged to a bubble
168      */
169     public static class DragData {
170         private final WindowContainerTransaction mPendingWct;
171         private final boolean mReleasedOnLeft;
172         private final float mTaskScale;
173         private final float mCornerRadius;
174         private final PointF mDragPosition;
175 
176         /**
177          * @param releasedOnLeft true if the bubble was released in the left drop target
178          * @param taskScale      the scale of the task when it was dragged to bubble
179          * @param cornerRadius   the corner radius of the task when it was dragged to bubble
180          * @param dragPosition   the position of the task when it was dragged to bubble
181          * @param wct            pending operations to be applied when finishing the drag
182          */
DragData(boolean releasedOnLeft, float taskScale, float cornerRadius, @Nullable PointF dragPosition, @Nullable WindowContainerTransaction wct)183         public DragData(boolean releasedOnLeft, float taskScale, float cornerRadius,
184                 @Nullable PointF dragPosition, @Nullable WindowContainerTransaction wct) {
185             mPendingWct = wct;
186             mReleasedOnLeft = releasedOnLeft;
187             mTaskScale = taskScale;
188             mCornerRadius = cornerRadius;
189             mDragPosition = dragPosition != null ? dragPosition : new PointF(0, 0);
190         }
191 
192         /**
193          * @return pending operations to be applied when finishing the drag
194          */
195         @Nullable
getPendingWct()196         public WindowContainerTransaction getPendingWct() {
197             return mPendingWct;
198         }
199 
200         /**
201          * @return true if the bubble was released in the left drop target
202          */
isReleasedOnLeft()203         public boolean isReleasedOnLeft() {
204             return mReleasedOnLeft;
205         }
206 
207         /**
208          * @return the scale of the task when it was dragged to bubble
209          */
getTaskScale()210         public float getTaskScale() {
211             return mTaskScale;
212         }
213 
214         /**
215          * @return the corner radius of the task when it was dragged to bubble
216          */
getCornerRadius()217         public float getCornerRadius() {
218             return mCornerRadius;
219         }
220 
221         /**
222          * @return position of the task when it was dragged to bubble
223          */
getDragPosition()224         public PointF getDragPosition() {
225             return mDragPosition;
226         }
227     }
228 
229     /**
230      * BubbleTransition that coordinates the process of a non-bubble task becoming a bubble. The
231      * steps are as follows:
232      *
233      * 1. Start inflating the bubble view
234      * 2. Once inflated (but not-yet visible), tell WM to do the shell-transition.
235      * 3. When the transition becomes ready, notify Launcher in parallel
236      * 4. Wait for surface to be created
237      * 5. Once surface is ready, animate the task to a bubble
238      *
239      * While the animation is pending, we keep a reference to the pending transition in the bubble.
240      * This allows us to check in other parts of the code that this bubble will be shown via the
241      * transition animation.
242      *
243      * startAnimation, continueExpand and surfaceCreated are set-up to happen in either order,
244      * to support UX/timing adjustments.
245      */
246     @VisibleForTesting
247     class ConvertToBubble implements Transitions.TransitionHandler, BubbleTransition {
248         final BubbleBarLayerView mLayerView;
249         Bubble mBubble;
250         @Nullable DragData mDragData;
251         IBinder mTransition;
252         Transitions.TransitionFinishCallback mFinishCb;
253         WindowContainerTransaction mFinishWct = null;
254         final Rect mStartBounds = new Rect();
255         SurfaceControl mSnapshot = null;
256         TaskInfo mTaskInfo;
257         BubbleViewProvider mPriorBubble = null;
258 
259         private final TransitionProgress mTransitionProgress = new TransitionProgress();
260         private SurfaceControl.Transaction mFinishT;
261         private SurfaceControl mTaskLeash;
262 
ConvertToBubble(Bubble bubble, TaskInfo taskInfo, Context context, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, BubblePositioner positioner, BubbleStackView stackView, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, @Nullable DragData dragData, boolean inflateSync)263         ConvertToBubble(Bubble bubble, TaskInfo taskInfo, Context context,
264                 BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory,
265                 BubblePositioner positioner, BubbleStackView stackView,
266                 BubbleBarLayerView layerView, BubbleIconFactory iconFactory,
267                 @Nullable DragData dragData, boolean inflateSync) {
268             mBubble = bubble;
269             mTaskInfo = taskInfo;
270             mLayerView = layerView;
271             mDragData = dragData;
272             mBubble.setInflateSynchronously(inflateSync);
273             mBubble.setPreparingTransition(this);
274             mBubble.inflate(
275                     this::onInflated,
276                     context,
277                     expandedViewManager,
278                     factory,
279                     positioner,
280                     stackView,
281                     layerView,
282                     iconFactory,
283                     false /* skipInflation */);
284         }
285 
286         @VisibleForTesting
onInflated(Bubble b)287         void onInflated(Bubble b) {
288             if (b != mBubble) {
289                 throw new IllegalArgumentException("inflate callback doesn't match bubble");
290             }
291             final Rect launchBounds = new Rect();
292             mLayerView.getExpandedViewRestBounds(launchBounds);
293             WindowContainerTransaction wct = new WindowContainerTransaction();
294             if (mDragData != null && mDragData.getPendingWct() != null) {
295                 wct.merge(mDragData.getPendingWct(), true);
296             }
297             if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) {
298                 if (mTaskInfo.getParentTaskId() != INVALID_TASK_ID) {
299                     wct.reparent(mTaskInfo.token, null, true);
300                 }
301             }
302 
303             wct.setAlwaysOnTop(mTaskInfo.token, true);
304             wct.setWindowingMode(mTaskInfo.token, WINDOWING_MODE_MULTI_WINDOW);
305             wct.setBounds(mTaskInfo.token, launchBounds);
306 
307             final TaskView tv = b.getTaskView();
308             tv.setSurfaceLifecycle(SurfaceView.SURFACE_LIFECYCLE_FOLLOWS_ATTACHMENT);
309             final TaskViewRepository.TaskViewState state = mRepository.byTaskView(
310                     tv.getController());
311             if (state != null) {
312                 state.mVisible = true;
313             }
314             mTaskViewTransitions.enqueueExternal(tv.getController(), () -> {
315                 mTransition = mTransitions.startTransition(TRANSIT_CONVERT_TO_BUBBLE, wct, this);
316                 return mTransition;
317             });
318         }
319 
320         @Override
skip()321         public void skip() {
322             mBubble.setPreparingTransition(null);
323             mFinishCb.onTransitionFinished(mFinishWct);
324             mFinishCb = null;
325         }
326 
327         @Override
handleRequest(@onNull IBinder transition, @Nullable TransitionRequestInfo request)328         public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
329                 @Nullable TransitionRequestInfo request) {
330             return null;
331         }
332 
333         @Override
mergeAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback)334         public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
335                 @NonNull SurfaceControl.Transaction startT,
336                 @NonNull SurfaceControl.Transaction finishT,
337                 @NonNull IBinder mergeTarget,
338                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
339         }
340 
341         @Override
onTransitionConsumed(@onNull IBinder transition, boolean aborted, @NonNull SurfaceControl.Transaction finishTransaction)342         public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
343                 @NonNull SurfaceControl.Transaction finishTransaction) {
344             if (!aborted) return;
345             mTransition = null;
346             mTaskViewTransitions.onExternalDone(transition);
347         }
348 
349         @Override
startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)350         public boolean startAnimation(@NonNull IBinder transition,
351                 @NonNull TransitionInfo info,
352                 @NonNull SurfaceControl.Transaction startTransaction,
353                 @NonNull SurfaceControl.Transaction finishTransaction,
354                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
355             if (mTransition != transition) return false;
356             boolean found = false;
357             for (int i = 0; i < info.getChanges().size(); ++i) {
358                 final TransitionInfo.Change chg = info.getChanges().get(i);
359                 if (chg.getTaskInfo() == null) continue;
360                 if (chg.getMode() != TRANSIT_CHANGE && chg.getMode() != TRANSIT_TO_FRONT) continue;
361                 if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue;
362                 mStartBounds.set(chg.getStartAbsBounds());
363                 // Converting a task into taskview, so treat as "new"
364                 mFinishWct = new WindowContainerTransaction();
365                 mTaskInfo = chg.getTaskInfo();
366                 mFinishT = finishTransaction;
367                 mTaskLeash = chg.getLeash();
368                 found = true;
369                 mSnapshot = chg.getSnapshot();
370                 break;
371             }
372             if (!found) {
373                 Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get "
374                         + "one, cleaning up the task view");
375                 mBubble.getTaskView().getController().setTaskNotFound();
376                 mTaskViewTransitions.onExternalDone(transition);
377                 return false;
378             }
379             mFinishCb = finishCallback;
380 
381             if (mDragData != null) {
382                 mStartBounds.offsetTo((int) mDragData.getDragPosition().x,
383                         (int) mDragData.getDragPosition().y);
384                 startTransaction.setScale(mSnapshot, mDragData.getTaskScale(),
385                         mDragData.getTaskScale());
386                 startTransaction.setCornerRadius(mSnapshot, mDragData.getCornerRadius());
387             }
388 
389             // Now update state (and talk to launcher) in parallel with snapshot stuff
390             mBubbleData.notificationEntryUpdated(mBubble, /* suppressFlyout= */ true,
391                     /* showInShade= */ false);
392 
393             final int left = mStartBounds.left - info.getRoot(0).getOffset().x;
394             final int top = mStartBounds.top - info.getRoot(0).getOffset().y;
395             startTransaction.setPosition(mTaskLeash, left, top);
396             startTransaction.show(mSnapshot);
397             // Move snapshot to root so that it remains visible while task is moved to taskview
398             startTransaction.reparent(mSnapshot, info.getRoot(0).getLeash());
399             startTransaction.setPosition(mSnapshot, left, top);
400             startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE);
401 
402             startTransaction.apply();
403 
404             mTaskViewTransitions.onExternalDone(transition);
405             mTransitionProgress.setTransitionReady();
406             startExpandAnim();
407             return true;
408         }
409 
startExpandAnim()410         private void startExpandAnim() {
411             final boolean animate = mLayerView.canExpandView(mBubble);
412             if (animate) {
413                 mPriorBubble = mLayerView.prepareConvertedView(mBubble);
414             }
415             if (mPriorBubble != null) {
416                 // TODO: an animation. For now though, just remove it.
417                 final BubbleBarExpandedView priorView = mPriorBubble.getBubbleBarExpandedView();
418                 mLayerView.removeView(priorView);
419                 mPriorBubble = null;
420             }
421             if (!animate || mTransitionProgress.isReadyToAnimate()) {
422                 playAnimation(animate);
423             }
424         }
425 
426         @Override
continueExpand()427         public void continueExpand() {
428             mTransitionProgress.setReadyToExpand();
429         }
430 
431         @Override
surfaceCreated()432         public void surfaceCreated() {
433             mTransitionProgress.setSurfaceReady();
434             mMainExecutor.execute(() -> {
435                 final TaskViewTaskController tvc = mBubble.getTaskView().getController();
436                 final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc);
437                 if (state == null) return;
438                 state.mVisible = true;
439                 if (mTransitionProgress.isReadyToAnimate()) {
440                     playAnimation(true /* animate */);
441                 }
442             });
443         }
444 
playAnimation(boolean animate)445         private void playAnimation(boolean animate) {
446             final TaskViewTaskController tv = mBubble.getTaskView().getController();
447             final SurfaceControl.Transaction startT = new SurfaceControl.Transaction();
448             // Set task position to 0,0 as it will be placed inside the TaskView
449             startT.setPosition(mTaskLeash, 0, 0);
450             mTaskViewTransitions.prepareOpenAnimation(tv, true /* new */, startT, mFinishT,
451                     (ActivityManager.RunningTaskInfo) mTaskInfo, mTaskLeash, mFinishWct);
452 
453             if (mFinishWct.isEmpty()) {
454                 mFinishWct = null;
455             }
456 
457             if (animate) {
458                 float startScale = mDragData != null ? mDragData.getTaskScale() : 1f;
459                 mLayerView.animateConvert(startT, mStartBounds, startScale, mSnapshot, mTaskLeash,
460                         () -> {
461                             mFinishCb.onTransitionFinished(mFinishWct);
462                             mFinishCb = null;
463                         });
464             } else {
465                 startT.apply();
466                 mFinishCb.onTransitionFinished(mFinishWct);
467                 mFinishCb = null;
468             }
469         }
470 
471         /**
472          * Keeps track of internal state of different steps of this BubbleTransition.
473          */
474         private class TransitionProgress {
475             private boolean mTransitionReady;
476             private boolean mReadyToExpand;
477             private boolean mSurfaceReady;
478 
setTransitionReady()479             void setTransitionReady() {
480                 mTransitionReady = true;
481                 onUpdate();
482             }
483 
setReadyToExpand()484             void setReadyToExpand() {
485                 mReadyToExpand = true;
486                 onUpdate();
487             }
488 
setSurfaceReady()489             void setSurfaceReady() {
490                 mSurfaceReady = true;
491                 onUpdate();
492             }
493 
isReadyToAnimate()494             boolean isReadyToAnimate() {
495                 // Animation only depends on transition and surface state
496                 return mTransitionReady && mSurfaceReady;
497             }
498 
onUpdate()499             private void onUpdate() {
500                 if (mTransitionReady && mReadyToExpand && mSurfaceReady) {
501                     // Clear the transition from bubble when all the steps are ready
502                     mBubble.setPreparingTransition(null);
503                 }
504             }
505         }
506     }
507 
508     /**
509      * BubbleTransition that coordinates the setup for moving a task out of a bubble. The actual
510      * animation is owned by the "receiver" of the task; however, because Bubbles uses TaskView,
511      * we need to do some extra coordination work to get the task surface out of the view
512      * "seamlessly".
513      *
514      * The process here looks like:
515      * 1. Send transition to WM for leaving bubbles mode
516      * 2. in startAnimation, set-up a "pluck" operation to pull the task surface out of taskview
517      * 3. Once "plucked", remove the view (calls continueCollapse when surfaces can be cleaned-up)
518      * 4. Then re-dispatch the transition animation so that the "receiver" can animate it.
519      *
520      * So, constructor -> startAnimation -> continueCollapse -> re-dispatch.
521      */
522     @VisibleForTesting
523     class ConvertFromBubble implements Transitions.TransitionHandler, BubbleTransition {
524         @NonNull final Bubble mBubble;
525         IBinder mTransition;
526         TaskInfo mTaskInfo;
527         SurfaceControl mTaskLeash;
528         SurfaceControl mRootLeash;
529 
ConvertFromBubble(@onNull Bubble bubble, TaskInfo taskInfo)530         ConvertFromBubble(@NonNull Bubble bubble, TaskInfo taskInfo) {
531             mBubble = bubble;
532             mTaskInfo = taskInfo;
533 
534             mBubble.setPreparingTransition(this);
535             WindowContainerTransaction wct = new WindowContainerTransaction();
536             WindowContainerToken token = mTaskInfo.getToken();
537             wct.setWindowingMode(token, WINDOWING_MODE_UNDEFINED);
538             wct.setAlwaysOnTop(token, false);
539             mTaskOrganizer.setInterceptBackPressedOnTaskRoot(token, false);
540             mTaskViewTransitions.enqueueExternal(
541                     mBubble.getTaskView().getController(),
542                     () -> {
543                         mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this);
544                         return mTransition;
545                     });
546         }
547 
548         @Override
skip()549         public void skip() {
550             mBubble.setPreparingTransition(null);
551             final TaskViewTaskController tv =
552                     mBubble.getTaskView().getController();
553             tv.notifyTaskRemovalStarted(tv.getTaskInfo());
554             mTaskLeash = null;
555         }
556 
557         @Override
handleRequest(@onNull IBinder transition, @android.annotation.Nullable TransitionRequestInfo request)558         public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
559                 @android.annotation.Nullable TransitionRequestInfo request) {
560             return null;
561         }
562 
563         @Override
mergeAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback)564         public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
565                 @NonNull SurfaceControl.Transaction startT,
566                 @NonNull SurfaceControl.Transaction finishT,
567                 @NonNull IBinder mergeTarget,
568                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
569         }
570 
571         @Override
onTransitionConsumed(@onNull IBinder transition, boolean aborted, @NonNull SurfaceControl.Transaction finishTransaction)572         public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
573                 @NonNull SurfaceControl.Transaction finishTransaction) {
574             if (!aborted) return;
575             mTransition = null;
576             skip();
577             mTaskViewTransitions.onExternalDone(transition);
578         }
579 
580         @Override
startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)581         public boolean startAnimation(@NonNull IBinder transition,
582                 @NonNull TransitionInfo info,
583                 @NonNull SurfaceControl.Transaction startTransaction,
584                 @NonNull SurfaceControl.Transaction finishTransaction,
585                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
586             if (mTransition != transition) return false;
587 
588             final TaskViewTaskController tv =
589                     mBubble.getTaskView().getController();
590             if (tv == null) {
591                 mTaskViewTransitions.onExternalDone(transition);
592                 return false;
593             }
594 
595             TransitionInfo.Change taskChg = null;
596 
597             boolean found = false;
598             for (int i = 0; i < info.getChanges().size(); ++i) {
599                 final TransitionInfo.Change chg = info.getChanges().get(i);
600                 if (chg.getTaskInfo() == null) continue;
601                 if (chg.getMode() != TRANSIT_CHANGE) continue;
602                 if (!mTaskInfo.token.equals(chg.getTaskInfo().token)) continue;
603                 found = true;
604                 mRepository.remove(tv);
605                 taskChg = chg;
606                 break;
607             }
608 
609             if (!found) {
610                 Slog.w(TAG, "Expected a TaskView conversion in this transition but didn't get "
611                         + "one, cleaning up the task view");
612                 tv.setTaskNotFound();
613                 skip();
614                 mTaskViewTransitions.onExternalDone(transition);
615                 return false;
616             }
617 
618             mTaskLeash = taskChg.getLeash();
619             mRootLeash = info.getRoot(0).getLeash();
620 
621             SurfaceControl dest = getExpandedView(mBubble).getViewRootImpl().getSurfaceControl();
622             final Runnable onPlucked = () -> {
623                 // Need to remove the taskview AFTER applying the startTransaction because
624                 // it isn't synchronized.
625                 tv.notifyTaskRemovalStarted(tv.getTaskInfo());
626                 // Unset after removeView so it can be used to pick a different animation.
627                 mBubble.setPreparingTransition(null);
628                 mBubbleData.setExpanded(false /* expanded */);
629             };
630             if (dest != null) {
631                 pluck(mTaskLeash, getExpandedView(mBubble), dest,
632                         taskChg.getStartAbsBounds().left - info.getRoot(0).getOffset().x,
633                         taskChg.getStartAbsBounds().top - info.getRoot(0).getOffset().y,
634                         getCornerRadius(mBubble), startTransaction,
635                         onPlucked);
636                 getExpandedView(mBubble).post(() -> mTransitions.dispatchTransition(
637                         mTransition, info, startTransaction, finishTransaction, finishCallback,
638                         null));
639             } else {
640                 onPlucked.run();
641                 mTransitions.dispatchTransition(mTransition, info, startTransaction,
642                         finishTransaction, finishCallback, null);
643             }
644 
645             mTaskViewTransitions.onExternalDone(transition);
646             return true;
647         }
648 
649         @Override
continueCollapse()650         public void continueCollapse() {
651             mBubble.cleanupTaskView();
652             if (mTaskLeash == null || !mTaskLeash.isValid() || !mRootLeash.isValid()) return;
653             SurfaceControl.Transaction t = new SurfaceControl.Transaction();
654             t.reparent(mTaskLeash, mRootLeash);
655             t.apply();
656         }
657 
getExpandedView(@onNull Bubble bubble)658         private View getExpandedView(@NonNull Bubble bubble) {
659             if (bubble.getBubbleBarExpandedView() != null) {
660                 return bubble.getBubbleBarExpandedView();
661             }
662             return bubble.getExpandedView();
663         }
664 
getCornerRadius(@onNull Bubble bubble)665         private float getCornerRadius(@NonNull Bubble bubble) {
666             if (bubble.getBubbleBarExpandedView() != null) {
667                 return bubble.getBubbleBarExpandedView().getCornerRadius();
668             }
669             return bubble.getExpandedView().getCornerRadius();
670         }
671     }
672 
673     /**
674      * A transition that converts a dragged bubble icon to a full screen window.
675      *
676      * <p>This transition assumes that the bubble is invisible so it is simply sent to front.
677      */
678     class DraggedBubbleIconToFullscreen implements Transitions.TransitionHandler, BubbleTransition {
679 
680         IBinder mTransition;
681         final Bubble mBubble;
682         final Point mDropLocation;
683         final TransactionProvider mTransactionProvider;
684 
DraggedBubbleIconToFullscreen(Bubble bubble, Point dropLocation)685         DraggedBubbleIconToFullscreen(Bubble bubble, Point dropLocation) {
686             this(bubble, dropLocation, SurfaceControl.Transaction::new);
687         }
688 
689         @VisibleForTesting
DraggedBubbleIconToFullscreen(Bubble bubble, Point dropLocation, TransactionProvider transactionProvider)690         DraggedBubbleIconToFullscreen(Bubble bubble, Point dropLocation,
691                 TransactionProvider transactionProvider) {
692             mBubble = bubble;
693             mDropLocation = dropLocation;
694             mTransactionProvider = transactionProvider;
695             bubble.setPreparingTransition(this);
696             WindowContainerToken token = bubble.getTaskView().getTaskInfo().getToken();
697             WindowContainerTransaction wct = new WindowContainerTransaction();
698             wct.setAlwaysOnTop(token, false);
699             wct.setWindowingMode(token, WINDOWING_MODE_UNDEFINED);
700             wct.reorder(token, /* onTop= */ true);
701             wct.setHidden(token, false);
702             mTaskOrganizer.setInterceptBackPressedOnTaskRoot(token, false);
703             mTaskViewTransitions.enqueueExternal(bubble.getTaskView().getController(), () -> {
704                 mTransition = mTransitions.startTransition(TRANSIT_TO_FRONT, wct, this);
705                 return mTransition;
706             });
707         }
708 
709         @Override
skip()710         public void skip() {
711             mBubble.setPreparingTransition(null);
712         }
713 
714         @Override
startAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback)715         public boolean startAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
716                 @NonNull SurfaceControl.Transaction startTransaction,
717                 @NonNull SurfaceControl.Transaction finishTransaction,
718                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
719             if (mTransition != transition) {
720                 return false;
721             }
722 
723             final TaskViewTaskController taskViewTaskController =
724                     mBubble.getTaskView().getController();
725             if (taskViewTaskController == null) {
726                 mTaskViewTransitions.onExternalDone(transition);
727                 finishCallback.onTransitionFinished(null);
728                 return true;
729             }
730 
731             TransitionInfo.Change change = findTransitionChange(info);
732             if (change == null) {
733                 Slog.w(TAG, "Expected a TaskView transition to front but didn't find "
734                         + "one, cleaning up the task view");
735                 taskViewTaskController.setTaskNotFound();
736                 mTaskViewTransitions.onExternalDone(transition);
737                 finishCallback.onTransitionFinished(null);
738                 return true;
739             }
740             mRepository.remove(taskViewTaskController);
741 
742             final SurfaceControl taskLeash = change.getLeash();
743             // set the initial position of the task with 0 scale
744             startTransaction.setPosition(taskLeash, mDropLocation.x, mDropLocation.y);
745             startTransaction.setScale(taskLeash, 0, 0);
746             startTransaction.apply();
747 
748             final SurfaceControl.Transaction animT = mTransactionProvider.get();
749             ValueAnimator animator = ValueAnimator.ofFloat(0, 1);
750             animator.setDuration(250);
751             animator.addUpdateListener(new AnimatorUpdateListener() {
752                 @Override
753                 public void onAnimationUpdate(@NonNull Animator animation) {
754                     float progress = animator.getAnimatedFraction();
755                     float x = mDropLocation.x * (1 - progress);
756                     float y = mDropLocation.y * (1 - progress);
757                     animT.setPosition(taskLeash, x, y);
758                     animT.setScale(taskLeash, progress, progress);
759                     animT.apply();
760                 }
761             });
762             animator.addListener(new AnimatorListenerAdapter() {
763                 @Override
764                 public void onAnimationEnd(@NonNull Animator animation) {
765                     animT.close();
766                     finishCallback.onTransitionFinished(null);
767                 }
768             });
769             animator.start();
770             taskViewTaskController.notifyTaskRemovalStarted(mBubble.getTaskView().getTaskInfo());
771             mTaskViewTransitions.onExternalDone(transition);
772             return true;
773         }
774 
findTransitionChange(TransitionInfo info)775         private TransitionInfo.Change findTransitionChange(TransitionInfo info) {
776             TransitionInfo.Change result = null;
777             WindowContainerToken token = mBubble.getTaskView().getTaskInfo().getToken();
778             for (int i = 0; i < info.getChanges().size(); ++i) {
779                 final TransitionInfo.Change change = info.getChanges().get(i);
780                 if (change.getTaskInfo() == null) {
781                     continue;
782                 }
783                 if (change.getMode() != TRANSIT_TO_FRONT) {
784                     continue;
785                 }
786                 if (!token.equals(change.getTaskInfo().token)) {
787                     continue;
788                 }
789                 result = change;
790                 break;
791             }
792             return result;
793         }
794 
795         @Override
mergeAnimation(@onNull IBinder transition, @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback)796         public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info,
797                 @NonNull SurfaceControl.Transaction startTransaction,
798                 @NonNull SurfaceControl.Transaction finishTransaction,
799                 @NonNull IBinder mergeTarget,
800                 @NonNull Transitions.TransitionFinishCallback finishCallback) {
801         }
802 
803         @Override
handleRequest(@onNull IBinder transition, @NonNull TransitionRequestInfo request)804         public WindowContainerTransaction handleRequest(@NonNull IBinder transition,
805                 @NonNull TransitionRequestInfo request) {
806             return null;
807         }
808 
809         @Override
onTransitionConsumed(@onNull IBinder transition, boolean aborted, @Nullable SurfaceControl.Transaction finishTransaction)810         public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted,
811                 @Nullable SurfaceControl.Transaction finishTransaction) {
812             if (!aborted) {
813                 return;
814             }
815             mTransition = null;
816             mTaskViewTransitions.onExternalDone(transition);
817         }
818     }
819 
820     interface TransactionProvider {
get()821         SurfaceControl.Transaction get();
822     }
823 }
824