Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +6 −0 Original line number Diff line number Diff line Loading @@ -156,6 +156,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; Loading Loading @@ -1197,6 +1198,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; Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) { Loading libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/MotionEvent.kt 0 → 100644 +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) libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +86 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,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 Loading @@ -67,6 +68,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 Loading Loading @@ -1085,6 +1087,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() { Loading Loading @@ -1628,6 +1678,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) Loading libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt +57 −6 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 /** Loading Loading @@ -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 } Loading Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +6 −0 Original line number Diff line number Diff line Loading @@ -156,6 +156,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; Loading Loading @@ -1197,6 +1198,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; Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragDetector.java +7 −0 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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) { Loading
libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/extension/MotionEvent.kt 0 → 100644 +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)
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +86 −0 Original line number Diff line number Diff line Loading @@ -45,6 +45,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 Loading @@ -67,6 +68,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 Loading Loading @@ -1085,6 +1087,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() { Loading Loading @@ -1628,6 +1678,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) Loading
libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragDetectorTest.kt +57 −6 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 /** Loading Loading @@ -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 } Loading