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.statusbar.phone;
18
19import android.app.ActivityManager;
20import android.content.Context;
21import android.content.res.Resources;
22import android.graphics.Rect;
23import android.view.GestureDetector;
24import android.view.MotionEvent;
25import android.view.VelocityTracker;
26import android.view.View;
27import android.view.ViewConfiguration;
28
29import com.android.internal.logging.MetricsLogger;
30import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
31import com.android.internal.policy.DividerSnapAlgorithm.SnapTarget;
32import com.android.systemui.Dependency;
33import com.android.systemui.R;
34import com.android.systemui.RecentsComponent;
35import com.android.systemui.plugins.statusbar.phone.NavGesture.GestureHelper;
36import com.android.systemui.stackdivider.Divider;
37import com.android.systemui.tuner.TunerService;
38
39import static android.view.WindowManager.DOCKED_INVALID;
40import static android.view.WindowManager.DOCKED_LEFT;
41import static android.view.WindowManager.DOCKED_TOP;
42
43/**
44 * Class to detect gestures on the navigation bar.
45 */
46public class NavigationBarGestureHelper extends GestureDetector.SimpleOnGestureListener
47        implements TunerService.Tunable, GestureHelper {
48
49    private static final String KEY_DOCK_WINDOW_GESTURE = "overview_nav_bar_gesture";
50    /**
51     * When dragging from the navigation bar, we drag in recents.
52     */
53    public static final int DRAG_MODE_NONE = -1;
54
55    /**
56     * When dragging from the navigation bar, we drag in recents.
57     */
58    public static final int DRAG_MODE_RECENTS = 0;
59
60    /**
61     * When dragging from the navigation bar, we drag the divider.
62     */
63    public static final int DRAG_MODE_DIVIDER = 1;
64
65    private RecentsComponent mRecentsComponent;
66    private Divider mDivider;
67    private Context mContext;
68    private NavigationBarView mNavigationBarView;
69    private boolean mIsVertical;
70    private boolean mIsRTL;
71
72    private final GestureDetector mTaskSwitcherDetector;
73    private final int mScrollTouchSlop;
74    private final int mMinFlingVelocity;
75    private int mTouchDownX;
76    private int mTouchDownY;
77    private boolean mDownOnRecents;
78    private VelocityTracker mVelocityTracker;
79
80    private boolean mDockWindowEnabled;
81    private boolean mDockWindowTouchSlopExceeded;
82    private int mDragMode;
83
84    public NavigationBarGestureHelper(Context context) {
85        mContext = context;
86        ViewConfiguration configuration = ViewConfiguration.get(context);
87        Resources r = context.getResources();
88        mScrollTouchSlop = r.getDimensionPixelSize(R.dimen.navigation_bar_min_swipe_distance);
89        mMinFlingVelocity = configuration.getScaledMinimumFlingVelocity();
90        mTaskSwitcherDetector = new GestureDetector(context, this);
91        Dependency.get(TunerService.class).addTunable(this, KEY_DOCK_WINDOW_GESTURE);
92    }
93
94    public void destroy() {
95        Dependency.get(TunerService.class).removeTunable(this);
96    }
97
98    public void setComponents(RecentsComponent recentsComponent, Divider divider,
99            NavigationBarView navigationBarView) {
100        mRecentsComponent = recentsComponent;
101        mDivider = divider;
102        mNavigationBarView = navigationBarView;
103    }
104
105    public void setBarState(boolean isVertical, boolean isRTL) {
106        mIsVertical = isVertical;
107        mIsRTL = isRTL;
108    }
109
110    public boolean onInterceptTouchEvent(MotionEvent event) {
111        // If we move more than a fixed amount, then start capturing for the
112        // task switcher detector
113        mTaskSwitcherDetector.onTouchEvent(event);
114        int action = event.getAction();
115        switch (action & MotionEvent.ACTION_MASK) {
116            case MotionEvent.ACTION_DOWN: {
117                mTouchDownX = (int) event.getX();
118                mTouchDownY = (int) event.getY();
119                break;
120            }
121            case MotionEvent.ACTION_MOVE: {
122                int x = (int) event.getX();
123                int y = (int) event.getY();
124                int xDiff = Math.abs(x - mTouchDownX);
125                int yDiff = Math.abs(y - mTouchDownY);
126                boolean exceededTouchSlop = !mIsVertical
127                        ? xDiff > mScrollTouchSlop && xDiff > yDiff
128                        : yDiff > mScrollTouchSlop && yDiff > xDiff;
129                if (exceededTouchSlop) {
130                    return true;
131                }
132                break;
133            }
134            case MotionEvent.ACTION_CANCEL:
135            case MotionEvent.ACTION_UP:
136                break;
137        }
138        return mDockWindowEnabled && interceptDockWindowEvent(event);
139    }
140
141    private boolean interceptDockWindowEvent(MotionEvent event) {
142        switch (event.getActionMasked()) {
143            case MotionEvent.ACTION_DOWN:
144                handleDragActionDownEvent(event);
145                break;
146            case MotionEvent.ACTION_MOVE:
147                return handleDragActionMoveEvent(event);
148            case MotionEvent.ACTION_UP:
149            case MotionEvent.ACTION_CANCEL:
150                handleDragActionUpEvent(event);
151                break;
152        }
153        return false;
154    }
155
156    private boolean handleDockWindowEvent(MotionEvent event) {
157        switch (event.getActionMasked()) {
158            case MotionEvent.ACTION_DOWN:
159                handleDragActionDownEvent(event);
160                break;
161            case MotionEvent.ACTION_MOVE:
162                handleDragActionMoveEvent(event);
163                break;
164            case MotionEvent.ACTION_UP:
165            case MotionEvent.ACTION_CANCEL:
166                handleDragActionUpEvent(event);
167                break;
168        }
169        return true;
170    }
171
172    private void handleDragActionDownEvent(MotionEvent event) {
173        mVelocityTracker = VelocityTracker.obtain();
174        mVelocityTracker.addMovement(event);
175        mDockWindowTouchSlopExceeded = false;
176        mTouchDownX = (int) event.getX();
177        mTouchDownY = (int) event.getY();
178
179        if (mNavigationBarView != null) {
180            View recentsButton = mNavigationBarView.getRecentsButton().getCurrentView();
181            if (recentsButton != null) {
182                mDownOnRecents = mTouchDownX >= recentsButton.getLeft()
183                        && mTouchDownX <= recentsButton.getRight()
184                        && mTouchDownY >= recentsButton.getTop()
185                        && mTouchDownY <= recentsButton.getBottom();
186            } else {
187                mDownOnRecents = false;
188            }
189        }
190    }
191
192    private boolean handleDragActionMoveEvent(MotionEvent event) {
193        mVelocityTracker.addMovement(event);
194        int x = (int) event.getX();
195        int y = (int) event.getY();
196        int xDiff = Math.abs(x - mTouchDownX);
197        int yDiff = Math.abs(y - mTouchDownY);
198        if (mDivider == null || mRecentsComponent == null) {
199            return false;
200        }
201        if (!mDockWindowTouchSlopExceeded) {
202            boolean touchSlopExceeded = !mIsVertical
203                    ? yDiff > mScrollTouchSlop && yDiff > xDiff
204                    : xDiff > mScrollTouchSlop && xDiff > yDiff;
205            if (mDownOnRecents && touchSlopExceeded
206                    && mDivider.getView().getWindowManagerProxy().getDockSide() == DOCKED_INVALID) {
207                Rect initialBounds = null;
208                int dragMode = calculateDragMode();
209                int createMode = ActivityManager.DOCKED_STACK_CREATE_MODE_TOP_OR_LEFT;
210                if (dragMode == DRAG_MODE_DIVIDER) {
211                    initialBounds = new Rect();
212                    mDivider.getView().calculateBoundsForPosition(mIsVertical
213                                    ? (int) event.getRawX()
214                                    : (int) event.getRawY(),
215                            mDivider.getView().isHorizontalDivision()
216                                    ? DOCKED_TOP
217                                    : DOCKED_LEFT,
218                            initialBounds);
219                } else if (dragMode == DRAG_MODE_RECENTS && mTouchDownX
220                        < mContext.getResources().getDisplayMetrics().widthPixels / 2) {
221                    createMode = ActivityManager.DOCKED_STACK_CREATE_MODE_BOTTOM_OR_RIGHT;
222                }
223                boolean docked = mRecentsComponent.dockTopTask(dragMode, createMode, initialBounds,
224                        MetricsEvent.ACTION_WINDOW_DOCK_SWIPE);
225                if (docked) {
226                    mDragMode = dragMode;
227                    if (mDragMode == DRAG_MODE_DIVIDER) {
228                        mDivider.getView().startDragging(false /* animate */, true /* touching*/);
229                    }
230                    mDockWindowTouchSlopExceeded = true;
231                    return true;
232                }
233            }
234        } else {
235            if (mDragMode == DRAG_MODE_DIVIDER) {
236                int position = !mIsVertical ? (int) event.getRawY() : (int) event.getRawX();
237                SnapTarget snapTarget = mDivider.getView().getSnapAlgorithm()
238                        .calculateSnapTarget(position, 0f /* velocity */, false /* hardDismiss */);
239                mDivider.getView().resizeStack(position, snapTarget.position, snapTarget);
240            } else if (mDragMode == DRAG_MODE_RECENTS) {
241                mRecentsComponent.onDraggingInRecents(event.getRawY());
242            }
243        }
244        return false;
245    }
246
247    private void handleDragActionUpEvent(MotionEvent event) {
248        mVelocityTracker.addMovement(event);
249        mVelocityTracker.computeCurrentVelocity(1000);
250        if (mDockWindowTouchSlopExceeded && mDivider != null && mRecentsComponent != null) {
251            if (mDragMode == DRAG_MODE_DIVIDER) {
252                mDivider.getView().stopDragging(mIsVertical
253                                ? (int) event.getRawX()
254                                : (int) event.getRawY(),
255                        mIsVertical
256                                ? mVelocityTracker.getXVelocity()
257                                : mVelocityTracker.getYVelocity(),
258                        true /* avoidDismissStart */, false /* logMetrics */);
259            } else if (mDragMode == DRAG_MODE_RECENTS) {
260                mRecentsComponent.onDraggingInRecentsEnded(mVelocityTracker.getYVelocity());
261            }
262        }
263        mVelocityTracker.recycle();
264        mVelocityTracker = null;
265    }
266
267    private int calculateDragMode() {
268        if (mIsVertical && !mDivider.getView().isHorizontalDivision()) {
269            return DRAG_MODE_DIVIDER;
270        }
271        if (!mIsVertical && mDivider.getView().isHorizontalDivision()) {
272            return DRAG_MODE_DIVIDER;
273        }
274        return DRAG_MODE_RECENTS;
275    }
276
277    public boolean onTouchEvent(MotionEvent event) {
278        boolean result = mTaskSwitcherDetector.onTouchEvent(event);
279        if (mDockWindowEnabled) {
280            result |= handleDockWindowEvent(event);
281        }
282        return result;
283    }
284
285    @Override
286    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
287        float absVelX = Math.abs(velocityX);
288        float absVelY = Math.abs(velocityY);
289        boolean isValidFling = absVelX > mMinFlingVelocity &&
290                mIsVertical ? (absVelY > absVelX) : (absVelX > absVelY);
291        if (isValidFling && mRecentsComponent != null) {
292            boolean showNext;
293            if (!mIsRTL) {
294                showNext = mIsVertical ? (velocityY < 0) : (velocityX < 0);
295            } else {
296                // In RTL, vertical is still the same, but horizontal is flipped
297                showNext = mIsVertical ? (velocityY < 0) : (velocityX > 0);
298            }
299            if (showNext) {
300                mRecentsComponent.showNextAffiliatedTask();
301            } else {
302                mRecentsComponent.showPrevAffiliatedTask();
303            }
304        }
305        return true;
306    }
307
308    @Override
309    public void onTuningChanged(String key, String newValue) {
310        switch (key) {
311            case KEY_DOCK_WINDOW_GESTURE:
312                mDockWindowEnabled = newValue != null && (Integer.parseInt(newValue) != 0);
313                break;
314        }
315    }
316}
317