1/*
2 * Copyright (C) 2014 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.systemui.recents.views;
18
19import android.content.Context;
20import android.view.InputDevice;
21import android.view.MotionEvent;
22import android.view.VelocityTracker;
23import android.view.View;
24import android.view.ViewConfiguration;
25import android.view.ViewParent;
26import com.android.internal.logging.MetricsLogger;
27import com.android.systemui.recents.Constants;
28import com.android.systemui.recents.Recents;
29import com.android.systemui.recents.RecentsConfiguration;
30
31import java.util.List;
32
33/* Handles touch events for a TaskStackView. */
34class TaskStackViewTouchHandler implements SwipeHelper.Callback {
35    static int INACTIVE_POINTER_ID = -1;
36
37    RecentsConfiguration mConfig;
38    TaskStackView mSv;
39    TaskStackViewScroller mScroller;
40    VelocityTracker mVelocityTracker;
41
42    boolean mIsScrolling;
43
44    float mInitialP;
45    float mLastP;
46    float mTotalPMotion;
47    int mInitialMotionX, mInitialMotionY;
48    int mLastMotionX, mLastMotionY;
49    int mActivePointerId = INACTIVE_POINTER_ID;
50    TaskView mActiveTaskView = null;
51
52    int mMinimumVelocity;
53    int mMaximumVelocity;
54    // The scroll touch slop is used to calculate when we start scrolling
55    int mScrollTouchSlop;
56    // The page touch slop is used to calculate when we start swiping
57    float mPagingTouchSlop;
58    // Used to calculate when a tap is outside a task view rectangle.
59    final int mWindowTouchSlop;
60
61    SwipeHelper mSwipeHelper;
62    boolean mInterceptedBySwipeHelper;
63
64    public TaskStackViewTouchHandler(Context context, TaskStackView sv,
65            RecentsConfiguration config, TaskStackViewScroller scroller) {
66        ViewConfiguration configuration = ViewConfiguration.get(context);
67        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
68        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
69        mScrollTouchSlop = configuration.getScaledTouchSlop();
70        mPagingTouchSlop = configuration.getScaledPagingTouchSlop();
71        mWindowTouchSlop = configuration.getScaledWindowTouchSlop();
72        mSv = sv;
73        mScroller = scroller;
74        mConfig = config;
75
76        float densityScale = context.getResources().getDisplayMetrics().density;
77        mSwipeHelper = new SwipeHelper(SwipeHelper.X, this, densityScale, mPagingTouchSlop);
78        mSwipeHelper.setMinAlpha(1f);
79    }
80
81    /** Velocity tracker helpers */
82    void initOrResetVelocityTracker() {
83        if (mVelocityTracker == null) {
84            mVelocityTracker = VelocityTracker.obtain();
85        } else {
86            mVelocityTracker.clear();
87        }
88    }
89    void initVelocityTrackerIfNotExists() {
90        if (mVelocityTracker == null) {
91            mVelocityTracker = VelocityTracker.obtain();
92        }
93    }
94    void recycleVelocityTracker() {
95        if (mVelocityTracker != null) {
96            mVelocityTracker.recycle();
97            mVelocityTracker = null;
98        }
99    }
100
101    /** Returns the view at the specified coordinates */
102    TaskView findViewAtPoint(int x, int y) {
103        List<TaskView> taskViews = mSv.getTaskViews();
104        int taskViewCount = taskViews.size();
105        for (int i = taskViewCount - 1; i >= 0; i--) {
106            TaskView tv = taskViews.get(i);
107            if (tv.getVisibility() == View.VISIBLE) {
108                if (mSv.isTransformedTouchPointInView(x, y, tv)) {
109                    return tv;
110                }
111            }
112        }
113        return null;
114    }
115
116    /** Constructs a simulated motion event for the current stack scroll. */
117    MotionEvent createMotionEventForStackScroll(MotionEvent ev) {
118        MotionEvent pev = MotionEvent.obtainNoHistory(ev);
119        pev.setLocation(0, mScroller.progressToScrollRange(mScroller.getStackScroll()));
120        return pev;
121    }
122
123    /** Touch preprocessing for handling below */
124    public boolean onInterceptTouchEvent(MotionEvent ev) {
125        // Return early if we have no children
126        boolean hasTaskViews = (mSv.getTaskViews().size() > 0);
127        if (!hasTaskViews) {
128            return false;
129        }
130
131        int action = ev.getAction();
132        if (mConfig.multiStackEnabled) {
133            // Check if we are within the bounds of the stack view contents
134            if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
135                if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) {
136                    return false;
137                }
138            }
139        }
140
141        // Pass through to swipe helper if we are swiping
142        mInterceptedBySwipeHelper = mSwipeHelper.onInterceptTouchEvent(ev);
143        if (mInterceptedBySwipeHelper) {
144            return true;
145        }
146
147        boolean wasScrolling = mScroller.isScrolling() ||
148                (mScroller.mScrollAnimator != null && mScroller.mScrollAnimator.isRunning());
149        switch (action & MotionEvent.ACTION_MASK) {
150            case MotionEvent.ACTION_DOWN: {
151                // Save the touch down info
152                mInitialMotionX = mLastMotionX = (int) ev.getX();
153                mInitialMotionY = mLastMotionY = (int) ev.getY();
154                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
155                mActivePointerId = ev.getPointerId(0);
156                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
157                // Stop the current scroll if it is still flinging
158                mScroller.stopScroller();
159                mScroller.stopBoundScrollAnimation();
160                // Initialize the velocity tracker
161                initOrResetVelocityTracker();
162                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
163                break;
164            }
165            case MotionEvent.ACTION_POINTER_DOWN: {
166                final int index = ev.getActionIndex();
167                mActivePointerId = ev.getPointerId(index);
168                mLastMotionX = (int) ev.getX(index);
169                mLastMotionY = (int) ev.getY(index);
170                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
171                break;
172            }
173            case MotionEvent.ACTION_MOVE: {
174                if (mActivePointerId == INACTIVE_POINTER_ID) break;
175
176                // Initialize the velocity tracker if necessary
177                initVelocityTrackerIfNotExists();
178                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
179
180                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
181                int y = (int) ev.getY(activePointerIndex);
182                int x = (int) ev.getX(activePointerIndex);
183                if (Math.abs(y - mInitialMotionY) > mScrollTouchSlop) {
184                    // Save the touch move info
185                    mIsScrolling = true;
186                    // Disallow parents from intercepting touch events
187                    final ViewParent parent = mSv.getParent();
188                    if (parent != null) {
189                        parent.requestDisallowInterceptTouchEvent(true);
190                    }
191                }
192
193                mLastMotionX = x;
194                mLastMotionY = y;
195                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
196                break;
197            }
198            case MotionEvent.ACTION_POINTER_UP: {
199                int pointerIndex = ev.getActionIndex();
200                int pointerId = ev.getPointerId(pointerIndex);
201                if (pointerId == mActivePointerId) {
202                    // Select a new active pointer id and reset the motion state
203                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
204                    mActivePointerId = ev.getPointerId(newPointerIndex);
205                    mLastMotionX = (int) ev.getX(newPointerIndex);
206                    mLastMotionY = (int) ev.getY(newPointerIndex);
207                    mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
208                    mVelocityTracker.clear();
209                }
210                break;
211            }
212            case MotionEvent.ACTION_CANCEL:
213            case MotionEvent.ACTION_UP: {
214                // Animate the scroll back if we've cancelled
215                mScroller.animateBoundScroll();
216                // Reset the drag state and the velocity tracker
217                mIsScrolling = false;
218                mActivePointerId = INACTIVE_POINTER_ID;
219                mActiveTaskView = null;
220                mTotalPMotion = 0;
221                recycleVelocityTracker();
222                break;
223            }
224        }
225
226        return wasScrolling || mIsScrolling;
227    }
228
229    /** Handles touch events once we have intercepted them */
230    public boolean onTouchEvent(MotionEvent ev) {
231        // Short circuit if we have no children
232        boolean hasTaskViews = (mSv.getTaskViews().size() > 0);
233        if (!hasTaskViews) {
234            return false;
235        }
236
237        int action = ev.getAction();
238        if (mConfig.multiStackEnabled) {
239            // Check if we are within the bounds of the stack view contents
240            if ((action & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
241                if (!mSv.getTouchableRegion().contains((int) ev.getX(), (int) ev.getY())) {
242                    return false;
243                }
244            }
245        }
246
247        // Pass through to swipe helper if we are swiping
248        if (mInterceptedBySwipeHelper && mSwipeHelper.onTouchEvent(ev)) {
249            return true;
250        }
251
252        // Update the velocity tracker
253        initVelocityTrackerIfNotExists();
254
255        switch (action & MotionEvent.ACTION_MASK) {
256            case MotionEvent.ACTION_DOWN: {
257                // Save the touch down info
258                mInitialMotionX = mLastMotionX = (int) ev.getX();
259                mInitialMotionY = mLastMotionY = (int) ev.getY();
260                mInitialP = mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
261                mActivePointerId = ev.getPointerId(0);
262                mActiveTaskView = findViewAtPoint(mLastMotionX, mLastMotionY);
263                // Stop the current scroll if it is still flinging
264                mScroller.stopScroller();
265                mScroller.stopBoundScrollAnimation();
266                // Initialize the velocity tracker
267                initOrResetVelocityTracker();
268                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
269                // Disallow parents from intercepting touch events
270                final ViewParent parent = mSv.getParent();
271                if (parent != null) {
272                    parent.requestDisallowInterceptTouchEvent(true);
273                }
274                break;
275            }
276            case MotionEvent.ACTION_POINTER_DOWN: {
277                final int index = ev.getActionIndex();
278                mActivePointerId = ev.getPointerId(index);
279                mLastMotionX = (int) ev.getX(index);
280                mLastMotionY = (int) ev.getY(index);
281                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
282                break;
283            }
284            case MotionEvent.ACTION_MOVE: {
285                if (mActivePointerId == INACTIVE_POINTER_ID) break;
286
287                mVelocityTracker.addMovement(createMotionEventForStackScroll(ev));
288
289                int activePointerIndex = ev.findPointerIndex(mActivePointerId);
290                int x = (int) ev.getX(activePointerIndex);
291                int y = (int) ev.getY(activePointerIndex);
292                int yTotal = Math.abs(y - mInitialMotionY);
293                float curP = mSv.mLayoutAlgorithm.screenYToCurveProgress(y);
294                float deltaP = mLastP - curP;
295                if (!mIsScrolling) {
296                    if (yTotal > mScrollTouchSlop) {
297                        mIsScrolling = true;
298                        // Disallow parents from intercepting touch events
299                        final ViewParent parent = mSv.getParent();
300                        if (parent != null) {
301                            parent.requestDisallowInterceptTouchEvent(true);
302                        }
303                    }
304                }
305                if (mIsScrolling) {
306                    float curStackScroll = mScroller.getStackScroll();
307                    float overScrollAmount = mScroller.getScrollAmountOutOfBounds(curStackScroll + deltaP);
308                    if (Float.compare(overScrollAmount, 0f) != 0) {
309                        // Bound the overscroll to a fixed amount, and inversely scale the y-movement
310                        // relative to how close we are to the max overscroll
311                        float maxOverScroll = mConfig.taskStackOverscrollPct;
312                        deltaP *= (1f - (Math.min(maxOverScroll, overScrollAmount)
313                                / maxOverScroll));
314                    }
315                    mScroller.setStackScroll(curStackScroll + deltaP);
316                }
317                mLastMotionX = x;
318                mLastMotionY = y;
319                mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
320                mTotalPMotion += Math.abs(deltaP);
321                break;
322            }
323            case MotionEvent.ACTION_UP: {
324                mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
325                int velocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
326                if (mIsScrolling && (Math.abs(velocity) > mMinimumVelocity)) {
327                    float overscrollRangePct = Math.abs((float) velocity / mMaximumVelocity);
328                    int overscrollRange = (int) (Math.min(1f, overscrollRangePct) *
329                            (Constants.Values.TaskStackView.TaskStackMaxOverscrollRange -
330                                    Constants.Values.TaskStackView.TaskStackMinOverscrollRange));
331                    mScroller.mScroller.fling(0,
332                            mScroller.progressToScrollRange(mScroller.getStackScroll()),
333                            0, velocity,
334                            0, 0,
335                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMinScrollP),
336                            mScroller.progressToScrollRange(mSv.mLayoutAlgorithm.mMaxScrollP),
337                            0, Constants.Values.TaskStackView.TaskStackMinOverscrollRange +
338                                    overscrollRange);
339                    // Invalidate to kick off computeScroll
340                    mSv.invalidate();
341                } else if (mIsScrolling && mScroller.isScrollOutOfBounds()) {
342                    // Animate the scroll back into bounds
343                    mScroller.animateBoundScroll();
344                } else if (mActiveTaskView == null) {
345                    // This tap didn't start on a task.
346                    maybeHideRecentsFromBackgroundTap((int) ev.getX(), (int) ev.getY());
347                }
348
349                mActivePointerId = INACTIVE_POINTER_ID;
350                mIsScrolling = false;
351                mTotalPMotion = 0;
352                recycleVelocityTracker();
353                break;
354            }
355            case MotionEvent.ACTION_POINTER_UP: {
356                int pointerIndex = ev.getActionIndex();
357                int pointerId = ev.getPointerId(pointerIndex);
358                if (pointerId == mActivePointerId) {
359                    // Select a new active pointer id and reset the motion state
360                    final int newPointerIndex = (pointerIndex == 0) ? 1 : 0;
361                    mActivePointerId = ev.getPointerId(newPointerIndex);
362                    mLastMotionX = (int) ev.getX(newPointerIndex);
363                    mLastMotionY = (int) ev.getY(newPointerIndex);
364                    mLastP = mSv.mLayoutAlgorithm.screenYToCurveProgress(mLastMotionY);
365                    mVelocityTracker.clear();
366                }
367                break;
368            }
369            case MotionEvent.ACTION_CANCEL: {
370                if (mScroller.isScrollOutOfBounds()) {
371                    // Animate the scroll back into bounds
372                    mScroller.animateBoundScroll();
373                }
374                mActivePointerId = INACTIVE_POINTER_ID;
375                mIsScrolling = false;
376                mTotalPMotion = 0;
377                recycleVelocityTracker();
378                break;
379            }
380        }
381        return true;
382    }
383
384    /** Hides recents if the up event at (x, y) is a tap on the background area. */
385    void maybeHideRecentsFromBackgroundTap(int x, int y) {
386        // Ignore the up event if it's too far from its start position. The user might have been
387        // trying to scroll or swipe.
388        int dx = Math.abs(mInitialMotionX - x);
389        int dy = Math.abs(mInitialMotionY - y);
390        if (dx > mScrollTouchSlop || dy > mScrollTouchSlop) {
391            return;
392        }
393
394        // Shift the tap position toward the center of the task stack and check to see if it would
395        // have hit a view. The user might have tried to tap on a task and missed slightly.
396        int shiftedX = x;
397        if (x > mSv.getTouchableRegion().centerX()) {
398            shiftedX -= mWindowTouchSlop;
399        } else {
400            shiftedX += mWindowTouchSlop;
401        }
402        if (findViewAtPoint(shiftedX, y) != null) {
403            return;
404        }
405
406        // The user intentionally tapped on the background, which is like a tap on the "desktop".
407        // Hide recents and transition to the launcher.
408        Recents recents = Recents.getInstanceAndStartIfNeeded(mSv.getContext());
409        recents.hideRecents(false /* altTab */, true /* homeKey */);
410    }
411
412    /** Handles generic motion events */
413    public boolean onGenericMotionEvent(MotionEvent ev) {
414        if ((ev.getSource() & InputDevice.SOURCE_CLASS_POINTER) ==
415                InputDevice.SOURCE_CLASS_POINTER) {
416            int action = ev.getAction();
417            switch (action & MotionEvent.ACTION_MASK) {
418                case MotionEvent.ACTION_SCROLL:
419                    // Find the front most task and scroll the next task to the front
420                    float vScroll = ev.getAxisValue(MotionEvent.AXIS_VSCROLL);
421                    if (vScroll > 0) {
422                        if (mSv.ensureFocusedTask(true)) {
423                            mSv.focusNextTask(true, false);
424                        }
425                    } else {
426                        if (mSv.ensureFocusedTask(true)) {
427                            mSv.focusNextTask(false, false);
428                        }
429                    }
430                    return true;
431            }
432        }
433        return false;
434    }
435
436    /**** SwipeHelper Implementation ****/
437
438    @Override
439    public View getChildAtPosition(MotionEvent ev) {
440        return findViewAtPoint((int) ev.getX(), (int) ev.getY());
441    }
442
443    @Override
444    public boolean canChildBeDismissed(View v) {
445        return true;
446    }
447
448    @Override
449    public void onBeginDrag(View v) {
450        TaskView tv = (TaskView) v;
451        // Disable clipping with the stack while we are swiping
452        tv.setClipViewInStack(false);
453        // Disallow touch events from this task view
454        tv.setTouchEnabled(false);
455        // Disallow parents from intercepting touch events
456        final ViewParent parent = mSv.getParent();
457        if (parent != null) {
458            parent.requestDisallowInterceptTouchEvent(true);
459        }
460        // Fade out the dismiss button
461        mSv.hideDismissAllButton(null);
462    }
463
464    @Override
465    public void onSwipeChanged(View v, float delta) {
466        // Do nothing
467    }
468
469    @Override
470    public void onChildDismissed(View v) {
471        TaskView tv = (TaskView) v;
472        // Re-enable clipping with the stack (we will reuse this view)
473        tv.setClipViewInStack(true);
474        // Re-enable touch events from this task view
475        tv.setTouchEnabled(true);
476        // Remove the task view from the stack
477        mSv.onTaskViewDismissed(tv);
478        // Keep track of deletions by keyboard
479        MetricsLogger.histogram(tv.getContext(), "overview_task_dismissed_source",
480                Constants.Metrics.DismissSourceSwipeGesture);
481    }
482
483    @Override
484    public void onSnapBackCompleted(View v) {
485        TaskView tv = (TaskView) v;
486        // Re-enable clipping with the stack
487        tv.setClipViewInStack(true);
488        // Re-enable touch events from this task view
489        tv.setTouchEnabled(true);
490        // Restore the dismiss button
491        mSv.showDismissAllButton();
492    }
493
494    @Override
495    public void onDragCancelled(View v) {
496        // Do nothing
497    }
498}
499