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