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 com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_NONE;
     20 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_CLOSE;
     21 import static com.android.systemui.pip.phone.PipMenuActivityController.MENU_STATE_FULL;
     22 
     23 import android.animation.Animator;
     24 import android.animation.AnimatorListenerAdapter;
     25 import android.animation.ValueAnimator;
     26 import android.animation.ValueAnimator.AnimatorUpdateListener;
     27 import android.app.IActivityManager;
     28 import android.content.ComponentName;
     29 import android.content.Context;
     30 import android.content.res.Resources;
     31 import android.graphics.Point;
     32 import android.graphics.PointF;
     33 import android.graphics.Rect;
     34 import android.os.Handler;
     35 import android.os.RemoteException;
     36 import android.util.Log;
     37 import android.util.Size;
     38 import android.view.IPinnedStackController;
     39 import android.view.MotionEvent;
     40 import android.view.ViewConfiguration;
     41 import android.view.accessibility.AccessibilityEvent;
     42 import android.view.accessibility.AccessibilityManager;
     43 import android.view.accessibility.AccessibilityNodeInfo;
     44 import android.view.accessibility.AccessibilityWindowInfo;
     45 
     46 import com.android.internal.logging.MetricsLogger;
     47 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
     48 import com.android.internal.policy.PipSnapAlgorithm;
     49 import com.android.systemui.R;
     50 import com.android.systemui.statusbar.FlingAnimationUtils;
     51 
     52 import java.io.PrintWriter;
     53 
     54 /**
     55  * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding
     56  * the PIP.
     57  */
     58 public class PipTouchHandler {
     59     private static final String TAG = "PipTouchHandler";
     60 
     61     // Allow the PIP to be dragged to the edge of the screen to be minimized.
     62     private static final boolean ENABLE_MINIMIZE = false;
     63     // Allow the PIP to be flung from anywhere on the screen to the bottom to be dismissed.
     64     private static final boolean ENABLE_FLING_DISMISS = false;
     65 
     66     // These values are used for metrics and should never change
     67     private static final int METRIC_VALUE_DISMISSED_BY_TAP = 0;
     68     private static final int METRIC_VALUE_DISMISSED_BY_DRAG = 1;
     69 
     70     private static final int SHOW_DISMISS_AFFORDANCE_DELAY = 225;
     71 
     72     // Allow dragging the PIP to a location to close it
     73     private static final boolean ENABLE_DISMISS_DRAG_TO_EDGE = true;
     74 
     75     private final Context mContext;
     76     private final IActivityManager mActivityManager;
     77     private final ViewConfiguration mViewConfig;
     78     private final PipMenuListener mMenuListener = new PipMenuListener();
     79     private IPinnedStackController mPinnedStackController;
     80 
     81     private final PipMenuActivityController mMenuController;
     82     private final PipDismissViewController mDismissViewController;
     83     private final PipSnapAlgorithm mSnapAlgorithm;
     84     private final AccessibilityManager mAccessibilityManager;
     85     private boolean mShowPipMenuOnAnimationEnd = false;
     86 
     87     // The current movement bounds
     88     private Rect mMovementBounds = new Rect();
     89 
     90     // The reference inset bounds, used to determine the dismiss fraction
     91     private Rect mInsetBounds = new Rect();
     92     // The reference bounds used to calculate the normal/expanded target bounds
     93     private Rect mNormalBounds = new Rect();
     94     private Rect mNormalMovementBounds = new Rect();
     95     private Rect mExpandedBounds = new Rect();
     96     private Rect mExpandedMovementBounds = new Rect();
     97     private int mExpandedShortestEdgeSize;
     98 
     99     // Used to workaround an issue where the WM rotation happens before we are notified, allowing
    100     // us to send stale bounds
    101     private int mDeferResizeToNormalBoundsUntilRotation = -1;
    102     private int mDisplayRotation;
    103 
    104     private Handler mHandler = new Handler();
    105     private Runnable mShowDismissAffordance = new Runnable() {
    106         @Override
    107         public void run() {
    108             if (ENABLE_DISMISS_DRAG_TO_EDGE) {
    109                 mDismissViewController.showDismissTarget();
    110             }
    111         }
    112     };
    113     private ValueAnimator.AnimatorUpdateListener mUpdateScrimListener =
    114             new AnimatorUpdateListener() {
    115                 @Override
    116                 public void onAnimationUpdate(ValueAnimator animation) {
    117                     updateDismissFraction();
    118                 }
    119             };
    120 
    121     // Behaviour states
    122     private int mMenuState = MENU_STATE_NONE;
    123     private boolean mIsMinimized;
    124     private boolean mIsImeShowing;
    125     private int mImeHeight;
    126     private int mImeOffset;
    127     private float mSavedSnapFraction = -1f;
    128     private boolean mSendingHoverAccessibilityEvents;
    129     private boolean mMovementWithinMinimize;
    130     private boolean mMovementWithinDismiss;
    131 
    132     // Touch state
    133     private final PipTouchState mTouchState;
    134     private final FlingAnimationUtils mFlingAnimationUtils;
    135     private final PipTouchGesture[] mGestures;
    136     private final PipMotionHelper mMotionHelper;
    137 
    138     // Temp vars
    139     private final Rect mTmpBounds = new Rect();
    140 
    141     /**
    142      * A listener for the PIP menu activity.
    143      */
    144     private class PipMenuListener implements PipMenuActivityController.Listener {
    145         @Override
    146         public void onPipMenuStateChanged(int menuState, boolean resize) {
    147             setMenuState(menuState, resize);
    148         }
    149 
    150         @Override
    151         public void onPipExpand() {
    152             if (!mIsMinimized) {
    153                 mMotionHelper.expandPip();
    154             }
    155         }
    156 
    157         @Override
    158         public void onPipMinimize() {
    159             setMinimizedStateInternal(true);
    160             mMotionHelper.animateToClosestMinimizedState(mMovementBounds, null /* updateListener */);
    161         }
    162 
    163         @Override
    164         public void onPipDismiss() {
    165             mMotionHelper.dismissPip();
    166             MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED,
    167                     METRIC_VALUE_DISMISSED_BY_TAP);
    168         }
    169 
    170         @Override
    171         public void onPipShowMenu() {
    172             mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
    173                     mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
    174         }
    175     }
    176 
    177     public PipTouchHandler(Context context, IActivityManager activityManager,
    178             PipMenuActivityController menuController,
    179             InputConsumerController inputConsumerController) {
    180 
    181         // Initialize the Pip input consumer
    182         mContext = context;
    183         mActivityManager = activityManager;
    184         mAccessibilityManager = context.getSystemService(AccessibilityManager.class);
    185         mViewConfig = ViewConfiguration.get(context);
    186         mMenuController = menuController;
    187         mMenuController.addListener(mMenuListener);
    188         mDismissViewController = new PipDismissViewController(context);
    189         mSnapAlgorithm = new PipSnapAlgorithm(mContext);
    190         mFlingAnimationUtils = new FlingAnimationUtils(context, 2.5f);
    191         mGestures = new PipTouchGesture[] {
    192                 mDefaultMovementGesture
    193         };
    194         mMotionHelper = new PipMotionHelper(mContext, mActivityManager, mMenuController,
    195                 mSnapAlgorithm, mFlingAnimationUtils);
    196         mTouchState = new PipTouchState(mViewConfig, mHandler,
    197                 () -> mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
    198                         mMovementBounds, true /* allowMenuTimeout */, willResizeMenu()));
    199 
    200         Resources res = context.getResources();
    201         mExpandedShortestEdgeSize = res.getDimensionPixelSize(
    202                 R.dimen.pip_expanded_shortest_edge_size);
    203         mImeOffset = res.getDimensionPixelSize(R.dimen.pip_ime_offset);
    204 
    205         // Register the listener for input consumer touch events
    206         inputConsumerController.setTouchListener(this::handleTouchEvent);
    207         inputConsumerController.setRegistrationListener(this::onRegistrationChanged);
    208         onRegistrationChanged(inputConsumerController.isRegistered());
    209     }
    210 
    211     public void setTouchEnabled(boolean enabled) {
    212         mTouchState.setAllowTouches(enabled);
    213     }
    214 
    215     public void showPictureInPictureMenu() {
    216         // Only show the menu if the user isn't currently interacting with the PiP
    217         if (!mTouchState.isUserInteracting()) {
    218             mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
    219                     mMovementBounds, false /* allowMenuTimeout */, willResizeMenu());
    220         }
    221     }
    222 
    223     public void onActivityPinned() {
    224         cleanUp();
    225         mShowPipMenuOnAnimationEnd = true;
    226     }
    227 
    228     public void onActivityUnpinned(ComponentName topPipActivity) {
    229         if (topPipActivity == null) {
    230             // Clean up state after the last PiP activity is removed
    231             cleanUp();
    232         }
    233     }
    234 
    235     public void onPinnedStackAnimationEnded() {
    236         // Always synchronize the motion helper bounds once PiP animations finish
    237         mMotionHelper.synchronizePinnedStackBounds();
    238 
    239         if (mShowPipMenuOnAnimationEnd) {
    240             mMenuController.showMenu(MENU_STATE_CLOSE, mMotionHelper.getBounds(),
    241                     mMovementBounds, true /* allowMenuTimeout */, false /* willResizeMenu */);
    242             mShowPipMenuOnAnimationEnd = false;
    243         }
    244     }
    245 
    246     public void onConfigurationChanged() {
    247         mMotionHelper.onConfigurationChanged();
    248         mMotionHelper.synchronizePinnedStackBounds();
    249     }
    250 
    251     public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
    252         mIsImeShowing = imeVisible;
    253         mImeHeight = imeHeight;
    254     }
    255 
    256     public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, Rect animatingBounds,
    257             boolean fromImeAdjustement, int displayRotation) {
    258         // Re-calculate the expanded bounds
    259         mNormalBounds = normalBounds;
    260         Rect normalMovementBounds = new Rect();
    261         mSnapAlgorithm.getMovementBounds(mNormalBounds, insetBounds, normalMovementBounds,
    262                 mIsImeShowing ? mImeHeight : 0);
    263 
    264         // Calculate the expanded size
    265         float aspectRatio = (float) normalBounds.width() / normalBounds.height();
    266         Point displaySize = new Point();
    267         mContext.getDisplay().getRealSize(displaySize);
    268         Size expandedSize = mSnapAlgorithm.getSizeForAspectRatio(aspectRatio,
    269                 mExpandedShortestEdgeSize, displaySize.x, displaySize.y);
    270         mExpandedBounds.set(0, 0, expandedSize.getWidth(), expandedSize.getHeight());
    271         Rect expandedMovementBounds = new Rect();
    272         mSnapAlgorithm.getMovementBounds(mExpandedBounds, insetBounds, expandedMovementBounds,
    273                 mIsImeShowing ? mImeHeight : 0);
    274 
    275         // If this is from an IME adjustment, then we should move the PiP so that it is not occluded
    276         // by the IME
    277         if (fromImeAdjustement) {
    278             if (mTouchState.isUserInteracting()) {
    279                 // Defer the update of the current movement bounds until after the user finishes
    280                 // touching the screen
    281             } else {
    282                 final Rect bounds = new Rect(animatingBounds);
    283                 final Rect toMovementBounds = mMenuState == MENU_STATE_FULL
    284                         ? expandedMovementBounds
    285                         : normalMovementBounds;
    286                 if (mIsImeShowing) {
    287                     // IME visible, apply the IME offset if the space allows for it
    288                     final int imeOffset = toMovementBounds.bottom - Math.max(toMovementBounds.top,
    289                             toMovementBounds.bottom - mImeOffset);
    290                     if (bounds.top == mMovementBounds.bottom) {
    291                         // If the PIP is currently resting on top of the IME, then adjust it with
    292                         // the showing IME
    293                         bounds.offsetTo(bounds.left, toMovementBounds.bottom - imeOffset);
    294                     } else {
    295                         bounds.offset(0, Math.min(0, toMovementBounds.bottom - imeOffset
    296                                 - bounds.top));
    297                     }
    298                 } else {
    299                     // IME hidden
    300                     if (bounds.top >= (mMovementBounds.bottom - mImeOffset)) {
    301                         // If the PIP is resting on top of the IME, then adjust it with the hiding
    302                         // IME
    303                         bounds.offsetTo(bounds.left, toMovementBounds.bottom);
    304                     }
    305                 }
    306                 mMotionHelper.animateToIMEOffset(bounds);
    307             }
    308         }
    309 
    310         // Update the movement bounds after doing the calculations based on the old movement bounds
    311         // above
    312         mNormalMovementBounds = normalMovementBounds;
    313         mExpandedMovementBounds = expandedMovementBounds;
    314         mDisplayRotation = displayRotation;
    315         mInsetBounds.set(insetBounds);
    316         updateMovementBounds(mMenuState);
    317 
    318         // If we have a deferred resize, apply it now
    319         if (mDeferResizeToNormalBoundsUntilRotation == displayRotation) {
    320             mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction,
    321                     mNormalMovementBounds, mMovementBounds, mIsMinimized,
    322                     true /* immediate */);
    323             mSavedSnapFraction = -1f;
    324             mDeferResizeToNormalBoundsUntilRotation = -1;
    325         }
    326     }
    327 
    328     private void onRegistrationChanged(boolean isRegistered) {
    329         mAccessibilityManager.setPictureInPictureActionReplacingConnection(isRegistered
    330                 ? new PipAccessibilityInteractionConnection(mMotionHelper,
    331                         this::onAccessibilityShowMenu, mHandler) : null);
    332 
    333         if (!isRegistered && mTouchState.isUserInteracting()) {
    334             // If the input consumer is unregistered while the user is interacting, then we may not
    335             // get the final TOUCH_UP event, so clean up the dismiss target as well
    336             cleanUpDismissTarget();
    337         }
    338     }
    339 
    340     private void onAccessibilityShowMenu() {
    341         mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
    342                 mMovementBounds, false /* allowMenuTimeout */, willResizeMenu());
    343     }
    344 
    345     private boolean handleTouchEvent(MotionEvent ev) {
    346         // Skip touch handling until we are bound to the controller
    347         if (mPinnedStackController == null) {
    348             return true;
    349         }
    350 
    351         // Update the touch state
    352         mTouchState.onTouchEvent(ev);
    353 
    354         switch (ev.getAction()) {
    355             case MotionEvent.ACTION_DOWN: {
    356                 mMotionHelper.synchronizePinnedStackBounds();
    357 
    358                 for (PipTouchGesture gesture : mGestures) {
    359                     gesture.onDown(mTouchState);
    360                 }
    361                 break;
    362             }
    363             case MotionEvent.ACTION_MOVE: {
    364                 for (PipTouchGesture gesture : mGestures) {
    365                     if (gesture.onMove(mTouchState)) {
    366                         break;
    367                     }
    368                 }
    369                 break;
    370             }
    371             case MotionEvent.ACTION_UP: {
    372                 // Update the movement bounds again if the state has changed since the user started
    373                 // dragging (ie. when the IME shows)
    374                 updateMovementBounds(mMenuState);
    375 
    376                 for (PipTouchGesture gesture : mGestures) {
    377                     if (gesture.onUp(mTouchState)) {
    378                         break;
    379                     }
    380                 }
    381 
    382                 // Fall through to clean up
    383             }
    384             case MotionEvent.ACTION_CANCEL: {
    385                 mTouchState.reset();
    386                 break;
    387             }
    388             case MotionEvent.ACTION_HOVER_ENTER:
    389             case MotionEvent.ACTION_HOVER_MOVE: {
    390                 if (mAccessibilityManager.isEnabled() && !mSendingHoverAccessibilityEvents) {
    391                     AccessibilityEvent event = AccessibilityEvent.obtain(
    392                             AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
    393                     event.setImportantForAccessibility(true);
    394                     event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
    395                     event.setWindowId(
    396                             AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
    397                     mAccessibilityManager.sendAccessibilityEvent(event);
    398                     mSendingHoverAccessibilityEvents = true;
    399                 }
    400                 break;
    401             }
    402             case MotionEvent.ACTION_HOVER_EXIT: {
    403                 if (mAccessibilityManager.isEnabled() && mSendingHoverAccessibilityEvents) {
    404                     AccessibilityEvent event = AccessibilityEvent.obtain(
    405                             AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
    406                     event.setImportantForAccessibility(true);
    407                     event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
    408                     event.setWindowId(
    409                             AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
    410                     mAccessibilityManager.sendAccessibilityEvent(event);
    411                     mSendingHoverAccessibilityEvents = false;
    412                 }
    413                 break;
    414             }
    415         }
    416         return mMenuState == MENU_STATE_NONE;
    417     }
    418 
    419     /**
    420      * Updates the appearance of the menu and scrim on top of the PiP while dismissing.
    421      */
    422     private void updateDismissFraction() {
    423         // Skip updating the dismiss fraction when the IME is showing. This is to work around an
    424         // issue where starting the menu activity for the dismiss overlay will steal the window
    425         // focus, which closes the IME.
    426         if (mMenuController != null && !mIsImeShowing) {
    427             Rect bounds = mMotionHelper.getBounds();
    428             final float target = mInsetBounds.bottom;
    429             float fraction = 0f;
    430             if (bounds.bottom > target) {
    431                 final float distance = bounds.bottom - target;
    432                 fraction = Math.min(distance / bounds.height(), 1f);
    433             }
    434             if (Float.compare(fraction, 0f) != 0 || mMenuController.isMenuActivityVisible()) {
    435                 // Update if the fraction > 0, or if fraction == 0 and the menu was already visible
    436                 mMenuController.setDismissFraction(fraction);
    437             }
    438         }
    439     }
    440 
    441     /**
    442      * Sets the controller to update the system of changes from user interaction.
    443      */
    444     void setPinnedStackController(IPinnedStackController controller) {
    445         mPinnedStackController = controller;
    446     }
    447 
    448     /**
    449      * Sets the minimized state.
    450      */
    451     private void setMinimizedStateInternal(boolean isMinimized) {
    452         if (!ENABLE_MINIMIZE) {
    453             return;
    454         }
    455         setMinimizedState(isMinimized, false /* fromController */);
    456     }
    457 
    458     /**
    459      * Sets the minimized state.
    460      */
    461     void setMinimizedState(boolean isMinimized, boolean fromController) {
    462         if (!ENABLE_MINIMIZE) {
    463             return;
    464         }
    465         if (mIsMinimized != isMinimized) {
    466             MetricsLogger.action(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MINIMIZED,
    467                     isMinimized);
    468         }
    469         mIsMinimized = isMinimized;
    470         mSnapAlgorithm.setMinimized(isMinimized);
    471 
    472         if (fromController) {
    473             if (isMinimized) {
    474                 // Move the PiP to the new bounds immediately if minimized
    475                 mMotionHelper.movePip(mMotionHelper.getClosestMinimizedBounds(mNormalBounds,
    476                         mMovementBounds));
    477             }
    478         } else if (mPinnedStackController != null) {
    479             try {
    480                 mPinnedStackController.setIsMinimized(isMinimized);
    481             } catch (RemoteException e) {
    482                 Log.e(TAG, "Could not set minimized state", e);
    483             }
    484         }
    485     }
    486 
    487     /**
    488      * Sets the menu visibility.
    489      */
    490     private void setMenuState(int menuState, boolean resize) {
    491         if (menuState == MENU_STATE_FULL) {
    492             // Save the current snap fraction and if we do not drag or move the PiP, then
    493             // we store back to this snap fraction.  Otherwise, we'll reset the snap
    494             // fraction and snap to the closest edge
    495             Rect expandedBounds = new Rect(mExpandedBounds);
    496             if (resize) {
    497                 mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds,
    498                         mMovementBounds, mExpandedMovementBounds);
    499             }
    500         } else if (menuState == MENU_STATE_NONE) {
    501             // Try and restore the PiP to the closest edge, using the saved snap fraction
    502             // if possible
    503             if (resize) {
    504                 if (mDeferResizeToNormalBoundsUntilRotation == -1) {
    505                     // This is a very special case: when the menu is expanded and visible,
    506                     // navigating to another activity can trigger auto-enter PiP, and if the
    507                     // revealed activity has a forced rotation set, then the controller will get
    508                     // updated with the new rotation of the display. However, at the same time,
    509                     // SystemUI will try to hide the menu by creating an animation to the normal
    510                     // bounds which are now stale.  In such a case we defer the animation to the
    511                     // normal bounds until after the next onMovementBoundsChanged() call to get the
    512                     // bounds in the new orientation
    513                     try {
    514                         int displayRotation = mPinnedStackController.getDisplayRotation();
    515                         if (mDisplayRotation != displayRotation) {
    516                             mDeferResizeToNormalBoundsUntilRotation = displayRotation;
    517                         }
    518                     } catch (RemoteException e) {
    519                         Log.e(TAG, "Could not get display rotation from controller");
    520                     }
    521                 }
    522 
    523                 if (mDeferResizeToNormalBoundsUntilRotation == -1) {
    524                     Rect normalBounds = new Rect(mNormalBounds);
    525                     mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction,
    526                             mNormalMovementBounds, mMovementBounds, mIsMinimized,
    527                             false /* immediate */);
    528                     mSavedSnapFraction = -1f;
    529                 }
    530             } else {
    531                 // If resizing is not allowed, then the PiP should be frozen until the transition
    532                 // ends as well
    533                 setTouchEnabled(false);
    534                 mSavedSnapFraction = -1f;
    535             }
    536         }
    537         mMenuState = menuState;
    538         updateMovementBounds(menuState);
    539         if (menuState != MENU_STATE_CLOSE) {
    540             MetricsLogger.visibility(mContext, MetricsEvent.ACTION_PICTURE_IN_PICTURE_MENU,
    541                     menuState == MENU_STATE_FULL);
    542         }
    543     }
    544 
    545     /**
    546      * @return the motion helper.
    547      */
    548     public PipMotionHelper getMotionHelper() {
    549         return mMotionHelper;
    550     }
    551 
    552     /**
    553      * Gesture controlling normal movement of the PIP.
    554      */
    555     private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() {
    556         // Whether the PiP was on the left side of the screen at the start of the gesture
    557         private boolean mStartedOnLeft;
    558         private final Point mStartPosition = new Point();
    559         private final PointF mDelta = new PointF();
    560 
    561         @Override
    562         public void onDown(PipTouchState touchState) {
    563             if (!touchState.isUserInteracting()) {
    564                 return;
    565             }
    566 
    567             Rect bounds = mMotionHelper.getBounds();
    568             mDelta.set(0f, 0f);
    569             mStartPosition.set(bounds.left, bounds.top);
    570             mStartedOnLeft = bounds.left < mMovementBounds.centerX();
    571             mMovementWithinMinimize = true;
    572             mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom;
    573 
    574             // If the menu is still visible, and we aren't minimized, then just poke the menu
    575             // so that it will timeout after the user stops touching it
    576             if (mMenuState != MENU_STATE_NONE && !mIsMinimized) {
    577                 mMenuController.pokeMenu();
    578             }
    579 
    580             if (ENABLE_DISMISS_DRAG_TO_EDGE) {
    581                 mDismissViewController.createDismissTarget();
    582                 mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY);
    583             }
    584         }
    585 
    586         @Override
    587         boolean onMove(PipTouchState touchState) {
    588             if (!touchState.isUserInteracting()) {
    589                 return false;
    590             }
    591 
    592             if (touchState.startedDragging()) {
    593                 mSavedSnapFraction = -1f;
    594 
    595                 if (ENABLE_DISMISS_DRAG_TO_EDGE) {
    596                     mHandler.removeCallbacks(mShowDismissAffordance);
    597                     mDismissViewController.showDismissTarget();
    598                 }
    599             }
    600 
    601             if (touchState.isDragging()) {
    602                 // Move the pinned stack freely
    603                 final PointF lastDelta = touchState.getLastTouchDelta();
    604                 float lastX = mStartPosition.x + mDelta.x;
    605                 float lastY = mStartPosition.y + mDelta.y;
    606                 float left = lastX + lastDelta.x;
    607                 float top = lastY + lastDelta.y;
    608                 if (!touchState.allowDraggingOffscreen() || !ENABLE_MINIMIZE) {
    609                     left = Math.max(mMovementBounds.left, Math.min(mMovementBounds.right, left));
    610                 }
    611                 if (ENABLE_DISMISS_DRAG_TO_EDGE) {
    612                     // Allow pip to move past bottom bounds
    613                     top = Math.max(mMovementBounds.top, top);
    614                 } else {
    615                     top = Math.max(mMovementBounds.top, Math.min(mMovementBounds.bottom, top));
    616                 }
    617 
    618                 // Add to the cumulative delta after bounding the position
    619                 mDelta.x += left - lastX;
    620                 mDelta.y += top - lastY;
    621 
    622                 mTmpBounds.set(mMotionHelper.getBounds());
    623                 mTmpBounds.offsetTo((int) left, (int) top);
    624                 mMotionHelper.movePip(mTmpBounds);
    625 
    626                 if (ENABLE_DISMISS_DRAG_TO_EDGE) {
    627                     updateDismissFraction();
    628                 }
    629 
    630                 final PointF curPos = touchState.getLastTouchPosition();
    631                 if (mMovementWithinMinimize) {
    632                     // Track if movement remains near starting edge to identify swipes to minimize
    633                     mMovementWithinMinimize = mStartedOnLeft
    634                             ? curPos.x <= mMovementBounds.left + mTmpBounds.width()
    635                             : curPos.x >= mMovementBounds.right;
    636                 }
    637                 if (mMovementWithinDismiss) {
    638                     // Track if movement remains near the bottom edge to identify swipe to dismiss
    639                     mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom;
    640                 }
    641                 return true;
    642             }
    643             return false;
    644         }
    645 
    646         @Override
    647         public boolean onUp(PipTouchState touchState) {
    648             if (ENABLE_DISMISS_DRAG_TO_EDGE) {
    649                 // Clean up the dismiss target regardless of the touch state in case the touch
    650                 // enabled state changes while the user is interacting
    651                 cleanUpDismissTarget();
    652             }
    653 
    654             if (!touchState.isUserInteracting()) {
    655                 return false;
    656             }
    657 
    658             final PointF vel = touchState.getVelocity();
    659             final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y);
    660             final float velocity = PointF.length(vel.x, vel.y);
    661             final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond();
    662             final boolean isUpWithinDimiss = ENABLE_FLING_DISMISS
    663                     && touchState.getLastTouchPosition().y >= mMovementBounds.bottom
    664                     && mMotionHelper.isGestureToDismissArea(mMotionHelper.getBounds(), vel.x,
    665                             vel.y, isFling);
    666             final boolean isFlingToBot = isFling && vel.y > 0 && !isHorizontal
    667                     && (mMovementWithinDismiss || isUpWithinDimiss);
    668             if (ENABLE_DISMISS_DRAG_TO_EDGE) {
    669                 // Check if the user dragged or flung the PiP offscreen to dismiss it
    670                 if (mMotionHelper.shouldDismissPip() || isFlingToBot) {
    671                     mMotionHelper.animateDismiss(mMotionHelper.getBounds(), vel.x,
    672                         vel.y, mUpdateScrimListener);
    673                     MetricsLogger.action(mContext,
    674                             MetricsEvent.ACTION_PICTURE_IN_PICTURE_DISMISSED,
    675                             METRIC_VALUE_DISMISSED_BY_DRAG);
    676                     return true;
    677                 }
    678             }
    679 
    680             if (touchState.isDragging()) {
    681                 final boolean isFlingToEdge = isFling && isHorizontal && mMovementWithinMinimize
    682                         && (mStartedOnLeft ? vel.x < 0 : vel.x > 0);
    683                 if (ENABLE_MINIMIZE &&
    684                         !mIsMinimized && (mMotionHelper.shouldMinimizePip() || isFlingToEdge)) {
    685                     // Pip should be minimized
    686                     setMinimizedStateInternal(true);
    687                     if (mMenuState == MENU_STATE_FULL) {
    688                         // If the user dragged the expanded PiP to the edge, then hiding the menu
    689                         // will trigger the PiP to be scaled back to the normal size with the
    690                         // minimize offset adjusted
    691                         mMenuController.hideMenu();
    692                     } else {
    693                         mMotionHelper.animateToClosestMinimizedState(mMovementBounds,
    694                                 mUpdateScrimListener);
    695                     }
    696                     return true;
    697                 }
    698                 if (mIsMinimized) {
    699                     // If we're dragging and it wasn't a minimize gesture then we shouldn't be
    700                     // minimized.
    701                     setMinimizedStateInternal(false);
    702                 }
    703 
    704                 AnimatorListenerAdapter postAnimationCallback = null;
    705                 if (mMenuState != MENU_STATE_NONE) {
    706                     // If the menu is still visible, and we aren't minimized, then just poke the
    707                     // menu so that it will timeout after the user stops touching it
    708                     mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(),
    709                             mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
    710                 } else {
    711                     // If the menu is not visible, then we can still be showing the activity for the
    712                     // dismiss overlay, so just finish it after the animation completes
    713                     postAnimationCallback = new AnimatorListenerAdapter() {
    714                         @Override
    715                         public void onAnimationEnd(Animator animation) {
    716                             mMenuController.hideMenu();
    717                         }
    718                     };
    719                 }
    720 
    721                 if (isFling) {
    722                     mMotionHelper.flingToSnapTarget(velocity, vel.x, vel.y, mMovementBounds,
    723                             mUpdateScrimListener, postAnimationCallback,
    724                             mStartPosition);
    725                 } else {
    726                     mMotionHelper.animateToClosestSnapTarget(mMovementBounds, mUpdateScrimListener,
    727                             postAnimationCallback);
    728                 }
    729             } else if (mIsMinimized) {
    730                 // This was a tap, so no longer minimized
    731                 mMotionHelper.animateToClosestSnapTarget(mMovementBounds, null /* updateListener */,
    732                         null /* animatorListener */);
    733                 setMinimizedStateInternal(false);
    734             } else if (mMenuState != MENU_STATE_FULL) {
    735                 if (mTouchState.isDoubleTap()) {
    736                     // Expand to fullscreen if this is a double tap
    737                     mMotionHelper.expandPip();
    738                 } else if (!mTouchState.isWaitingForDoubleTap()) {
    739                     // User has stalled long enough for this not to be a drag or a double tap, just
    740                     // expand the menu
    741                     mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
    742                             mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
    743                 } else {
    744                     // Next touch event _may_ be the second tap for the double-tap, schedule a
    745                     // fallback runnable to trigger the menu if no touch event occurs before the
    746                     // next tap
    747                     mTouchState.scheduleDoubleTapTimeoutCallback();
    748                 }
    749             } else {
    750                 mMenuController.hideMenu();
    751                 mMotionHelper.expandPip();
    752             }
    753             return true;
    754         }
    755     };
    756 
    757     /**
    758      * Updates the current movement bounds based on whether the menu is currently visible.
    759      */
    760     private void updateMovementBounds(int menuState) {
    761         boolean isMenuExpanded = menuState == MENU_STATE_FULL;
    762         mMovementBounds = isMenuExpanded
    763                 ? mExpandedMovementBounds
    764                 : mNormalMovementBounds;
    765         try {
    766             mPinnedStackController.setMinEdgeSize(isMenuExpanded ? mExpandedShortestEdgeSize : 0);
    767         } catch (RemoteException e) {
    768             Log.e(TAG, "Could not set minimized state", e);
    769         }
    770     }
    771 
    772     /**
    773      * Removes the dismiss target and cancels any pending callbacks to show it.
    774      */
    775     private void cleanUpDismissTarget() {
    776         mHandler.removeCallbacks(mShowDismissAffordance);
    777         mDismissViewController.destroyDismissTarget();
    778     }
    779 
    780     /**
    781      * Resets some states related to the touch handling.
    782      */
    783     private void cleanUp() {
    784         if (mIsMinimized) {
    785             setMinimizedStateInternal(false);
    786         }
    787         cleanUpDismissTarget();
    788     }
    789 
    790     /**
    791      * @return whether the menu will resize as a part of showing the full menu.
    792      */
    793     private boolean willResizeMenu() {
    794         return mExpandedBounds.width() != mNormalBounds.width() ||
    795                 mExpandedBounds.height() != mNormalBounds.height();
    796     }
    797 
    798     public void dump(PrintWriter pw, String prefix) {
    799         final String innerPrefix = prefix + "  ";
    800         pw.println(prefix + TAG);
    801         pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds);
    802         pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds);
    803         pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds);
    804         pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds);
    805         pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds);
    806         pw.println(innerPrefix + "mMenuState=" + mMenuState);
    807         pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
    808         pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
    809         pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
    810         pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction);
    811         pw.println(innerPrefix + "mEnableDragToEdgeDismiss=" + ENABLE_DISMISS_DRAG_TO_EDGE);
    812         pw.println(innerPrefix + "mEnableMinimize=" + ENABLE_MINIMIZE);
    813         mSnapAlgorithm.dump(pw, innerPrefix);
    814         mTouchState.dump(pw, innerPrefix);
    815         mMotionHelper.dump(pw, innerPrefix);
    816     }
    817 
    818 }
    819