Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f5751858 authored by Garfield Tan's avatar Garfield Tan
Browse files

Support moving and resizing freeform windows

Instead of an InputMonitor for the entire display, we use window
decoration surface to monitor window resizing gestures. This allows us
to consolidate hit test to InputFlinger.

There is an issue that gesture monitor for gesture nav steals touches
from the resize window while resizing if user moves pointers too fast.

Bug: 165794880
Test: Drag resize works.
Change-Id: I2685e074dd02af55928d20a93f7c0690cc498fc5
parent 2d3382a8
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -137,11 +137,13 @@ public class WMShellModule {
    @Provides
    static WindowDecorViewModel<?> provideWindowDecorViewModel(
            Context context,
            @ShellMainThread Handler mainHandler,
            ShellTaskOrganizer taskOrganizer,
            DisplayController displayController,
            SyncTransactionQueue syncQueue) {
        return new CaptionWindowDecorViewModel(
                        context,
                        mainHandler,
                        taskOrganizer,
                        displayController,
                        syncQueue);
+40 −2
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.app.ActivityManager;
import android.app.ActivityManager.RunningTaskInfo;
import android.app.ActivityTaskManager;
import android.content.Context;
import android.os.Handler;
import android.view.MotionEvent;
import android.view.SurfaceControl;
import android.view.View;
@@ -43,15 +44,18 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption
    private final ActivityTaskManager mActivityTaskManager;
    private final ShellTaskOrganizer mTaskOrganizer;
    private final Context mContext;
    private final Handler mMainHandler;
    private final DisplayController mDisplayController;
    private final SyncTransactionQueue mSyncQueue;

    public CaptionWindowDecorViewModel(
            Context context,
            Handler mainHandler,
            ShellTaskOrganizer taskOrganizer,
            DisplayController displayController,
            SyncTransactionQueue syncQueue) {
        mContext = context;
        mMainHandler = mainHandler;
        mActivityTaskManager = mContext.getSystemService(ActivityTaskManager.class);
        mTaskOrganizer = taskOrganizer;
        mDisplayController = displayController;
@@ -67,9 +71,13 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption
                mTaskOrganizer,
                taskInfo,
                taskSurface,
                mMainHandler,
                mSyncQueue);
        CaptionTouchEventListener touchEventListener = new CaptionTouchEventListener(taskInfo);
        TaskPositioner taskPositioner = new TaskPositioner(mTaskOrganizer, windowDecoration);
        CaptionTouchEventListener touchEventListener =
                new CaptionTouchEventListener(taskInfo, taskPositioner);
        windowDecoration.setCaptionListeners(touchEventListener, touchEventListener);
        windowDecoration.setDragResizeCallback(taskPositioner);
        onTaskInfoChanged(taskInfo, windowDecoration);
        return windowDecoration;
    }
@@ -87,10 +95,15 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption

        private final int mTaskId;
        private final WindowContainerToken mTaskToken;
        private final DragResizeCallback mDragResizeCallback;

        private CaptionTouchEventListener(RunningTaskInfo taskInfo) {
        private int mDragPointerId = -1;

        private CaptionTouchEventListener(
                RunningTaskInfo taskInfo, DragResizeCallback dragResizeCallback) {
            mTaskId = taskInfo.taskId;
            mTaskToken = taskInfo.token;
            mDragResizeCallback = dragResizeCallback;
        }

        @Override
@@ -120,6 +133,8 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption
            if (v.getId() != R.id.caption) {
                return false;
            }
            handleEventForMove(e);

            if (e.getAction() != MotionEvent.ACTION_DOWN) {
                return false;
            }
@@ -132,5 +147,28 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel<Caption
            mSyncQueue.queue(wct);
            return true;
        }

        private void handleEventForMove(MotionEvent e) {
            switch (e.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
                    mDragPointerId  = e.getPointerId(0);
                    mDragResizeCallback.onDragResizeStart(
                            0 /* ctrlType */, e.getRawX(0), e.getRawY(0));
                    break;
                case MotionEvent.ACTION_MOVE: {
                    int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                    mDragResizeCallback.onDragResizeMove(
                            e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                    break;
                }
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL: {
                    int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                    mDragResizeCallback.onDragResizeEnd(
                            e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                    break;
                }
            }
        }
    }
}
+57 −1
Original line number Diff line number Diff line
@@ -17,12 +17,14 @@
package com.android.wm.shell.windowdecor;

import android.app.ActivityManager;
import android.app.WindowConfiguration;
import android.content.Context;
import android.content.res.ColorStateList;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.GradientDrawable;
import android.graphics.drawable.VectorDrawable;
import android.os.Handler;
import android.view.SurfaceControl;
import android.view.View;
import android.window.WindowContainerTransaction;
@@ -48,13 +50,22 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
    // The thickness of shadows of a window that doesn't have focus in DIP.
    private static final int DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP = 5;

    // Height of button (32dp)  + 2 * margin (5dp each)
    private static final int DECOR_CAPTION_HEIGHT_IN_DIP = 42;
    private static final int RESIZE_HANDLE_IN_DIP = 30;

    private static final Rect EMPTY_OUTSET = new Rect();
    private static final Rect RESIZE_HANDLE_OUTSET = new Rect(
            RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP, RESIZE_HANDLE_IN_DIP);

    private final Handler mHandler;
    private final SyncTransactionQueue mSyncQueue;

    private View.OnClickListener mOnCaptionButtonClickListener;
    private View.OnTouchListener mOnCaptionTouchListener;
    private DragResizeCallback mDragResizeCallback;

    private DragResizeInputListener mDragResizeListener;

    private final WindowDecoration.RelayoutResult<WindowDecorLinearLayout> mResult =
            new WindowDecoration.RelayoutResult<>();
@@ -65,9 +76,11 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
            ShellTaskOrganizer taskOrganizer,
            ActivityManager.RunningTaskInfo taskInfo,
            SurfaceControl taskSurface,
            Handler handler,
            SyncTransactionQueue syncQueue) {
        super(context, displayController, taskOrganizer, taskInfo, taskSurface);

        mHandler = handler;
        mSyncQueue = syncQueue;
    }

