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 static android.app.WindowConfiguration.ACTIVITY_TYPE_UNDEFINED;
20 import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
21 import static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN;
22 import static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
23 import static com.android.systemui.Interpolators.LINEAR_OUT_SLOW_IN;
24 
25 import android.animation.AnimationHandler;
26 import android.animation.Animator;
27 import android.animation.Animator.AnimatorListener;
28 import android.animation.AnimatorListenerAdapter;
29 import android.animation.RectEvaluator;
30 import android.animation.ValueAnimator;
31 import android.animation.ValueAnimator.AnimatorUpdateListener;
32 import android.app.ActivityManager.StackInfo;
33 import android.app.IActivityManager;
34 import android.content.Context;
35 import android.graphics.Point;
36 import android.graphics.PointF;
37 import android.graphics.Rect;
38 import android.os.Debug;
39 import android.os.Handler;
40 import android.os.Message;
41 import android.os.RemoteException;
42 import android.util.Log;
43 import android.view.animation.Interpolator;
44 
45 import com.android.internal.graphics.SfVsyncFrameCallbackProvider;
46 import com.android.internal.os.SomeArgs;
47 import com.android.internal.policy.PipSnapAlgorithm;
48 import com.android.systemui.recents.misc.ForegroundThread;
49 import com.android.systemui.recents.misc.SystemServicesProxy;
50 import com.android.systemui.statusbar.FlingAnimationUtils;
51 
52 import java.io.PrintWriter;
53 
54 /**
55  * A helper to animate and manipulate the PiP.
56  */
57 public class PipMotionHelper implements Handler.Callback {
58 
59     private static final String TAG = "PipMotionHelper";
60     private static final boolean DEBUG = false;
61 
62     private static final RectEvaluator RECT_EVALUATOR = new RectEvaluator(new Rect());
63 
64     private static final int DEFAULT_MOVE_STACK_DURATION = 225;
65     private static final int SNAP_STACK_DURATION = 225;
66     private static final int DRAG_TO_TARGET_DISMISS_STACK_DURATION = 375;
67     private static final int DRAG_TO_DISMISS_STACK_DURATION = 175;
68     private static final int SHRINK_STACK_FROM_MENU_DURATION = 250;
69     private static final int EXPAND_STACK_TO_MENU_DURATION = 250;
70     private static final int EXPAND_STACK_TO_FULLSCREEN_DURATION = 300;
71     private static final int MINIMIZE_STACK_MAX_DURATION = 200;
72     private static final int SHIFT_DURATION = 300;
73 
74     // The fraction of the stack width that the user has to drag offscreen to minimize the PiP
75     private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.3f;
76     // The fraction of the stack height that the user has to drag offscreen to dismiss the PiP
77     private static final float DISMISS_OFFSCREEN_FRACTION = 0.3f;
78 
79     private static final int MSG_RESIZE_IMMEDIATE = 1;
80     private static final int MSG_RESIZE_ANIMATE = 2;
81 
82     private Context mContext;
83     private IActivityManager mActivityManager;
84     private Handler mHandler;
85 
86     private PipMenuActivityController mMenuController;
87     private PipSnapAlgorithm mSnapAlgorithm;
88     private FlingAnimationUtils mFlingAnimationUtils;
89     private AnimationHandler mAnimationHandler;
90 
91     private final Rect mBounds = new Rect();
92     private final Rect mStableInsets = new Rect();
93 
94     private ValueAnimator mBoundsAnimator = null;
95 
PipMotionHelper(Context context, IActivityManager activityManager, PipMenuActivityController menuController, PipSnapAlgorithm snapAlgorithm, FlingAnimationUtils flingAnimationUtils)96     public PipMotionHelper(Context context, IActivityManager activityManager,
97             PipMenuActivityController menuController, PipSnapAlgorithm snapAlgorithm,
98             FlingAnimationUtils flingAnimationUtils) {
99         mContext = context;
100         mHandler = new Handler(ForegroundThread.get().getLooper(), this);
101         mActivityManager = activityManager;
102         mMenuController = menuController;
103         mSnapAlgorithm = snapAlgorithm;
104         mFlingAnimationUtils = flingAnimationUtils;
105         mAnimationHandler = new AnimationHandler();
106         mAnimationHandler.setProvider(new SfVsyncFrameCallbackProvider());
107         onConfigurationChanged();
108     }
109 
110     /**
111      * Updates whenever the configuration changes.
112      */
onConfigurationChanged()113     void onConfigurationChanged() {
114         mSnapAlgorithm.onConfigurationChanged();
115         SystemServicesProxy.getInstance(mContext).getStableInsets(mStableInsets);
116     }
117 
118     /**
119      * Synchronizes the current bounds with the pinned stack.
120      */
synchronizePinnedStackBounds()121     void synchronizePinnedStackBounds() {
122         cancelAnimations();
123         try {
124             StackInfo stackInfo =
125                     mActivityManager.getStackInfo(WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
126             if (stackInfo != null) {
127                 mBounds.set(stackInfo.bounds);
128             }
129         } catch (RemoteException e) {
130             Log.w(TAG, "Failed to get pinned stack bounds");
131         }
132     }
133 
134     /**
135      * Tries to the move the pinned stack to the given {@param bounds}.
136      */
movePip(Rect toBounds)137     void movePip(Rect toBounds) {
138         cancelAnimations();
139         resizePipUnchecked(toBounds);
140         mBounds.set(toBounds);
141     }
142 
143     /**
144      * Resizes the pinned stack back to fullscreen.
145      */
expandPip()146     void expandPip() {
147         expandPip(false /* skipAnimation */);
148     }
149 
150     /**
151      * Resizes the pinned stack back to fullscreen.
152      */
expandPip(boolean skipAnimation)153     void expandPip(boolean skipAnimation) {
154         if (DEBUG) {
155             Log.d(TAG, "expandPip: skipAnimation=" + skipAnimation
156                     + " callers=\n" + Debug.getCallers(5, "    "));
157         }
158         cancelAnimations();
159         mMenuController.hideMenuWithoutResize();
160         mHandler.post(() -> {
161             try {
162                 mActivityManager.dismissPip(!skipAnimation, EXPAND_STACK_TO_FULLSCREEN_DURATION);
163             } catch (RemoteException e) {
164                 Log.e(TAG, "Error expanding PiP activity", e);
165             }
166         });
167     }
168 
169     /**
170      * Dismisses the pinned stack.
171      */
dismissPip()172     void dismissPip() {
173         if (DEBUG) {
174             Log.d(TAG, "dismissPip: callers=\n" + Debug.getCallers(5, "    "));
175         }
176         cancelAnimations();
177         mMenuController.hideMenuWithoutResize();
178         mHandler.post(() -> {
179             try {
180                 mActivityManager.removeStacksInWindowingModes(new int[]{ WINDOWING_MODE_PINNED });
181             } catch (RemoteException e) {
182                 Log.e(TAG, "Failed to remove PiP", e);
183             }
184         });
185     }
186 
187     /**
188      * @return the PiP bounds.
189      */
getBounds()190     Rect getBounds() {
191         return mBounds;
192     }
193 
194     /**
195      * @return the closest minimized PiP bounds.
196      */
getClosestMinimizedBounds(Rect stackBounds, Rect movementBounds)197     Rect getClosestMinimizedBounds(Rect stackBounds, Rect movementBounds) {
198         Point displaySize = new Point();
199         mContext.getDisplay().getRealSize(displaySize);
200         Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, stackBounds);
201         mSnapAlgorithm.applyMinimizedOffset(toBounds, movementBounds, displaySize, mStableInsets);
202         return toBounds;
203     }
204 
205     /**
206      * @return whether the PiP at the current bounds should be minimized.
207      */
shouldMinimizePip()208     boolean shouldMinimizePip() {
209         Point displaySize = new Point();
210         mContext.getDisplay().getRealSize(displaySize);
211         if (mBounds.left < 0) {
212             float offscreenFraction = (float) -mBounds.left / mBounds.width();
213             return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION;
214         } else if (mBounds.right > displaySize.x) {
215             float offscreenFraction = (float) (mBounds.right - displaySize.x) /
216                     mBounds.width();
217             return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION;
218         } else {
219             return false;
220         }
221     }
222 
223     /**
224      * @return whether the PiP at the current bounds should be dismissed.
225      */
shouldDismissPip()226     boolean shouldDismissPip() {
227         Point displaySize = new Point();
228         mContext.getDisplay().getRealSize(displaySize);
229         final int y = displaySize.y - mStableInsets.bottom;
230         if (mBounds.bottom > y) {
231             float offscreenFraction = (float) (mBounds.bottom - y) / mBounds.height();
232             return offscreenFraction >= DISMISS_OFFSCREEN_FRACTION;
233         }
234         return false;
235     }
236 
237     /**
238      * Flings the minimized PiP to the closest minimized snap target.
239      */
flingToMinimizedState(float velocityY, Rect movementBounds, Point dragStartPosition)240     Rect flingToMinimizedState(float velocityY, Rect movementBounds, Point dragStartPosition) {
241         cancelAnimations();
242         // We currently only allow flinging the minimized stack up and down, so just lock the
243         // movement bounds to the current stack bounds horizontally
244         movementBounds = new Rect(mBounds.left, movementBounds.top, mBounds.left,
245                 movementBounds.bottom);
246         Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds,
247                 0 /* velocityX */, velocityY, dragStartPosition);
248         if (!mBounds.equals(toBounds)) {
249             mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 0, FAST_OUT_SLOW_IN);
250             mFlingAnimationUtils.apply(mBoundsAnimator, 0,
251                     distanceBetweenRectOffsets(mBounds, toBounds),
252                     velocityY);
253             mBoundsAnimator.start();
254         }
255         return toBounds;
256     }
257 
258     /**
259      * Animates the PiP to the minimized state, slightly offscreen.
260      */
animateToClosestMinimizedState(Rect movementBounds, AnimatorUpdateListener updateListener)261     Rect animateToClosestMinimizedState(Rect movementBounds,
262             AnimatorUpdateListener updateListener) {
263         cancelAnimations();
264         Rect toBounds = getClosestMinimizedBounds(mBounds, movementBounds);
265         if (!mBounds.equals(toBounds)) {
266             mBoundsAnimator = createAnimationToBounds(mBounds, toBounds,
267                     MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN);
268             if (updateListener != null) {
269                 mBoundsAnimator.addUpdateListener(updateListener);
270             }
271             mBoundsAnimator.start();
272         }
273         return toBounds;
274     }
275 
276     /**
277      * Flings the PiP to the closest snap target.
278      */
flingToSnapTarget(float velocity, float velocityX, float velocityY, Rect movementBounds, AnimatorUpdateListener updateListener, AnimatorListener listener, Point startPosition)279     Rect flingToSnapTarget(float velocity, float velocityX, float velocityY, Rect movementBounds,
280             AnimatorUpdateListener updateListener, AnimatorListener listener,
281             Point startPosition) {
282         cancelAnimations();
283         Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds,
284                 velocityX, velocityY, startPosition);
285         if (!mBounds.equals(toBounds)) {
286             mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, 0, FAST_OUT_SLOW_IN);
287             mFlingAnimationUtils.apply(mBoundsAnimator, 0,
288                     distanceBetweenRectOffsets(mBounds, toBounds),
289                     velocity);
290             if (updateListener != null) {
291                 mBoundsAnimator.addUpdateListener(updateListener);
292             }
293             if (listener != null){
294                 mBoundsAnimator.addListener(listener);
295             }
296             mBoundsAnimator.start();
297         }
298         return toBounds;
299     }
300 
301     /**
302      * Animates the PiP to the closest snap target.
303      */
animateToClosestSnapTarget(Rect movementBounds, AnimatorUpdateListener updateListener, AnimatorListener listener)304     Rect animateToClosestSnapTarget(Rect movementBounds, AnimatorUpdateListener updateListener,
305             AnimatorListener listener) {
306         cancelAnimations();
307         Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mBounds);
308         if (!mBounds.equals(toBounds)) {
309             mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, SNAP_STACK_DURATION,
310                     FAST_OUT_SLOW_IN);
311             if (updateListener != null) {
312                 mBoundsAnimator.addUpdateListener(updateListener);
313             }
314             if (listener != null){
315                 mBoundsAnimator.addListener(listener);
316             }
317             mBoundsAnimator.start();
318         }
319         return toBounds;
320     }
321 
322     /**
323      * Animates the PiP to the expanded state to show the menu.
324      */
animateToExpandedState(Rect expandedBounds, Rect movementBounds, Rect expandedMovementBounds)325     float animateToExpandedState(Rect expandedBounds, Rect movementBounds,
326             Rect expandedMovementBounds) {
327         float savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds), movementBounds);
328         mSnapAlgorithm.applySnapFraction(expandedBounds, expandedMovementBounds, savedSnapFraction);
329         resizeAndAnimatePipUnchecked(expandedBounds, EXPAND_STACK_TO_MENU_DURATION);
330         return savedSnapFraction;
331     }
332 
333     /**
334      * Animates the PiP from the expanded state to the normal state after the menu is hidden.
335      */
animateToUnexpandedState(Rect normalBounds, float savedSnapFraction, Rect normalMovementBounds, Rect currentMovementBounds, boolean minimized, boolean immediate)336     void animateToUnexpandedState(Rect normalBounds, float savedSnapFraction,
337             Rect normalMovementBounds, Rect currentMovementBounds, boolean minimized,
338             boolean immediate) {
339         if (savedSnapFraction < 0f) {
340             // If there are no saved snap fractions, then just use the current bounds
341             savedSnapFraction = mSnapAlgorithm.getSnapFraction(new Rect(mBounds),
342                     currentMovementBounds);
343         }
344         mSnapAlgorithm.applySnapFraction(normalBounds, normalMovementBounds, savedSnapFraction);
345         if (minimized) {
346             normalBounds = getClosestMinimizedBounds(normalBounds, normalMovementBounds);
347         }
348         if (immediate) {
349             movePip(normalBounds);
350         } else {
351             resizeAndAnimatePipUnchecked(normalBounds, SHRINK_STACK_FROM_MENU_DURATION);
352         }
353     }
354 
355     /**
356      * Animates the PiP to offset it from the IME or shelf.
357      */
animateToOffset(Rect toBounds)358     void animateToOffset(Rect toBounds) {
359         cancelAnimations();
360         resizeAndAnimatePipUnchecked(toBounds, SHIFT_DURATION);
361     }
362 
363     /**
364      * Animates the dismissal of the PiP off the edge of the screen.
365      */
animateDismiss(Rect pipBounds, float velocityX, float velocityY, AnimatorUpdateListener listener)366     Rect animateDismiss(Rect pipBounds, float velocityX, float velocityY,
367             AnimatorUpdateListener listener) {
368         cancelAnimations();
369         final float velocity = PointF.length(velocityX, velocityY);
370         final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond();
371         Point p = getDismissEndPoint(pipBounds, velocityX, velocityY, isFling);
372         Rect toBounds = new Rect(pipBounds);
373         toBounds.offsetTo(p.x, p.y);
374         mBoundsAnimator = createAnimationToBounds(mBounds, toBounds, DRAG_TO_DISMISS_STACK_DURATION,
375                 FAST_OUT_LINEAR_IN);
376         mBoundsAnimator.addListener(new AnimatorListenerAdapter() {
377             @Override
378             public void onAnimationEnd(Animator animation) {
379                 dismissPip();
380             }
381         });
382         if (isFling) {
383             mFlingAnimationUtils.apply(mBoundsAnimator, 0,
384                     distanceBetweenRectOffsets(mBounds, toBounds), velocity);
385         }
386         if (listener != null) {
387             mBoundsAnimator.addUpdateListener(listener);
388         }
389         mBoundsAnimator.start();
390         return toBounds;
391     }
392 
393     /**
394      * Cancels all existing animations.
395      */
cancelAnimations()396     void cancelAnimations() {
397         if (mBoundsAnimator != null) {
398             mBoundsAnimator.cancel();
399             mBoundsAnimator = null;
400         }
401     }
402 
403     /**
404      * Creates an animation to move the PiP to give given {@param toBounds}.
405      */
createAnimationToBounds(Rect fromBounds, Rect toBounds, int duration, Interpolator interpolator)406     private ValueAnimator createAnimationToBounds(Rect fromBounds, Rect toBounds, int duration,
407             Interpolator interpolator) {
408         ValueAnimator anim = new ValueAnimator() {
409             @Override
410             public AnimationHandler getAnimationHandler() {
411                 return mAnimationHandler;
412             }
413         };
414         anim.setObjectValues(fromBounds, toBounds);
415         anim.setEvaluator(RECT_EVALUATOR);
416         anim.setDuration(duration);
417         anim.setInterpolator(interpolator);
418         anim.addUpdateListener((ValueAnimator animation) -> {
419             resizePipUnchecked((Rect) animation.getAnimatedValue());
420         });
421         return anim;
422     }
423 
424     /**
425      * Directly resizes the PiP to the given {@param bounds}.
426      */
resizePipUnchecked(Rect toBounds)427     private void resizePipUnchecked(Rect toBounds) {
428         if (DEBUG) {
429             Log.d(TAG, "resizePipUnchecked: toBounds=" + toBounds
430                     + " callers=\n" + Debug.getCallers(5, "    "));
431         }
432         if (!toBounds.equals(mBounds)) {
433             SomeArgs args = SomeArgs.obtain();
434             args.arg1 = toBounds;
435             mHandler.sendMessage(mHandler.obtainMessage(MSG_RESIZE_IMMEDIATE, args));
436         }
437     }
438 
439     /**
440      * Directly resizes the PiP to the given {@param bounds}.
441      */
resizeAndAnimatePipUnchecked(Rect toBounds, int duration)442     private void resizeAndAnimatePipUnchecked(Rect toBounds, int duration) {
443         if (DEBUG) {
444             Log.d(TAG, "resizeAndAnimatePipUnchecked: toBounds=" + toBounds
445                     + " duration=" + duration + " callers=\n" + Debug.getCallers(5, "    "));
446         }
447         if (!toBounds.equals(mBounds)) {
448             SomeArgs args = SomeArgs.obtain();
449             args.arg1 = toBounds;
450             args.argi1 = duration;
451             mHandler.sendMessage(mHandler.obtainMessage(MSG_RESIZE_ANIMATE, args));
452         }
453     }
454 
455     /**
456      * @return the coordinates the PIP should animate to based on the direction of velocity when
457      *         dismissing.
458      */
getDismissEndPoint(Rect pipBounds, float velX, float velY, boolean isFling)459     private Point getDismissEndPoint(Rect pipBounds, float velX, float velY, boolean isFling) {
460         Point displaySize = new Point();
461         mContext.getDisplay().getRealSize(displaySize);
462         final float bottomBound = displaySize.y + pipBounds.height() * .1f;
463         if (isFling && velX != 0 && velY != 0) {
464             // Line is defined by: y = mx + b, m = slope, b = y-intercept
465             // Find the slope
466             final float slope = velY / velX;
467             // Sub in slope and PiP position to solve for y-intercept: b = y - mx
468             final float yIntercept = pipBounds.top - slope * pipBounds.left;
469             // Now find the point on this line when y = bottom bound: x = (y - b) / m
470             final float x = (bottomBound - yIntercept) / slope;
471             return new Point((int) x, (int) bottomBound);
472         } else {
473             // If it wasn't a fling the velocity on 'up' is not reliable for direction of movement,
474             // just animate downwards.
475             return new Point(pipBounds.left, (int) bottomBound);
476         }
477     }
478 
479     /**
480      * @return whether the gesture it towards the dismiss area based on the velocity when
481      *         dismissing.
482      */
isGestureToDismissArea(Rect pipBounds, float velX, float velY, boolean isFling)483     public boolean isGestureToDismissArea(Rect pipBounds, float velX, float velY,
484             boolean isFling) {
485         Point endpoint = getDismissEndPoint(pipBounds, velX, velY, isFling);
486         // Center the point
487         endpoint.x += pipBounds.width() / 2;
488         endpoint.y += pipBounds.height() / 2;
489 
490         // The dismiss area is the middle third of the screen, half the PIP's height from the bottom
491         Point size = new Point();
492         mContext.getDisplay().getRealSize(size);
493         final int left = size.x / 3;
494         Rect dismissArea = new Rect(left, size.y - (pipBounds.height() / 2), left * 2,
495                 size.y + pipBounds.height());
496         return dismissArea.contains(endpoint.x, endpoint.y);
497     }
498 
499     /**
500      * @return the distance between points {@param p1} and {@param p2}.
501      */
distanceBetweenRectOffsets(Rect r1, Rect r2)502     private float distanceBetweenRectOffsets(Rect r1, Rect r2) {
503         return PointF.length(r1.left - r2.left, r1.top - r2.top);
504     }
505 
506     /**
507      * Handles messages to be processed on the background thread.
508      */
handleMessage(Message msg)509     public boolean handleMessage(Message msg) {
510         switch (msg.what) {
511             case MSG_RESIZE_IMMEDIATE: {
512                 SomeArgs args = (SomeArgs) msg.obj;
513                 Rect toBounds = (Rect) args.arg1;
514                 try {
515                     mActivityManager.resizePinnedStack(toBounds, null /* tempPinnedTaskBounds */);
516                     mBounds.set(toBounds);
517                 } catch (RemoteException e) {
518                     Log.e(TAG, "Could not resize pinned stack to bounds: " + toBounds, e);
519                 }
520                 return true;
521             }
522 
523             case MSG_RESIZE_ANIMATE: {
524                 SomeArgs args = (SomeArgs) msg.obj;
525                 Rect toBounds = (Rect) args.arg1;
526                 int duration = args.argi1;
527                 try {
528                     StackInfo stackInfo = mActivityManager.getStackInfo(
529                             WINDOWING_MODE_PINNED, ACTIVITY_TYPE_UNDEFINED);
530                     if (stackInfo == null) {
531                         // In the case where we've already re-expanded or dismissed the PiP, then
532                         // just skip the resize
533                         return true;
534                     }
535 
536                     mActivityManager.resizeStack(stackInfo.stackId, toBounds,
537                             false /* allowResizeInDockedMode */, true /* preserveWindows */,
538                             true /* animate */, duration);
539                     mBounds.set(toBounds);
540                 } catch (RemoteException e) {
541                     Log.e(TAG, "Could not animate resize pinned stack to bounds: " + toBounds, e);
542                 }
543                 return true;
544             }
545 
546             default:
547                 return false;
548         }
549     }
550 
dump(PrintWriter pw, String prefix)551     public void dump(PrintWriter pw, String prefix) {
552         final String innerPrefix = prefix + "  ";
553         pw.println(prefix + TAG);
554         pw.println(innerPrefix + "mBounds=" + mBounds);
555         pw.println(innerPrefix + "mStableInsets=" + mStableInsets);
556     }
557 }
558