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