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

Commit 11334d38 authored by Qijing Yao's avatar Qijing Yao Committed by Android (Google) Code Review
Browse files

Merge "Block window drag to non-desktop-mode displays" into main

parents 9a42e3af 9461adee
Loading
Loading
Loading
Loading
+59 −8
Original line number Diff line number Diff line
@@ -71,6 +71,7 @@ import android.view.InputEventReceiver;
import android.view.InputMonitor;
import android.view.InsetsState;
import android.view.MotionEvent;
import android.view.PointerIcon;
import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
import android.view.View;
@@ -1037,7 +1038,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
        mDesktopTilingDecorViewModel.onDeskRemoved(deskId);
    }

    private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
    @VisibleForTesting
    public class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener
            implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener,
            View.OnGenericMotionListener, DragDetector.MotionEventHandler {
        private static final long APP_HANDLE_HOLD_TO_DRAG_DURATION_MS = 100;
@@ -1051,6 +1053,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
        private final GestureDetector mGestureDetector;
        private final int mDisplayId;
        private final Rect mOnDragStartInitialBounds = new Rect();
        private final Rect mCurrentBounds = new Rect();

        /**
         * Whether to pilfer the next motion event to send cancellations to the windows below.
@@ -1067,6 +1070,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
        private boolean mLongClickDisabled;
        private int mDragPointerId = -1;
        private MotionEvent mMotionEvent;
        private int mCurrentPointerIconType = PointerIcon.TYPE_ARROW;

        private DesktopModeTouchEventListener(
                RunningTaskInfo taskInfo,
@@ -1357,6 +1361,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
                                e.getRawY(0));
                        updateDragStatus(decoration, e);
                        mOnDragStartInitialBounds.set(initialBounds);
                        mCurrentBounds.set(initialBounds);
                    }
                    // Do not consume input event if a button is touched, otherwise it would
                    // prevent the button's ripple effect from showing.
@@ -1372,17 +1377,36 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
                        mDragPointerId = e.getPointerId(0);
                    }
                    final int dragPointerIdx = e.findPointerIndex(mDragPointerId);
                    final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningMove(

                    if (DesktopExperienceFlags
                            .ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX.isTrue()) {
                        final boolean inDesktopModeDisplay = isDisplayInDesktopMode(
                                e.getDisplayId());
                        // TODO: b/418651425 - Use a more specific pointer icon when available.
                        updatePointerIcon(e, dragPointerIdx, v.getViewRootImpl().getInputToken(),
                                inDesktopModeDisplay ? PointerIcon.TYPE_ARROW
                                        : PointerIcon.TYPE_NO_DROP);
                        // Allow bounds update only when cursor is on desktop-mode displays.
                        // Otherwise, ignore the MOVE event and the window holds its current bounds.
                        if (inDesktopModeDisplay) {
                            mCurrentBounds.set(mDragPositioningCallback.onDragPositioningMove(
                                    e.getDisplayId(),
                            e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));
                                    e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)));
                        }
                    } else {
                        mCurrentBounds.set(mDragPositioningCallback.onDragPositioningMove(
                                e.getDisplayId(),
                                e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)));
                    }

                    mDesktopTasksController.onDragPositioningMove(taskInfo,
                            decoration.mTaskSurface,
                            decoration.getLeash(),
                            e.getDisplayId(),
                            e.getRawX(dragPointerIdx),
                            e.getRawY(dragPointerIdx),
                            newTaskBounds);
                            mCurrentBounds);
                    //  Flip mIsDragging only if the bounds actually changed.
                    if (mIsDragging || !newTaskBounds.equals(mOnDragStartInitialBounds)) {
                    if (mIsDragging || !mCurrentBounds.equals(mOnDragStartInitialBounds)) {
                        updateDragStatus(decoration, e);
                    }
                    return true;
@@ -1402,11 +1426,23 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
                    final Rect newTaskBounds = mDragPositioningCallback.onDragPositioningEnd(
                            e.getDisplayId(),
                            e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx));

                    if (DesktopExperienceFlags
                            .ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX.isTrue()) {
                        updatePointerIcon(e, dragPointerIdx, v.getViewRootImpl().getInputToken(),
                                PointerIcon.TYPE_ARROW);
                        // If the cursor ends on a non-desktop-mode display, revert the window
                        // to its initial bounds prior to the drag starting.
                        if (!isDisplayInDesktopMode(e.getDisplayId())) {
                            newTaskBounds.set(mOnDragStartInitialBounds);
                        }
                    }

                    // Tasks bounds haven't actually been updated (only its leash), so pass to
                    // DesktopTasksController to allow secondary transformations (i.e. snap resizing
                    // or transforming to fullscreen) before setting new task bounds.
                    mDesktopTasksController.onDragPositioningEnd(
                            taskInfo, decoration.mTaskSurface,
                            taskInfo, decoration.getLeash(),
                            e.getDisplayId(),
                            new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)),
                            newTaskBounds, decoration.calculateValidDragArea(),
@@ -1425,6 +1461,21 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
            return true;
        }

        private void updatePointerIcon(MotionEvent e, int dragPointerIdx, IBinder inputToken,
                int iconType) {
            if (mCurrentPointerIconType == iconType) {
                return;
            }
            mInputManager.setPointerIcon(PointerIcon.getSystemIcon(mContext, iconType),
                    e.getDisplayId(), e.getDeviceId(), e.getPointerId(dragPointerIdx), inputToken);
            mCurrentPointerIconType = iconType;
        }

        private boolean isDisplayInDesktopMode(int displayId) {
            return mDesktopState.isDesktopModeSupportedOnDisplay(displayId)
                    && mDesktopTasksController.getActiveDeskId(displayId) != null;
        }

        private void updateDragStatus(DesktopModeWindowDecoration decor, MotionEvent e) {
            switch (e.getActionMasked()) {
                case MotionEvent.ACTION_DOWN:
+201 −5
Original line number Diff line number Diff line
@@ -29,12 +29,14 @@ import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.Intent.ACTION_MAIN
import android.graphics.PointF
import android.graphics.Rect
import android.graphics.Region
import android.hardware.display.DisplayManager
import android.hardware.display.VirtualDisplay
import android.hardware.input.InputManager
import android.net.Uri
import android.os.IBinder
import android.os.SystemClock
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
@@ -46,6 +48,7 @@ import android.view.InsetsSource
import android.view.InsetsState
import android.view.KeyEvent
import android.view.MotionEvent
import android.view.PointerIcon
import android.view.Surface
import android.view.SurfaceControl
import android.view.SurfaceView
@@ -74,14 +77,13 @@ import com.android.wm.shell.util.StubTransaction
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.assertFalse
import junit.framework.Assert.assertTrue
import junit.framework.Assert.fail
import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.junit.Assume.assumeTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.Mockito.never
import org.mockito.Mockito.times
import org.mockito.kotlin.KArgumentCaptor
import org.mockito.kotlin.any
import org.mockito.kotlin.anyOrNull
@@ -90,13 +92,15 @@ import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.doNothing
import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.isNotNull
import org.mockito.kotlin.isNull
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.stub
import org.mockito.kotlin.times
import org.mockito.kotlin.verify
import org.mockito.quality.Strictness
import org.mockito.kotlin.isNotNull
import org.mockito.kotlin.isNull
import org.mockito.kotlin.whenever
import org.mockito.quality.Strictness

/**
 * Tests of [DesktopModeWindowDecorViewModel]
@@ -1320,6 +1324,194 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
        verify(mockDecoration).close()
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX)
    fun testOnFreeformWindowDragEnd_toDesktopModeDisplay_updateBounds() {
        val onTouchListenerCaptor = argumentCaptor<View.OnTouchListener>()
        val decor =
            createOpenTaskDecoration(
                windowingMode = WINDOWING_MODE_FREEFORM,
                onCaptionButtonTouchListener = onTouchListenerCaptor,
            )

        val touchListener = onTouchListenerCaptor.firstValue
        if (touchListener is DesktopModeWindowDecorViewModel.DesktopModeTouchEventListener) {
            val taskInfo = decor.mTaskInfo
            mockDesktopTasksController.stub { on { getActiveDeskId(DEFAULT_DISPLAY) } doReturn 1 }
            mockDesktopTasksController.stub { on { getActiveDeskId(SECOND_DISPLAY) } doReturn 2 }
            val mockInputToken = mock<IBinder>()
            val mockViewRootImpl = mock<ViewRootImpl> { on { inputToken } doReturn mockInputToken }
            val view = mock<View> { on { getViewRootImpl() } doReturn mockViewRootImpl }
            mockTaskPositioner.stub {
                on { onDragPositioningStart(any(), any(), any(), any()) } doReturn INITIAL_BOUNDS
                on { onDragPositioningMove(any(), any(), any()) } doReturn BOUNDS_AFTER_FIRST_MOVE
                on { onDragPositioningEnd(any(), any(), any()) } doReturn
                    BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED
            }

            touchListener.handleMotionEvent(
                view,
                MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0).apply {
                    displayId = DEFAULT_DISPLAY
                },
            )
            // ACTION_MOVE on desktop-mode display
            touchListener.handleMotionEvent(
                view,
                MotionEvent.obtain(0L, 1L, MotionEvent.ACTION_MOVE, 10f, 10f, 0).apply {
                    displayId = SECOND_DISPLAY
                },
            )

            // Verify point icon does not change and bounds changes
            verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any())
            verify(mockDesktopTasksController)
                .onDragPositioningMove(
                    eq(taskInfo),
                    any<SurfaceControl>(),
                    eq(SECOND_DISPLAY),
                    eq(10f),
                    eq(10f),
                    eq(BOUNDS_AFTER_FIRST_MOVE),
                )

            // ACTION_UP on desktop-mode display
            touchListener.handleMotionEvent(
                view,
                MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_UP, 20f, 20f, 0).apply {
                    displayId = SECOND_DISPLAY
                },
            )

            // Verify point icon does not change and bounds changes
            verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any())
            verify(mockDesktopTasksController)
                .onDragPositioningEnd(
                    eq(taskInfo),
                    any<SurfaceControl>(),
                    eq(SECOND_DISPLAY),
                    eq(PointF(20f, 20f)),
                    eq(BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED),
                    any<Rect>(),
                    any<Rect>(),
                    any<MotionEvent>(),
                )
        } else {
            fail("touchListener was not a DesktopModeTouchEventListener as expected.")
        }
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_BLOCK_NON_DESKTOP_DISPLAY_WINDOW_DRAG_BUGFIX)
    fun testOnFreeformWindowDragMove_toNonDesktopModeDisplay_setsNoDropIconAndKeepsBounds() {
        val onTouchListenerCaptor = argumentCaptor<View.OnTouchListener>()
        val decor =
            createOpenTaskDecoration(
                windowingMode = WINDOWING_MODE_FREEFORM,
                onCaptionButtonTouchListener = onTouchListenerCaptor,
            )

        val touchListener = onTouchListenerCaptor.firstValue
        if (touchListener is DesktopModeWindowDecorViewModel.DesktopModeTouchEventListener) {
            val taskInfo = decor.mTaskInfo
            mockDesktopTasksController.stub { on { getActiveDeskId(DEFAULT_DISPLAY) } doReturn 1 }
            mockDesktopTasksController.stub { on { getActiveDeskId(SECOND_DISPLAY) } doReturn null }
            val mockInputToken = mock<IBinder>()
            val mockViewRootImpl = mock<ViewRootImpl> { on { inputToken } doReturn mockInputToken }
            val view = mock<View> { on { getViewRootImpl() } doReturn mockViewRootImpl }
            mockTaskPositioner.stub {
                on { onDragPositioningStart(any(), any(), any(), any()) } doReturn INITIAL_BOUNDS
                on { onDragPositioningMove(any(), any(), any()) } doReturn BOUNDS_AFTER_FIRST_MOVE
                on { onDragPositioningEnd(any(), any(), any()) } doReturn
                    BOUNDS_IGNORED_ON_NON_DESKTOP
            }

            touchListener.handleMotionEvent(
                view,
                MotionEvent.obtain(0L, 0L, MotionEvent.ACTION_DOWN, 0f, 0f, 0).apply {
                    displayId = DEFAULT_DISPLAY
                },
            )
            // ACTION_MOVE on desktop-mode display
            touchListener.handleMotionEvent(
                view,
                MotionEvent.obtain(0L, 1L, MotionEvent.ACTION_MOVE, 10f, 10f, 0).apply {
                    displayId = DEFAULT_DISPLAY
                },
            )

            // Verify point icon does not change and bounds changes
            verify(mockInputManager, never()).setPointerIcon(any(), any(), any(), any(), any())
            verify(mockDesktopTasksController)
                .onDragPositioningMove(
                    eq(taskInfo),
                    any(),
                    eq(DEFAULT_DISPLAY),
                    eq(10f),
                    eq(10f),
                    eq(BOUNDS_AFTER_FIRST_MOVE),
                )

            // ACTION_MOVE to non-desktop-mode display
            touchListener.handleMotionEvent(
                view,
                MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_MOVE, 20f, 20f, 0).apply {
                    displayId = SECOND_DISPLAY
                },
            )

            // Verify point icon changes and bounds stays the same
            verify(mockInputManager)
                .setPointerIcon(
                    argThat { icon -> icon.type == PointerIcon.TYPE_NO_DROP },
                    eq(SECOND_DISPLAY),
                    any(),
                    eq(0),
                    eq(mockInputToken),
                )
            verify(mockDesktopTasksController)
                .onDragPositioningMove(
                    eq(taskInfo),
                    any(),
                    eq(SECOND_DISPLAY),
                    eq(20f),
                    eq(20f),
                    eq(BOUNDS_AFTER_FIRST_MOVE),
                )

            // ACTION_UP on non-desktop-mode display
            touchListener.handleMotionEvent(
                view,
                MotionEvent.obtain(0L, 2L, MotionEvent.ACTION_UP, 30f, 30f, 0).apply {
                    displayId = SECOND_DISPLAY
                },
            )

            // Verify point icon changes and bounds resets to initial bounds
            verify(mockInputManager)
                .setPointerIcon(
                    argThat { icon -> icon.type == PointerIcon.TYPE_ARROW },
                    eq(SECOND_DISPLAY),
                    any(),
                    eq(0),
                    eq(mockInputToken),
                )
            verify(mockDesktopTasksController)
                .onDragPositioningEnd(
                    eq(taskInfo),
                    any<SurfaceControl>(),
                    eq(SECOND_DISPLAY),
                    eq(PointF(30f, 30f)),
                    eq(INITIAL_BOUNDS),
                    any<Rect>(),
                    any<Rect>(),
                    any<MotionEvent>(),
                )
        } else {
            fail("touchListener was not a DesktopModeTouchEventListener as expected.")
        }
    }

    private fun createOpenTaskDecoration(
        @WindowingMode windowingMode: Int,
        taskSurface: SurfaceControl = SurfaceControl(),
@@ -1338,6 +1530,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
            windowDecorationActions
        )
        onTaskOpening(decor.mTaskInfo, taskSurface)
        decor.stub { on { leash } doReturn taskSurface }
        verify(decor).setCaptionListeners(
            onCaptionButtonClickListener.capture(), onCaptionButtonTouchListener.capture(),
            any(), any())
@@ -1363,5 +1556,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest

    private companion object {
        const val SECOND_DISPLAY = 2
        private val BOUNDS_AFTER_FIRST_MOVE = Rect(10, 10, 110, 110)
        private val BOUNDS_IGNORED_ON_NON_DESKTOP = Rect(20, 20, 120, 120)
        private val BOUNDS_ON_DRAG_END_DESKTOP_ACCEPTED = Rect(50, 50, 150, 150)
    }
}