blob: 287d8618728859c4b02e8585afe65564adf814d4 [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.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;
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.ViewConfiguration;
import android.view.WindowManagerGlobal;
import com.android.internal.view.BaseIWindow;
/**
* 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 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 int mTaskWidth;
private int mTaskHeight;
private int mResizeHandleThickness;
private int mCornerSize;
private Rect mLeftTopCornerBounds;
private Rect mRightTopCornerBounds;
private Rect mLeftBottomCornerBounds;
private Rect mRightBottomCornerBounds;
private int mDragPointerId = -1;
private DragDetector mDragDetector;
DragResizeInputListener(
Context context,
Handler handler,
Choreographer choreographer,
int displayId,
SurfaceControl decorationSurface,
DragPositioningCallback callback) {
mInputManager = context.getSystemService(InputManager.class);
mHandler = handler;
mChoreographer = choreographer;
mDisplayId = displayId;
mDecorationSurface = decorationSurface;
// 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,
null /* hostInputToken */,
FLAG_NOT_FOCUSABLE,
PRIVATE_FLAG_TRUSTED_OVERLAY,
0 /* inputFeatures */,
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());
}
/**
* Updates geometry of this drag resize handler. Needs to be called every time there is a size
* change to notify the input event receiver it's ready to take the next input event. Otherwise
* it'll keep batching move events and the drag resize process is stalled.
*
* This is also used to update the touch regions of this handler every event dispatched here is
* a potential resize request.
*
* @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);
Region touchRegion = new Region();
final Rect topInputBounds = new Rect(
-mResizeHandleThickness,
-mResizeHandleThickness,
mTaskWidth + mResizeHandleThickness,
0);
touchRegion.union(topInputBounds);
final Rect leftInputBounds = new Rect(
-mResizeHandleThickness,
0,
0,
mTaskHeight);
touchRegion.union(leftInputBounds);
final Rect rightInputBounds = new Rect(
mTaskWidth,
0,
mTaskWidth + mResizeHandleThickness,
mTaskHeight);
touchRegion.union(rightInputBounds);
final Rect bottomInputBounds = new Rect(
-mResizeHandleThickness,
mTaskHeight,
mTaskWidth + mResizeHandleThickness,
mTaskHeight + mResizeHandleThickness);
touchRegion.union(bottomInputBounds);
// Set up touch areas in each corner.
int cornerRadius = mCornerSize / 2;
mLeftTopCornerBounds = new Rect(
-cornerRadius,
-cornerRadius,
cornerRadius,
cornerRadius);
touchRegion.union(mLeftTopCornerBounds);
mRightTopCornerBounds = new Rect(
mTaskWidth - cornerRadius,
-cornerRadius,
mTaskWidth + cornerRadius,
cornerRadius);
touchRegion.union(mRightTopCornerBounds);
mLeftBottomCornerBounds = new Rect(
-cornerRadius,
mTaskHeight - cornerRadius,
cornerRadius,
mTaskHeight + cornerRadius);
touchRegion.union(mLeftBottomCornerBounds);
mRightBottomCornerBounds = new Rect(
mTaskWidth - cornerRadius,
mTaskHeight - cornerRadius,
mTaskWidth + cornerRadius,
mTaskHeight + cornerRadius);
touchRegion.union(mRightBottomCornerBounds);
try {
mWindowSession.updateInputChannel(
mInputChannel.getToken(),
mDisplayId,
mDecorationSurface,
FLAG_NOT_FOCUSABLE,
PRIVATE_FLAG_TRUSTED_OVERLAY,
0 /* inputFeatures */,
touchRegion);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
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;
}
@Override
public void close() {
mInputEventReceiver.dispose();
mInputChannel.dispose();
try {
mWindowSession.remove(mFakeWindow);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}
private class TaskResizeInputEventReceiver extends InputEventReceiver
implements DragDetector.MotionEventHandler {
private final Choreographer mChoreographer;
private final Runnable mConsumeBatchEventRunnable;
private boolean mConsumeBatchEventScheduled;
private boolean mShouldHandleEvents;
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(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) {
mDragPointerId = e.getPointerId(0);
float rawX = e.getRawX(0);
float rawY = e.getRawY(0);
int ctrlType = calculateCtrlType(isTouch, x, y);
mCallback.onDragPositioningStart(ctrlType, rawX, rawY);
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);
mCallback.onDragPositioningMove(rawX, rawY);
result = true;
break;
}
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_CANCEL: {
if (mShouldHandleEvents) {
int dragPointerIndex = e.findPointerIndex(mDragPointerId);
mCallback.onDragPositioningEnd(
e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
}
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:
mInputManager.setPointerIconType(PointerIcon.TYPE_DEFAULT);
result = true;
break;
}
return result;
}
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;
if (x < 0) {
ctrlType |= CTRL_TYPE_LEFT;
}
if (x > mTaskWidth) {
ctrlType |= CTRL_TYPE_RIGHT;
}
if (y < 0) {
ctrlType |= CTRL_TYPE_TOP;
}
if (y > mTaskHeight) {
ctrlType |= CTRL_TYPE_BOTTOM;
}
return ctrlType;
}
@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;
}
mInputManager.setPointerIconType(cursorType);
}
}
}