/**
|
* 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.Gravity;
|
import android.view.IPinnedStackController;
|
import android.view.IPinnedStackListener;
|
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.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.QuickStepContract;
|
import com.android.systemui.shared.system.WindowManagerWrapper;
|
|
import java.io.PrintWriter;
|
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 static final int MAX_LONG_PRESS_TIMEOUT = 250;
|
|
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 int mEdgeWidth;
|
// 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;
|
// 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;
|
private int mLeftInset;
|
private int mRightInset;
|
|
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;
|
|
// 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());
|
|
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);
|
updateCurrentUserResources(res);
|
}
|
|
public void updateCurrentUserResources(Resources res) {
|
mEdgeWidth = res.getDimensionPixelSize(
|
com.android.internal.R.dimen.config_backGestureInset);
|
}
|
|
/**
|
* @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());
|
}
|
|
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 SysUiInputEventReceiver(
|
mInputMonitor.getInputChannel(), Looper.getMainLooper());
|
|
// 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.privateFlags |=
|
WindowManager.LayoutParams.PRIVATE_FLAG_SHOW_FOR_ALL_USERS;
|
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 + mLeftInset && x < (mDisplaySize.x - mEdgeWidth - mRightInset)) {
|
return false;
|
}
|
boolean isInExcludedRegion = mExcludeRegion.contains(x, y);
|
if (isInExcludedRegion) {
|
mOverviewProxyService.notifyBackAction(false /* completed */, -1, -1,
|
false /* isButton */, !mIsOnLeftEdge);
|
}
|
return !isInExcludedRegion;
|
}
|
|
private void cancelGesture(MotionEvent ev) {
|
// 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();
|
}
|
|
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
|
int stateFlags = mOverviewProxyService.getSystemUiStateFlags();
|
mIsOnLeftEdge = ev.getX() <= mEdgeWidth + mLeftInset;
|
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) {
|
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
|
mEdgePanel.handleTouch(ev);
|
|
boolean isUp = action == 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 || action == 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 ? mLeftInset : mDisplaySize.x - mRightInset - 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);
|
}
|
|
public void setInsets(int leftInset, int rightInset) {
|
mLeftInset = leftInset;
|
mRightInset = rightInset;
|
}
|
|
public void dump(PrintWriter pw) {
|
pw.println("EdgeBackGestureHandler:");
|
pw.println(" mIsEnabled=" + mIsEnabled);
|
pw.println(" mAllowGesture=" + mAllowGesture);
|
pw.println(" mExcludeRegion=" + mExcludeRegion);
|
pw.println(" mImeHeight=" + mImeHeight);
|
pw.println(" mIsAttached=" + mIsAttached);
|
pw.println(" mEdgeWidth=" + mEdgeWidth);
|
}
|
|
class SysUiInputEventReceiver extends InputEventReceiver {
|
SysUiInputEventReceiver(InputChannel channel, Looper looper) {
|
super(channel, looper);
|
}
|
|
public void onInputEvent(InputEvent event) {
|
EdgeBackGestureHandler.this.onInputEvent(event);
|
finishInputEvent(event, true);
|
}
|
}
|
}
|