1 /*
2  * Copyright (C) 2019 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.quickstep.inputconsumers;
18 
19 import static android.view.MotionEvent.ACTION_CANCEL;
20 import static android.view.MotionEvent.ACTION_DOWN;
21 import static android.view.MotionEvent.ACTION_MOVE;
22 import static android.view.MotionEvent.ACTION_UP;
23 
24 import static com.android.launcher3.ResourceUtils.NAVBAR_BOTTOM_GESTURE_SIZE;
25 import static com.android.launcher3.Utilities.squaredHypot;
26 
27 import android.content.Context;
28 import android.graphics.Point;
29 import android.graphics.PointF;
30 import android.view.MotionEvent;
31 
32 import com.android.launcher3.R;
33 import com.android.launcher3.ResourceUtils;
34 import com.android.launcher3.Utilities;
35 import com.android.launcher3.util.DisplayController;
36 import com.android.quickstep.InputConsumer;
37 import com.android.quickstep.RecentsAnimationDeviceState;
38 import com.android.quickstep.SystemUiProxy;
39 import com.android.systemui.shared.system.InputMonitorCompat;
40 
41 /**
42  * Touch consumer for handling gesture event to launch one handed
43  * One handed gestural in quickstep only active on NO_BUTTON, TWO_BUTTONS, and portrait mode
44  */
45 public class OneHandedModeInputConsumer extends DelegateInputConsumer {
46 
47     private static final int ANGLE_MAX = 150;
48     private static final int ANGLE_MIN = 30;
49 
50     private final Context mContext;
51     private final Point mDisplaySize;
52     private final RecentsAnimationDeviceState mDeviceState;
53 
54     private final float mDragDistThreshold;
55     private final float mSquaredSlop;
56 
57     private final int mNavBarSize;
58 
59     private final PointF mDownPos = new PointF();
60     private final PointF mLastPos = new PointF();
61 
62     private boolean mPassedSlop;
63     private boolean mIsStopGesture;
64 
OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState, InputConsumer delegate, InputMonitorCompat inputMonitor)65     public OneHandedModeInputConsumer(Context context, RecentsAnimationDeviceState deviceState,
66             InputConsumer delegate, InputMonitorCompat inputMonitor) {
67         super(delegate, inputMonitor);
68         mContext = context;
69         mDeviceState = deviceState;
70         mDragDistThreshold = context.getResources().getDimensionPixelSize(
71                 R.dimen.gestures_onehanded_drag_threshold);
72         mSquaredSlop = Utilities.squaredTouchSlop(context);
73         mDisplaySize = DisplayController.INSTANCE.get(mContext).getInfo().currentSize;
74         mNavBarSize = ResourceUtils.getNavbarSize(NAVBAR_BOTTOM_GESTURE_SIZE,
75                 mContext.getResources());
76     }
77 
78     @Override
getType()79     public int getType() {
80         return TYPE_ONE_HANDED | mDelegate.getType();
81     }
82 
83     @Override
onMotionEvent(MotionEvent ev)84     public void onMotionEvent(MotionEvent ev) {
85         switch (ev.getActionMasked()) {
86             case ACTION_DOWN: {
87                 mDownPos.set(ev.getX(), ev.getY());
88                 mLastPos.set(mDownPos);
89                 break;
90             }
91             case ACTION_MOVE: {
92                 if (mState == STATE_DELEGATE_ACTIVE) {
93                     break;
94                 }
95                 if (!mDelegate.allowInterceptByParent()) {
96                     mState = STATE_DELEGATE_ACTIVE;
97                     break;
98                 }
99 
100                 mLastPos.set(ev.getX(), ev.getY());
101                 if (!mPassedSlop) {
102                     if (squaredHypot(mLastPos.x - mDownPos.x, mLastPos.y - mDownPos.y)
103                             > mSquaredSlop) {
104                         if ((!mDeviceState.isOneHandedModeActive() && isValidStartAngle(
105                                 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))
106                                 || (mDeviceState.isOneHandedModeActive() && isValidExitAngle(
107                                 mDownPos.x - mLastPos.x, mDownPos.y - mLastPos.y))) {
108                             // To avoid mis-trigger when motion not touch system gesture region.
109                             mPassedSlop = isInSystemGestureRegion(mLastPos);
110                             setActive(ev);
111                         } else {
112                             mState = STATE_DELEGATE_ACTIVE;
113                         }
114                     }
115                 } else {
116                     float distance = (float) Math.hypot(mLastPos.x - mDownPos.x,
117                             mLastPos.y - mDownPos.y);
118                     if (distance > mDragDistThreshold && mPassedSlop) {
119                         mIsStopGesture = true;
120                     }
121                 }
122                 break;
123             }
124             case ACTION_UP: {
125                 if (mLastPos.y >= mDownPos.y && mPassedSlop) {
126                     onStartGestureDetected();
127                 } else if (mIsStopGesture) {
128                     onStopGestureDetected();
129                 }
130                 clearState();
131                 break;
132             }
133             case ACTION_CANCEL:
134                 clearState();
135                 break;
136         }
137 
138         if (mState != STATE_ACTIVE) {
139             mDelegate.onMotionEvent(ev);
140         }
141     }
142 
clearState()143     private void clearState() {
144         mPassedSlop = false;
145         mState = STATE_INACTIVE;
146         mIsStopGesture = false;
147     }
148 
onStartGestureDetected()149     private void onStartGestureDetected() {
150         if (mDeviceState.isSwipeToNotificationEnabled()) {
151             SystemUiProxy.INSTANCE.get(mContext).expandNotificationPanel();
152         } else if (!mDeviceState.isOneHandedModeActive()) {
153             SystemUiProxy.INSTANCE.get(mContext).startOneHandedMode();
154         }
155     }
156 
onStopGestureDetected()157     private void onStopGestureDetected() {
158         if (!mDeviceState.isOneHandedModeEnabled() || !mDeviceState.isOneHandedModeActive()) {
159             return;
160         }
161 
162         SystemUiProxy.INSTANCE.get(mContext).stopOneHandedMode();
163     }
164 
isInSystemGestureRegion(PointF lastPos)165     private boolean isInSystemGestureRegion(PointF lastPos) {
166         final int navBarUpperBound = mDisplaySize.y - mNavBarSize;
167         return mDeviceState.isGesturalNavMode() && lastPos.y > navBarUpperBound;
168     }
169 
isValidStartAngle(float deltaX, float deltaY)170     private boolean isValidStartAngle(float deltaX, float deltaY) {
171         final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
172         return angle > -(ANGLE_MAX) && angle < -(ANGLE_MIN);
173     }
174 
isValidExitAngle(float deltaX, float deltaY)175     private boolean isValidExitAngle(float deltaX, float deltaY) {
176         final float angle = (float) Math.toDegrees(Math.atan2(deltaY, deltaX));
177         return angle > ANGLE_MIN && angle < ANGLE_MAX;
178     }
179 }
180