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