PipTouchHandler.java revision 114aeea51677345daca89c392adaef84d4499bd9
1c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk/*
2f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz * Copyright (C) 2016 The Android Open Source Project
3c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk *
4c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * Licensed under the Apache License, Version 2.0 (the "License");
5c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * you may not use this file except in compliance with the License.
6c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * You may obtain a copy of the License at
7c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk *
8f7a9eea8fe577f2f5edbbe6e73891a54351286c6Benjamin Franz *      http://www.apache.org/licenses/LICENSE-2.0
9c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk *
10c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * Unless required by applicable law or agreed to in writing, software
11c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * distributed under the License is distributed on an "AS IS" BASIS,
12c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * See the License for the specific language governing permissions and
14c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * limitations under the License.
15c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk */
16c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
17f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzpackage com.android.systemui.pip.phone;
18c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
19808bdc569c7da3a587696d228c645db7bd82b2c5Lorenzo Colittiimport static android.app.ActivityManager.StackId.PINNED_STACK_ID;
20f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport static android.view.WindowManager.INPUT_CONSUMER_PIP;
21c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
22c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport static com.android.systemui.Interpolators.FAST_OUT_LINEAR_IN;
23c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport static com.android.systemui.Interpolators.FAST_OUT_SLOW_IN;
24f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport static com.android.systemui.Interpolators.LINEAR_OUT_SLOW_IN;
25f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz
268e1c5db7598f647c9a4c4d0b46776ac142679538Rubin Xuimport android.animation.Animator;
27c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.animation.AnimatorListenerAdapter;
28f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.animation.ValueAnimator;
29f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.animation.ValueAnimator.AnimatorUpdateListener;
30c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.app.ActivityManager.StackInfo;
31f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.app.IActivityManager;
32c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.content.Context;
33f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.graphics.Point;
34f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.graphics.PointF;
35f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.graphics.Rect;
36f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.os.Looper;
37f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.os.RemoteException;
38f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.util.Log;
39c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.view.IPinnedStackController;
40c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.view.IWindowManager;
41f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.view.InputChannel;
42f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.view.InputEvent;
43c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.view.InputEventReceiver;
44f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport android.view.MotionEvent;
45c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport android.view.ViewConfiguration;
46f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz
47c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport com.android.internal.os.BackgroundThread;
48f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport com.android.internal.policy.PipMotionHelper;
496d90275bb14ddd11841be54b42c603e10cf93a7fJulia Reynoldsimport com.android.internal.policy.PipSnapAlgorithm;
50f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzimport com.android.systemui.statusbar.FlingAnimationUtils;
51c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monkimport com.android.systemui.tuner.TunerService;
52c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
53f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz/**
54c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * Manages all the touch handling for PIP on the Phone, including moving, dismissing and expanding
55c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk * the PIP.
56f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz */
57f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franzpublic class PipTouchHandler implements TunerService.Tunable {
58f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    private static final String TAG = "PipTouchHandler";
59f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    private static final boolean DEBUG_ALLOW_OUT_OF_BOUNDS_STACK = false;
60f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz
61c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private static final String TUNER_KEY_DRAG_TO_DISMISS = "pip_drag_to_dismiss";
62c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private static final String TUNER_KEY_ALLOW_MINIMIZE = "pip_allow_minimize";
63c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
64f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    private static final int SNAP_STACK_DURATION = 225;
65f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    private static final int DISMISS_STACK_DURATION = 375;
66f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    private static final int EXPAND_STACK_DURATION = 225;
67f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    private static final int MINIMIZE_STACK_MAX_DURATION = 200;
68f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz
69f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    // The fraction of the stack width that the user has to drag offscreen to minimize the PIP
70f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    private static final float MINIMIZE_OFFSCREEN_FRACTION = 0.15f;
71c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
72c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final Context mContext;
73f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    private final IActivityManager mActivityManager;
74c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final IWindowManager mWindowManager;
75c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final ViewConfiguration mViewConfig;
76c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final PipMenuListener mMenuListener = new PipMenuListener();
77c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private IPinnedStackController mPinnedStackController;
78c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
79c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private PipInputEventReceiver mInputEventReceiver;
80c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private PipMenuActivityController mMenuController;
81c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private PipDismissViewController mDismissViewController;
82c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final PipSnapAlgorithm mSnapAlgorithm;
83c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private PipMotionHelper mMotionHelper;
84c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
85c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    // Allow dragging the PIP to a location to close it
86c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private boolean mEnableDragToDismiss = false;
87f39eff106c8090d4e7b4a88b6b27d568257f900fBenjamin Franz    // Allow the PIP to be "docked" slightly offscreen
88c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private boolean mEnableMinimizing = true;
89c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
90c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final Rect mStableInsets = new Rect();
91c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final Rect mPinnedStackBounds = new Rect();
92c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final Rect mBoundedPinnedStackBounds = new Rect();
93c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private ValueAnimator mPinnedStackBoundsAnimator = null;
94c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private ValueAnimator.AnimatorUpdateListener mUpdatePinnedStackBoundsListener =
95c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk            new AnimatorUpdateListener() {
96c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk        @Override
97c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk        public void onAnimationUpdate(ValueAnimator animation) {
98c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk            mPinnedStackBounds.set((Rect) animation.getAnimatedValue());
99c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk        }
100c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    };
101c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk
102c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    // Behaviour states
103c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private boolean mIsTappingThrough;
104c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private boolean mIsMinimized;
1052daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk
1062daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk    // Touch state
107c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final PipTouchState mTouchState;
108c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final FlingAnimationUtils mFlingAnimationUtils;
1092daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk    private final PipTouchGesture[] mGestures;
1102daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk
1112daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk    // Temporary vars
1122daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk    private final Rect mTmpBounds = new Rect();
1132daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk
1142daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk    /**
1152daab0a2c2bcb07a0595f93c4367ed1ca673e0e6Jason Monk     * Input handler used for Pip windows.
116c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk     */
117c6d71b86baf721d5de568248d1f538cc7d4adcd0Jason Monk    private final class PipInputEventReceiver extends InputEventReceiver {
118
119        public PipInputEventReceiver(InputChannel inputChannel, Looper looper) {
120            super(inputChannel, looper);
121        }
122
123        @Override
124        public void onInputEvent(InputEvent event) {
125            boolean handled = true;
126            try {
127                // To be implemented for input handling over Pip windows
128                if (event instanceof MotionEvent) {
129                    MotionEvent ev = (MotionEvent) event;
130                    handled = handleTouchEvent(ev);
131                }
132            } finally {
133                finishInputEvent(event, handled);
134            }
135        }
136    }
137
138    /**
139     * A listener for the PIP menu activity.
140     */
141    private class PipMenuListener implements PipMenuActivityController.Listener {
142        @Override
143        public void onPipMenuVisibilityChanged(boolean visible) {
144            if (!visible) {
145                mIsTappingThrough = false;
146                registerInputConsumer();
147            } else {
148                unregisterInputConsumer();
149            }
150        }
151
152        @Override
153        public void onPipExpand() {
154            if (!mIsMinimized) {
155                expandPinnedStackToFullscreen();
156            }
157        }
158
159        @Override
160        public void onPipMinimize() {
161            setMinimizedState(true);
162            animateToClosestMinimizedTarget();
163        }
164
165        @Override
166        public void onPipDismiss() {
167            BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack);
168        }
169    }
170
171    public PipTouchHandler(Context context, PipMenuActivityController menuController,
172            IActivityManager activityManager, IWindowManager windowManager) {
173
174        // Initialize the Pip input consumer
175        mContext = context;
176        mActivityManager = activityManager;
177        mWindowManager = windowManager;
178        mViewConfig = ViewConfiguration.get(context);
179        mMenuController = menuController;
180        mMenuController.addListener(mMenuListener);
181        mDismissViewController = new PipDismissViewController(context);
182        mSnapAlgorithm = new PipSnapAlgorithm(mContext);
183        mTouchState = new PipTouchState(mViewConfig);
184        mFlingAnimationUtils = new FlingAnimationUtils(context, 2f);
185        mGestures = new PipTouchGesture[] {
186                mDragToDismissGesture, mTapThroughGesture, mMinimizeGesture, mDefaultMovementGesture
187        };
188        mMotionHelper = new PipMotionHelper(BackgroundThread.getHandler());
189        registerInputConsumer();
190        setSnapToEdge(true);
191
192        // Register any tuner settings changes
193        TunerService.get(context).addTunable(this, TUNER_KEY_DRAG_TO_DISMISS,
194                TUNER_KEY_ALLOW_MINIMIZE);
195    }
196
197    @Override
198    public void onTuningChanged(String key, String newValue) {
199        if (newValue == null) {
200            // Reset back to default
201            mEnableDragToDismiss = false;
202            mEnableMinimizing = true;
203            setMinimizedState(false);
204            return;
205        }
206        switch (key) {
207            case TUNER_KEY_DRAG_TO_DISMISS:
208                mEnableDragToDismiss = Integer.parseInt(newValue) != 0;
209                break;
210            case TUNER_KEY_ALLOW_MINIMIZE:
211                mEnableMinimizing = Integer.parseInt(newValue) != 0;
212                break;
213        }
214    }
215
216    public void onActivityPinned() {
217        // Reset some states once we are pinned
218        if (mIsTappingThrough) {
219            mIsTappingThrough = false;
220            registerInputConsumer();
221        }
222        if (mIsMinimized) {
223            setMinimizedState(false);
224        }
225    }
226
227    public void onConfigurationChanged() {
228        mSnapAlgorithm.onConfigurationChanged();
229        updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */);
230    }
231
232    public void onMinimizedStateChanged(boolean isMinimized) {
233        mIsMinimized = isMinimized;
234    }
235
236    public void onSnapToEdgeStateChanged(boolean isSnapToEdge) {
237        mSnapAlgorithm.setSnapToEdge(isSnapToEdge);
238    }
239
240    private boolean handleTouchEvent(MotionEvent ev) {
241        // Skip touch handling until we are bound to the controller
242        if (mPinnedStackController == null) {
243            return true;
244        }
245
246        // Update the touch state
247        mTouchState.onTouchEvent(ev);
248
249        switch (ev.getAction()) {
250            case MotionEvent.ACTION_DOWN: {
251                // Cancel any existing animations on the pinned stack
252                if (mPinnedStackBoundsAnimator != null) {
253                    mPinnedStackBoundsAnimator.cancel();
254                }
255
256                updateBoundedPinnedStackBounds(true /* updatePinnedStackBounds */);
257                for (PipTouchGesture gesture : mGestures) {
258                    gesture.onDown(mTouchState);
259                }
260                try {
261                    mPinnedStackController.setInInteractiveMode(true);
262                } catch (RemoteException e) {
263                    Log.e(TAG, "Could not set dragging state", e);
264                }
265                break;
266            }
267            case MotionEvent.ACTION_MOVE: {
268                for (PipTouchGesture gesture : mGestures) {
269                    if (gesture.onMove(mTouchState)) {
270                        break;
271                    }
272                }
273                break;
274            }
275            case MotionEvent.ACTION_UP: {
276                // Update the movement bounds again if the state has changed since the user started
277                // dragging (ie. when the IME shows)
278                updateBoundedPinnedStackBounds(false /* updatePinnedStackBounds */);
279
280                for (PipTouchGesture gesture : mGestures) {
281                    if (gesture.onUp(mTouchState)) {
282                        break;
283                    }
284                }
285
286                // Fall through to clean up
287            }
288            case MotionEvent.ACTION_CANCEL: {
289                try {
290                    mPinnedStackController.setInInteractiveMode(false);
291                } catch (RemoteException e) {
292                    Log.e(TAG, "Could not set dragging state", e);
293                }
294                break;
295            }
296        }
297        return !mIsTappingThrough;
298    }
299
300    /**
301     * @return whether the current touch state is a horizontal drag offscreen.
302     */
303    private boolean isDraggingOffscreen(PipTouchState touchState) {
304        PointF lastDelta = touchState.getLastTouchDelta();
305        PointF downDelta = touchState.getDownTouchDelta();
306        float left = mPinnedStackBounds.left + lastDelta.x;
307        return !(mBoundedPinnedStackBounds.left <= left && left <= mBoundedPinnedStackBounds.right)
308                && Math.abs(downDelta.x) > Math.abs(downDelta.y);
309    }
310
311    /**
312     * Registers the input consumer.
313     */
314    private void registerInputConsumer() {
315        if (mInputEventReceiver == null) {
316            final InputChannel inputChannel = new InputChannel();
317            try {
318                mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP);
319                mWindowManager.createInputConsumer(INPUT_CONSUMER_PIP, inputChannel);
320            } catch (RemoteException e) {
321                Log.e(TAG, "Failed to create PIP input consumer", e);
322            }
323            mInputEventReceiver = new PipInputEventReceiver(inputChannel, Looper.myLooper());
324        }
325    }
326
327    /**
328     * Unregisters the input consumer.
329     */
330    private void unregisterInputConsumer() {
331        if (mInputEventReceiver != null) {
332            try {
333                mWindowManager.destroyInputConsumer(INPUT_CONSUMER_PIP);
334            } catch (RemoteException e) {
335                Log.e(TAG, "Failed to destroy PIP input consumer", e);
336            }
337            mInputEventReceiver.dispose();
338            mInputEventReceiver = null;
339        }
340    }
341
342    /**
343     * Sets the controller to update the system of changes from user interaction.
344     */
345    void setPinnedStackController(IPinnedStackController controller) {
346        mPinnedStackController = controller;
347    }
348
349    /**
350     * Sets the snap-to-edge state and notifies the controller.
351     */
352    private void setSnapToEdge(boolean snapToEdge) {
353        onSnapToEdgeStateChanged(snapToEdge);
354
355        if (mPinnedStackController != null) {
356            try {
357                mPinnedStackController.setSnapToEdge(snapToEdge);
358            } catch (RemoteException e) {
359                Log.e(TAG, "Could not set snap mode to edge", e);
360            }
361        }
362    }
363
364    /**
365     * Sets the minimized state and notifies the controller.
366     */
367    private void setMinimizedState(boolean isMinimized) {
368        onMinimizedStateChanged(isMinimized);
369
370        if (mPinnedStackController != null) {
371            try {
372                mPinnedStackController.setIsMinimized(isMinimized);
373            } catch (RemoteException e) {
374                Log.e(TAG, "Could not set minimized state", e);
375            }
376        }
377    }
378
379    /**
380     * @return whether the given {@param pinnedStackBounds} indicates the PIP should be minimized.
381     */
382    private boolean shouldMinimizedPinnedStack() {
383        Point displaySize = new Point();
384        mContext.getDisplay().getRealSize(displaySize);
385        if (mPinnedStackBounds.left < 0) {
386            float offscreenFraction = (float) -mPinnedStackBounds.left / mPinnedStackBounds.width();
387            return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION;
388        } else if (mPinnedStackBounds.right > displaySize.x) {
389            float offscreenFraction = (float) (mPinnedStackBounds.right - displaySize.x) /
390                    mPinnedStackBounds.width();
391            return offscreenFraction >= MINIMIZE_OFFSCREEN_FRACTION;
392        } else {
393            return false;
394        }
395    }
396
397    /**
398     * Flings the minimized PIP to the closest minimized snap target.
399     */
400    private void flingToMinimizedSnapTarget(float velocityY) {
401        // We currently only allow flinging the minimized stack up and down, so just lock the
402        // movement bounds to the current stack bounds horizontally
403        Rect movementBounds = new Rect(mPinnedStackBounds.left, mBoundedPinnedStackBounds.top,
404                mPinnedStackBounds.left, mBoundedPinnedStackBounds.bottom);
405        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(movementBounds, mPinnedStackBounds,
406                0 /* velocityX */, velocityY);
407        if (!mPinnedStackBounds.equals(toBounds)) {
408            mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
409                    toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener);
410            mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0,
411                    distanceBetweenRectOffsets(mPinnedStackBounds, toBounds),
412                    velocityY);
413            mPinnedStackBoundsAnimator.start();
414        }
415    }
416
417    /**
418     * Animates the PIP to the minimized state, slightly offscreen.
419     */
420    private void animateToClosestMinimizedTarget() {
421        Point displaySize = new Point();
422        mContext.getDisplay().getRealSize(displaySize);
423        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
424                mPinnedStackBounds);
425        mSnapAlgorithm.applyMinimizedOffset(toBounds, mBoundedPinnedStackBounds, displaySize,
426                mStableInsets);
427        mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
428                toBounds, MINIMIZE_STACK_MAX_DURATION, LINEAR_OUT_SLOW_IN,
429                mUpdatePinnedStackBoundsListener);
430        mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() {
431            @Override
432            public void onAnimationEnd(Animator animation) {
433                mMenuController.hideMenu();
434            }
435        });
436        mPinnedStackBoundsAnimator.start();
437    }
438
439    /**
440     * Flings the PIP to the closest snap target.
441     */
442    private void flingToSnapTarget(float velocity, float velocityX, float velocityY) {
443        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
444                mPinnedStackBounds, velocityX, velocityY);
445        if (!mPinnedStackBounds.equals(toBounds)) {
446            mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
447                toBounds, 0, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener);
448            mFlingAnimationUtils.apply(mPinnedStackBoundsAnimator, 0,
449                distanceBetweenRectOffsets(mPinnedStackBounds, toBounds),
450                velocity);
451            mPinnedStackBoundsAnimator.start();
452        }
453    }
454
455    /**
456     * Animates the PIP to the closest snap target.
457     */
458    private void animateToClosestSnapTarget() {
459        Rect toBounds = mSnapAlgorithm.findClosestSnapBounds(mBoundedPinnedStackBounds,
460                mPinnedStackBounds);
461        if (!mPinnedStackBounds.equals(toBounds)) {
462            mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
463                toBounds, SNAP_STACK_DURATION, FAST_OUT_SLOW_IN, mUpdatePinnedStackBoundsListener);
464            mPinnedStackBoundsAnimator.start();
465        }
466    }
467
468    /**
469     * Animates the dismissal of the PIP over the dismiss target bounds.
470     */
471    private void animateDismissPinnedStack(Rect dismissBounds) {
472        Rect toBounds = new Rect(dismissBounds.centerX(),
473            dismissBounds.centerY(),
474            dismissBounds.centerX() + 1,
475            dismissBounds.centerY() + 1);
476        mPinnedStackBoundsAnimator = mMotionHelper.createAnimationToBounds(mPinnedStackBounds,
477            toBounds, DISMISS_STACK_DURATION, FAST_OUT_LINEAR_IN, mUpdatePinnedStackBoundsListener);
478        mPinnedStackBoundsAnimator.addListener(new AnimatorListenerAdapter() {
479            @Override
480            public void onAnimationEnd(Animator animation) {
481                BackgroundThread.getHandler().post(PipTouchHandler.this::dismissPinnedStack);
482            }
483        });
484        mPinnedStackBoundsAnimator.start();
485    }
486
487    /**
488     * Resizes the pinned stack back to fullscreen.
489     */
490    private void expandPinnedStackToFullscreen() {
491        BackgroundThread.getHandler().post(() -> {
492            try {
493                mActivityManager.resizeStack(PINNED_STACK_ID, null /* bounds */,
494                        true /* allowResizeInDockedMode */, true /* preserveWindows */,
495                        true /* animate */, EXPAND_STACK_DURATION);
496            } catch (RemoteException e) {
497                Log.e(TAG, "Error showing PIP menu activity", e);
498            }
499        });
500    }
501
502    /**
503     * Tries to the move the pinned stack to the given {@param bounds}.
504     */
505    private void movePinnedStack(Rect bounds) {
506        if (!bounds.equals(mPinnedStackBounds)) {
507            mPinnedStackBounds.set(bounds);
508            mMotionHelper.resizeToBounds(mPinnedStackBounds);
509        }
510    }
511
512    /**
513     * Dismisses the pinned stack.
514     */
515    private void dismissPinnedStack() {
516        try {
517            mActivityManager.removeStack(PINNED_STACK_ID);
518        } catch (RemoteException e) {
519            Log.e(TAG, "Failed to remove PIP", e);
520        }
521    }
522
523    /**
524     * Updates the movement bounds of the pinned stack.
525     */
526    private void updateBoundedPinnedStackBounds(boolean updatePinnedStackBounds) {
527        try {
528            StackInfo info = mActivityManager.getStackInfo(PINNED_STACK_ID);
529            if (info != null) {
530                if (updatePinnedStackBounds) {
531                    mPinnedStackBounds.set(info.bounds);
532                }
533                mWindowManager.getStableInsets(info.displayId, mStableInsets);
534                mBoundedPinnedStackBounds.set(mWindowManager.getPictureInPictureMovementBounds(
535                        info.displayId));
536            }
537        } catch (RemoteException e) {
538            Log.e(TAG, "Could not fetch PIP movement bounds.", e);
539        }
540    }
541
542    /**
543     * @return the distance between points {@param p1} and {@param p2}.
544     */
545    private float distanceBetweenRectOffsets(Rect r1, Rect r2) {
546        return PointF.length(r1.left - r2.left, r1.top - r2.top);
547    }
548
549    /**
550     * Gesture controlling dragging over a target to dismiss the PIP.
551     */
552    private PipTouchGesture mDragToDismissGesture = new PipTouchGesture() {
553        @Override
554        public void onDown(PipTouchState touchState) {
555            if (mEnableDragToDismiss) {
556                // TODO: Consider setting a timer such at after X time, we show the dismiss
557                //       target if the user hasn't already dragged some distance
558                mDismissViewController.createDismissTarget();
559            }
560        }
561
562        @Override
563        boolean onMove(PipTouchState touchState) {
564            if (mEnableDragToDismiss && touchState.startedDragging()) {
565                mDismissViewController.showDismissTarget();
566            }
567            return false;
568        }
569
570        @Override
571        public boolean onUp(PipTouchState touchState) {
572            if (mEnableDragToDismiss) {
573                try {
574                    if (touchState.isDragging()) {
575                        Rect dismissBounds = mDismissViewController.getDismissBounds();
576                        PointF lastTouch = touchState.getLastTouchPosition();
577                        if (dismissBounds.contains((int) lastTouch.x, (int) lastTouch.y)) {
578                            animateDismissPinnedStack(dismissBounds);
579                            return true;
580                        }
581                    }
582                } finally {
583                    mDismissViewController.destroyDismissTarget();
584                }
585            }
586            return false;
587        }
588    };
589
590    /**** Gestures ****/
591
592    /**
593     * Gesture controlling dragging the PIP slightly offscreen to minimize it.
594     */
595    private PipTouchGesture mMinimizeGesture = new PipTouchGesture() {
596        @Override
597        boolean onMove(PipTouchState touchState) {
598            if (mEnableMinimizing) {
599                boolean isDraggingOffscreen = isDraggingOffscreen(touchState);
600                if (touchState.startedDragging() && isDraggingOffscreen) {
601                    // Reset the minimized state once we drag horizontally
602                    setMinimizedState(false);
603                }
604
605                if (touchState.allowDraggingOffscreen() && isDraggingOffscreen) {
606                    // Move the pinned stack, but ignore the vertical movement
607                    float left = mPinnedStackBounds.left + touchState.getLastTouchDelta().x;
608                    mTmpBounds.set(mPinnedStackBounds);
609                    mTmpBounds.offsetTo((int) left, mPinnedStackBounds.top);
610                    if (!mTmpBounds.equals(mPinnedStackBounds)) {
611                        mPinnedStackBounds.set(mTmpBounds);
612                        mMotionHelper.resizeToBounds(mPinnedStackBounds);
613                    }
614                    return true;
615                } else if (mIsMinimized && touchState.isDragging()) {
616                    // Move the pinned stack, but ignore the horizontal movement
617                    PointF lastDelta = touchState.getLastTouchDelta();
618                    float top = mPinnedStackBounds.top + lastDelta.y;
619                    top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
620                            mBoundedPinnedStackBounds.bottom, top));
621                    mTmpBounds.set(mPinnedStackBounds);
622                    mTmpBounds.offsetTo(mPinnedStackBounds.left, (int) top);
623                    movePinnedStack(mTmpBounds);
624                    return true;
625                }
626            }
627            return false;
628        }
629
630        @Override
631        public boolean onUp(PipTouchState touchState) {
632            if (mEnableMinimizing) {
633                if (touchState.isDragging()) {
634                    if (isDraggingOffscreen(touchState)) {
635                        if (shouldMinimizedPinnedStack()) {
636                            setMinimizedState(true);
637                            animateToClosestMinimizedTarget();
638                            return true;
639                        }
640                    } else if (mIsMinimized) {
641                        PointF vel = touchState.getVelocity();
642                        if (vel.length() > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
643                            flingToMinimizedSnapTarget(vel.y);
644                        } else {
645                            animateToClosestMinimizedTarget();
646                        }
647                        return true;
648                    }
649                } else if (mIsMinimized) {
650                    setMinimizedState(false);
651                    animateToClosestSnapTarget();
652                    return true;
653                }
654            }
655            return false;
656        }
657    };
658
659    /**
660     * Gesture controlling tapping on the PIP to show an overlay.
661     */
662    private PipTouchGesture mTapThroughGesture = new PipTouchGesture() {
663        @Override
664        boolean onMove(PipTouchState touchState) {
665            return false;
666        }
667
668        @Override
669        public boolean onUp(PipTouchState touchState) {
670            if (!touchState.isDragging() && !mIsMinimized && !mIsTappingThrough) {
671                mMenuController.showMenu();
672                mIsTappingThrough = true;
673                return true;
674            }
675            return false;
676        }
677    };
678
679    /**
680     * Gesture controlling normal movement of the PIP.
681     */
682    private PipTouchGesture mDefaultMovementGesture = new PipTouchGesture() {
683        @Override
684        boolean onMove(PipTouchState touchState) {
685            if (touchState.startedDragging()) {
686                // For now, once the user has started a drag that the other gestures have not
687                // intercepted, disallow those gestures from intercepting again to drag offscreen
688                touchState.setDisallowDraggingOffscreen();
689            }
690
691            if (touchState.isDragging()) {
692                // Move the pinned stack freely
693                PointF lastDelta = touchState.getLastTouchDelta();
694                float left = mPinnedStackBounds.left + lastDelta.x;
695                float top = mPinnedStackBounds.top + lastDelta.y;
696                if (!DEBUG_ALLOW_OUT_OF_BOUNDS_STACK) {
697                    left = Math.max(mBoundedPinnedStackBounds.left, Math.min(
698                            mBoundedPinnedStackBounds.right, left));
699                    top = Math.max(mBoundedPinnedStackBounds.top, Math.min(
700                            mBoundedPinnedStackBounds.bottom, top));
701                }
702                mTmpBounds.set(mPinnedStackBounds);
703                mTmpBounds.offsetTo((int) left, (int) top);
704                movePinnedStack(mTmpBounds);
705                return true;
706            }
707            return false;
708        }
709
710        @Override
711        public boolean onUp(PipTouchState touchState) {
712            if (touchState.isDragging()) {
713                PointF vel = mTouchState.getVelocity();
714                float velocity = PointF.length(vel.x, vel.y);
715                if (velocity > mFlingAnimationUtils.getMinVelocityPxPerSecond()) {
716                    flingToSnapTarget(velocity, vel.x, vel.y);
717                } else {
718                    animateToClosestSnapTarget();
719                }
720            } else {
721                expandPinnedStackToFullscreen();
722            }
723            return true;
724        }
725    };
726}
727