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