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         boolean shouldDeliverToMenu = mMenuState != MENU_STATE_NONE;
367 
368         switch (ev.getAction()) {
369             case MotionEvent.ACTION_DOWN: {
370                 mMotionHelper.synchronizePinnedStackBounds();
371 
372                 for (PipTouchGesture gesture : mGestures) {
373                     gesture.onDown(mTouchState);
374                 }
375                 break;
376             }
377             case MotionEvent.ACTION_MOVE: {
378                 for (PipTouchGesture gesture : mGestures) {
379                     if (gesture.onMove(mTouchState)) {
380                         break;
381                     }
382                 }
383 
384                 shouldDeliverToMenu = !mTouchState.isDragging();
385                 break;
386             }
387             case MotionEvent.ACTION_UP: {
388                 // Update the movement bounds again if the state has changed since the user started
389                 // dragging (ie. when the IME shows)
390                 updateMovementBounds(mMenuState);
391 
392                 for (PipTouchGesture gesture : mGestures) {
393                     if (gesture.onUp(mTouchState)) {
394                         break;
395                     }
396                 }
397 
398                 // Fall through to clean up
399             }
400             case MotionEvent.ACTION_CANCEL: {
401                 shouldDeliverToMenu = !mTouchState.startedDragging() && !mTouchState.isDragging();
402                 mTouchState.reset();
403                 break;
404             }
405             case MotionEvent.ACTION_HOVER_ENTER:
406             case MotionEvent.ACTION_HOVER_MOVE: {
407                 if (mAccessibilityManager.isEnabled() && !mSendingHoverAccessibilityEvents) {
408                     AccessibilityEvent event = AccessibilityEvent.obtain(
409                             AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
410                     event.setImportantForAccessibility(true);
411                     event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
412                     event.setWindowId(
413                             AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
414                     mAccessibilityManager.sendAccessibilityEvent(event);
415                     mSendingHoverAccessibilityEvents = true;
416                 }
417                 break;
418             }
419             case MotionEvent.ACTION_HOVER_EXIT: {
420                 if (mAccessibilityManager.isEnabled() && mSendingHoverAccessibilityEvents) {
421                     AccessibilityEvent event = AccessibilityEvent.obtain(
422                             AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
423                     event.setImportantForAccessibility(true);
424                     event.setSourceNodeId(AccessibilityNodeInfo.ROOT_NODE_ID);
425                     event.setWindowId(
426                             AccessibilityWindowInfo.PICTURE_IN_PICTURE_ACTION_REPLACER_WINDOW_ID);
427                     mAccessibilityManager.sendAccessibilityEvent(event);
428                     mSendingHoverAccessibilityEvents = false;
429                 }
430                 break;
431             }
432         }
433 
434         // Deliver the event to PipMenuActivity to handle button click if the menu has shown.
435         if (shouldDeliverToMenu) {
436             final MotionEvent cloneEvent = MotionEvent.obtain(ev);
437             // Send the cancel event and cancel menu timeout if it starts to drag.
438             if (mTouchState.startedDragging()) {
439                 cloneEvent.setAction(MotionEvent.ACTION_CANCEL);
440                 mMenuController.pokeMenu();
441             }
442 
443             mMenuController.handleTouchEvent(cloneEvent);
444         }
445 
446         return true;
447     }
448 
449     /**
450      * Updates the appearance of the menu and scrim on top of the PiP while dismissing.
451      */
452     private void updateDismissFraction() {
453         // Skip updating the dismiss fraction when the IME is showing. This is to work around an
454         // issue where starting the menu activity for the dismiss overlay will steal the window
455         // focus, which closes the IME.
456         if (mMenuController != null && !mIsImeShowing) {
457             Rect bounds = mMotionHelper.getBounds();
458             final float target = mInsetBounds.bottom;
459             float fraction = 0f;
460             if (bounds.bottom > target) {
461                 final float distance = bounds.bottom - target;
462                 fraction = Math.min(distance / bounds.height(), 1f);
463             }
464             if (Float.compare(fraction, 0f) != 0 || mMenuController.isMenuActivityVisible()) {
465                 // Update if the fraction > 0, or if fraction == 0 and the menu was already visible
466                 mMenuController.setDismissFraction(fraction);
467             }
468         }
469     }
470 
471     /**
472      * Sets the controller to update the system of changes from user interaction.
473      */
474     void setPinnedStackController(IPinnedStackController controller) {
475         mPinnedStackController = controller;
476     }
477 
478     /**
479      * Sets the minimized state.
480      */
481     private void setMinimizedStateInternal(boolean isMinimized) {
482         if (!ENABLE_MINIMIZE) {
483             return;
484         }
485         setMinimizedState(isMinimized, false /* fromController */);
486     }
487 
488     /**
489      * Sets the minimized state.
490      */
491     void setMinimizedState(boolean isMinimized, boolean fromController) {
492         if (!ENABLE_MINIMIZE) {
493             return;
494         }
495         if (mIsMinimized != isMinimized) {
496             MetricsLoggerWrapper.logPictureInPictureMinimize(mContext,
497                     isMinimized, PipUtils.getTopPinnedActivity(mContext, mActivityManager));
498         }
499         mIsMinimized = isMinimized;
500         mSnapAlgorithm.setMinimized(isMinimized);
501 
502         if (fromController) {
503             if (isMinimized) {
504                 // Move the PiP to the new bounds immediately if minimized
505                 mMotionHelper.movePip(mMotionHelper.getClosestMinimizedBounds(mNormalBounds,
506                         mMovementBounds));
507             }
508         } else if (mPinnedStackController != null) {
509             try {
510                 mPinnedStackController.setIsMinimized(isMinimized);
511             } catch (RemoteException e) {
512                 Log.e(TAG, "Could not set minimized state", e);
513             }
514         }
515     }
516 
517     /**
518      * Sets the menu visibility.
519      */
520     private void setMenuState(int menuState, boolean resize) {
521         if (menuState == MENU_STATE_FULL && mMenuState != MENU_STATE_FULL) {
522             // Save the current snap fraction and if we do not drag or move the PiP, then
523             // we store back to this snap fraction.  Otherwise, we'll reset the snap
524             // fraction and snap to the closest edge
525             Rect expandedBounds = new Rect(mExpandedBounds);
526             if (resize) {
527                 mSavedSnapFraction = mMotionHelper.animateToExpandedState(expandedBounds,
528                         mMovementBounds, mExpandedMovementBounds);
529             }
530         } else if (menuState == MENU_STATE_NONE && mMenuState == MENU_STATE_FULL) {
531             // Try and restore the PiP to the closest edge, using the saved snap fraction
532             // if possible
533             if (resize) {
534                 if (mDeferResizeToNormalBoundsUntilRotation == -1) {
535                     // This is a very special case: when the menu is expanded and visible,
536                     // navigating to another activity can trigger auto-enter PiP, and if the
537                     // revealed activity has a forced rotation set, then the controller will get
538                     // updated with the new rotation of the display. However, at the same time,
539                     // SystemUI will try to hide the menu by creating an animation to the normal
540                     // bounds which are now stale.  In such a case we defer the animation to the
541                     // normal bounds until after the next onMovementBoundsChanged() call to get the
542                     // bounds in the new orientation
543                     try {
544                         int displayRotation = mPinnedStackController.getDisplayRotation();
545                         if (mDisplayRotation != displayRotation) {
546                             mDeferResizeToNormalBoundsUntilRotation = displayRotation;
547                         }
548                     } catch (RemoteException e) {
549                         Log.e(TAG, "Could not get display rotation from controller");
550                     }
551                 }
552 
553                 if (mDeferResizeToNormalBoundsUntilRotation == -1) {
554                     Rect normalBounds = new Rect(mNormalBounds);
555                     mMotionHelper.animateToUnexpandedState(normalBounds, mSavedSnapFraction,
556                             mNormalMovementBounds, mMovementBounds, mIsMinimized,
557                             false /* immediate */);
558                     mSavedSnapFraction = -1f;
559                 }
560             } else {
561                 // If resizing is not allowed, then the PiP should be frozen until the transition
562                 // ends as well
563                 setTouchEnabled(false);
564                 mSavedSnapFraction = -1f;
565             }
566         }
567         mMenuState = menuState;
568         updateMovementBounds(menuState);
569         if (menuState != MENU_STATE_CLOSE) {
570             MetricsLoggerWrapper.logPictureInPictureMenuVisible(mContext, menuState == MENU_STATE_FULL);
571         }
572     }
573 
574     /**
575      * @return the motion helper.
576      */
577     public PipMotionHelper getMotionHelper() {
578         return mMotionHelper;
579     }
580 
581     /**
582      * Gesture controlling normal movement of the PIP.
583      */
584     private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() {
585         // Whether the PiP was on the left side of the screen at the start of the gesture
586         private boolean mStartedOnLeft;
587         private final Point mStartPosition = new Point();
588         private final PointF mDelta = new PointF();
589 
590         @Override
591         public void onDown(PipTouchState touchState) {
592             if (!touchState.isUserInteracting()) {
593                 return;
594             }
595 
596             Rect bounds = mMotionHelper.getBounds();
597             mDelta.set(0f, 0f);
598             mStartPosition.set(bounds.left, bounds.top);
599             mStartedOnLeft = bounds.left < mMovementBounds.centerX();
600             mMovementWithinMinimize = true;
601             mMovementWithinDismiss = touchState.getDownTouchPosition().y >= mMovementBounds.bottom;
602 
603             // If the menu is still visible, and we aren't minimized, then just poke the menu
604             // so that it will timeout after the user stops touching it
605             if (mMenuState != MENU_STATE_NONE && !mIsMinimized) {
606                 mMenuController.pokeMenu();
607             }
608 
609             if (mEnableDimissDragToEdge) {
610                 mDismissViewController.createDismissTarget();
611                 mHandler.postDelayed(mShowDismissAffordance, SHOW_DISMISS_AFFORDANCE_DELAY);
612             }
613         }
614 
615         @Override
616         boolean onMove(PipTouchState touchState) {
617             if (!touchState.isUserInteracting()) {
618                 return false;
619             }
620 
621             if (touchState.startedDragging()) {
622                 mSavedSnapFraction = -1f;
623 
624                 if (mEnableDimissDragToEdge) {
625                     mHandler.removeCallbacks(mShowDismissAffordance);
626                     mDismissViewController.showDismissTarget();
627                 }
628             }
629 
630             if (touchState.isDragging()) {
631                 // Move the pinned stack freely
632                 final PointF lastDelta = touchState.getLastTouchDelta();
633                 float lastX = mStartPosition.x + mDelta.x;
634                 float lastY = mStartPosition.y + mDelta.y;
635                 float left = lastX + lastDelta.x;
636                 float top = lastY + lastDelta.y;
637                 if (!touchState.allowDraggingOffscreen() || !ENABLE_MINIMIZE) {
638                     left = Math.max(mMovementBounds.left, Math.min(mMovementBounds.right, left));
639                 }
640                 if (mEnableDimissDragToEdge) {
641                     // Allow pip to move past bottom bounds
642                     top = Math.max(mMovementBounds.top, top);
643                 } else {
644                     top = Math.max(mMovementBounds.top, Math.min(mMovementBounds.bottom, top));
645                 }
646 
647                 // Add to the cumulative delta after bounding the position
648                 mDelta.x += left - lastX;
649                 mDelta.y += top - lastY;
650 
651                 mTmpBounds.set(mMotionHelper.getBounds());
652                 mTmpBounds.offsetTo((int) left, (int) top);
653                 mMotionHelper.movePip(mTmpBounds);
654 
655                 if (mEnableDimissDragToEdge) {
656                     updateDismissFraction();
657                 }
658 
659                 final PointF curPos = touchState.getLastTouchPosition();
660                 if (mMovementWithinMinimize) {
661                     // Track if movement remains near starting edge to identify swipes to minimize
662                     mMovementWithinMinimize = mStartedOnLeft
663                             ? curPos.x <= mMovementBounds.left + mTmpBounds.width()
664                             : curPos.x >= mMovementBounds.right;
665                 }
666                 if (mMovementWithinDismiss) {
667                     // Track if movement remains near the bottom edge to identify swipe to dismiss
668                     mMovementWithinDismiss = curPos.y >= mMovementBounds.bottom;
669                 }
670                 return true;
671             }
672             return false;
673         }
674 
675         @Override
676         public boolean onUp(PipTouchState touchState) {
677             if (mEnableDimissDragToEdge) {
678                 // Clean up the dismiss target regardless of the touch state in case the touch
679                 // enabled state changes while the user is interacting
680                 cleanUpDismissTarget();
681             }
682 
683             if (!touchState.isUserInteracting()) {
684                 return false;
685             }
686 
687             final PointF vel = touchState.getVelocity();
688             final boolean isHorizontal = Math.abs(vel.x) > Math.abs(vel.y);
689             final float velocity = PointF.length(vel.x, vel.y);
690             final boolean isFling = velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond();
691             final boolean isUpWithinDimiss = ENABLE_FLING_DISMISS
692                     && touchState.getLastTouchPosition().y >= mMovementBounds.bottom
693                     && mMotionHelper.isGestureToDismissArea(mMotionHelper.getBounds(), vel.x,
694                             vel.y, isFling);
695             final boolean isFlingToBot = isFling && vel.y > 0 && !isHorizontal
696                     && (mMovementWithinDismiss || isUpWithinDimiss);
697             if (mEnableDimissDragToEdge) {
698                 // Check if the user dragged or flung the PiP offscreen to dismiss it
699                 if (mMotionHelper.shouldDismissPip() || isFlingToBot) {
700                     MetricsLoggerWrapper.logPictureInPictureDismissByDrag(mContext,
701                             PipUtils.getTopPinnedActivity(mContext, mActivityManager));
702                     mMotionHelper.animateDismiss(mMotionHelper.getBounds(), vel.x,
703                         vel.y, mUpdateScrimListener);
704                     return true;
705                 }
706             }
707 
708             if (touchState.isDragging()) {
709                 final boolean isFlingToEdge = isFling && isHorizontal && mMovementWithinMinimize
710                         && (mStartedOnLeft ? vel.x < 0 : vel.x > 0);
711                 if (ENABLE_MINIMIZE &&
712                         !mIsMinimized && (mMotionHelper.shouldMinimizePip() || isFlingToEdge)) {
713                     // Pip should be minimized
714                     setMinimizedStateInternal(true);
715                     if (mMenuState == MENU_STATE_FULL) {
716                         // If the user dragged the expanded PiP to the edge, then hiding the menu
717                         // will trigger the PiP to be scaled back to the normal size with the
718                         // minimize offset adjusted
719                         mMenuController.hideMenu();
720                     } else {
721                         mMotionHelper.animateToClosestMinimizedState(mMovementBounds,
722                                 mUpdateScrimListener);
723                     }
724                     return true;
725                 }
726                 if (mIsMinimized) {
727                     // If we're dragging and it wasn't a minimize gesture then we shouldn't be
728                     // minimized.
729                     setMinimizedStateInternal(false);
730                 }
731 
732                 AnimatorListenerAdapter postAnimationCallback = null;
733                 if (mMenuState != MENU_STATE_NONE) {
734                     // If the menu is still visible, and we aren't minimized, then just poke the
735                     // menu so that it will timeout after the user stops touching it
736                     mMenuController.showMenu(mMenuState, mMotionHelper.getBounds(),
737                             mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
738                 } else {
739                     // If the menu is not visible, then we can still be showing the activity for the
740                     // dismiss overlay, so just finish it after the animation completes
741                     postAnimationCallback = new AnimatorListenerAdapter() {
742                         @Override
743                         public void onAnimationEnd(Animator animation) {
744                             mMenuController.hideMenu();
745                         }
746                     };
747                 }
748 
749                 if (isFling) {
750                     mMotionHelper.flingToSnapTarget(velocity, vel.x, vel.y, mMovementBounds,
751                             mUpdateScrimListener, postAnimationCallback,
752                             mStartPosition);
753                 } else {
754                     mMotionHelper.animateToClosestSnapTarget(mMovementBounds, mUpdateScrimListener,
755                             postAnimationCallback);
756                 }
757             } else if (mIsMinimized) {
758                 // This was a tap, so no longer minimized
759                 mMotionHelper.animateToClosestSnapTarget(mMovementBounds, null /* updateListener */,
760                         null /* animatorListener */);
761                 setMinimizedStateInternal(false);
762             } else if (mTouchState.isDoubleTap()) {
763                 // Expand to fullscreen if this is a double tap
764                 mMotionHelper.expandPip();
765             } else if (mMenuState != MENU_STATE_FULL) {
766                 if (!mTouchState.isWaitingForDoubleTap()) {
767                     // User has stalled long enough for this not to be a drag or a double tap, just
768                     // expand the menu
769                     mMenuController.showMenu(MENU_STATE_FULL, mMotionHelper.getBounds(),
770                             mMovementBounds, true /* allowMenuTimeout */, willResizeMenu());
771                 } else {
772                     // Next touch event _may_ be the second tap for the double-tap, schedule a
773                     // fallback runnable to trigger the menu if no touch event occurs before the
774                     // next tap
775                     mTouchState.scheduleDoubleTapTimeoutCallback();
776                 }
777             }
778             return true;
779         }
780     };
781 
782     /**
783      * Updates the current movement bounds based on whether the menu is currently visible.
784      */
updateMovementBounds(int menuState)785     private void updateMovementBounds(int menuState) {
786         boolean isMenuExpanded = menuState == MENU_STATE_FULL;
787         mMovementBounds = isMenuExpanded
788                 ? mExpandedMovementBounds
789                 : mNormalMovementBounds;
790         try {
791             if (mPinnedStackController != null) {
792                 mPinnedStackController.setMinEdgeSize(
793                         isMenuExpanded ? mExpandedShortestEdgeSize : 0);
794             }
795         } catch (RemoteException e) {
796             Log.e(TAG, "Could not set minimized state", e);
797         }
798     }
799 
800     /**
801      * Removes the dismiss target and cancels any pending callbacks to show it.
802      */
cleanUpDismissTarget()803     private void cleanUpDismissTarget() {
804         mHandler.removeCallbacks(mShowDismissAffordance);
805         mDismissViewController.destroyDismissTarget();
806     }
807 
808     /**
809      * Resets some states related to the touch handling.
810      */
cleanUp()811     private void cleanUp() {
812         if (mIsMinimized) {
813             setMinimizedStateInternal(false);
814         }
815         cleanUpDismissTarget();
816     }
817 
818     /**
819      * @return whether the menu will resize as a part of showing the full menu.
820      */
willResizeMenu()821     private boolean willResizeMenu() {
822         return mExpandedBounds.width() != mNormalBounds.width() ||
823                 mExpandedBounds.height() != mNormalBounds.height();
824     }
825 
dump(PrintWriter pw, String prefix)826     public void dump(PrintWriter pw, String prefix) {
827         final String innerPrefix = prefix + "  ";
828         pw.println(prefix + TAG);
829         pw.println(innerPrefix + "mMovementBounds=" + mMovementBounds);
830         pw.println(innerPrefix + "mNormalBounds=" + mNormalBounds);
831         pw.println(innerPrefix + "mNormalMovementBounds=" + mNormalMovementBounds);
832         pw.println(innerPrefix + "mExpandedBounds=" + mExpandedBounds);
833         pw.println(innerPrefix + "mExpandedMovementBounds=" + mExpandedMovementBounds);
834         pw.println(innerPrefix + "mMenuState=" + mMenuState);
835         pw.println(innerPrefix + "mIsMinimized=" + mIsMinimized);
836         pw.println(innerPrefix + "mIsImeShowing=" + mIsImeShowing);
837         pw.println(innerPrefix + "mImeHeight=" + mImeHeight);
838         pw.println(innerPrefix + "mIsShelfShowing=" + mIsShelfShowing);
839         pw.println(innerPrefix + "mShelfHeight=" + mShelfHeight);
840         pw.println(innerPrefix + "mSavedSnapFraction=" + mSavedSnapFraction);
841         pw.println(innerPrefix + "mEnableDragToEdgeDismiss=" + mEnableDimissDragToEdge);
842         pw.println(innerPrefix + "mEnableMinimize=" + ENABLE_MINIMIZE);
843         mSnapAlgorithm.dump(pw, innerPrefix);
844         mTouchState.dump(pw, innerPrefix);
845         mMotionHelper.dump(pw, innerPrefix);
846     }
847 
848 }
849