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