blob: 190b635477a21fcd7472178e9d563955ced74a34 [file] [log] [blame]
/*
* Copyright (C) 2022 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.wm.shell.windowdecor;
import static android.view.InputDevice.SOURCE_TOUCHSCREEN;
import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
import static android.view.WindowManager.LayoutParams.TYPE_INPUT_CONSUMER;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT;
import static com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP;
import android.content.Context;
import android.graphics.Rect;
import android.graphics.Region;
import android.hardware.input.InputManager;
import android.os.Binder;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
import android.view.Choreographer;
import android.view.IWindowSession;
import android.view.InputChannel;
import android.view.InputEvent;
import android.view.InputEventReceiver;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.SurfaceControl;
import android.view.View;
import android.view.ViewConfiguration;
import android.view.WindowManagerGlobal;
import com.android.internal.view.BaseIWindow;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayLayout;
import java.util.function.Supplier;
/**
* An input event listener registered to InputDispatcher to receive input events on task edges and
* and corners. Converts them to drag resize requests.
* Task edges are for resizing with a mouse.
* Task corners are for resizing with touch input.
*/
class DragResizeInputListener implements AutoCloseable {
private static final String TAG = "DragResizeInputListener";
private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession();
private final Handler mHandler;
private final Choreographer mChoreographer;
private final InputManager mInputManager;
private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier;
private final int mDisplayId;
private final BaseIWindow mFakeWindow;
private final IBinder mFocusGrantToken;
private final SurfaceControl mDecorationSurface;
private final InputChannel mInputChannel;
private final TaskResizeInputEventReceiver mInputEventReceiver;
private final DragPositioningCallback mCallback;
private final SurfaceControl mInputSinkSurface;
private final BaseIWindow mFakeSinkWindow;
private final InputChannel mSinkInputChannel;
private final DisplayController mDisplayController;
private int mTaskWidth;
private int mTaskHeight;
private int mResizeHandleThickness;
private int mCornerSize;
private int mTaskCornerRadius;
private Rect mLeftTopCornerBounds;
private Rect mRightTopCornerBounds;
private Rect mLeftBottomCornerBounds;
private Rect mRightBottomCornerBounds;
private int mDragPointerId = -1;
private DragDetector mDragDetector;
private final Region mTouchRegion = new Region();
DragResizeInputListener(
Context context,
Handler handler,
Choreographer choreographer,
int displayId,
int taskCornerRadius,
SurfaceControl decorationSurface,
DragPositioningCallback callback,
Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier,
Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier,
DisplayController displayController) {
mInputManager = context.getSystemService(InputManager.class);
mHandler = handler;
mChoreographer = choreographer;
mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier;
mDisplayId = displayId;
mTaskCornerRadius = taskCornerRadius;
mDecorationSurface = decorationSurface;
mDisplayController = displayController;
// Use a fake window as the backing surface is a container layer, and we don't want to
// create a buffer layer for it, so we can't use ViewRootImpl.
mFakeWindow = new BaseIWindow();
mFakeWindow.setSession(mWindowSession);
mFocusGrantToken = new Binder();
mInputChannel = new InputChannel();
try {
mWindowSession.grantInputChannel(
mDisplayId,
mDecorationSurface,
mFakeWindow.asBinder(),
null /* hostInputToken */,
FLAG_NOT_FOCUSABLE,
PRIVATE_FLAG_TRUSTED_OVERLAY,
INPUT_FEATURE_SPY,
TYPE_APPLICATION,
null /* windowToken */,
mFocusGrantToken,
TAG + " of " + decorationSurface.toString(),
mInputChannel);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
mInputEventReceiver = new TaskResizeInputEventReceiver(
mInputChannel, mHandler, mChoreographer);
mCallback = callback;
mDragDetector = new DragDetector(mInputEventReceiver);
mDragDetector.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop());
mInputSinkSurface = surfaceControlBuilderSupplier.get()
.setName("TaskInputSink of " + decorationSurface)
.setContainerLayer()
.setParent(mDecorationSurface)
.build();
mSurfaceControlTransactionSupplier.get()
.setLayer(mInputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER)
.show(mInputSinkSurface)
.apply();
mFakeSinkWindow = new BaseIWindow();
mSinkInputChannel = new InputChannel();
try {
mWindowSession.grantInputChannel(
mDisplayId,
mInputSinkSurface,
mFakeSinkWindow.asBinder(),
null /* hostInputToken */,
FLAG_NOT_FOCUSABLE,
0 /* privateFlags */,
INPUT_FEATURE_NO_INPUT_CHANNEL,
TYPE_INPUT_CONSUMER,
null /* windowToken */,
mFocusGrantToken,
"TaskInputSink of " + decorationSurface,
mSinkInputChannel);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
/**
* Updates the geometry (the touch region) of this drag resize handler.
*
* @param taskWidth The width of the task.
* @param taskHeight The height of the task.
* @param resizeHandleThickness The thickness of the resize handle in pixels.
* @param cornerSize The size of the resize handle centered in each corner.
* @param touchSlop The distance in pixels user has to drag with touch for it to register as
* a resize action.
* @return whether the geometry has changed or not
*/
boolean setGeometry(int taskWidth, int taskHeight, int resizeHandleThickness, int cornerSize,
int touchSlop) {
if (mTaskWidth == taskWidth && mTaskHeight == taskHeight
&& mResizeHandleThickness == resizeHandleThickness
&& mCornerSize == cornerSize) {
return false;
}
mTaskWidth = taskWidth;
mTaskHeight = taskHeight;
mResizeHandleThickness = resizeHandleThickness;
mCornerSize = cornerSize;
mDragDetector.setTouchSlop(touchSlop);
mTouchRegion.setEmpty();
final Rect topInputBounds = new Rect(
-mResizeHandleThickness,
-mResizeHandleThickness,
mTaskWidth + mResizeHandleThickness,
0);
mTouchRegion.union(topInputBounds);
final Rect leftInputBounds = new Rect(
-mResizeHandleThickness,
0,
0,
mTaskHeight);
mTouchRegion.union(leftInputBounds);
final Rect rightInputBounds = new Rect(
mTaskWidth,
0,
mTaskWidth + mResizeHandleThickness,
mTaskHeight);
mTouchRegion.union(rightInputBounds);
final Rect bottomInputBounds = new Rect(
-mResizeHandleThickness,
mTaskHeight,
mTaskWidth + mResizeHandleThickness,
mTaskHeight + mResizeHandleThickness);
mTouchRegion.union(bottomInputBounds);
// Set up touch areas in each corner.
int cornerRadius = mCornerSize / 2;
mLeftTopCornerBounds = new Rect(
-cornerRadius,
-cornerRadius,
cornerRadius,
cornerRadius);
mTouchRegion.union(mLeftTopCornerBounds);
mRightTopCornerBounds = new Rect(
mTaskWidth - cornerRadius,
-cornerRadius,
mTaskWidth + cornerRadius,
cornerRadius);
mTouchRegion.union(mRightTopCornerBounds);
mLeftBottomCornerBounds = new Rect(
-cornerRadius,
mTaskHeight - cornerRadius,
cornerRadius,
mTaskHeight + cornerRadius);
mTouchRegion.union(mLeftBottomCornerBounds);
mRightBottomCornerBounds = new Rect(
mTaskWidth - cornerRadius,
mTaskHeight - cornerRadius,
mTaskWidth + cornerRadius,
mTaskHeight + cornerRadius);
mTouchRegion.union(mRightBottomCornerBounds);
try {
mWindowSession.updateInputChannel(
mInputChannel.getToken(),
mDisplayId,
mDecorationSurface,
FLAG_NOT_FOCUSABLE,
PRIVATE_FLAG_TRUSTED_OVERLAY,
INPUT_FEATURE_SPY,
mTouchRegion);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
mSurfaceControlTransactionSupplier.get()
.setWindowCrop(mInputSinkSurface, mTaskWidth, mTaskHeight)
.apply();
// The touch region of the TaskInputSink should be the touch region of this
// DragResizeInputHandler minus the task bounds. Pilfering events isn't enough to prevent
// input windows from handling down events, which will bring tasks in the back to front.
//
// Note not the entire touch region responds to both mouse and touchscreen events.
// Therefore, in the region that only responds to one of them, it would be a no-op to
// perform a gesture in the other type of events. We currently only have a mouse-only region
// out of the task bounds, and due to the roughness of touchscreen events, it's not a severe
// issue. However, were there touchscreen-only a region out of the task bounds, mouse
// gestures will become no-op in that region, even though the mouse gestures may appear to
// be performed on the input window behind the resize handle.
mTouchRegion.op(0, 0, mTaskWidth, mTaskHeight, Region.Op.DIFFERENCE);
updateSinkInputChannel(mTouchRegion);
return true;
}
/**
* Generate a Region that encapsulates all 4 corner handles
*/
Region getCornersRegion() {
Region region = new Region();
region.union(mLeftTopCornerBounds);
region.union(mLeftBottomCornerBounds);
region.union(mRightTopCornerBounds);
region.union(mRightBottomCornerBounds);
return region;
}
private void updateSinkInputChannel(Region region) {
try {
mWindowSession.updateInputChannel(
mSinkInputChannel.getToken(),
mDisplayId,
mInputSinkSurface,
FLAG_NOT_FOCUSABLE,
0 /* privateFlags */,
INPUT_FEATURE_NO_INPUT_CHANNEL,
region);
} catch (RemoteException ex) {
ex.rethrowFromSystemServer();
}
}
@Override
public void close() {
mInputEventReceiver.dispose();
mInputChannel.dispose();
try {
mWindowSession.remove(mFakeWindow);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
mSinkInputChannel.dispose();
try {
mWindowSession.remove(mFakeSinkWindow);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
mSurfaceControlTransactionSupplier.get()
.remove(mInputSinkSurface)
.apply();
}
private class TaskResizeInputEventReceiver extends InputEventReceiver
implements DragDetector.MotionEventHandler {
private final Choreographer mChoreographer;
private final Runnable mConsumeBatchEventRunnable;
private boolean mConsumeBatchEventScheduled;
private boolean mShouldHandleEvents;
private int mLastCursorType = PointerIcon.TYPE_DEFAULT;
private Rect mDragStartTaskBounds;
private TaskResizeInputEventReceiver(
InputChannel inputChannel, Handler handler, Choreographer choreographer) {
super(inputChannel, handler.getLooper());
mChoreographer = choreographer;
mConsumeBatchEventRunnable = () -> {
mConsumeBatchEventScheduled = false;
if (consumeBatchedInputEvents(mChoreographer.getFrameTimeNanos())) {
// If we consumed a batch here, we want to go ahead and schedule the
// consumption of batched input events on the next frame. Otherwise, we would
// wait until we have more input events pending and might get starved by other
// things occurring in the process.
scheduleConsumeBatchEvent();
}
};
}
@Override
public void onBatchedInputEventPending(int source) {
scheduleConsumeBatchEvent();
}
private void scheduleConsumeBatchEvent() {
if (mConsumeBatchEventScheduled) {
return;
}
mChoreographer.postCallback(
Choreographer.CALLBACK_INPUT, mConsumeBatchEventRunnable, null);
mConsumeBatchEventScheduled = true;
}
@Override
public void onInputEvent(InputEvent inputEvent) {
finishInputEvent(inputEvent, handleInputEvent(inputEvent));
}
private boolean handleInputEvent(InputEvent inputEvent) {
if (!(inputEvent instanceof MotionEvent)) {
return false;
}
return mDragDetector.onMotionEvent((MotionEvent) inputEvent);
}
@Override
public boolean handleMotionEvent(View v, MotionEvent e) {
boolean result = false;
// Check if this is a touch event vs mouse event.
// Touch events are tracked in four corners. Other events are tracked in resize edges.
boolean isTouch = (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
switch (e.getActionMasked()) {
case MotionEvent.ACTION_DOWN: {
float x = e.getX(0);
float y = e.getY(0);
if (isTouch) {
mShouldHandleEvents = isInCornerBounds(x, y);
} else {
mShouldHandleEvents = isInResizeHandleBounds(x, y);
}
if (mShouldHandleEvents) {
mInputManager.pilferPointers(mInputChannel.getToken());
mDragPointerId = e.getPointerId(0);
float rawX = e.getRawX(0);
float rawY = e.getRawY(0);
int ctrlType = calculateCtrlType(isTouch, x, y);
mDragStartTaskBounds = mCallback.onDragPositioningStart(ctrlType,
rawX, rawY);
// Increase the input sink region to cover the whole screen; this is to
// prevent input and focus from going to other tasks during a drag resize.
updateInputSinkRegionForDrag(mDragStartTaskBounds);
result = true;
}
break;
}
case MotionEvent.ACTION_MOVE: {
if (!mShouldHandleEvents) {
break;
}
int dragPointerIndex = e.findPointerIndex(mDragPointerId);
float rawX = e.getRawX(dragPointerIndex);
float rawY = e.getRawY(dragPointerIndex);
final Rect taskBounds = mCallback.onDragPositioningMove(rawX, rawY);
updateInputSinkRegionForDrag(taskBounds);
result = true;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (mShouldHandleEvents) {
int dragPointerIndex = e.findPointerIndex(mDragPointerId);
final Rect taskBounds = mCallback.onDragPositioningEnd(
e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
// If taskBounds has changed, setGeometry will be called and update the
// sink region. Otherwise, we should revert it here.
if (taskBounds.equals(mDragStartTaskBounds)) {
updateSinkInputChannel(mTouchRegion);
}
}
mShouldHandleEvents = false;
mDragPointerId = -1;
result = true;
break;
}
case MotionEvent.ACTION_HOVER_ENTER:
case MotionEvent.ACTION_HOVER_MOVE: {
updateCursorType(e.getXCursorPosition(), e.getYCursorPosition());
result = true;
break;
}
case MotionEvent.ACTION_HOVER_EXIT:
result = true;
break;
}
return result;
}
private void updateInputSinkRegionForDrag(Rect taskBounds) {
final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId);
final Region dragTouchRegion = new Region(-taskBounds.left,
-taskBounds.top,
-taskBounds.left + layout.width(),
-taskBounds.top + layout.height());
// Remove the localized task bounds from the touch region.
taskBounds.offsetTo(0, 0);
dragTouchRegion.op(taskBounds, Region.Op.DIFFERENCE);
updateSinkInputChannel(dragTouchRegion);
}
private boolean isInCornerBounds(float xf, float yf) {
return calculateCornersCtrlType(xf, yf) != 0;
}
private boolean isInResizeHandleBounds(float x, float y) {
return calculateResizeHandlesCtrlType(x, y) != 0;
}
@DragPositioningCallback.CtrlType
private int calculateCtrlType(boolean isTouch, float x, float y) {
if (isTouch) {
return calculateCornersCtrlType(x, y);
}
return calculateResizeHandlesCtrlType(x, y);
}
@DragPositioningCallback.CtrlType
private int calculateResizeHandlesCtrlType(float x, float y) {
int ctrlType = 0;
// mTaskCornerRadius is only used in comparing with corner regions. Comparisons with
// sides will use the bounds specified in setGeometry and not go into task bounds.
if (x < mTaskCornerRadius) {
ctrlType |= CTRL_TYPE_LEFT;
}
if (x > mTaskWidth - mTaskCornerRadius) {
ctrlType |= CTRL_TYPE_RIGHT;
}
if (y < mTaskCornerRadius) {
ctrlType |= CTRL_TYPE_TOP;
}
if (y > mTaskHeight - mTaskCornerRadius) {
ctrlType |= CTRL_TYPE_BOTTOM;
}
// Check distances from the center if it's in one of four corners.
if ((ctrlType & (CTRL_TYPE_LEFT | CTRL_TYPE_RIGHT)) != 0
&& (ctrlType & (CTRL_TYPE_TOP | CTRL_TYPE_BOTTOM)) != 0) {
return checkDistanceFromCenter(ctrlType, x, y);
}
// Otherwise, we should make sure we don't resize tasks inside task bounds.
return (x < 0 || y < 0 || x >= mTaskWidth || y >= mTaskHeight) ? ctrlType : 0;
}
// If corner input is not within appropriate distance of corner radius, do not use it.
// If input is not on a corner or is within valid distance, return ctrlType.
@DragPositioningCallback.CtrlType
private int checkDistanceFromCenter(@DragPositioningCallback.CtrlType int ctrlType,
float x, float y) {
int centerX;
int centerY;
// Determine center of rounded corner circle; this is simply the corner if radius is 0.
switch (ctrlType) {
case CTRL_TYPE_LEFT | CTRL_TYPE_TOP: {
centerX = mTaskCornerRadius;
centerY = mTaskCornerRadius;
break;
}
case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM: {
centerX = mTaskCornerRadius;
centerY = mTaskHeight - mTaskCornerRadius;
break;
}
case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP: {
centerX = mTaskWidth - mTaskCornerRadius;
centerY = mTaskCornerRadius;
break;
}
case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM: {
centerX = mTaskWidth - mTaskCornerRadius;
centerY = mTaskHeight - mTaskCornerRadius;
break;
}
default: {
throw new IllegalArgumentException("ctrlType should be complex, but it's 0x"
+ Integer.toHexString(ctrlType));
}
}
double distanceFromCenter = Math.hypot(x - centerX, y - centerY);
if (distanceFromCenter < mTaskCornerRadius + mResizeHandleThickness
&& distanceFromCenter >= mTaskCornerRadius) {
return ctrlType;
}
return 0;
}
@DragPositioningCallback.CtrlType
private int calculateCornersCtrlType(float x, float y) {
int xi = (int) x;
int yi = (int) y;
if (mLeftTopCornerBounds.contains(xi, yi)) {
return CTRL_TYPE_LEFT | CTRL_TYPE_TOP;
}
if (mLeftBottomCornerBounds.contains(xi, yi)) {
return CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM;
}
if (mRightTopCornerBounds.contains(xi, yi)) {
return CTRL_TYPE_RIGHT | CTRL_TYPE_TOP;
}
if (mRightBottomCornerBounds.contains(xi, yi)) {
return CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM;
}
return 0;
}
private void updateCursorType(float x, float y) {
@DragPositioningCallback.CtrlType int ctrlType = calculateResizeHandlesCtrlType(x, y);
int cursorType = PointerIcon.TYPE_DEFAULT;
switch (ctrlType) {
case CTRL_TYPE_LEFT:
case CTRL_TYPE_RIGHT:
cursorType = PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
break;
case CTRL_TYPE_TOP:
case CTRL_TYPE_BOTTOM:
cursorType = PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
break;
case CTRL_TYPE_LEFT | CTRL_TYPE_TOP:
case CTRL_TYPE_RIGHT | CTRL_TYPE_BOTTOM:
cursorType = PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
break;
case CTRL_TYPE_LEFT | CTRL_TYPE_BOTTOM:
case CTRL_TYPE_RIGHT | CTRL_TYPE_TOP:
cursorType = PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
break;
}
// Only update the cursor type to default once so that views behind the decor container
// layer that aren't in the active resizing regions have chances to update the cursor
// type. We would like to enforce the cursor type by setting the cursor type multilple
// times in active regions because we shouldn't allow the views behind to change it, as
// we'll pilfer the gesture initiated in this area. This is necessary because 1) we
// should allow the views behind regions only for touches to set the cursor type; and 2)
// there is a small region out of each rounded corner that's inside the task bounds,
// where views in the task can receive input events because we can't set touch regions
// of input sinks to have rounded corners.
if (mLastCursorType != cursorType || cursorType != PointerIcon.TYPE_DEFAULT) {
mInputManager.setPointerIconType(cursorType);
mLastCursorType = cursorType;
}
}
}
}