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.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 
     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 =
    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      */
    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      */
    146     void expandPip() {
    147         expandPip(false /* skipAnimation */);
    148     }
    149 
    150     /**
    151      * Resizes the pinned stack back to fullscreen.
    152      */
    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      */
    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      */
    190     Rect getBounds() {
    191         return mBounds;
    192     }
    193 
    194     /**
    195      * @return the closest minimized PiP bounds.
    196      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
    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