blob: 8aa50a7a31e424ed9a8786dbd4105439a22c711d [file] [log] [blame]
Sunny Goyala6a58122019-04-02 10:20:29 -07001/**
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 */
16package com.android.systemui.statusbar.phone;
17
Mady Mellor390bff42019-04-05 15:09:01 -070018import static android.view.Display.INVALID_DISPLAY;
19
Sunny Goyala6a58122019-04-02 10:20:29 -070020import android.content.Context;
21import android.content.pm.ParceledListSlice;
Matthew Ng682ad7a2019-04-09 15:48:01 -070022import android.content.res.Resources;
Sunny Goyala6a58122019-04-02 10:20:29 -070023import android.graphics.PixelFormat;
24import android.graphics.Point;
25import android.graphics.PointF;
26import android.graphics.Rect;
27import android.graphics.Region;
28import android.hardware.display.DisplayManager;
29import android.hardware.display.DisplayManager.DisplayListener;
30import android.hardware.input.InputManager;
31import android.os.Looper;
32import android.os.RemoteException;
33import android.os.SystemClock;
34import android.util.Log;
35import android.util.MathUtils;
36import android.view.Choreographer;
37import android.view.Gravity;
38import android.view.IPinnedStackController;
39import android.view.IPinnedStackListener;
40import android.view.ISystemGestureExclusionListener;
41import android.view.InputDevice;
42import android.view.InputEvent;
43import android.view.InputMonitor;
44import android.view.KeyCharacterMap;
45import android.view.KeyEvent;
46import android.view.MotionEvent;
47import android.view.ViewConfiguration;
48import android.view.WindowManager;
49import android.view.WindowManagerGlobal;
50
Mady Mellor390bff42019-04-05 15:09:01 -070051import com.android.systemui.Dependency;
Sunny Goyala6a58122019-04-02 10:20:29 -070052import com.android.systemui.R;
Mady Mellor390bff42019-04-05 15:09:01 -070053import com.android.systemui.bubbles.BubbleController;
Winson Chung04ff8bda2019-04-02 15:08:59 -070054import com.android.systemui.recents.OverviewProxyService;
Sunny Goyala6a58122019-04-02 10:20:29 -070055import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver;
56import com.android.systemui.shared.system.QuickStepContract;
57import com.android.systemui.shared.system.WindowManagerWrapper;
58
59import java.util.concurrent.Executor;
60
61/**
62 * Utility class to handle edge swipes for back gesture
63 */
64public class EdgeBackGestureHandler implements DisplayListener {
65
66 private static final String TAG = "EdgeBackGestureHandler";
67
68 private final IPinnedStackListener.Stub mImeChangedListener = new IPinnedStackListener.Stub() {
69 @Override
70 public void onListenerRegistered(IPinnedStackController controller) {
71 }
72
73 @Override
74 public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
75 // No need to thread jump, assignments are atomic
76 mImeHeight = imeVisible ? imeHeight : 0;
77 // TODO: Probably cancel any existing gesture
78 }
79
80 @Override
81 public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
82 }
83
84 @Override
85 public void onMinimizedStateChanged(boolean isMinimized) {
86 }
87
88 @Override
89 public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds,
90 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment,
91 int displayRotation) {
92 }
93
94 @Override
95 public void onActionsChanged(ParceledListSlice actions) {
96 }
97 };
98
99 private ISystemGestureExclusionListener mGestureExclusionListener =
100 new ISystemGestureExclusionListener.Stub() {
101 @Override
102 public void onSystemGestureExclusionChanged(int displayId,
103 Region systemGestureExclusion) {
104 if (displayId == mDisplayId) {
105 mMainExecutor.execute(() -> mExcludeRegion.set(systemGestureExclusion));
106 }
107 }
108 };
109
110 private final Context mContext;
Winson Chung04ff8bda2019-04-02 15:08:59 -0700111 private final OverviewProxyService mOverviewProxyService;
Sunny Goyala6a58122019-04-02 10:20:29 -0700112
113 private final Point mDisplaySize = new Point();
114 private final int mDisplayId;
115
116 private final Executor mMainExecutor;
117
118 private final Region mExcludeRegion = new Region();
119 // The edge width where touch down is allowed
120 private final int mEdgeWidth;
121 // The slop to distinguish between horizontal and vertical motion
122 private final float mTouchSlop;
123 // Minimum distance to move so that is can be considerd as a back swipe
124 private final float mSwipeThreshold;
125
126 private final int mNavBarHeight;
127
128 private final PointF mDownPoint = new PointF();
129 private boolean mThresholdCrossed = false;
130 private boolean mIgnoreThisGesture = false;
131 private boolean mIsOnLeftEdge;
132
133 private int mImeHeight = 0;
134
135 private boolean mIsAttached;
136 private boolean mIsGesturalModeEnabled;
137 private boolean mIsEnabled;
138
139 private InputMonitor mInputMonitor;
140 private InputEventReceiver mInputEventReceiver;
141
142 private final WindowManager mWm;
143
144 private NavigationBarEdgePanel mEdgePanel;
145 private WindowManager.LayoutParams mEdgePanelLp;
146
Winson Chung04ff8bda2019-04-02 15:08:59 -0700147 public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService) {
Matthew Ng682ad7a2019-04-09 15:48:01 -0700148 final Resources res = context.getResources();
Sunny Goyala6a58122019-04-02 10:20:29 -0700149 mContext = context;
150 mDisplayId = context.getDisplayId();
151 mMainExecutor = context.getMainExecutor();
152 mWm = context.getSystemService(WindowManager.class);
Winson Chung04ff8bda2019-04-02 15:08:59 -0700153 mOverviewProxyService = overviewProxyService;
Sunny Goyala6a58122019-04-02 10:20:29 -0700154
155 mEdgeWidth = QuickStepContract.getEdgeSensitivityWidth(context);
156 mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
Matthew Ng682ad7a2019-04-09 15:48:01 -0700157 mSwipeThreshold = res.getDimension(R.dimen.navigation_edge_action_drag_threshold);
Sunny Goyala6a58122019-04-02 10:20:29 -0700158
Matthew Ng682ad7a2019-04-09 15:48:01 -0700159 mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height);
Sunny Goyala6a58122019-04-02 10:20:29 -0700160 }
161
162 /**
163 * @see NavigationBarView#onAttachedToWindow()
164 */
165 public void onNavBarAttached() {
166 mIsAttached = true;
167 onOverlaysChanged();
168 }
169
170 /**
171 * @see NavigationBarView#onDetachedFromWindow()
172 */
173 public void onNavBarDetached() {
174 mIsAttached = false;
175 updateIsEnabled();
176 }
177
178 /**
179 * Called when system overlays has changed
180 */
181 public void onOverlaysChanged() {
182 mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mContext);
183 updateIsEnabled();
184 }
185
186 private void disposeInputChannel() {
187 if (mInputEventReceiver != null) {
188 mInputEventReceiver.dispose();
189 mInputEventReceiver = null;
190 }
191 if (mInputMonitor != null) {
192 mInputMonitor.dispose();
193 mInputMonitor = null;
194 }
195 }
196
197 private void updateIsEnabled() {
198 boolean isEnabled = mIsAttached && mIsGesturalModeEnabled;
199 if (isEnabled == mIsEnabled) {
200 return;
201 }
202 mIsEnabled = isEnabled;
203 disposeInputChannel();
204
205 if (mEdgePanel != null) {
206 mWm.removeView(mEdgePanel);
207 mEdgePanel = null;
208 }
209
210 if (!mIsEnabled) {
211 WindowManagerWrapper.getInstance().removePinnedStackListener(mImeChangedListener);
Sunny Goyal19049692019-04-08 11:56:07 -0700212 mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
Sunny Goyala6a58122019-04-02 10:20:29 -0700213
214 try {
215 WindowManagerGlobal.getWindowManagerService()
216 .unregisterSystemGestureExclusionListener(
217 mGestureExclusionListener, mDisplayId);
218 } catch (RemoteException e) {
219 Log.e(TAG, "Failed to unregister window manager callbacks", e);
220 }
221
222 } else {
223 updateDisplaySize();
Sunny Goyal19049692019-04-08 11:56:07 -0700224 mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
225 mContext.getMainThreadHandler());
Sunny Goyala6a58122019-04-02 10:20:29 -0700226
227 try {
228 WindowManagerWrapper.getInstance().addPinnedStackListener(mImeChangedListener);
229 WindowManagerGlobal.getWindowManagerService()
230 .registerSystemGestureExclusionListener(
231 mGestureExclusionListener, mDisplayId);
232 } catch (RemoteException e) {
233 Log.e(TAG, "Failed to register window manager callbacks", e);
234 }
235
236 // Register input event receiver
237 mInputMonitor = InputManager.getInstance().monitorGestureInput(
238 "edge-swipe", mDisplayId);
239 mInputEventReceiver = new InputEventReceiver(mInputMonitor.getInputChannel(),
240 Looper.getMainLooper(), Choreographer.getMainThreadInstance(),
241 this::onInputEvent);
242
243 // Add a nav bar panel window
244 mEdgePanel = new NavigationBarEdgePanel(mContext);
245 mEdgePanelLp = new WindowManager.LayoutParams(
246 mContext.getResources()
247 .getDimensionPixelSize(R.dimen.navigation_edge_panel_width),
248 mContext.getResources()
249 .getDimensionPixelSize(R.dimen.navigation_edge_panel_height),
250 WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
251 WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
252 | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
253 | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
254 | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
255 PixelFormat.TRANSLUCENT);
256 mEdgePanelLp.setTitle(TAG + mDisplayId);
257 mEdgePanelLp.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel);
258 mEdgePanelLp.windowAnimations = 0;
259 mEdgePanel.setLayoutParams(mEdgePanelLp);
260 mWm.addView(mEdgePanel, mEdgePanelLp);
261 }
262 }
263
264 private void onInputEvent(InputEvent ev) {
265 if (ev instanceof MotionEvent) {
266 onMotionEvent((MotionEvent) ev);
267 }
268 }
269
270 private boolean isWithinTouchRegion(int x, int y) {
271 if (y > (mDisplaySize.y - Math.max(mImeHeight, mNavBarHeight))) {
272 return false;
273 }
274
275 if (x > mEdgeWidth && x < (mDisplaySize.x - mEdgeWidth)) {
276 return false;
277 }
278 return !mExcludeRegion.contains(x, y);
279 }
280
281 private void onMotionEvent(MotionEvent ev) {
282 if (ev.getAction() == MotionEvent.ACTION_DOWN) {
283 // Verify if this is in within the touch region
284 mIgnoreThisGesture = !isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
285 if (!mIgnoreThisGesture) {
286 mIsOnLeftEdge = ev.getX() < mEdgeWidth;
287 mEdgePanelLp.gravity = mIsOnLeftEdge
288 ? (Gravity.LEFT | Gravity.TOP)
289 : (Gravity.RIGHT | Gravity.TOP);
290 mEdgePanel.setIsLeftPanel(mIsOnLeftEdge);
291 mEdgePanelLp.y = MathUtils.constrain(
292 (int) (ev.getY() - mEdgePanelLp.height / 2),
293 0, mDisplaySize.y);
294 mWm.updateViewLayout(mEdgePanel, mEdgePanelLp);
295
296 mDownPoint.set(ev.getX(), ev.getY());
297 mThresholdCrossed = false;
298 mEdgePanel.handleTouch(ev);
299 }
300 } else if (!mIgnoreThisGesture) {
301 if (!mThresholdCrossed && ev.getAction() == MotionEvent.ACTION_MOVE) {
302 float dx = Math.abs(ev.getX() - mDownPoint.x);
303 float dy = Math.abs(ev.getY() - mDownPoint.y);
304 if (dy > dx && dy > mTouchSlop) {
305 // Send action cancel to reset all the touch events
306 mIgnoreThisGesture = true;
307 MotionEvent cancelEv = MotionEvent.obtain(ev);
308 cancelEv.setAction(MotionEvent.ACTION_CANCEL);
309 mEdgePanel.handleTouch(cancelEv);
310 cancelEv.recycle();
311 return;
312
313 } else if (dx > dy && dx > mTouchSlop) {
314 mThresholdCrossed = true;
315 // Capture inputs
316 mInputMonitor.pilferPointers();
317 }
318 }
319
320 // forward touch
321 mEdgePanel.handleTouch(ev);
322
323 if (ev.getAction() == MotionEvent.ACTION_UP) {
324 float xDiff = ev.getX() - mDownPoint.x;
325 boolean exceedsThreshold = mIsOnLeftEdge
326 ? (xDiff > mSwipeThreshold) : (-xDiff > mSwipeThreshold);
Winson Chung04ff8bda2019-04-02 15:08:59 -0700327 boolean performAction = exceedsThreshold
328 && Math.abs(xDiff) > Math.abs(ev.getY() - mDownPoint.y);
329 if (performAction) {
Sunny Goyala6a58122019-04-02 10:20:29 -0700330 // Perform back
331 sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
332 sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
333 }
Winson Chung04ff8bda2019-04-02 15:08:59 -0700334 mOverviewProxyService.notifyBackAction(performAction, (int) mDownPoint.x,
335 (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge);
Sunny Goyala6a58122019-04-02 10:20:29 -0700336 }
337 }
338 }
339
340 @Override
341 public void onDisplayAdded(int displayId) { }
342
343 @Override
344 public void onDisplayRemoved(int displayId) { }
345
346 @Override
347 public void onDisplayChanged(int displayId) {
348 if (displayId == mDisplayId) {
349 updateDisplaySize();
350 }
351 }
352
353 private void updateDisplaySize() {
354 mContext.getSystemService(DisplayManager.class)
Sunny Goyal19049692019-04-08 11:56:07 -0700355 .getDisplay(mDisplayId)
356 .getRealSize(mDisplaySize);
Sunny Goyala6a58122019-04-02 10:20:29 -0700357 }
358
359 private void sendEvent(int action, int code) {
360 long when = SystemClock.uptimeMillis();
361 final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
362 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
363 KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
364 InputDevice.SOURCE_KEYBOARD);
Mady Mellor390bff42019-04-05 15:09:01 -0700365
366 // Bubble controller will give us a valid display id if it should get the back event
367 BubbleController bubbleController = Dependency.get(BubbleController.class);
368 int bubbleDisplayId = bubbleController.getExpandedDisplayId(mContext);
369 if (code == KeyEvent.KEYCODE_BACK && bubbleDisplayId != INVALID_DISPLAY) {
370 ev.setDisplayId(bubbleDisplayId);
371 }
Sunny Goyala6a58122019-04-02 10:20:29 -0700372 InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
373 }
374}