@@ -78,15 +91,24 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
        mOnCaptionTouchListener = onCaptionTouchListener;
    }

    void setDragResizeCallback(DragResizeCallback dragResizeCallback) {
        mDragResizeCallback = dragResizeCallback;
    }

    void relayout(ActivityManager.RunningTaskInfo taskInfo) {
        final int shadowRadiusDp = taskInfo.isFocused
                ? DECOR_SHADOW_FOCUSED_THICKNESS_IN_DIP : DECOR_SHADOW_UNFOCUSED_THICKNESS_IN_DIP;
        final boolean isFreeform = mTaskInfo.configuration.windowConfiguration.getWindowingMode()
                == WindowConfiguration.WINDOWING_MODE_FREEFORM;
        final boolean isDragResizeable = isFreeform && mTaskInfo.isResizeable;
        final Rect outset = isDragResizeable ? RESIZE_HANDLE_OUTSET : EMPTY_OUTSET;

        WindowDecorLinearLayout oldRootView = mResult.mRootView;
        final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
        final SurfaceControl.Transaction t = new SurfaceControl.Transaction();
        final WindowContainerTransaction wct = new WindowContainerTransaction();
        relayout(taskInfo, R.layout.caption_window_decoration, oldRootView,
                DECOR_CAPTION_HEIGHT_IN_DIP, EMPTY_OUTSET, shadowRadiusDp, t, wct, mResult);
                DECOR_CAPTION_HEIGHT_IN_DIP, outset, shadowRadiusDp, t, wct, mResult);
        taskInfo = null; // Clear it just in case we use it accidentally

        mSyncQueue.runInSync(transaction -> {
@@ -97,11 +119,31 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
        });

        if (mResult.mRootView == null) {
            // This means the task is hidden. Nothing is set up in this case including the
            // decoration surface.
            return;
        }
        if (oldRootView != mResult.mRootView) {
            setupRootView();
        }

        if (!isDragResizeable) {
            closeDragResizeListener();
            return;
        }

        if (oldDecorationSurface != mDecorationContainerSurface) {
            closeDragResizeListener();
            mDragResizeListener = new DragResizeInputListener(
                    mContext,
                    mHandler,
                    mDisplay.getDisplayId(),
                    mDecorationContainerSurface,
                    mDragResizeCallback);
        }

        mDragResizeListener.setGeometry(
                mResult.mWidth, mResult.mHeight, (int) (mResult.mDensity * RESIZE_HANDLE_IN_DIP));
    }

    /**
@@ -140,4 +182,18 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
        VectorDrawable closeBackground = (VectorDrawable) close.getBackground();
        closeBackground.setTintList(buttonTintColor);
    }

    private void closeDragResizeListener() {
        if (mDragResizeListener == null) {
            return;
        }
        mDragResizeListener.close();
        mDragResizeListener = null;
    }

    @Override
    public void close() {
        closeDragResizeListener();
        super.close();
    }
}
+46 −0
Original line number Diff line number Diff line
/*
 * 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;

/**
 * Callback called when receiving drag-resize or drag-move related input events.
 */
