• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2016 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.systemui.pip.phone;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.ComponentName;
22 import android.content.Context;
23 import android.graphics.PointF;
24 import android.graphics.Rect;
25 import android.os.Debug;
26 import android.util.Log;
27 import android.view.Choreographer;
28 
29 import androidx.dynamicanimation.animation.AnimationHandler;
30 import androidx.dynamicanimation.animation.AnimationHandler.FrameCallbackScheduler;
31 import androidx.dynamicanimation.animation.SpringForce;
32 
33 import com.android.systemui.pip.PipSnapAlgorithm;
34 import com.android.systemui.pip.PipTaskOrganizer;
35 import com.android.systemui.util.FloatingContentCoordinator;
36 import com.android.systemui.util.animation.FloatProperties;
37 import com.android.systemui.util.animation.PhysicsAnimator;
38 import com.android.systemui.util.magnetictarget.MagnetizedObject;
39 
40 import java.io.PrintWriter;
41 import java.util.function.Consumer;
42 
43 import kotlin.Unit;
44 import kotlin.jvm.functions.Function0;
45 
46 /**
47  * A helper to animate and manipulate the PiP.
48  */
49 public class PipMotionHelper implements PipAppOpsListener.Callback,
50         FloatingContentCoordinator.FloatingContent {
51 
52     private static final String TAG = "PipMotionHelper";
53     private static final boolean DEBUG = false;
54 
55     private static final int SHRINK_STACK_FROM_MENU_DURATION = 250;
56     private static final int EXPAND_STACK_TO_MENU_DURATION = 250;
57     private static final int EXPAND_STACK_TO_FULLSCREEN_DURATION = 300;
58     private static final int SHIFT_DURATION = 300;
59 
60     /** Friction to use for PIP when it moves via physics fling animations. */
61     private static final float DEFAULT_FRICTION = 2f;
62 
63     private final Context mContext;
64     private final PipTaskOrganizer mPipTaskOrganizer;
65 
66     private PipMenuActivityController mMenuController;
67     private PipSnapAlgorithm mSnapAlgorithm;
68 
69     /** PIP's current bounds on the screen. */
70     private final Rect mBounds = new Rect();
71 
72     /** The bounds within which PIP's top-left coordinate is allowed to move. */
73     private final Rect mMovementBounds = new Rect();
74 
75     /** The region that all of PIP must stay within. */
76     private final Rect mFloatingAllowedArea = new Rect();
77 
78     /**
79      * Temporary bounds used when PIP is being dragged or animated. These bounds are applied to PIP
80      * using {@link PipTaskOrganizer#scheduleUserResizePip}, so that we can animate shrinking into
81      * and expanding out of the magnetic dismiss target.
82      *
83      * Once PIP is done being dragged or animated, we set {@link #mBounds} equal to these temporary
84      * bounds, and call {@link PipTaskOrganizer#scheduleFinishResizePip} to 'officially' move PIP to
85      * its new bounds.
86      */
87     private final Rect mTemporaryBounds = new Rect();
88 
89     /** The destination bounds to which PIP is animating. */
90     private final Rect mAnimatingToBounds = new Rect();
91 
92     /** Coordinator instance for resolving conflicts with other floating content. */
93     private FloatingContentCoordinator mFloatingContentCoordinator;
94 
95     private ThreadLocal<AnimationHandler> mSfAnimationHandlerThreadLocal =
96             ThreadLocal.withInitial(() -> {
97                 FrameCallbackScheduler scheduler = runnable ->
98                         Choreographer.getSfInstance().postFrameCallback(t -> runnable.run());
99                 AnimationHandler handler = new AnimationHandler(scheduler);
100                 return handler;
101             });
102 
103     /**
104      * PhysicsAnimator instance for animating {@link #mTemporaryBounds} using physics animations.
105      */
106     private PhysicsAnimator<Rect> mTemporaryBoundsPhysicsAnimator = PhysicsAnimator.getInstance(
107             mTemporaryBounds);
108 
109     private MagnetizedObject<Rect> mMagnetizedPip;
110 
111     /**
112      * Update listener that resizes the PIP to {@link #mTemporaryBounds}.
113      */
114     private final PhysicsAnimator.UpdateListener<Rect> mResizePipUpdateListener;
115 
116     /** FlingConfig instances provided to PhysicsAnimator for fling gestures. */
117     private PhysicsAnimator.FlingConfig mFlingConfigX;
118     private PhysicsAnimator.FlingConfig mFlingConfigY;
119 
120     /** SpringConfig to use for fling-then-spring animations. */
121     private final PhysicsAnimator.SpringConfig mSpringConfig =
122             new PhysicsAnimator.SpringConfig(
123                     SpringForce.STIFFNESS_MEDIUM, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
124 
125     /** SpringConfig to use for springing PIP away from conflicting floating content. */
126     private final PhysicsAnimator.SpringConfig mConflictResolutionSpringConfig =
127                 new PhysicsAnimator.SpringConfig(
128                         SpringForce.STIFFNESS_LOW, SpringForce.DAMPING_RATIO_LOW_BOUNCY);
129 
130     private final Consumer<Rect> mUpdateBoundsCallback = mBounds::set;
131 
132     /**
133      * Whether we're springing to the touch event location (vs. moving it to that position
134      * instantly). We spring-to-touch after PIP is dragged out of the magnetic target, since it was
135      * 'stuck' in the target and needs to catch up to the touch location.
136      */
137     private boolean mSpringingToTouch = false;
138 
139     /**
140      * Whether PIP was released in the dismiss target, and will be animated out and dismissed
141      * shortly.
142      */
143     private boolean mDismissalPending = false;
144 
145     /**
146      * Gets set in {@link #animateToExpandedState(Rect, Rect, Rect, Runnable)}, this callback is
147      * used to show menu activity when the expand animation is completed.
148      */
149     private Runnable mPostPipTransitionCallback;
150 
151     private final PipTaskOrganizer.PipTransitionCallback mPipTransitionCallback =
152             new PipTaskOrganizer.PipTransitionCallback() {
153         @Override
154         public void onPipTransitionStarted(ComponentName activity, int direction) {}
155 
156         @Override
157         public void onPipTransitionFinished(ComponentName activity, int direction) {
158             if (mPostPipTransitionCallback != null) {
159                 mPostPipTransitionCallback.run();
160                 mPostPipTransitionCallback = null;
161             }
162         }
163 
164         @Override
165         public void onPipTransitionCanceled(ComponentName activity, int direction) {}
166     };
167 
PipMotionHelper(Context context, PipTaskOrganizer pipTaskOrganizer, PipMenuActivityController menuController, PipSnapAlgorithm snapAlgorithm, FloatingContentCoordinator floatingContentCoordinator)168     public PipMotionHelper(Context context, PipTaskOrganizer pipTaskOrganizer,
169             PipMenuActivityController menuController, PipSnapAlgorithm snapAlgorithm,
170             FloatingContentCoordinator floatingContentCoordinator) {
171         mContext = context;
172         mPipTaskOrganizer = pipTaskOrganizer;
173         mMenuController = menuController;
174         mSnapAlgorithm = snapAlgorithm;
175         mFloatingContentCoordinator = floatingContentCoordinator;
176         mPipTaskOrganizer.registerPipTransitionCallback(mPipTransitionCallback);
177         mTemporaryBoundsPhysicsAnimator.setCustomAnimationHandler(
178                 mSfAnimationHandlerThreadLocal.get());
179 
180         mResizePipUpdateListener = (target, values) -> {
181             if (!mTemporaryBounds.isEmpty()) {
182                 mPipTaskOrganizer.scheduleUserResizePip(
183                         mBounds, mTemporaryBounds, null);
184             }
185         };
186     }
187 
188     @NonNull
189     @Override
getFloatingBoundsOnScreen()190     public Rect getFloatingBoundsOnScreen() {
191         return !mAnimatingToBounds.isEmpty() ? mAnimatingToBounds : mBounds;
192     }
193 
194     @NonNull
195     @Override
getAllowedFloatingBoundsRegion()196     public Rect getAllowedFloatingBoundsRegion() {
197         return mFloatingAllowedArea;
198     }
199 
200     @Override
moveToBounds(@onNull Rect bounds)201     public void moveToBounds(@NonNull Rect bounds) {
202         animateToBounds(bounds, mConflictResolutionSpringConfig);
203     }
204 
205     /**
206      * Synchronizes the current bounds with the pinned stack, cancelling any ongoing animations.
207      */
synchronizePinnedStackBounds()208     void synchronizePinnedStackBounds() {
209         cancelAnimations();
210         mBounds.set(mPipTaskOrganizer.getLastReportedBounds());
211         mTemporaryBounds.setEmpty();
212 
213         if (mPipTaskOrganizer.isInPip()) {
214             mFloatingContentCoordinator.onContentMoved(this);
215         }
216     }
217 
isAnimating()218     boolean isAnimating() {
219         return mTemporaryBoundsPhysicsAnimator.isRunning();
220     }
221 
222     /**
223      * Tries to move the pinned stack to the given {@param bounds}.
224      */
movePip(Rect toBounds)225     void movePip(Rect toBounds) {
226         movePip(toBounds, false /* isDragging */);
227     }
228 
229     /**
230      * Tries to move the pinned stack to the given {@param bounds}.
231      *
232      * @param isDragging Whether this movement is the result of a drag touch gesture. If so, we
233      *                   won't notify the floating content coordinator of this move, since that will
234      *                   happen when the gesture ends.
235      */
movePip(Rect toBounds, boolean isDragging)236     void movePip(Rect toBounds, boolean isDragging) {
237         if (!isDragging) {
238             mFloatingContentCoordinator.onContentMoved(this);
239         }
240 
241         if (!mSpringingToTouch) {
242             // If we are moving PIP directly to the touch event locations, cancel any animations and
243             // move PIP to the given bounds.
244             cancelAnimations();
245 
246             if (!isDragging) {
247                 resizePipUnchecked(toBounds);
248                 mBounds.set(toBounds);
249             } else {
250                 mTemporaryBounds.set(toBounds);
251                 mPipTaskOrganizer.scheduleUserResizePip(mBounds, mTemporaryBounds, null);
252             }
253         } else {
254             // If PIP is 'catching up' after being stuck in the dismiss target, update the animation
255             // to spring towards the new touch location.
256             mTemporaryBoundsPhysicsAnimator
257                     .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig)
258                     .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig)
259                     .spring(FloatProperties.RECT_X, toBounds.left, mSpringConfig)
260                     .spring(FloatProperties.RECT_Y, toBounds.top, mSpringConfig);
261 
262             startBoundsAnimator(toBounds.left /* toX */, toBounds.top /* toY */,
263                     false /* dismiss */);
264         }
265     }
266 
267     /** Animates the PIP into the dismiss target, scaling it down. */
animateIntoDismissTarget( MagnetizedObject.MagneticTarget target, float velX, float velY, boolean flung, Function0<Unit> after)268     void animateIntoDismissTarget(
269             MagnetizedObject.MagneticTarget target,
270             float velX, float velY,
271             boolean flung, Function0<Unit> after) {
272         final PointF targetCenter = target.getCenterOnScreen();
273 
274         final float desiredWidth = mBounds.width() / 2;
275         final float desiredHeight = mBounds.height() / 2;
276 
277         final float destinationX = targetCenter.x - (desiredWidth / 2f);
278         final float destinationY = targetCenter.y - (desiredHeight / 2f);
279 
280         // If we're already in the dismiss target area, then there won't be a move to set the
281         // temporary bounds, so just initialize it to the current bounds
282         if (mTemporaryBounds.isEmpty()) {
283             mTemporaryBounds.set(mBounds);
284         }
285         mTemporaryBoundsPhysicsAnimator
286                 .spring(FloatProperties.RECT_X, destinationX, velX, mSpringConfig)
287                 .spring(FloatProperties.RECT_Y, destinationY, velY, mSpringConfig)
288                 .spring(FloatProperties.RECT_WIDTH, desiredWidth, mSpringConfig)
289                 .spring(FloatProperties.RECT_HEIGHT, desiredHeight, mSpringConfig)
290                 .withEndActions(after);
291 
292         startBoundsAnimator(destinationX, destinationY, false);
293     }
294 
295     /** Set whether we're springing-to-touch to catch up after being stuck in the dismiss target. */
setSpringingToTouch(boolean springingToTouch)296     void setSpringingToTouch(boolean springingToTouch) {
297         mSpringingToTouch = springingToTouch;
298     }
299 
300     /**
301      * Resizes the pinned stack back to fullscreen.
302      */
expandPipToFullscreen()303     void expandPipToFullscreen() {
304         expandPipToFullscreen(false /* skipAnimation */);
305     }
306 
307     /**
308      * Resizes the pinned stack back to fullscreen.
309      */
expandPipToFullscreen(boolean skipAnimation)310     void expandPipToFullscreen(boolean skipAnimation) {
311         if (DEBUG) {
312             Log.d(TAG, "exitPip: skipAnimation=" + skipAnimation
313                     + " callers=\n" + Debug.getCallers(5, "    "));
314         }
315         cancelAnimations();
316         mMenuController.hideMenuWithoutResize();
317         mPipTaskOrganizer.getUpdateHandler().post(() -> {
318             mPipTaskOrganizer.exitPip(skipAnimation
319                     ? 0
320                     : EXPAND_STACK_TO_FULLSCREEN_DURATION);
321         });
322     }
323 
324     /**
325      * Dismisses the pinned stack.
326      */
327     @Override
dismissPip()328     public void dismissPip() {
329         if (DEBUG) {
330             Log.d(TAG, "removePip: callers=\n" + Debug.getCallers(5, "    "));
331         }
332         cancelAnimations();
333         mMenuController.hideMenuWithoutResize();
334         mPipTaskOrganizer.removePip();
335     }
336 
337     /** Sets the movement bounds to use to constrain PIP position animations. */
setCurrentMovementBounds(Rect movementBounds)338     void setCurrentMovementBounds(Rect movementBounds) {
339         mMovementBounds.set(movementBounds);
340         rebuildFlingConfigs();
341 
342         // The movement bounds represent the area within which we can move PIP's top-left position.
343         // The allowed area for all of PIP is those bounds plus PIP's width and height.
344         mFloatingAllowedArea.set(mMovementBounds);
345         mFloatingAllowedArea.right += mBounds.width();
346         mFloatingAllowedArea.bottom += mBounds.height();
347     }
348 
349     /**
350      * @return the PiP bounds.
351      */
getBounds()352     Rect getBounds() {
353         return mBounds;
354     }
355 
356     /**
357      * Returns the PIP bounds if we're not animating, or the current, temporary animating bounds
358      * otherwise.
359      */
getPossiblyAnimatingBounds()360     Rect getPossiblyAnimatingBounds() {
361         return mTemporaryBounds.isEmpty() ? mBounds : mTemporaryBounds;
362     }
363 
364     /**
365      * Flings the PiP to the closest snap target.
366      */
flingToSnapTarget( float velocityX, float velocityY, @Nullable Runnable updateAction, @Nullable Runnable endAction)367     void flingToSnapTarget(
368             float velocityX, float velocityY,
369             @Nullable Runnable updateAction, @Nullable Runnable endAction) {
370         // If we're flinging to a snap target now, we're not springing to catch up to the touch
371         // location now.
372         mSpringingToTouch = false;
373 
374         mTemporaryBoundsPhysicsAnimator
375                 .spring(FloatProperties.RECT_WIDTH, mBounds.width(), mSpringConfig)
376                 .spring(FloatProperties.RECT_HEIGHT, mBounds.height(), mSpringConfig)
377                 .flingThenSpring(
378                         FloatProperties.RECT_X, velocityX, mFlingConfigX, mSpringConfig,
379                         true /* flingMustReachMinOrMax */)
380                 .flingThenSpring(
381                         FloatProperties.RECT_Y, velocityY, mFlingConfigY, mSpringConfig)
382                 .withEndActions(endAction);
383 
384         if (updateAction != null) {
385             mTemporaryBoundsPhysicsAnimator.addUpdateListener(
386                     (target, values) -> updateAction.run());
387         }
388 
389         final float xEndValue = velocityX < 0 ? mMovementBounds.left : mMovementBounds.right;
390         final float estimatedFlingYEndValue =
391                 PhysicsAnimator.estimateFlingEndValue(
392                         mTemporaryBounds.top, velocityY, mFlingConfigY);
393 
394         startBoundsAnimator(xEndValue /* toX */, estimatedFlingYEndValue /* toY */,
395                 false /* dismiss */);
396     }
397 
398     /**
399      * Animates PIP to the provided bounds, using physics animations and the given spring
400      * configuration
401      */
402     void animateToBounds(Rect bounds, PhysicsAnimator.SpringConfig springConfig) {
403         if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
404             // Animate from the current bounds if we're not already animating.
405             mTemporaryBounds.set(mBounds);
406         }
407 
408         mTemporaryBoundsPhysicsAnimator
409                 .spring(FloatProperties.RECT_X, bounds.left, springConfig)
410                 .spring(FloatProperties.RECT_Y, bounds.top, springConfig);
411         startBoundsAnimator(bounds.left /* toX */, bounds.top /* toY */,
412                 false /* dismiss */);
413     }
414 
415     /**
416      * Animates the dismissal of the PiP off the edge of the screen.
417      */
418     void animateDismiss() {
419         // Animate off the bottom of the screen, then dismiss PIP.
420         mTemporaryBoundsPhysicsAnimator
421                 .spring(FloatProperties.RECT_Y,
422                         mMovementBounds.bottom + mBounds.height() * 2,
423                         0,
424                         mSpringConfig)
425                 .withEndActions(this::dismissPip);
426 
427         startBoundsAnimator(
428                 mBounds.left /* toX */, mBounds.bottom + mBounds.height() /* toY */,
429                 true /* dismiss */);
430 
431         mDismissalPending = false;
432     }
433 
434     /**
435      * Animates the PiP to the expanded state to show the menu.
436      */
437     float animateToExpandedState(Rect expandedBounds, Rect movementBounds,
438             Rect expandedMovementBounds, Runnable callback) {
439         float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), movementBounds);
440         mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction);
441         mPostPipTransitionCallback = callback;
442         resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION);
443         return savedSnapFraction;
444     }
445 
446     /**
447      * Animates the PiP from the expanded state to the normal state after the menu is hidden.
448      */
449     void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction,
450             Rect normalMovementBounds, Rect currentMovementBounds, boolean immediate) {
451         if (savedSnapFraction < 0f) {
452             // If there are no saved snap fractions, then just use the current bounds
453             savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds),
454                     currentMovementBounds);
455         }
456         mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction);
457 
458         if (immediate) {
459             movePip(normalBounds);
460         } else {
461             resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION);
462         }
463     }
464 
465     /**
466      * Animates the PiP to offset it from the IME or shelf.
467      */
468     void animateToOffset(Rect originalBounds, int offset) {
469         if (DEBUG) {
470             Log.d(TAG, "animateToOffset: originalBounds=" + originalBounds + " offset=" + offset
471                     + " callers=\n" + Debug.getCallers(5, "    "));
472         }
473         cancelAnimations();
474         mPipTaskOrganizer.scheduleOffsetPip(originalBounds, offset, SHIFT_DURATION,
475                 mUpdateBoundsCallback);
476     }
477 
478     /**
479      * Cancels all existing animations.
480      */
481     private void cancelAnimations() {
482         mTemporaryBoundsPhysicsAnimator.cancel();
483         mAnimatingToBounds.setEmpty();
484         mSpringingToTouch = false;
485     }
486 
487     /** Set new fling configs whose min/max values respect the given movement bounds. */
488     private void rebuildFlingConfigs() {
489         mFlingConfigX = new PhysicsAnimator.FlingConfig(
490                 DEFAULT_FRICTION, mMovementBounds.left, mMovementBounds.right);
491         mFlingConfigY = new PhysicsAnimator.FlingConfig(
492                 DEFAULT_FRICTION, mMovementBounds.top, mMovementBounds.bottom);
493     }
494 
495     /**
496      * Starts the physics animator which will update the animated PIP bounds using physics
497      * animations, as well as the TimeAnimator which will apply those bounds to PIP.
498      *
499      * This will also add end actions to the bounds animator that cancel the TimeAnimator and update
500      * the 'real' bounds to equal the final animated bounds.
501      */
502     private void startBoundsAnimator(float toX, float toY, boolean dismiss) {
503         if (!mSpringingToTouch) {
504             cancelAnimations();
505         }
506 
507         // Set animatingToBounds directly to avoid allocating a new Rect, but then call
508         // setAnimatingToBounds to run the normal logic for changing animatingToBounds.
509         mAnimatingToBounds.set(
510                 (int) toX,
511                 (int) toY,
512                 (int) toX + mBounds.width(),
513                 (int) toY + mBounds.height());
514         setAnimatingToBounds(mAnimatingToBounds);
515 
516         if (!mTemporaryBoundsPhysicsAnimator.isRunning()) {
517             mTemporaryBoundsPhysicsAnimator
518                     .addUpdateListener(mResizePipUpdateListener)
519                     .withEndActions(this::onBoundsAnimationEnd);
520         }
521 
522         mTemporaryBoundsPhysicsAnimator.start();
523     }
524 
525     /**
526      * Notify that PIP was released in the dismiss target and will be animated out and dismissed
527      * shortly.
528      */
529     void notifyDismissalPending() {
530         mDismissalPending = true;
531     }
532 
533     private void onBoundsAnimationEnd() {
534         if (!mDismissalPending
535                 && !mSpringingToTouch
536                 && !mMagnetizedPip.getObjectStuckToTarget()) {
537             mBounds.set(mTemporaryBounds);
538             if (!mDismissalPending) {
539                 // do not schedule resize if PiP is dismissing, which may cause app re-open to
540                 // mBounds instead of it's normal bounds.
541                 mPipTaskOrganizer.scheduleFinishResizePip(mBounds);
542             }
543             mTemporaryBounds.setEmpty();
544         }
545 
546         mAnimatingToBounds.setEmpty();
547         mSpringingToTouch = false;
548         mDismissalPending = false;
549     }
550 
551     /**
552      * Notifies the floating coordinator that we're moving, and sets {@link #mAnimatingToBounds} so
553      * we return these bounds from
554      * {@link FloatingContentCoordinator.FloatingContent#getFloatingBoundsOnScreen()}.
555      */
556     private void setAnimatingToBounds(Rect bounds) {
557         mAnimatingToBounds.set(bounds);
558         mFloatingContentCoordinator.onContentMoved(this);
559     }
560 
561     /**
562      * Directly resizes the PiP to the given {@param bounds}.
563      */
564     private void resizePipUnchecked(Rect toBounds) {
565         if (DEBUG) {
566             Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds
567                     + " callers=\n" + Debug.getCallers(5, "    "));
568         }
569         if (!toBounds.equals(mBounds)) {
570             mPipTaskOrganizer.scheduleResizePip(toBounds, mUpdateBoundsCallback);
571         }
572     }
573 
574     /**
575      * Directly resizes the PiP to the given {@param bounds}.
576      */
577     private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) {
578         if (DEBUG) {
579             Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds
580                     + " duration=" + duration + " callers=\n" + Debug.getCallers(5, "    "));
581         }
582 
583         // Intentionally resize here even if the current bounds match the destination bounds.
584         // This is so all the proper callbacks are performed.
585         mPipTaskOrganizer.scheduleAnimateResizePip(toBounds, duration, mUpdateBoundsCallback);
586         setAnimatingToBounds(toBounds);
587     }
588 
589     /**
590      * Returns a MagnetizedObject wrapper for PIP's animated bounds. This is provided to the
591      * magnetic dismiss target so it can calculate PIP's size and position.
592      */
593     MagnetizedObject<Rect> getMagnetizedPip() {
594         if (mMagnetizedPip == null) {
595             mMagnetizedPip = new MagnetizedObject<Rect>(
596                     mContext, mTemporaryBounds, FloatProperties.RECT_X, FloatProperties.RECT_Y) {
597                 @Override
598                 public float getWidth(@NonNull Rect animatedPipBounds) {
599                     return animatedPipBounds.width();
600                 }
601 
602                 @Override
603                 public float getHeight(@NonNull Rect animatedPipBounds) {
604                     return animatedPipBounds.height();
605                 }
606 
607                 @Override
608                 public void getLocationOnScreen(
609                         @NonNull Rect animatedPipBounds, @NonNull int[] loc) {
610                     loc[0] = animatedPipBounds.left;
611                     loc[1] = animatedPipBounds.top;
612                 }
613             };
614         }
615 
616         return mMagnetizedPip;
617     }
618 
619     public void dump(PrintWriter pw, String prefix) {
620         final String innerPrefix = prefix + "  ";
621         pw.println(prefix + TAG);
622         pw.println(innerPrefix + "mBounds=" + mBounds);
623     }
624 }
625