• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 package com.android.systemui.statusbar.phone;
17 
18 import static android.view.Display.INVALID_DISPLAY;
19 
20 import android.content.Context;
21 import android.content.pm.ParceledListSlice;
22 import android.content.res.Resources;
23 import android.graphics.PixelFormat;
24 import android.graphics.Point;
25 import android.graphics.PointF;
26 import android.graphics.Rect;
27 import android.graphics.Region;
28 import android.hardware.display.DisplayManager;
29 import android.hardware.display.DisplayManager.DisplayListener;
30 import android.hardware.input.InputManager;
31 import android.os.Looper;
32 import android.os.RemoteException;
33 import android.os.SystemClock;
34 import android.util.Log;
35 import android.util.MathUtils;
36 import android.view.Gravity;
37 import android.view.IPinnedStackController;
38 import android.view.IPinnedStackListener;
39 import android.view.ISystemGestureExclusionListener;
40 import android.view.InputChannel;
41 import android.view.InputDevice;
42 import android.view.InputEvent;
43 import android.view.InputEventReceiver;
44 import android.view.InputMonitor;
45 import android.view.KeyCharacterMap;
46 import android.view.KeyEvent;
47 import android.view.MotionEvent;
48 import android.view.View;
49 import android.view.ViewConfiguration;
50 import android.view.WindowManager;
51 import android.view.WindowManagerGlobal;
52 
53 import com.android.systemui.Dependency;
54 import com.android.systemui.R;
55 import com.android.systemui.bubbles.BubbleController;
56 import com.android.systemui.recents.OverviewProxyService;
57 import com.android.systemui.shared.system.QuickStepContract;
58 import com.android.systemui.shared.system.WindowManagerWrapper;
59 
60 import java.io.PrintWriter;
61 import java.util.concurrent.Executor;
62 
63 /**
64  * Utility class to handle edge swipes for back gesture
65  */
66 public class EdgeBackGestureHandler implements DisplayListener {
67 
68     private static final String TAG = "EdgeBackGestureHandler";
69     private static final int MAX_LONG_PRESS_TIMEOUT = 250;
70 
71     private final IPinnedStackListener.Stub mImeChangedListener = new IPinnedStackListener.Stub() {
72         @Override
73         public void onListenerRegistered(IPinnedStackController controller) {
74         }
75 
76         @Override
77         public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
78             // No need to thread jump, assignments are atomic
79             mImeHeight = imeVisible ? imeHeight : 0;
80             // TODO: Probably cancel any existing gesture
81         }
82 
83         @Override
84         public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) {
85         }
86 
87         @Override
88         public void onMinimizedStateChanged(boolean isMinimized) {
89         }
90 
91         @Override
92         public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds,
93                 Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment,
94                 int displayRotation) {
95         }
96 
97         @Override
98         public void onActionsChanged(ParceledListSlice actions) {
99         }
100     };
101 
102     private ISystemGestureExclusionListener mGestureExclusionListener =
103             new ISystemGestureExclusionListener.Stub() {
104                 @Override
105                 public void onSystemGestureExclusionChanged(int displayId,
106                         Region systemGestureExclusion) {
107                     if (displayId == mDisplayId) {
108                         mMainExecutor.execute(() -> mExcludeRegion.set(systemGestureExclusion));
109                     }
110                 }
111             };
112 
113     private final Context mContext;
114     private final OverviewProxyService mOverviewProxyService;
115 
116     private final Point mDisplaySize = new Point();
117     private final int mDisplayId;
118 
119     private final Executor mMainExecutor;
120 
121     private final Region mExcludeRegion = new Region();
122     // The edge width where touch down is allowed
123     private int mEdgeWidth;
124     // The slop to distinguish between horizontal and vertical motion
125     private final float mTouchSlop;
126     // Duration after which we consider the event as longpress.
127     private final int mLongPressTimeout;
128     // The threshold where the touch needs to be at most, such that the arrow is displayed above the
129     // finger, otherwise it will be below
130     private final int mMinArrowPosition;
131     // The amount by which the arrow is shifted to avoid the finger
132     private final int mFingerOffset;
133 
134 
135     private final int mNavBarHeight;
136 
137     private final PointF mDownPoint = new PointF();
138     private boolean mThresholdCrossed = false;
139     private boolean mAllowGesture = false;
140     private boolean mIsOnLeftEdge;
141 
142     private int mImeHeight = 0;
143 
144     private boolean mIsAttached;
145     private boolean mIsGesturalModeEnabled;
146     private boolean mIsEnabled;
147 
148     private InputMonitor mInputMonitor;
149     private InputEventReceiver mInputEventReceiver;
150 
151     private final WindowManager mWm;
152 
153     private NavigationBarEdgePanel mEdgePanel;
154     private WindowManager.LayoutParams mEdgePanelLp;
155     private final Rect mSamplingRect = new Rect();
156     private RegionSamplingHelper mRegionSamplingHelper;
157     private int mLeftInset;
158     private int mRightInset;
159 
EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService)160     public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService) {
161         final Resources res = context.getResources();
162         mContext = context;
163         mDisplayId = context.getDisplayId();
164         mMainExecutor = context.getMainExecutor();
165         mWm = context.getSystemService(WindowManager.class);
166         mOverviewProxyService = overviewProxyService;
167 
168         // Reduce the default touch slop to ensure that we can intercept the gesture
169         // before the app starts to react to it.
170         // TODO(b/130352502) Tune this value and extract into a constant
171         mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 0.75f;
172         mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT,
173                 ViewConfiguration.getLongPressTimeout());
174 
175         mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height);
176         mMinArrowPosition = res.getDimensionPixelSize(R.dimen.navigation_edge_arrow_min_y);
177         mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset);
178         updateCurrentUserResources(res);
179     }
180 
updateCurrentUserResources(Resources res)181     public void updateCurrentUserResources(Resources res) {
182         mEdgeWidth = res.getDimensionPixelSize(
183                 com.android.internal.R.dimen.config_backGestureInset);
184     }
185 
186     /**
187      * @see NavigationBarView#onAttachedToWindow()
188      */
onNavBarAttached()189     public void onNavBarAttached() {
190         mIsAttached = true;
191         updateIsEnabled();
192     }
193 
194     /**
195      * @see NavigationBarView#onDetachedFromWindow()
196      */
onNavBarDetached()197     public void onNavBarDetached() {
198         mIsAttached = false;
199         updateIsEnabled();
200     }
201 
onNavigationModeChanged(int mode, Context currentUserContext)202     public void onNavigationModeChanged(int mode, Context currentUserContext) {
203         mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode);
204         updateIsEnabled();
205         updateCurrentUserResources(currentUserContext.getResources());
206     }
207 
disposeInputChannel()208     private void disposeInputChannel() {
209         if (mInputEventReceiver != null) {
210             mInputEventReceiver.dispose();
211             mInputEventReceiver = null;
212         }
213         if (mInputMonitor != null) {
214             mInputMonitor.dispose();
215             mInputMonitor = null;
216         }
217     }
218 
updateIsEnabled()219     private void updateIsEnabled() {
220         boolean isEnabled = mIsAttached && mIsGesturalModeEnabled;
221         if (isEnabled == mIsEnabled) {
222             return;
223         }
224         mIsEnabled = isEnabled;
225         disposeInputChannel();
226 
227         if (mEdgePanel != null) {
228             mWm.removeView(mEdgePanel);
229             mEdgePanel = null;
230             mRegionSamplingHelper.stop();
231             mRegionSamplingHelper = null;
232         }
233 
234         if (!mIsEnabled) {
235             WindowManagerWrapper.getInstance().removePinnedStackListener(mImeChangedListener);
236             mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this);
237 
238             try {
239                 WindowManagerGlobal.getWindowManagerService()
240                         .unregisterSystemGestureExclusionListener(
241                                 mGestureExclusionListener, mDisplayId);
242             } catch (RemoteException e) {
243                 Log.e(TAG, "Failed to unregister window manager callbacks", e);
244             }
245 
246         } else {
247             updateDisplaySize();
248             mContext.getSystemService(DisplayManager.class).registerDisplayListener(this,
249                     mContext.getMainThreadHandler());
250 
251             try {
252                 WindowManagerWrapper.getInstance().addPinnedStackListener(mImeChangedListener);
253                 WindowManagerGlobal.getWindowManagerService()
254                         .registerSystemGestureExclusionListener(
255                                 mGestureExclusionListener, mDisplayId);
256             } catch (RemoteException e) {
257                 Log.e(TAG, "Failed to register window manager callbacks", e);
258             }
259 
260             // Register input event receiver
261             mInputMonitor = InputManager.getInstance().monitorGestureInput(
262                     "edge-swipe", mDisplayId);
263             mInputEventReceiver = new SysUiInputEventReceiver(
264                     mInputMonitor.getInputChannel(), Looper.getMainLooper());
265 
266             // Add a nav bar panel window
267             mEdgePanel = new NavigationBarEdgePanel(mContext);
268             mEdgePanelLp = new WindowManager.LayoutParams(
269                     mContext.getResources()
270                             .getDimensionPixelSize(R.dimen.navigation_edge_panel_width),
271                     mContext.getResources()
272                             .getDimensionPixelSize(R.dimen.navigation_edge_panel_height),
273                     WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL,
274                     WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
275                             | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
276                             | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH
277                             | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN,
278                     PixelFormat.TRANSLUCENT);
279             mEdgePanelLp.privateFlags |=
280                     WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
281             mEdgePanelLp.setTitle(TAG + mDisplayId);
282             mEdgePanelLp.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel);
283             mEdgePanelLp.windowAnimations = 0;
284             mEdgePanel.setLayoutParams(mEdgePanelLp);
285             mWm.addView(mEdgePanel, mEdgePanelLp);
286             mRegionSamplingHelper = new RegionSamplingHelper(mEdgePanel,
287                     new RegionSamplingHelper.SamplingCallback() {
288                         @Override
289                         public void onRegionDarknessChanged(boolean isRegionDark) {
290                             mEdgePanel.setIsDark(!isRegionDark, true /* animate */);
291                         }
292 
293                         @Override
294                         public Rect getSampledRegion(View sampledView) {
295                             return mSamplingRect;
296                         }
297                     });
298         }
299     }
300 
onInputEvent(InputEvent ev)301     private void onInputEvent(InputEvent ev) {
302         if (ev instanceof MotionEvent) {
303             onMotionEvent((MotionEvent) ev);
304         }
305     }
306 
isWithinTouchRegion(int x, int y)307     private boolean isWithinTouchRegion(int x, int y) {
308         if (y > (mDisplaySize.y - Math.max(mImeHeight, mNavBarHeight))) {
309             return false;
310         }
311 
312         if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
313             return false;
314         }
315         boolean isInExcludedRegion = mExcludeRegion.contains(x, y);
316         if (isInExcludedRegion) {
317             mOverviewProxyService.notifyBackAction(false /* completed */, -1, -1,
318                     false /* isButton */, !mIsOnLeftEdge);
319         }
320         return !isInExcludedRegion;
321     }
322 
cancelGesture(MotionEvent ev)323     private void cancelGesture(MotionEvent ev) {
324         // Send action cancel to reset all the touch events
325         mAllowGesture = false;
326         MotionEvent cancelEv = MotionEvent.obtain(ev);
327         cancelEv.setAction(MotionEvent.ACTION_CANCEL);
328         mEdgePanel.handleTouch(cancelEv);
329         cancelEv.recycle();
330     }
331 
onMotionEvent(MotionEvent ev)332     private void onMotionEvent(MotionEvent ev) {
333         int action = ev.getActionMasked();
334         if (action == MotionEvent.ACTION_DOWN) {
335             // Verify if this is in within the touch region and we aren't in immersive mode, and
336             // either the bouncer is showing or the notification panel is hidden
337             int stateFlags = mOverviewProxyService.getSystemUiStateFlags();
338             mIsOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
339             mAllowGesture = !QuickStepContract.isBackGestureDisabled(stateFlags)
340                     && isWithinTouchRegion((int) ev.getX(), (int) ev.getY());
341             if (mAllowGesture) {
342                 mEdgePanelLp.gravity = mIsOnLeftEdge
343                         ? (Gravity.LEFT | Gravity.TOP)
344                         : (Gravity.RIGHT | Gravity.TOP);
345                 mEdgePanel.setIsLeftPanel(mIsOnLeftEdge);
346                 mEdgePanel.handleTouch(ev);
347                 updateEdgePanelPosition(ev.getY());
348                 mWm.updateViewLayout(mEdgePanel, mEdgePanelLp);
349                 mRegionSamplingHelper.start(mSamplingRect);
350 
351                 mDownPoint.set(ev.getX(), ev.getY());
352                 mThresholdCrossed = false;
353             }
354         } else if (mAllowGesture) {
355             if (!mThresholdCrossed) {
356                 if (action == MotionEvent.ACTION_POINTER_DOWN) {
357                     // We do not support multi touch for back gesture
358                     cancelGesture(ev);
359                     return;
360                 } else if (action == MotionEvent.ACTION_MOVE) {
361                     if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) {
362                         cancelGesture(ev);
363                         return;
364                     }
365                     float dx = Math.abs(ev.getX() - mDownPoint.x);
366                     float dy = Math.abs(ev.getY() - mDownPoint.y);
367                     if (dy > dx && dy > mTouchSlop) {
368                         cancelGesture(ev);
369                         return;
370 
371                     } else if (dx > dy && dx > mTouchSlop) {
372                         mThresholdCrossed = true;
373                         // Capture inputs
374                         mInputMonitor.pilferPointers();
375                     }
376                 }
377 
378             }
379 
380             // forward touch
381             mEdgePanel.handleTouch(ev);
382 
383             boolean isUp = action == MotionEvent.ACTION_UP;
384             if (isUp) {
385                 boolean performAction = mEdgePanel.shouldTriggerBack();
386                 if (performAction) {
387                     // Perform back
388                     sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK);
389                     sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK);
390                 }
391                 mOverviewProxyService.notifyBackAction(performAction, (int) mDownPoint.x,
392                         (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge);
393             }
394             if (isUp || action == MotionEvent.ACTION_CANCEL) {
395                 mRegionSamplingHelper.stop();
396             } else {
397                 updateSamplingRect();
398                 mRegionSamplingHelper.updateSamplingRect();
399             }
400         }
401     }
402 
updateEdgePanelPosition(float touchY)403     private void updateEdgePanelPosition(float touchY) {
404         float position = touchY - mFingerOffset;
405         position = Math.max(position, mMinArrowPosition);
406         position = (position - mEdgePanelLp.height / 2.0f);
407         mEdgePanelLp.y = MathUtils.constrain((int) position, 0, mDisplaySize.y);
408         updateSamplingRect();
409     }
410 
updateSamplingRect()411     private void updateSamplingRect() {
412         int top = mEdgePanelLp.y;
413         int left = mIsOnLeftEdge ? mLeftInset : mDisplaySize.x - mRightInset - mEdgePanelLp.width;
414         int right = left + mEdgePanelLp.width;
415         int bottom = top + mEdgePanelLp.height;
416         mSamplingRect.set(left, top, right, bottom);
417         mEdgePanel.adjustRectToBoundingBox(mSamplingRect);
418     }
419 
420     @Override
onDisplayAdded(int displayId)421     public void onDisplayAdded(int displayId) { }
422 
423     @Override
onDisplayRemoved(int displayId)424     public void onDisplayRemoved(int displayId) { }
425 
426     @Override
onDisplayChanged(int displayId)427     public void onDisplayChanged(int displayId) {
428         if (displayId == mDisplayId) {
429             updateDisplaySize();
430         }
431     }
432 
updateDisplaySize()433     private void updateDisplaySize() {
434         mContext.getSystemService(DisplayManager.class)
435                 .getDisplay(mDisplayId)
436                 .getRealSize(mDisplaySize);
437     }
438 
sendEvent(int action, int code)439     private void sendEvent(int action, int code) {
440         long when = SystemClock.uptimeMillis();
441         final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */,
442                 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */,
443                 KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY,
444                 InputDevice.SOURCE_KEYBOARD);
445 
446         // Bubble controller will give us a valid display id if it should get the back event
447         BubbleController bubbleController = Dependency.get(BubbleController.class);
448         int bubbleDisplayId = bubbleController.getExpandedDisplayId(mContext);
449         if (code == KeyEvent.KEYCODE_BACK && bubbleDisplayId != INVALID_DISPLAY) {
450             ev.setDisplayId(bubbleDisplayId);
451         }
452         InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC);
453     }
454 
setInsets(int leftInset, int rightInset)455     public void setInsets(int leftInset, int rightInset) {
456         mLeftInset = leftInset;
457         mRightInset = rightInset;
458     }
459 
dump(PrintWriter pw)460     public void dump(PrintWriter pw) {
461         pw.println("EdgeBackGestureHandler:");
462         pw.println("  mIsEnabled=" + mIsEnabled);
463         pw.println("  mAllowGesture=" + mAllowGesture);
464         pw.println("  mExcludeRegion=" + mExcludeRegion);
465         pw.println("  mImeHeight=" + mImeHeight);
466         pw.println("  mIsAttached=" + mIsAttached);
467         pw.println("  mEdgeWidth=" + mEdgeWidth);
468     }
469 
470     class SysUiInputEventReceiver extends InputEventReceiver {
SysUiInputEventReceiver(InputChannel channel, Looper looper)471         SysUiInputEventReceiver(InputChannel channel, Looper looper) {
472             super(channel, looper);
473         }
474 
onInputEvent(InputEvent event)475         public void onInputEvent(InputEvent event) {
476             EdgeBackGestureHandler.this.onInputEvent(event);
477             finishInputEvent(event, true);
478         }
479     }
480 }
481