| /** |
| * 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.pm.ParceledListSlice; |
| import android.content.res.Resources; |
| import android.graphics.PixelFormat; |
| import android.graphics.Point; |
| import android.graphics.PointF; |
| import android.graphics.Rect; |
| 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.util.Log; |
| import android.util.MathUtils; |
| import android.view.Choreographer; |
| import android.view.Gravity; |
| import android.view.IPinnedStackController; |
| import android.view.IPinnedStackListener; |
| import android.view.ISystemGestureExclusionListener; |
| import android.view.InputDevice; |
| import android.view.InputEvent; |
| import android.view.InputMonitor; |
| import android.view.KeyCharacterMap; |
| import android.view.KeyEvent; |
| import android.view.MotionEvent; |
| import android.view.View; |
| 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.recents.OverviewProxyService; |
| import com.android.systemui.shared.system.InputChannelCompat.InputEventReceiver; |
| import com.android.systemui.shared.system.QuickStepContract; |
| import com.android.systemui.shared.system.WindowManagerWrapper; |
| |
| import java.util.concurrent.Executor; |
| |
| /** |
| * Utility class to handle edge swipes for back gesture |
| */ |
| public class EdgeBackGestureHandler implements DisplayListener { |
| |
| private static final String TAG = "EdgeBackGestureHandler"; |
| |
| private final IPinnedStackListener.Stub mImeChangedListener = new IPinnedStackListener.Stub() { |
| @Override |
| public void onListenerRegistered(IPinnedStackController controller) { |
| } |
| |
| @Override |
| public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) { |
| // No need to thread jump, assignments are atomic |
| mImeHeight = imeVisible ? imeHeight : 0; |
| // TODO: Probably cancel any existing gesture |
| } |
| |
| @Override |
| public void onShelfVisibilityChanged(boolean shelfVisible, int shelfHeight) { |
| } |
| |
| @Override |
| public void onMinimizedStateChanged(boolean isMinimized) { |
| } |
| |
| @Override |
| public void onMovementBoundsChanged(Rect insetBounds, Rect normalBounds, |
| Rect animatingBounds, boolean fromImeAdjustment, boolean fromShelfAdjustment, |
| int displayRotation) { |
| } |
| |
| @Override |
| public void onActionsChanged(ParceledListSlice actions) { |
| } |
| }; |
| |
| private ISystemGestureExclusionListener mGestureExclusionListener = |
| new ISystemGestureExclusionListener.Stub() { |
| @Override |
| public void onSystemGestureExclusionChanged(int displayId, |
| Region systemGestureExclusion) { |
| if (displayId == mDisplayId) { |
| mMainExecutor.execute(() -> mExcludeRegion.set(systemGestureExclusion)); |
| } |
| } |
| }; |
| |
| private final Context mContext; |
| private final OverviewProxyService mOverviewProxyService; |
| |
| private final Point mDisplaySize = new Point(); |
| private final int mDisplayId; |
| |
| private final Executor mMainExecutor; |
| |
| private final Region mExcludeRegion = new Region(); |
| // The edge width where touch down is allowed |
| private final int mEdgeWidth; |
| // The slop to distinguish between horizontal and vertical motion |
| private final float mTouchSlop; |
| // Minimum distance to move so that is can be considerd as a back swipe |
| private final float mSwipeThreshold; |
| // The threshold where the touch needs to be at most, such that the arrow is displayed above the |
| // finger, otherwise it will be below |
| private final int mMinArrowPosition; |
| // The amount by which the arrow is shifted to avoid the finger |
| private final int mFingerOffset; |
| |
| |
| private final int mNavBarHeight; |
| |
| private final PointF mDownPoint = new PointF(); |
| private boolean mThresholdCrossed = false; |
| private boolean mAllowGesture = false; |
| private boolean mIsOnLeftEdge; |
| |
| private int mImeHeight = 0; |
| |
| private boolean mIsAttached; |
| private boolean mIsGesturalModeEnabled; |
| private boolean mIsEnabled; |
| |
| private InputMonitor mInputMonitor; |
| private InputEventReceiver mInputEventReceiver; |
| |
| private final WindowManager mWm; |
| |
| private NavigationBarEdgePanel mEdgePanel; |
| private WindowManager.LayoutParams mEdgePanelLp; |
| private final Rect mSamplingRect = new Rect(); |
| private RegionSamplingHelper mRegionSamplingHelper; |
| |
| public EdgeBackGestureHandler(Context context, OverviewProxyService overviewProxyService) { |
| final Resources res = context.getResources(); |
| mContext = context; |
| mDisplayId = context.getDisplayId(); |
| mMainExecutor = context.getMainExecutor(); |
| mWm = context.getSystemService(WindowManager.class); |
| mOverviewProxyService = overviewProxyService; |
| |
| // TODO: Get this for the current user |
| mEdgeWidth = res.getDimensionPixelSize( |
| com.android.internal.R.dimen.config_backGestureInset); |
| mTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop(); |
| mSwipeThreshold = res.getDimension(R.dimen.navigation_edge_action_drag_threshold); |
| |
| mNavBarHeight = res.getDimensionPixelSize(R.dimen.navigation_bar_frame_height); |
| mMinArrowPosition = res.getDimensionPixelSize( |
| R.dimen.navigation_edge_arrow_min_y); |
| mFingerOffset = res.getDimensionPixelSize(R.dimen.navigation_edge_finger_offset); |
| } |
| |
| /** |
| * @see NavigationBarView#onAttachedToWindow() |
| */ |
| public void onNavBarAttached() { |
| mIsAttached = true; |
| updateIsEnabled(); |
| } |
| |
| /** |
| * @see NavigationBarView#onDetachedFromWindow() |
| */ |
| public void onNavBarDetached() { |
| mIsAttached = false; |
| updateIsEnabled(); |
| } |
| |
| public void onNavigationModeChanged(int mode) { |
| mIsGesturalModeEnabled = QuickStepContract.isGesturalMode(mode); |
| updateIsEnabled(); |
| } |
| |
| 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 (mEdgePanel != null) { |
| mWm.removeView(mEdgePanel); |
| mEdgePanel = null; |
| mRegionSamplingHelper.stop(); |
| mRegionSamplingHelper = null; |
| } |
| |
| if (!mIsEnabled) { |
| WindowManagerWrapper.getInstance().removePinnedStackListener(mImeChangedListener); |
| mContext.getSystemService(DisplayManager.class).unregisterDisplayListener(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 { |
| WindowManagerWrapper.getInstance().addPinnedStackListener(mImeChangedListener); |
| 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 InputEventReceiver(mInputMonitor.getInputChannel(), |
| Looper.getMainLooper(), Choreographer.getMainThreadInstance(), |
| this::onInputEvent); |
| |
| // Add a nav bar panel window |
| mEdgePanel = new NavigationBarEdgePanel(mContext); |
| mEdgePanelLp = new WindowManager.LayoutParams( |
| mContext.getResources() |
| .getDimensionPixelSize(R.dimen.navigation_edge_panel_width), |
| mContext.getResources() |
| .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); |
| mEdgePanelLp.setTitle(TAG + mDisplayId); |
| mEdgePanelLp.accessibilityTitle = mContext.getString(R.string.nav_bar_edge_panel); |
| mEdgePanelLp.windowAnimations = 0; |
| mEdgePanel.setLayoutParams(mEdgePanelLp); |
| mWm.addView(mEdgePanel, mEdgePanelLp); |
| mRegionSamplingHelper = new RegionSamplingHelper(mEdgePanel, |
| new RegionSamplingHelper.SamplingCallback() { |
| @Override |
| public void onRegionDarknessChanged(boolean isRegionDark) { |
| mEdgePanel.setIsDark(!isRegionDark, true /* animate */); |
| } |
| |
| @Override |
| public Rect getSampledRegion(View sampledView) { |
| return mSamplingRect; |
| } |
| }); |
| } |
| } |
| |
| private void onInputEvent(InputEvent ev) { |
| if (ev instanceof MotionEvent) { |
| onMotionEvent((MotionEvent) ev); |
| } |
| } |
| |
| private boolean isWithinTouchRegion(int x, int y) { |
| if (y > (mDisplaySize.y - Math.max(mImeHeight, mNavBarHeight))) { |
| return false; |
| } |
| |
| if (x > mEdgeWidth && x < (mDisplaySize.x - mEdgeWidth)) { |
| return false; |
| } |
| boolean isInExcludedRegion = mExcludeRegion.contains(x, y); |
| if (isInExcludedRegion) { |
| mOverviewProxyService.notifyBackAction(false /* completed */, -1, -1, |
| false /* isButton */, !mIsOnLeftEdge); |
| } |
| return !isInExcludedRegion; |
| } |
| |
| private void onMotionEvent(MotionEvent ev) { |
| if (ev.getAction() == 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 |
| int stateFlags = mOverviewProxyService.getSystemUiStateFlags(); |
| mIsOnLeftEdge = ev.getX() <= mEdgeWidth; |
| mAllowGesture = !QuickStepContract.isBackGestureDisabled(stateFlags) |
| && isWithinTouchRegion((int) ev.getX(), (int) ev.getY()); |
| if (mAllowGesture) { |
| mEdgePanelLp.gravity = mIsOnLeftEdge |
| ? (Gravity.LEFT | Gravity.TOP) |
| : (Gravity.RIGHT | Gravity.TOP); |
| mEdgePanel.setIsLeftPanel(mIsOnLeftEdge); |
| mEdgePanel.handleTouch(ev); |
| updateEdgePanelPosition(ev.getY()); |
| mWm.updateViewLayout(mEdgePanel, mEdgePanelLp); |
| mRegionSamplingHelper.start(mSamplingRect); |
| |
| mDownPoint.set(ev.getX(), ev.getY()); |
| mThresholdCrossed = false; |
| } |
| } else if (mAllowGesture) { |
| if (!mThresholdCrossed && ev.getAction() == MotionEvent.ACTION_MOVE) { |
| float dx = Math.abs(ev.getX() - mDownPoint.x); |
| float dy = Math.abs(ev.getY() - mDownPoint.y); |
| if (dy > dx && dy > mTouchSlop) { |
| // Send action cancel to reset all the touch events |
| mAllowGesture = false; |
| MotionEvent cancelEv = MotionEvent.obtain(ev); |
| cancelEv.setAction(MotionEvent.ACTION_CANCEL); |
| mEdgePanel.handleTouch(cancelEv); |
| cancelEv.recycle(); |
| return; |
| |
| } else if (dx > dy && dx > mTouchSlop) { |
| mThresholdCrossed = true; |
| // Capture inputs |
| mInputMonitor.pilferPointers(); |
| } |
| } |
| |
| // forward touch |
| mEdgePanel.handleTouch(ev); |
| |
| boolean isUp = ev.getAction() == MotionEvent.ACTION_UP; |
| if (isUp) { |
| boolean performAction = mEdgePanel.shouldTriggerBack(); |
| if (performAction) { |
| // Perform back |
| sendEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_BACK); |
| sendEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_BACK); |
| } |
| mOverviewProxyService.notifyBackAction(performAction, (int) mDownPoint.x, |
| (int) mDownPoint.y, false /* isButton */, !mIsOnLeftEdge); |
| } |
| if (isUp || ev.getAction() == MotionEvent.ACTION_CANCEL) { |
| mRegionSamplingHelper.stop(); |
| } else { |
| updateSamplingRect(); |
| mRegionSamplingHelper.updateSamplingRect(); |
| } |
| } |
| } |
| |
| private void updateEdgePanelPosition(float touchY) { |
| float position = touchY - mFingerOffset; |
| position = Math.max(position, mMinArrowPosition); |
| position = (position - mEdgePanelLp.height / 2.0f); |
| mEdgePanelLp.y = MathUtils.constrain((int) position, 0, mDisplaySize.y); |
| updateSamplingRect(); |
| } |
| |
| private void updateSamplingRect() { |
| int top = mEdgePanelLp.y; |
| int left = mIsOnLeftEdge ? 0 : mDisplaySize.x - mEdgePanelLp.width; |
| int right = left + mEdgePanelLp.width; |
| int bottom = top + mEdgePanelLp.height; |
| mSamplingRect.set(left, top, right, bottom); |
| mEdgePanel.adjustRectToBoundingBox(mSamplingRect); |
| } |
| |
| @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); |
| } |
| |
| 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); |
| } |
| } |