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