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

Commit 1d58d5a3 authored by Hiroki Sato's avatar Hiroki Sato
Browse files

Prevent handling touchpad gesture on desktop window decorations

When a user is performing a touchpad gesture, e.g. two finger swipe,
MotionEvent contains classification tag. When there's a such tag, we
should not respond to the synthesized finger movements.

Bug: 393330032
Test: DesktopModeWindowDecorViewModelTests
Test: DragDetectorTest
Test: Manually try touchpad gesture on window edges and caption bar.
Flag: EXEMPT bug fix
Change-Id: I93fa5684562a2b4ee8c0eed7811a868f0e7bd29f
parent 6f92689b
Loading
Loading
Loading
Loading
+6 −0
Original line number Diff line number Diff line
@@ -155,6 +155,7 @@ import com.android.wm.shell.windowdecor.common.WindowDecorationGestureExclusionT
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost;
import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier;
import com.android.wm.shell.windowdecor.extension.InsetsStateKt;
import com.android.wm.shell.windowdecor.extension.MotionEventKt;
import com.android.wm.shell.windowdecor.extension.TaskInfoKt;
import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel;
import com.android.wm.shell.windowdecor.tiling.SnapEventHandler;
@@ -1187,6 +1188,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel,
                    && id != R.id.maximize_window && id != R.id.minimize_window) {
                return false;
            }
            if (MotionEventKt.isTouchpadGesture(e)) {
                // Touchpad finger gestures are ignored.
                return false;
            }

            final boolean isAppHandle = !taskInfo.isFreeform();
            final int actionMasked = e.getActionMasked();
            final boolean isDown = actionMasked == MotionEvent.ACTION_DOWN;
+7 −0
Original line number Diff line number Diff line
@@ -34,6 +34,8 @@ import android.view.View;

import androidx.annotation.Nullable;

import com.android.wm.shell.windowdecor.extension.MotionEventKt;

