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