1 /*
2  * Copyright (C) 2018 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 package com.android.launcher3.uioverrides.touchcontrollers;
17 
18 import static android.view.MotionEvent.ACTION_CANCEL;
19 import static android.view.MotionEvent.ACTION_DOWN;
20 import static android.view.MotionEvent.ACTION_MOVE;
21 import static android.view.MotionEvent.ACTION_UP;
22 import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY;
23 
24 import static com.android.launcher3.MotionEventsUtils.isTrackpadScroll;
25 import static com.android.launcher3.logging.StatsLogManager.LauncherEvent.LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN;
26 
27 import android.graphics.PointF;
28 import android.util.SparseArray;
29 import android.view.MotionEvent;
30 import android.view.ViewConfiguration;
31 import android.view.Window;
32 import android.view.WindowManager;
33 
34 import com.android.launcher3.AbstractFloatingView;
35 import com.android.launcher3.DeviceProfile;
36 import com.android.launcher3.Launcher;
37 import com.android.launcher3.LauncherState;
38 import com.android.launcher3.util.TouchController;
39 import com.android.quickstep.SystemUiProxy;
40 
41 import java.io.PrintWriter;
42 
43 /**
44  * TouchController for handling touch events that get sent to the StatusBar. Once the
45  * Once the event delta mDownY passes the touch slop, the events start getting forwarded.
46  * All events are offset by initial Y value of the pointer.
47  */
48 public class StatusBarTouchController implements TouchController {
49 
50     private static final String TAG = "StatusBarController";
51 
52     private final Launcher mLauncher;
53     private final SystemUiProxy mSystemUiProxy;
54     private final float mTouchSlop;
55     private int mLastAction;
56     private final SparseArray<PointF> mDownEvents;
57 
58     /* If {@code false}, this controller should not handle the input {@link MotionEvent}.*/
59     private boolean mCanIntercept;
60 
61     private boolean mIsTrackpadReverseScroll;
62 
StatusBarTouchController(Launcher l)63     public StatusBarTouchController(Launcher l) {
64         mLauncher = l;
65         mSystemUiProxy = SystemUiProxy.INSTANCE.get(mLauncher);
66         // Guard against TAPs by increasing the touch slop.
67         mTouchSlop = 2 * ViewConfiguration.get(l).getScaledTouchSlop();
68         mDownEvents = new SparseArray<>();
69     }
70 
71     @Override
dump(String prefix, PrintWriter writer)72     public void dump(String prefix, PrintWriter writer) {
73         writer.println(prefix + "mCanIntercept:" + mCanIntercept);
74         writer.println(prefix + "mLastAction:" + MotionEvent.actionToString(mLastAction));
75         writer.println(prefix + "mSysUiProxy available:"
76                 + SystemUiProxy.INSTANCE.get(mLauncher).isActive());
77     }
78 
dispatchTouchEvent(MotionEvent ev)79     private void dispatchTouchEvent(MotionEvent ev) {
80         if (mSystemUiProxy.isActive()) {
81             mLastAction = ev.getActionMasked();
82             mSystemUiProxy.onStatusBarTouchEvent(ev);
83         }
84     }
85 
86     @Override
onControllerInterceptTouchEvent(MotionEvent ev)87     public final boolean onControllerInterceptTouchEvent(MotionEvent ev) {
88         int action = ev.getActionMasked();
89         int idx = ev.getActionIndex();
90         int pid = ev.getPointerId(idx);
91         if (action == ACTION_DOWN) {
92             mCanIntercept = canInterceptTouch(ev);
93             if (!mCanIntercept) {
94                 return false;
95             }
96             mDownEvents.clear();
97             mDownEvents.put(pid, new PointF(ev.getX(), ev.getY()));
98             mIsTrackpadReverseScroll = !mLauncher.isNaturalScrollingEnabled()
99                     && isTrackpadScroll(ev);
100         } else if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) {
101             // Check!! should only set it only when threshold is not entered.
102             mDownEvents.put(pid, new PointF(ev.getX(idx), ev.getY(idx)));
103         }
104         if (!mCanIntercept) {
105             return false;
106         }
107         if (action == ACTION_MOVE && mDownEvents.contains(pid)) {
108             float dy = ev.getY(idx) - mDownEvents.get(pid).y;
109             float dx = ev.getX(idx) - mDownEvents.get(pid).x;
110             if (mIsTrackpadReverseScroll) {
111                 dy = -dy;
112             }
113             // Currently input dispatcher will not do touch transfer if there are more than
114             // one touch pointer. Hence, even if slope passed, only set the slippery flag
115             // when there is single touch event. (context: InputDispatcher.cpp line 1445)
116             if (dy > mTouchSlop && dy > Math.abs(dx) && ev.getPointerCount() == 1) {
117                 ev.setAction(ACTION_DOWN);
118                 dispatchTouchEvent(ev);
119                 setWindowSlippery(true);
120                 return true;
121             }
122             if (Math.abs(dx) > mTouchSlop) {
123                 mCanIntercept = false;
124             }
125         }
126         return false;
127     }
128 
129     @Override
onControllerTouchEvent(MotionEvent ev)130     public final boolean onControllerTouchEvent(MotionEvent ev) {
131         int action = ev.getAction();
132         if (action == ACTION_UP || action == ACTION_CANCEL) {
133             dispatchTouchEvent(ev);
134             mLauncher.getStatsLogManager().logger()
135                     .log(LAUNCHER_SWIPE_DOWN_WORKSPACE_NOTISHADE_OPEN);
136             setWindowSlippery(false);
137             mIsTrackpadReverseScroll = false;
138             return true;
139         }
140         return true;
141     }
142 
143     /**
144      * FLAG_SLIPPERY enables touches to slide out of a window into neighboring
145      * windows in mid-gesture instead of being captured for the duration of
146      * the gesture.
147      *
148      * This flag changes the behavior of touch focus for this window only.
149      * Touches can slide out of the window but they cannot necessarily slide
150      * back in (unless the other window with touch focus permits it).
151      */
setWindowSlippery(boolean enable)152     private void setWindowSlippery(boolean enable) {
153         Window w = mLauncher.getWindow();
154         WindowManager.LayoutParams wlp = w.getAttributes();
155         if (enable) {
156             wlp.flags |= FLAG_SLIPPERY;
157         } else {
158             wlp.flags &= ~FLAG_SLIPPERY;
159         }
160         w.setAttributes(wlp);
161     }
162 
canInterceptTouch(MotionEvent ev)163     private boolean canInterceptTouch(MotionEvent ev) {
164         if (!mLauncher.isInState(LauncherState.NORMAL) ||
165                 AbstractFloatingView.getTopOpenViewWithType(mLauncher,
166                         AbstractFloatingView.TYPE_STATUS_BAR_SWIPE_DOWN_DISALLOW) != null) {
167             return false;
168         } else {
169             // For NORMAL state, only listen if the event originated above the navbar height
170             DeviceProfile dp = mLauncher.getDeviceProfile();
171             if (ev.getY() > (mLauncher.getDragLayer().getHeight() - dp.getInsets().bottom)) {
172                 return false;
173             }
174         }
175         return SystemUiProxy.INSTANCE.get(mLauncher).isActive();
176     }
177 }