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