/**
 * A detector for touch inputs that differentiates between drag and click inputs. It receives a flow
 * of {@link MotionEvent} and generates a new flow of motion events with slop in consideration to
@@ -90,6 +92,11 @@ public class DragDetector {
     * {@link #mEventHandler} handles the previous down event if the event shouldn't be passed
     */
    public boolean onMotionEvent(View v, MotionEvent ev) {
        if (MotionEventKt.isTouchpadGesture(ev)) {
            // Touchpad finger gestures are ignored.
            return false;
        }

        final boolean isTouchScreen =
                (ev.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN;
        if (!isTouchScreen) {
+32 −0
Original line number Diff line number Diff line
/*
 * Copyright 2025 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.extension

import android.view.InputDevice
import android.view.MotionEvent

/**
 * Checks if the [MotionEvent] is a part of a synthesized touchpad gesture.
 *
 * @return true if the [MotionEvent] is a touchpad gesture, false otherwise.
 */
fun MotionEvent.isTouchpadGesture(): Boolean =
    source == InputDevice.SOURCE_MOUSE
        && getToolType(0) == MotionEvent.TOOL_TYPE_FINGER
        && (classification == MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
        || classification == MotionEvent.CLASSIFICATION_PINCH
        || classification == MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE)
+86 −0
Original line number Diff line number Diff line
@@ -44,6 +44,7 @@ import android.testing.AndroidTestingRunner
import android.testing.TestableLooper.RunWithLooper
import android.view.Display.DEFAULT_DISPLAY
import android.view.ISystemGestureExclusionListener
import android.view.InputDevice
import android.view.InsetsSource
import android.view.InsetsState
import android.view.KeyEvent
@@ -66,6 +67,7 @@ import com.android.wm.shell.desktopmode.DesktopImmersiveController
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger
import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.UnminimizeReason
import com.android.wm.shell.desktopmode.DesktopTasksController
import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition
import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction
@@ -1061,6 +1063,54 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
        assertThat(hierarchyOp.container).isEqualTo(decor.mTaskInfo.token.asBinder())
    }

    @Test
    fun testOnTouchWithClassification_doesNothing() {
        val onClickListenerCaptor = argumentCaptor<View.OnClickListener>()
        val onTouchListenerCaptor = argumentCaptor<View.OnTouchListener>()
        val decor = createOpenTaskDecoration(
            windowingMode = WINDOWING_MODE_FREEFORM,
            onCaptionButtonClickListener = onClickListenerCaptor,
            onCaptionButtonTouchListener = onTouchListenerCaptor,
        )

        val view = mock<View> {
            on { id } doReturn R.id.desktop_mode_caption
        }

        val onTouchListener = onTouchListenerCaptor.firstValue
        assertFalse(
            onTouchListener.onTouch(
                view,
                createMotionEvent(
                    MotionEvent.ACTION_DOWN,
                    x = 0f,
                    y = 0f,
                    source = InputDevice.SOURCE_MOUSE,
                    classification = MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
                )
            )
        )

        verify(mockDesktopTasksController, never()).moveTaskToFront(
            anyOrNull<RunningTaskInfo>(),
            anyOrNull(),
            anyOrNull<UnminimizeReason>(),
        )

        assertFalse(
            onTouchListener.onTouch(
                view,
                createMotionEvent(
                    MotionEvent.ACTION_UP,
                    x = 0f,
                    y = 0f,
                    source = InputDevice.SOURCE_MOUSE,
                    classification = MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
                )
            )
        )
    }

    @Test
    @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP)
    fun testImmersiveRestoreButtonClick_exitsImmersiveMode() {
@@ -1604,6 +1654,42 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest
        )
    }

    private fun createMotionEvent(
        action: Int,
        x: Float = 0f,
        y: Float = 0f,
        source: Int = InputDevice.SOURCE_TOUCHSCREEN,
        classification: Int = MotionEvent.CLASSIFICATION_NONE,
    ): MotionEvent {
        val pointerProperties = arrayOf(MotionEvent.PointerProperties().apply {
            this.id = 0
            this.toolType = MotionEvent.TOOL_TYPE_FINGER
        })
        val pointerCoords = arrayOf(MotionEvent.PointerCoords().apply {
            this.x = x
            this.y = y
        })
        val ev = MotionEvent.obtain(
            /* downTime= */ SystemClock.uptimeMillis(),
            /* eventTime= */ SystemClock.uptimeMillis(),
            action,
            /* pointerCount= */ 1,
            pointerProperties,
            pointerCoords,
            /* metaState= */ 0,
            /* buttonState= */ 0,
            /* xPrecision= */ 0f,
            /* yPrecision= */ 0f,
            /* deviceId= */ 0,
            /* edgeFlags= */ 0,
            source,
            /* displayId= */ 0,
            /* flags= */ 0,
            classification,
        )!!
        return ev
    }

    private companion object {
        const val SECOND_DISPLAY = 2
        private val BOUNDS_AFTER_FIRST_MOVE = Rect(10, 10, 110, 110)
+57 −6
Original line number Diff line number Diff line
@@ -18,8 +18,8 @@ package com.android.wm.shell.windowdecor

import android.os.SystemClock
import android.testing.AndroidTestingRunner
import android.view.MotionEvent
import android.view.InputDevice
import android.view.MotionEvent
import androidx.test.filters.SmallTest
import com.android.wm.shell.ShellTestCase
import org.junit.After
@@ -29,12 +29,12 @@ import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.MockitoAnnotations
import org.mockito.Mockito.`when`
import org.mockito.Mockito.any
import org.mockito.Mockito.argThat
import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
import org.mockito.kotlin.times

/**
@@ -467,16 +467,67 @@ class DragDetectorTest : ShellTestCase() {
            .handleMotionEvent(any(), argThat { ev -> ev.action == MotionEvent.ACTION_MOVE })
    }

    @Test
    fun testEventWithMotionClassification_doesNothing() {
        val dragDetector = createDragDetector()
        assertFalse(
            dragDetector.onMotionEvent(
                createMotionEvent(
                    MotionEvent.ACTION_DOWN,
                    isTouch = false,
                    classification = MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
                )
            )
        )
        verify(eventHandler, never()).handleMotionEvent(any(), any())

        assertFalse(
            dragDetector.onMotionEvent(
                createMotionEvent(
                    MotionEvent.ACTION_UP,
                    isTouch = false,
                    classification = MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE
                )
            )
        )
        verify(eventHandler, never()).handleMotionEvent(any(), any())
    }

    private fun createMotionEvent(
        action: Int,
        x: Float = X,
        y: Float = Y,
        isTouch: Boolean = true,
        downTime: Long = SystemClock.uptimeMillis(),
        eventTime: Long = SystemClock.uptimeMillis()
        eventTime: Long = SystemClock.uptimeMillis(),
        classification: Int = MotionEvent.CLASSIFICATION_NONE,
    ): MotionEvent {
        val ev = MotionEvent.obtain(downTime, eventTime, action, x, y, 0)
        ev.source = if (isTouch) InputDevice.SOURCE_TOUCHSCREEN else InputDevice.SOURCE_MOUSE
        val pointerProperties = arrayOf(MotionEvent.PointerProperties().apply {
            this.id = 0
            this.toolType = MotionEvent.TOOL_TYPE_FINGER
        })
        val pointerCoords = arrayOf(MotionEvent.PointerCoords().apply {
            this.x = x
            this.y = y
        })
        val ev = MotionEvent.obtain(
            downTime,
            eventTime,
            action,
            /* pointerCount= */ 1,
            pointerProperties,
            pointerCoords,
            /* metaState= */ 0,
            /* buttonState= */ 0,
            /* xPrecision= */ 0f,
            /* yPrecision= */ 0f,
            /* deviceId= */ 0,
            /* edgeFlags= */ 0,
            if (isTouch) InputDevice.SOURCE_TOUCHSCREEN else InputDevice.SOURCE_MOUSE,
            /* displayId= */ 0,
            /* flags= */ 0,
            classification,
        )!!
        motionEvents.add(ev)
        return ev
    }