public interface DragResizeCallback {
    /**
     * Called when a drag resize starts.
     *
     * @param ctrlType {@link TaskPositioner.CtrlType} indicating the direction of resizing, use
     *                 {@code 0} to indicate it's a move
     * @param x x coordinate in window decoration coordinate system where the drag resize starts
     * @param y y coordinate in window decoration coordinate system where the drag resize starts
     */
    void onDragResizeStart(@TaskPositioner.CtrlType int ctrlType, float x, float y);

    /**
     * Called when the pointer moves during a drag resize.
     * @param x x coordinate in window decoration coordinate system of the new pointer location
     * @param y y coordinate in window decoration coordinate system of the new pointer location
     */
    void onDragResizeMove(float x, float y);

    /**
     * Called when a drag resize stops.
     * @param x x coordinate in window decoration coordinate system where the drag resize stops
     * @param y y coordinate in window decoration coordinate system where the drag resize stops
     */
    void onDragResizeEnd(float x, float y);
}
+288 −0
Original line number Diff line number Diff line
/*
 * 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.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY;
import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION;

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.Looper;
import android.os.RemoteException;
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.WindowManagerGlobal;

import com.android.internal.view.BaseIWindow;

/**
 * An input event listener registered to InputDispatcher to receive input events on task edges and
 * convert them to drag resize requests.
 */
class DragResizeInputListener implements AutoCloseable {
    private static final String TAG = "DragResizeInputListener";

    private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession();
    private final Handler mHandler;
    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 com.android.wm.shell.windowdecor.DragResizeCallback mCallback;

    private int mWidth;
    private int mHeight;
    private int mResizeHandleThickness;

    private int mDragPointerId = -1;

