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.pip.phone;
18 
19 import static androidx.dynamicanimation.animation.SpringForce.DAMPING_RATIO_NO_BOUNCY;
20 import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_LOW;
21 import static androidx.dynamicanimation.animation.SpringForce.STIFFNESS_MEDIUM;
22 
23 import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND;
24 import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_LEFT;
25 import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_NONE;
26 import static com.android.wm.shell.pip.PipBoundsState.STASH_TYPE_RIGHT;
27 import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_DISMISS;
28 import static com.android.wm.shell.pip.phone.PipMenuView.ANIM_TYPE_NONE;
29 
30 import android.annotation.NonNull;
31 import android.annotation.Nullable;
32 import android.content.Context;
33 import android.graphics.PointF;
34 import android.graphics.Rect;
35 import android.os.Debug;
36 import android.os.Looper;
37 import android.util.Log;
38 import android.view.Choreographer;
39 
40 import androidx.dynamicanimation.animation.AnimationHandler;
41 import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler;
42 
43 import com.android.wm.shell.R;
44 import com.android.wm.shell.animation.FloatProperties;
45 import com.android.wm.shell.animation.PhysicsAnimator;
46 import com.android.wm.shell.common.FloatingContentCoordinator;
47 import com.android.wm.shell.common.magnetictarget.MagnetizedObject;
48 import com.android.wm.shell.pip.PipBoundsState;
49 import com.android.wm.shell.pip.PipSnapAlgorithm;
50 import com.android.wm.shell.pip.PipTaskOrganizer;
51 import com.android.wm.shell.pip.PipTransitionController;
52 
53 import java.util.function.Consumer;
54 
55 import kotlin.Unit;
56 import kotlin.jvm.functions.Function0;
57 
58 /**
59  * A helper to animate and manipulate the PiP.
60  */
61 public class PipMotionHelper implements PipAppOpsListener.Callback,
62         FloatingContentCoordinator.FloatingContent {
63 
64     private static final String TAG = "PipMotionHelper";
65     private static final boolean DEBUG = false;
66 
67     private static final int SHRINK_STACK_FROM_MENU_DURATION = 250;
68     private static final int EXPAND_STACK_TO_MENU_DURATION = 250;
69     private static final int UNSTASH_DURATION = 250;
70     private static final int LEAVE_PIP_DURATION = 300;
71     private static final int SHIFT_DURATION = 300;
72 
73     /** Friction to use for PIP when it moves via physics fling animations. */
74     private static final float DEFAULT_FRICTION = 1.9f;
75     /** How much of the dismiss circle size to use when scaling down PIP. **/
76     private static final float DISMISS_CIRCLE_PERCENT = 0.85f;
77 
78     private final Context mContext;
79     private final PipTaskOrganizer mPipTaskOrganizer;
80     private @NonNull PipBoundsState mPipBoundsState;
81 
82     private PhonePipMenuController mMenuController;
83     private PipSnapAlgorithm mSnapAlgorithm;
84 
85     /** The region that all of PIP must stay within. */
86     private final Rect mFloatingAllowedArea = new Rect();
87 
88     /** Coordinator instance for resolving conflicts with other floating content. */
89     private FloatingContentCoordinator mFloatingContentCoordinator;
90 
91     private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
92             ThreadLocal.withInitial(() -> {
93                 final Looper initialLooper = Looper.myLooper();
94                 final FrameCallbackScheduler scheduler = new FrameCallbackScheduler() {
95                     @Override
96                     public void postFrameCallback(@androidx.annotation.NonNull Runnable runnable) {
97                         Choreographer.getSfInstance().postFrameCallback(t -> runnable.run());
98                     }
99 
100                     @Override
101                     public boolean isCurrentThread() {
102                         return Looper.myLooper() == initialLooper;
103                     }
104                 };
105                 AnimationHandler handler = new AnimationHandler(scheduler);
106                 return handler;
107             });
108 
109     /**
110      * PhysicsAnimator instance for animating {@link PipBoundsState#getMotionBoundsState()}
111      * using physics animations.
112      */
113     private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator;
114 
115     private MagnetizedObject<Rect> mMagnetizedPip;
116 
117     /**
118      * Update listener that resizes the PIP to {@link PipBoundsState#getMotionBoundsState()}.
119      */
120     private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener;
121 
122     /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */
123     private PhysicsAnimator.FlingConfig mFlingConfigX;
124     private PhysicsAnimator.FlingConfig mFlingConfigY;
125     /** FlingConfig instances proviced to PhysicsAnimator for stashing. */
126     private PhysicsAnimator.FlingConfig mStashConfigX;
127 
128     /** SpringConfig to use for fling-then-spring animations. */
129     private final PhysicsAnimator.SpringConfig mSpringConfig =
130             new PhysicsAnimator.SpringConfig(700f, DAMPING_RATIO_NO_BOUNCY);
131 
132     /** SpringConfig used for animating into the dismiss region, matches the one in
133      * {@link MagnetizedObject}. */
134     private final PhysicsAnimator.SpringConfig mAnimateToDismissSpringConfig =
135             new PhysicsAnimator.SpringConfig(STIFFNESS_MEDIUM, DAMPING_RATIO_NO_BOUNCY);
136 
137     /** SpringConfig used for animating the pip to catch up to the finger once it leaves the dismiss
138      * drag region. */
139     private final PhysicsAnimator.SpringConfig mCatchUpSpringConfig =
140             new PhysicsAnimator.SpringConfig(5000f, DAMPING_RATIO_NO_BOUNCY);
141 
142     /** SpringConfig to use for springing PIP away from conflicting floating content. */
143     private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig =
144             new PhysicsAnimator.SpringConfig(STIFFNESS_LOW, DAMPING_RATIO_NO_BOUNCY);
145 
146     private final Consumer<Rect> mUpdateBoundsCallback = (Rect newBounds) -> {
147         if (mPipBoundsState.getBounds().equals(newBounds)) {
148             return;
149         }
150 
151         mMenuController.updateMenuLayout(newBounds);
152         mPipBoundsState.setBounds(newBounds);
153     };
154 
155     /**
156      * Whether we're springing to the touch event location (vs. moving it to that position
157      * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was
158      * 'stuck' in the target and needs to catch up to the touch location.
159      */
160     private boolean mSpringingToTouch = false;
161 
162     /**
163      * Whether PIP was released in the dismiss target, and will be animated out and dismissed
164      * shortly.
165      */
166     private boolean mDismissalPending = false;
167 
168     /**
169      * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is
170      * used to show menu activity when the expand animation is completed.
171      */
172     private Runnable mPostPipTransitionCallback;
173 
174     private final PipTransitionController.PipTransitionCallback mPipTransitionCallback =
175             new PipTransitionController.PipTransitionCallback() {
176         @Override
177         public void onPipTransitionStarted(int direction, Rect pipBounds) {}
178 
179         @Override
180         public void onPipTransitionFinished(int direction) {
181             if (mPostPipTransitionCallback != null) {
182                 mPostPipTransitionCallback.run();
183                 mPostPipTransitionCallback = null;
184             }
185         }
186 
187         @Override
188         public void onPipTransitionCanceled(int direction) {}
189     };
190 
PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState, PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController, PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController, FloatingContentCoordinator floatingContentCoordinator)191     public PipMotionHelper(Context context, @NonNull PipBoundsState pipBoundsState,
192             PipTaskOrganizer pipTaskOrganizer, PhonePipMenuController menuController,
193             PipSnapAlgorithm snapAlgorithm, PipTransitionController pipTransitionController,
194             FloatingContentCoordinator floatingContentCoordinator) {
195         mContext = context;
196         mPipTaskOrganizer = pipTaskOrganizer;
197         mPipBoundsState = pipBoundsState;
198         mMenuController = menuController;
199         mSnapAlgorithm = snapAlgorithm;
200         mFloatingContentCoordinator = floatingContentCoordinator;
201         pipTransitionController.registerPipTransitionCallback(mPipTransitionCallback);
202         mResizePipUpdateListener = (target, values) -> {
203             if (mPipBoundsState.getMotionBoundsState().isInMotion()) {
204                 mPipTaskOrganizer.scheduleUserResizePip(getBounds(),
205                         mPipBoundsState.getMotionBoundsState().getBoundsInMotion(), null);
206             }
207         };
208     }
209 
init()210     public void init() {
211         // Note: Needs to get the shell main thread sf vsync animation handler
212         mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance(
213                 mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
214         mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler(
215                 mSfAnimationHandlerThreadLocal.get());
216     }
217 
218     @NonNull
219     @Override
getFloatingBoundsOnScreen()220     public Rect getFloatingBoundsOnScreen() {
221         return !mPipBoundsState.getMotionBoundsState().getAnimatingToBounds().isEmpty()
222                 ? mPipBoundsState.getMotionBoundsState().getAnimatingToBounds() : getBounds();
223     }
224 
225     @NonNull
226     @Override
getAllowedFloatingBoundsRegion()227     public Rect getAllowedFloatingBoundsRegion() {
228         return mFloatingAllowedArea;
229     }
230 
231     @Override
moveToBounds(@onNull Rect bounds)232     public void moveToBounds(@NonNull Rect bounds) {
233         animateToBounds(bounds, mConflictResolutionSpringConfig);
234     }
235 
236     /**
237      * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations.
238      */
synchronizePinnedStackBounds()239     void synchronizePinnedStackBounds() {
240         cancelPhysicsAnimation();
241         mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded();
242 
243         if (mPipTaskOrganizer.isInPip()) {
244             mFloatingContentCoordinator.onContentMoved(this);
245         }
246     }
247 
248     /**
249      * Tries to move the pinned stack to the given {@param bounds}.
250      */
movePip(Rect toBounds)251     void movePip(Rect toBounds) {
252         movePip(toBounds, false /* isDragging */);
253     }
254 
255     /**
256      * Tries to move the pinned stack to the given {@param bounds}.
257      *
258      * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we
259      *                   won't notify the floating content coordinator of this move, since that will
260      *                   happen when the gesture ends.
261      */
movePip(Rect toBounds, boolean isDragging)262     void movePip(Rect toBounds, boolean isDragging) {
263         if (!isDragging) {
264             mFloatingContentCoordinator.onContentMoved(this);
265         }
266 
267         if (!mSpringingToTouch) {
268             // If we are moving PIP directly to the touch event locations, cancel any animations and
269             // move PIP to the given bounds.
270             cancelPhysicsAnimation();
271 
272             if (!isDragging) {
273                 resizePipUnchecked(toBounds);
274                 mPipBoundsState.setBounds(toBounds);
275             } else {
276                 mPipBoundsState.getMotionBoundsState().setBoundsInMotion(toBounds);
277                 mPipTaskOrganizer.scheduleUserResizePip(getBounds(), toBounds,
278                         (Rect newBounds) -> {
279                                 mMenuController.updateMenuLayout(newBounds);
280                         });
281             }
282         } else {
283             // If PIP is 'catching up' after being stuck in the dismiss target, update the animation
284             // to spring towards the new touch location.
285             mTemporaryBoundsPhysicsAnimator
286                     .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mCatchUpSpringConfig)
287                     .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mCatchUpSpringConfig)
288                     .spring(FloatProperties.RECT_X, toBounds.left, mCatchUpSpringConfig)
289                     .spring(FloatProperties.RECT_Y, toBounds.top, mCatchUpSpringConfig);
290 
291             startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */);
292         }
293     }
294 
295     /** Animates the PIP into the dismiss target, scaling it down. */
animateIntoDismissTarget( MagnetizedObject.MagneticTarget target, float velX, float velY, boolean flung, Function0<Unit> after)296     void animateIntoDismissTarget(
297             MagnetizedObject.MagneticTarget target,
298             float velX, float velY,
299             boolean flung, Function0<Unit> after) {
300         final PointF targetCenter = target.getCenterOnScreen();
301 
302         // PIP should fit in the circle
303         final float dismissCircleSize = mContext.getResources().getDimensionPixelSize(
304                 R.dimen.dismiss_circle_size);
305 
306         final float width = getBounds().width();
307         final float height = getBounds().height();
308         final float ratio = width / height;
309 
310         // Width should be a little smaller than the circle size.
311         final float desiredWidth = dismissCircleSize * DISMISS_CIRCLE_PERCENT;
312         final float desiredHeight = desiredWidth / ratio;
313         final float destinationX = targetCenter.x - (desiredWidth / 2f);
314         final float destinationY = targetCenter.y - (desiredHeight / 2f);
315 
316         // If we're already in the dismiss target area, then there won't be a move to set the
317         // temporary bounds, so just initialize it to the current bounds.
318         if (!mPipBoundsState.getMotionBoundsState().isInMotion()) {
319             mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds());
320         }
321         mTemporaryBoundsPhysicsAnimator
322                 .spring(FloatProperties.RECT_X, destinationX, velX, mAnimateToDismissSpringConfig)
323                 .spring(FloatProperties.RECT_Y, destinationY, velY, mAnimateToDismissSpringConfig)
324                 .spring(FloatProperties.RECT_WIDTH, desiredWidth, mAnimateToDismissSpringConfig)
325                 .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mAnimateToDismissSpringConfig)
326                 .withEndActions(after);
327 
328         startBoundsAnimator(destinationX, destinationY);
329     }
330 
331     /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */
setSpringingToTouch(boolean springingToTouch)332     void setSpringingToTouch(boolean springingToTouch) {
333         mSpringingToTouch = springingToTouch;
334     }
335 
336     /**
337      * Resizes the pinned stack back to unknown windowing mode, which could be freeform or
338      *      * fullscreen depending on the display area's windowing mode.
339      */
expandLeavePip(boolean skipAnimation)340     void expandLeavePip(boolean skipAnimation) {
341         expandLeavePip(skipAnimation, false /* enterSplit */);
342     }
343 
344     /**
345      * Resizes the pinned task to split-screen mode.
346      */
expandIntoSplit()347     void expandIntoSplit() {
348         expandLeavePip(false, true /* enterSplit */);
349     }
350 
351     /**
352      * Resizes the pinned stack back to unknown windowing mode, which could be freeform or
353      * fullscreen depending on the display area's windowing mode.
354      */
expandLeavePip(boolean skipAnimation, boolean enterSplit)355     private void expandLeavePip(boolean skipAnimation, boolean enterSplit) {
356         if (DEBUG) {
357             Log.d(TAG, "exitPip: skipAnimation=" + skipAnimation
358                     + " callers=\n" + Debug.getCallers(5, "    "));
359         }
360         cancelPhysicsAnimation();
361         mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */);
362         mPipTaskOrganizer.exitPip(skipAnimation ? 0 : LEAVE_PIP_DURATION, enterSplit);
363     }
364 
365     /**
366      * Dismisses the pinned stack.
367      */
368     @Override
dismissPip()369     public void dismissPip() {
370         if (DEBUG) {
371             Log.d(TAG, "removePip: callers=\n" + Debug.getCallers(5, "    "));
372         }
373         cancelPhysicsAnimation();
374         mMenuController.hideMenu(ANIM_TYPE_DISMISS, false /* resize */);
375         mPipTaskOrganizer.removePip();
376     }
377 
378     /** Sets the movement bounds to use to constrain PIP position animations. */
onMovementBoundsChanged()379     void onMovementBoundsChanged() {
380         rebuildFlingConfigs();
381 
382         // The movement bounds represent the area within which we can move PIP's top-left position.
383         // The allowed area for all of PIP is those bounds plus PIP's width and height.
384         mFloatingAllowedArea.set(mPipBoundsState.getMovementBounds());
385         mFloatingAllowedArea.right += getBounds().width();
386         mFloatingAllowedArea.bottom += getBounds().height();
387     }
388 
389     /**
390      * @return the PiP bounds.
391      */
getBounds()392     private Rect getBounds() {
393         return mPipBoundsState.getBounds();
394     }
395 
396     /**
397      * Flings the PiP to the closest snap target.
398      */
flingToSnapTarget( float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback)399     void flingToSnapTarget(
400             float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback) {
401         movetoTarget(velocityX, velocityY, postBoundsUpdateCallback, false /* isStash */);
402     }
403 
404     /**
405      * Stash PiP to the closest edge. We set velocityY to 0 to limit pure horizontal motion.
406      */
stashToEdge(float velX, float velY, @Nullable Runnable postBoundsUpdateCallback)407     void stashToEdge(float velX, float velY, @Nullable Runnable postBoundsUpdateCallback) {
408         velY = mPipBoundsState.getStashedState() == STASH_TYPE_NONE ? 0 : velY;
409         movetoTarget(velX, velY, postBoundsUpdateCallback, true /* isStash */);
410     }
411 
movetoTarget( float velocityX, float velocityY, @Nullable Runnable postBoundsUpdateCallback, boolean isStash)412     private void movetoTarget(
413             float velocityX,
414             float velocityY,
415             @Nullable Runnable postBoundsUpdateCallback,
416             boolean isStash) {
417         // If we're flinging to a snap target now, we're not springing to catch up to the touch
418         // location now.
419         mSpringingToTouch = false;
420 
421         mTemporaryBoundsPhysicsAnimator
422                 .spring(FloatProperties.RECT_WIDTH, getBounds().width(), mSpringConfig)
423                 .spring(FloatProperties.RECT_HEIGHT, getBounds().height(), mSpringConfig)
424                 .flingThenSpring(
425                         FloatProperties.RECT_X, velocityX,
426                         isStash ? mStashConfigX : mFlingConfigX,
427                         mSpringConfig, true /* flingMustReachMinOrMax */)
428                 .flingThenSpring(
429                         FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig);
430 
431         final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets();
432         final float leftEdge = isStash
433                 ? mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width()
434                 + insetBounds.left
435                 : mPipBoundsState.getMovementBounds().left;
436         final float rightEdge = isStash
437                 ?  mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset()
438                 - insetBounds.right
439                 : mPipBoundsState.getMovementBounds().right;
440 
441         final float xEndValue = velocityX < 0 ? leftEdge : rightEdge;
442 
443         final int startValueY = mPipBoundsState.getMotionBoundsState().getBoundsInMotion().top;
444         final float estimatedFlingYEndValue =
445                 PhysicsAnimator.estimateFlingEndValue(startValueY, velocityY, mFlingConfigY);
446 
447         startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */,
448                 postBoundsUpdateCallback);
449     }
450 
451     /**
452      * Animates PIP to the provided bounds, using physics animations and the given spring
453      * configuration
454      */
455     void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) {
456         if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
457             // Animate from the current bounds if we're not already animating.
458             mPipBoundsState.getMotionBoundsState().setBoundsInMotion(getBounds());
459         }
460 
461         mTemporaryBoundsPhysicsAnimator
462                 .spring(FloatProperties.RECT_X, bounds.left, springConfig)
463                 .spring(FloatProperties.RECT_Y, bounds.top, springConfig);
464         startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */);
465     }
466 
467     /**
468      * Animates the dismissal of the PiP off the edge of the screen.
469      */
470     void animateDismiss() {
471         // Animate off the bottom of the screen, then dismiss PIP.
472         mTemporaryBoundsPhysicsAnimator
473                 .spring(FloatProperties.RECT_Y,
474                         mPipBoundsState.getMovementBounds().bottom + getBounds().height() * 2,
475                         0,
476                         mSpringConfig)
477                 .withEndActions(this::dismissPip);
478 
479         startBoundsAnimator(
480                 getBounds().left /* toX */, getBounds().bottom + getBounds().height() /* toY */);
481 
482         mDismissalPending = false;
483     }
484 
485     /**
486      * Animates the PiP to the expanded state to show the menu.
487      */
488     float animateToExpandedState(Rect expandedBounds, Rect movementBounds,
489             Rect expandedMovementBounds, Runnable callback) {
490         float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()),
491                 movementBounds);
492         mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction);
493         mPostPipTransitionCallback = callback;
494         resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION);
495         return savedSnapFraction;
496     }
497 
498     /**
499      * Animates the PiP from the expanded state to the normal state after the menu is hidden.
500      */
501     void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction,
502             Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) {
503         if (savedSnapFraction < 0f) {
504             // If there are no saved snap fractions, then just use the current bounds
505             savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(getBounds()),
506                     currentMovementBounds, mPipBoundsState.getStashedState());
507         }
508 
509         mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction,
510                 mPipBoundsState.getStashedState(), mPipBoundsState.getStashOffset(),
511                 mPipBoundsState.getDisplayBounds(),
512                 mPipBoundsState.getDisplayLayout().stableInsets());
513 
514         if (immediate) {
515             movePip(normalBounds);
516         } else {
517             resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION);
518         }
519     }
520 
521     /**
522      * Animates the PiP to the stashed state, choosing the closest edge.
523      */
524     void animateToStashedClosestEdge() {
525         Rect tmpBounds = new Rect();
526         final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets();
527         final int stashType =
528                 mPipBoundsState.getBounds().left == mPipBoundsState.getMovementBounds().left
529                 ? STASH_TYPE_LEFT : STASH_TYPE_RIGHT;
530         final float leftEdge = stashType == STASH_TYPE_LEFT
531                 ? mPipBoundsState.getStashOffset()
532                 - mPipBoundsState.getBounds().width() + insetBounds.left
533                 : mPipBoundsState.getDisplayBounds().right
534                         - mPipBoundsState.getStashOffset() - insetBounds.right;
535         tmpBounds.set((int) leftEdge,
536                 mPipBoundsState.getBounds().top,
537                 (int) (leftEdge + mPipBoundsState.getBounds().width()),
538                 mPipBoundsState.getBounds().bottom);
539         resizeAndAnimatePipUnchecked(tmpBounds, UNSTASH_DURATION);
540         mPipBoundsState.setStashed(stashType);
541     }
542 
543     /**
544      * Animates the PiP from stashed state into un-stashed, popping it out from the edge.
545      */
546     void animateToUnStashedBounds(Rect unstashedBounds) {
547         resizeAndAnimatePipUnchecked(unstashedBounds, UNSTASH_DURATION);
548     }
549 
550     /**
551      * Animates the PiP to offset it from the IME or shelf.
552      */
553     void animateToOffset(Rect originalBounds, int offset) {
554         if (DEBUG) {
555             Log.d(TAG, "animateToOffset: originalBounds=" + originalBounds + " offset=" + offset
556                     + " callers=\n" + Debug.getCallers(5, "    "));
557         }
558         cancelPhysicsAnimation();
559         mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION,
560                 mUpdateBoundsCallback);
561     }
562 
563     /**
564      * Cancels all existing animations.
565      */
566     private void cancelPhysicsAnimation() {
567         mTemporaryBoundsPhysicsAnimator.cancel();
568         mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded();
569         mSpringingToTouch = false;
570     }
571 
572     /** Set new fling configs whose min/max values respect the given movement bounds. */
573     private void rebuildFlingConfigs() {
574         mFlingConfigX = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION,
575                 mPipBoundsState.getMovementBounds().left,
576                 mPipBoundsState.getMovementBounds().right);
577         mFlingConfigY = new PhysicsAnimator.FlingConfig(DEFAULT_FRICTION,
578                 mPipBoundsState.getMovementBounds().top,
579                 mPipBoundsState.getMovementBounds().bottom);
580         final Rect insetBounds = mPipBoundsState.getDisplayLayout().stableInsets();
581         mStashConfigX = new PhysicsAnimator.FlingConfig(
582                 DEFAULT_FRICTION,
583                 mPipBoundsState.getStashOffset() - mPipBoundsState.getBounds().width()
584                         + insetBounds.left,
585                 mPipBoundsState.getDisplayBounds().right - mPipBoundsState.getStashOffset()
586                         - insetBounds.right);
587     }
588 
589     private void startBoundsAnimator(float toX, float toY) {
590         startBoundsAnimator(toX, toY, null /* postBoundsUpdateCallback */);
591     }
592 
593     /**
594      * Starts the physics animator which will update the animated PIP bounds using physics
595      * animations, as well as the TimeAnimator which will apply those bounds to PIP.
596      *
597      * This will also add end actions to the bounds animator that cancel the TimeAnimator and update
598      * the 'real' bounds to equal the final animated bounds.
599      *
600      * If one wishes to supply a callback after all the 'real' bounds update has happened,
601      * pass @param postBoundsUpdateCallback.
602      */
603     private void startBoundsAnimator(float toX, float toY, Runnable postBoundsUpdateCallback) {
604         if (!mSpringingToTouch) {
605             cancelPhysicsAnimation();
606         }
607 
608         setAnimatingToBounds(new Rect(
609                 (int) toX,
610                 (int) toY,
611                 (int) toX + getBounds().width(),
612                 (int) toY + getBounds().height()));
613 
614         if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
615             if (postBoundsUpdateCallback != null) {
616                 mTemporaryBoundsPhysicsAnimator
617                         .addUpdateListener(mResizePipUpdateListener)
618                         .withEndActions(this::onBoundsPhysicsAnimationEnd,
619                                 postBoundsUpdateCallback);
620             } else {
621                 mTemporaryBoundsPhysicsAnimator
622                         .addUpdateListener(mResizePipUpdateListener)
623                         .withEndActions(this::onBoundsPhysicsAnimationEnd);
624             }
625         }
626 
627         mTemporaryBoundsPhysicsAnimator.start();
628     }
629 
630     /**
631      * Notify that PIP was released in the dismiss target and will be animated out and dismissed
632      * shortly.
633      */
634     void notifyDismissalPending() {
635         mDismissalPending = true;
636     }
637 
638     private void onBoundsPhysicsAnimationEnd() {
639         // The physics animation ended, though we may not necessarily be done animating, such as
640         // when we're still dragging after moving out of the magnetic target.
641         if (!mDismissalPending
642                 && !mSpringingToTouch
643                 && !mMagnetizedPip.getObjectStuckToTarget()) {
644             // All motion operations have actually finished.
645             mPipBoundsState.setBounds(
646                     mPipBoundsState.getMotionBoundsState().getBoundsInMotion());
647             mPipBoundsState.getMotionBoundsState().onAllAnimationsEnded();
648             if (!mDismissalPending) {
649                 // do not schedule resize if PiP is dismissing, which may cause app re-open to
650                 // mBounds instead of it's normal bounds.
651                 mPipTaskOrganizer.scheduleFinishResizePip(getBounds());
652             }
653         }
654         mPipBoundsState.getMotionBoundsState().onPhysicsAnimationEnded();
655         mSpringingToTouch = false;
656         mDismissalPending = false;
657     }
658 
659     /**
660      * Notifies the floating coordinator that we're moving, and sets the animating to bounds so
661      * we return these bounds from
662      * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
663      */
664     private void setAnimatingToBounds(Rect bounds) {
665         mPipBoundsState.getMotionBoundsState().setAnimatingToBounds(bounds);
666         mFloatingContentCoordinator.onContentMoved(this);
667     }
668 
669     /**
670      * Directly resizes the PiP to the given {@param bounds}.
671      */
672     private void resizePipUnchecked(Rect toBounds) {
673         if (DEBUG) {
674             Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds
675                     + " callers=\n" + Debug.getCallers(5, "    "));
676         }
677         if (!toBounds.equals(getBounds())) {
678             mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback);
679         }
680     }
681 
682     /**
683      * Directly resizes the PiP to the given {@param bounds}.
684      */
685     private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) {
686         if (DEBUG) {
687             Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds
688                     + " duration=" + duration + " callers=\n" + Debug.getCallers(5, "    "));
689         }
690 
691         // Intentionally resize here even if the current bounds match the destination bounds.
692         // This is so all the proper callbacks are performed.
693         mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration,
694                 TRANSITION_DIRECTION_EXPAND_OR_UNEXPAND, null /* updateBoundsCallback */);
695         setAnimatingToBounds(toBounds);
696     }
697 
698     /**
699      * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the
700      * magnetic dismiss target so it can calculate PIP's size and position.
701      */
702     MagnetizedObject<Rect> getMagnetizedPip() {
703         if (mMagnetizedPip == null) {
704             mMagnetizedPip = new MagnetizedObject<Rect>(
705                     mContext, mPipBoundsState.getMotionBoundsState().getBoundsInMotion(),
706                     FloatProperties.RECT_X, FloatProperties.RECT_Y) {
707                 @Override
708                 public float getWidth(@NonNull Rect animatedPipBounds) {
709                     return animatedPipBounds.width();
710                 }
711 
712                 @Override
713                 public float getHeight(@NonNull Rect animatedPipBounds) {
714                     return animatedPipBounds.height();
715                 }
716 
717                 @Override
718                 public void getLocationOnScreen(
719                         @NonNull Rect animatedPipBounds, @NonNull int[] loc) {
720                     loc[0] = animatedPipBounds.left;
721                     loc[1] = animatedPipBounds.top;
722                 }
723             };
724         }
725 
726         return mMagnetizedPip;
727     }
728 }
729