| /** |
| * Copyright (C) 2019 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package com.android.systemui.statusbar.phone; |
| |
| import static android.view.Display.INVALID_DISPLAY; |
| |
| import android.content.Context; |
| import android.content.res.Resources; |
| import android.graphics.PixelFormat; |
| import android.graphics.Point; |
| import android.graphics.PointF; |
| import android.graphics.Region; |
| import android.hardware.display.DisplayManager; |
| import android.hardware.display.DisplayManager.DisplayListener; |
| import android.hardware.input.InputManager; |
| import android.os.Looper; |
| import android.os.RemoteException; |
| import android.os.SystemClock; |
| import android.os.SystemProperties; |
| import android.util.Log; |
| import android.view.ISystemGestureExclusionListener; |
| import android.view.InputChannel; |
| import android.view.InputDevice; |
| import android.view.InputEvent; |
| import android.view.InputEventReceiver; |
| import android.view.InputMonitor; |
| import android.view.KeyCharacterMap; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.ViewConfiguration; |
| import android.view.WindowManager; |
| import android.view.WindowManagerGlobal; |
| |
| import com.android.systemui.Dependency; |
| import com.android.systemui.R; |
| import com.android.systemui.bubbles.BubbleController; |
| import com.android.systemui.model.SysUiState; |
| import com.android.systemui.plugins.NavigationEdgeBackPlugin; |
| import com.android.systemui.plugins.PluginListener; |
| import com.android.systemui.recents.OverviewProxyService; |
| import com.android.systemui.shared.plugins.PluginManager; |
| import com.android.systemui.shared.system.QuickStepContract; |
| import com.android.systemui.shared.system.SysUiStatsLog; |
| import com.android.systemui.shared.tracing.ProtoTraceable; |
| import com.android.systemui.tracing.ProtoTracer; |
| import com.android.systemui.tracing.nano.EdgeBackGestureHandlerProto; |
| import com.android.systemui.tracing.nano.SystemUiTraceProto; |
| |
| import java.io.PrintWriter; |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Utility class to handle edge swipes for back gesture |
| */ |
| public class EdgeBackGestureHandler implements DisplayListener, |
| PluginListener<NavigationEdgeBackPlugin>, ProtoTraceable<SystemUiTraceProto> { |
| |
| private static final String TAG = "EdgeBackGestureHandler"; |
| private static final int MAX_LONG_PRESS_TIMEOUT = SystemProperties.getInt( |
| "gestures.back_timeout", 250); |
| |
| private ISystemGestureExclusionListener mGestureExclusionListener = |
| new ISystemGestureExclusionListener.Stub() { |
| @Override |
| public void onSystemGestureExclusionChanged(int displayId, |
| Region systemGestureExclusion, Region unrestrictedOrNull) { |
| if (displayId == mDisplayId) { |
| mMainExecutor.execute(() -> { |
| mExcludeRegion.set(systemGestureExclusion); |
| mUnrestrictedExcludeRegion.set(unrestrictedOrNull != null |
| ? unrestrictedOrNull : systemGestureExclusion); |
| }); |
| } |
| } |
| }; |
| |
| private final Context mContext; |
| private final OverviewProxyService mOverviewProxyService; |
| private PluginManager mPluginManager; |
| |
| private final Point mDisplaySize = new Point(); |
| private final int mDisplayId; |
| |
| private final Executor mMainExecutor; |
| |
| private final Region mExcludeRegion = new Region(); |
| private final Region mUnrestrictedExcludeRegion = new Region(); |
| |
| // The edge width where touch down is allowed |
| private int mEdgeWidth; |
| // The bottom gesture area height |
| private int mBottomGestureHeight; |
| // The slop to distinguish between horizontal and vertical motion |
| private final float mTouchSlop; |
| // Duration after which we consider the event as longpress. |
| private final int mLongPressTimeout; |
| |
| private final PointF mDownPoint = new PointF(); |
| private boolean mThresholdCrossed = false; |
| private boolean mAllowGesture = false; |
| private boolean mInRejectedExclusion = false; |
| private boolean mIsOnLeftEdge; |
| |
| private boolean mIsAttached; |
| private boolean mIsGesturalModeEnabled; |
| private boolean mIsEnabled; |
| private boolean mIsNavBarShownTransiently; |
| |
| private InputMonitor mInputMonitor; |
| private InputEventReceiver mInputEventReceiver; |
| |
| private NavigationEdgeBackPlugin mEdgeBackPlugin; |
| private int mLeftInset; |
| private int mRightInset; |
| private int mSysUiFlags; |
| |
| private final NavigationEdgeBackPlugin.BackCallback mBackCallback = |
| new NavigationEdgeBackPlugin.BackCallback() { |
| @Override |
| public void triggerBack() { |
| sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); |
| sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); |
| |
| mOverviewProxyService.notifyBackAction(true, (int) mDownPoint.x, |
| (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); |
| int backtype = (mInRejectedExclusion |
| ? SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED_REJECTED : |
| SysUiStatsLog.BACK_GESTURE__TYPE__COMPLETED); |
| SysUiStatsLog.write(SysUiStatsLog.BACK_GESTURE_REPORTED_REPORTED, backtype, |
| (int) mDownPoint.y, mIsOnLeftEdge |
| ? SysUiStatsLog.BACK_GESTURE__X_LOCATION__LEFT : |
| SysUiStatsLog.BACK_GESTURE__X_LOCATION__RIGHT); |
| } |
| |
| @Override |
| public void cancelBack() { |
| mOverviewProxyService.notifyBackAction(false, (int) mDownPoint.x, |
| (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); |
| int backtype = SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE; |
| SysUiStatsLog.write(SysUiStatsLog.BACK_GESTURE_REPORTED_REPORTED, backtype, |
| (int) mDownPoint.y, mIsOnLeftEdge |
| ? SysUiStatsLog.BACK_GESTURE__X_LOCATION__LEFT : |
| SysUiStatsLog.BACK_GESTURE__X_LOCATION__RIGHT); |
| } |
| }; |
| |
| public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService, |
| SysUiState sysUiFlagContainer, PluginManager pluginManager) { |
| final Resources res = context.getResources(); |
| mContext = context; |
| mDisplayId = context.getDisplayId(); |
| mMainExecutor = context.getMainExecutor(); |
| mOverviewProxyService = overviewProxyService; |
| mPluginManager = pluginManager; |
| Dependency.get(ProtoTracer.class).add(this); |
| |
| // Reduce the default touch slop to ensure that we can intercept the gesture |
| // before the app starts to react to it. |
| // TODO(b/130352502) Tune this value and extract into a constant |
| mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop() * 0.75f; |
| mLongPressTimeout = Math.min(MAX_LONG_PRESS_TIMEOUT, |
| ViewConfiguration.getLongPressTimeout()); |
| |
| updateCurrentUserResources(res); |
| sysUiFlagContainer.addCallback(sysUiFlags -> mSysUiFlags = sysUiFlags); |
| } |
| |
| public void updateCurrentUserResources(Resources res) { |
| mEdgeWidth = res.getDimensionPixelSize( |
| com.android.internal.R.dimen.config_backGestureInset); |
| mBottomGestureHeight = res.getDimensionPixelSize( |
| com.android.internal.R.dimen.navigation_bar_gesture_height); |
| } |
| |
| /** |
| * @see NavigationBarView#onAttachedToWindow() |
| */ |
| public void onNavBarAttached() { |
| mIsAttached = true; |
| updateIsEnabled(); |
| } |
| |
| /** |
| * @see NavigationBarView#onDetachedFromWindow() |
| */ |
| public void onNavBarDetached() { |
| mIsAttached = false; |
| updateIsEnabled(); |
| } |
| |
| public void onNavigationModeChanged(int mode, Context currentUserContext) { |
| mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode); |
| updateIsEnabled(); |
| updateCurrentUserResources(currentUserContext.getResources()); |
| } |
| |
| public void onNavBarTransientStateChanged(boolean isTransient) { |
| mIsNavBarShownTransiently = isTransient; |
| } |
| |
| private void disposeInputChannel() { |
| if (mInputEventReceiver != null) { |
| mInputEventReceiver.dispose(); |
| mInputEventReceiver = null; |
| } |
| if (mInputMonitor != null) { |
| mInputMonitor.dispose(); |
| mInputMonitor = null; |
| } |
| } |
| |
| private void updateIsEnabled() { |
| boolean isEnabled = mIsAttached && mIsGesturalModeEnabled; |
| if (isEnabled == mIsEnabled) { |
| return; |
| } |
| mIsEnabled = isEnabled; |
| disposeInputChannel(); |
| |
| if (mEdgeBackPlugin != null) { |
| mEdgeBackPlugin.onDestroy(); |
| mEdgeBackPlugin = null; |
| } |
| |
| if (!mIsEnabled) { |
| mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(this); |
| mPluginManager.removePluginListener(this); |
| |
| try { |
| WindowManagerGlobal.getWindowManagerService() |
| .unregisterSystemGestureExclusionListener( |
| mGestureExclusionListener, mDisplayId); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to unregister window manager callbacks", e); |
| } |
| |
| } else { |
| updateDisplaySize(); |
| mContext.getSystemService(DisplayManager.class).registerDisplayListener(this, |
| mContext.getMainThreadHandler()); |
| |
| try { |
| WindowManagerGlobal.getWindowManagerService() |
| .registerSystemGestureExclusionListener( |
| mGestureExclusionListener, mDisplayId); |
| } catch (RemoteException e) { |
| Log.e(TAG, "Failed to register window manager callbacks", e); |
| } |
| |
| // Register input event receiver |
| mInputMonitor = InputManager.getInstance().monitorGestureInput( |
| "edge-swipe", mDisplayId); |
| mInputEventReceiver = new SysUiInputEventReceiver( |
| mInputMonitor.getInputChannel(), Looper.getMainLooper()); |
| |
| // Add a nav bar panel window |
| setEdgeBackPlugin(new NavigationBarEdgePanel(mContext)); |
| mPluginManager.addPluginListener( |
| this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false); |
| } |
| } |
| |
| @Override |
| public void onPluginConnected(NavigationEdgeBackPlugin plugin, Context context) { |
| setEdgeBackPlugin(plugin); |
| } |
| |
| @Override |
| public void onPluginDisconnected(NavigationEdgeBackPlugin plugin) { |
| setEdgeBackPlugin(new NavigationBarEdgePanel(mContext)); |
| } |
| |
| private void setEdgeBackPlugin(NavigationEdgeBackPlugin edgeBackPlugin) { |
| if (mEdgeBackPlugin != null) { |
| mEdgeBackPlugin.onDestroy(); |
| } |
| mEdgeBackPlugin = edgeBackPlugin; |
| mEdgeBackPlugin.setBackCallback(mBackCallback); |
| mEdgeBackPlugin.setLayoutParams(createLayoutParams()); |
| updateDisplaySize(); |
| } |
| |
| private WindowManager.LayoutParams createLayoutParams() { |
| Resources resources = mContext.getResources(); |
| WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams( |
| resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_width), |
| resources.getDimensionPixelSize(R.dimen.navigation_edge_panel_height), |
| WindowManager.LayoutParams.TYPE_NAVIGATION_BAR_PANEL, |
| WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
| | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
| | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH |
| | WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN, |
| PixelFormat.TRANSLUCENT); |
| layoutParams.privateFlags |= |
| WindowManager.LayoutParams.SYSTEM_FLAG_SHOW_FOR_ALL_USERS; |
| layoutParams.setTitle(TAG + mContext.getDisplayId()); |
| layoutParams.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel); |
| layoutParams.windowAnimations = 0; |
| layoutParams.setFitInsetsTypes(0 /* types */); |
| return layoutParams; |
| } |
| |
| private void onInputEvent(InputEvent ev) { |
| if (ev instanceof MotionEvent) { |
| onMotionEvent((MotionEvent) ev); |
| } |
| } |
| |
| private boolean isWithinTouchRegion(int x, int y) { |
| // Disallow if too far from the edge |
| if (x > mEdgeWidth + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) { |
| return false; |
| } |
| |
| // Disallow if we are in the bottom gesture area |
| if (y >= (mDisplaySize.y - mBottomGestureHeight)) { |
| return false; |
| } |
| |
| // Always allow if the user is in a transient sticky immersive state |
| if (mIsNavBarShownTransiently) { |
| return true; |
| } |
| |
| boolean isInExcludedRegion = mExcludeRegion.contains(x, y); |
| if (isInExcludedRegion) { |
| mOverviewProxyService.notifyBackAction(false /* completed */, -1, -1, |
| false /* isButton */, !mIsOnLeftEdge); |
| SysUiStatsLog.write(SysUiStatsLog.BACK_GESTURE_REPORTED_REPORTED, |
| SysUiStatsLog.BACK_GESTURE__TYPE__INCOMPLETE_EXCLUDED, y, |
| mIsOnLeftEdge ? SysUiStatsLog.BACK_GESTURE__X_LOCATION__LEFT : |
| SysUiStatsLog.BACK_GESTURE__X_LOCATION__RIGHT); |
| } else { |
| mInRejectedExclusion = mUnrestrictedExcludeRegion.contains(x, y); |
| } |
| return !isInExcludedRegion; |
| } |
| |
| private void cancelGesture(MotionEvent ev) { |
| // Send action cancel to reset all the touch events |
| mAllowGesture = false; |
| mInRejectedExclusion = false; |
| MotionEvent cancelEv = MotionEvent.obtain(ev); |
| cancelEv.setAction(MotionEvent.ACTION_CANCEL); |
| mEdgeBackPlugin.onMotionEvent(cancelEv); |
| cancelEv.recycle(); |
| } |
| |
| private void onMotionEvent(MotionEvent ev) { |
| int action = ev.getActionMasked(); |
| if (action == MotionEvent.ACTION_DOWN) { |
| // Verify if this is in within the touch region and we aren't in immersive mode, and |
| // either the bouncer is showing or the notification panel is hidden |
| mIsOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset; |
| mInRejectedExclusion = false; |
| mAllowGesture = !QuickStepContract.isBackGestureDisabled(mSysUiFlags) |
| && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()); |
| if (mAllowGesture) { |
| mEdgeBackPlugin.setIsLeftPanel(mIsOnLeftEdge); |
| mEdgeBackPlugin.onMotionEvent(ev); |
| |
| mDownPoint.set(ev.getX(), ev.getY()); |
| mThresholdCrossed = false; |
| } |
| } else if (mAllowGesture) { |
| if (!mThresholdCrossed) { |
| if (action == MotionEvent.ACTION_POINTER_DOWN) { |
| // We do not support multi touch for back gesture |
| cancelGesture(ev); |
| return; |
| } else if (action == MotionEvent.ACTION_MOVE) { |
| if ((ev.getEventTime() - ev.getDownTime()) > mLongPressTimeout) { |
| cancelGesture(ev); |
| return; |
| } |
| float dx = Math.abs(ev.getX() - mDownPoint.x); |
| float dy = Math.abs(ev.getY() - mDownPoint.y); |
| if (dy > dx && dy > mTouchSlop) { |
| cancelGesture(ev); |
| return; |
| |
| } else if (dx > dy && dx > mTouchSlop) { |
| mThresholdCrossed = true; |
| // Capture inputs |
| mInputMonitor.pilferPointers(); |
| } |
| } |
| |
| } |
| |
| // forward touch |
| mEdgeBackPlugin.onMotionEvent(ev); |
| } |
| |
| Dependency.get(ProtoTracer.class).update(); |
| } |
| |
| @Override |
| public void onDisplayAdded(int displayId) { } |
| |
| @Override |
| public void onDisplayRemoved(int displayId) { } |
| |
| @Override |
| public void onDisplayChanged(int displayId) { |
| if (displayId == mDisplayId) { |
| updateDisplaySize(); |
| } |
| } |
| |
| private void updateDisplaySize() { |
| mContext.getSystemService(DisplayManager.class) |
| .getDisplay(mDisplayId) |
| .getRealSize(mDisplaySize); |
| if (mEdgeBackPlugin != null) { |
| mEdgeBackPlugin.setDisplaySize(mDisplaySize); |
| } |
| } |
| |
| private void sendEvent(int action, int code) { |
| long when = SystemClock.uptimeMillis(); |
| final KeyEvent ev = new KeyEvent(when, when, action, code, 0 /* repeat */, |
| 0 /* metaState */, KeyCharacterMap.VIRTUAL_KEYBOARD, 0 /* scancode */, |
| KeyEvent.FLAG_FROM_SYSTEM | KeyEvent.FLAG_VIRTUAL_HARD_KEY, |
| InputDevice.SOURCE_KEYBOARD); |
| |
| // Bubble controller will give us a valid display id if it should get the back event |
| BubbleController bubbleController = Dependency.get(BubbleController.class); |
| int bubbleDisplayId = bubbleController.getExpandedDisplayId(mContext); |
| if (code == KeyEvent.KEYCODE_BACK && bubbleDisplayId != INVALID_DISPLAY) { |
| ev.setDisplayId(bubbleDisplayId); |
| } |
| InputManager.getInstance().injectInputEvent(ev, InputManager.INJECT_INPUT_EVENT_MODE_ASYNC); |
| } |
| |
| public void setInsets(int leftInset, int rightInset) { |
| mLeftInset = leftInset; |
| mRightInset = rightInset; |
| if (mEdgeBackPlugin != null) { |
| mEdgeBackPlugin.setInsets(leftInset, rightInset); |
| } |
| } |
| |
| public void dump(PrintWriter pw) { |
| pw.println("EdgeBackGestureHandler:"); |
| pw.println(" mIsEnabled=" + mIsEnabled); |
| pw.println(" mAllowGesture=" + mAllowGesture); |
| pw.println(" mInRejectedExclusion" + mInRejectedExclusion); |
| pw.println(" mExcludeRegion=" + mExcludeRegion); |
| pw.println(" mUnrestrictedExcludeRegion=" + mUnrestrictedExcludeRegion); |
| pw.println(" mIsAttached=" + mIsAttached); |
| pw.println(" mEdgeWidth=" + mEdgeWidth); |
| } |
| |
| @Override |
| public void writeToProto(SystemUiTraceProto proto) { |
| if (proto.edgeBackGestureHandler == null) { |
| proto.edgeBackGestureHandler = new EdgeBackGestureHandlerProto(); |
| } |
| proto.edgeBackGestureHandler.allowGesture = mAllowGesture; |
| } |
| |
| class SysUiInputEventReceiver extends InputEventReceiver { |
| SysUiInputEventReceiver(InputChannel channel, Looper looper) { |
| super(channel, looper); |
| } |
| |
| public void onInputEvent(InputEvent event) { |
| EdgeBackGestureHandler.this.onInputEvent(event); |
| finishInputEvent(event, true); |
| } |
| } |
| } |