    DragResizeInputListener(
            Context context,
            Handler handler,
            int displayId,
            SurfaceControl decorationSurface,
            DragResizeCallback callback) {
        mInputManager = context.getSystemService(InputManager.class);
        mHandler = handler;
        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,
                    new SurfaceControl(mDecorationSurface, TAG),
                    mFakeWindow,
                    null /* hostInputToken */,
                    FLAG_NOT_FOCUSABLE,
                    PRIVATE_FLAG_TRUSTED_OVERLAY,
                    TYPE_APPLICATION,
                    mFocusGrantToken,
                    TAG + " of " + decorationSurface.toString(),
                    mInputChannel);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }

        mInputEventReceiver = new TaskResizeInputEventReceiver(mInputChannel, mHandler.getLooper());
        mCallback = callback;
    }

    /**
     * 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 width The width of the drag resize handler in pixels, including resize handle
     *              thickness. That is task width + 2 * resize handle thickness.
     * @param height The height of the drag resize handler in pixels, including resize handle
     *               thickness. That is task height + 2 * resize handle thickness.
     * @param resizeHandleThickness The thickness of the resize handle in pixels.
     */
    void setGeometry(int width, int height, int resizeHandleThickness) {
        if (mWidth == width && mHeight == height
                && mResizeHandleThickness == resizeHandleThickness) {
            return;
        }

        mWidth = width;
        mHeight = height;
        mResizeHandleThickness = resizeHandleThickness;

        Region touchRegion = new Region();
        final Rect topInputBounds = new Rect(0, 0, mWidth, mResizeHandleThickness);
        touchRegion.union(topInputBounds);

        final Rect leftInputBounds = new Rect(0, mResizeHandleThickness,
                mResizeHandleThickness, mHeight - mResizeHandleThickness);
        touchRegion.union(leftInputBounds);

        final Rect rightInputBounds = new Rect(
                mWidth - mResizeHandleThickness, mResizeHandleThickness,
                mWidth, mHeight - mResizeHandleThickness);
        touchRegion.union(rightInputBounds);

        final Rect bottomInputBounds = new Rect(0, mHeight - mResizeHandleThickness,
                mWidth, mHeight);
        touchRegion.union(bottomInputBounds);

        try {
            mWindowSession.updateInputChannel(
                    mInputChannel.getToken(),
                    mDisplayId,
                    new SurfaceControl(
                            mDecorationSurface, "DragResizeInputListener#setTouchRegion"),
                    FLAG_NOT_FOCUSABLE,
                    PRIVATE_FLAG_TRUSTED_OVERLAY,
                    touchRegion);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }

        // This marks all relevant components have handled the previous resize event and can take
        // the next one now.
        mInputEventReceiver.onHandledLastResizeEvent();
    }

    @Override
    public void close() {
        mInputChannel.dispose();
        try {
            mWindowSession.remove(mFakeWindow);
        } catch (RemoteException e) {
            e.rethrowFromSystemServer();
        }
    }

    private class TaskResizeInputEventReceiver extends InputEventReceiver {
        private boolean mWaitingForLastResizeEventHandled;

        private TaskResizeInputEventReceiver(InputChannel inputChannel, Looper looper) {
            super(inputChannel, looper);
        }

        private void onHandledLastResizeEvent() {
            mWaitingForLastResizeEventHandled = false;
            consumeBatchedInputEvents(-1);
        }

        @Override
        public void onBatchedInputEventPending(int source) {
            // InputEventReceiver keeps continuous move events in a batched event until explicitly
            // consuming it or an incompatible event shows up (likely an up event in this case). We
            // continue to keep move events in the next batched event until we receive a geometry
            // update so that we don't put too much pressure on the framework with excessive number
            // of input events if it can't handle them fast enough. It's more responsive to always
            // resize the task to the latest received coordinates.
            if (!mWaitingForLastResizeEventHandled) {
                consumeBatchedInputEvents(-1);
            }
        }

        @Override
        public void onInputEvent(InputEvent inputEvent) {
            finishInputEvent(inputEvent, handleInputEvent(inputEvent));
        }

        private boolean handleInputEvent(InputEvent inputEvent) {
            if (!(inputEvent instanceof MotionEvent)) {
                return false;
            }

            MotionEvent e = (MotionEvent) inputEvent;
            switch (e.getActionMasked()) {
                case MotionEvent.ACTION_DOWN: {
                    mDragPointerId = e.getPointerId(0);
                    mCallback.onDragResizeStart(
                            calculateCtrlType(e.getX(0), e.getY(0)), e.getRawX(0), e.getRawY(0));
                    mWaitingForLastResizeEventHandled = false;
                    break;
                }
                case MotionEvent.ACTION_MOVE: {
                    int dragPointerIndex = e.findPointerIndex(mDragPointerId);
                    mCallback.onDragResizeMove(
                            e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
                    mWaitingForLastResizeEventHandled = true;
                    break;
                }
                case MotionEvent.ACTION_UP:
                case MotionEvent.ACTION_CANCEL: {
                    int dragPointerIndex = e.findPointerIndex(mDragPointerId);
                    mCallback.onDragResizeEnd(
                            e.getRawX(dragPointerIndex), e.getRawY(dragPointerIndex));
                    mWaitingForLastResizeEventHandled = false;
                    mDragPointerId = -1;
                    break;
                }
                case MotionEvent.ACTION_HOVER_ENTER:
                case MotionEvent.ACTION_HOVER_MOVE: {
                    updateCursorType(e.getXCursorPosition(), e.getYCursorPosition());
                    break;
                }
                case MotionEvent.ACTION_HOVER_EXIT:
                    mInputManager.setPointerIconType(PointerIcon.TYPE_DEFAULT);
                    break;
            }
            return true;
        }

        @TaskPositioner.CtrlType
        private int calculateCtrlType(float x, float y) {
            int ctrlType = 0;
            if (x < mResizeHandleThickness) {
                ctrlType |= TaskPositioner.CTRL_TYPE_LEFT;
            }
            if (x > mWidth - mResizeHandleThickness) {
                ctrlType |= TaskPositioner.CTRL_TYPE_RIGHT;
            }
            if (y < mResizeHandleThickness) {
                ctrlType |= TaskPositioner.CTRL_TYPE_TOP;
            }
            if (y > mHeight - mResizeHandleThickness) {
                ctrlType |= TaskPositioner.CTRL_TYPE_BOTTOM;
            }
            return ctrlType;
        }

        private void updateCursorType(float x, float y) {
            @TaskPositioner.CtrlType int ctrlType = calculateCtrlType(x, y);

            int cursorType = PointerIcon.TYPE_DEFAULT;
            switch (ctrlType) {
                case TaskPositioner.CTRL_TYPE_LEFT:
                case TaskPositioner.CTRL_TYPE_RIGHT:
                    cursorType = PointerIcon.TYPE_HORIZONTAL_DOUBLE_ARROW;
                    break;
                case TaskPositioner.CTRL_TYPE_TOP:
                case TaskPositioner.CTRL_TYPE_BOTTOM:
                    cursorType = PointerIcon.TYPE_VERTICAL_DOUBLE_ARROW;
                    break;
                case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_TOP:
                case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_BOTTOM:
                    cursorType = PointerIcon.TYPE_TOP_LEFT_DIAGONAL_DOUBLE_ARROW;
                    break;
                case TaskPositioner.CTRL_TYPE_LEFT | TaskPositioner.CTRL_TYPE_BOTTOM:
                case TaskPositioner.CTRL_TYPE_RIGHT | TaskPositioner.CTRL_TYPE_TOP:
                    cursorType = PointerIcon.TYPE_TOP_RIGHT_DIAGONAL_DOUBLE_ARROW;
                    break;
            }
            mInputManager.setPointerIconType(cursorType);
        }
    }
}
Loading