diff options
10 files changed, 555 insertions, 63 deletions
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 82dafc3ed1df..5d568b78bcdc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -3577,6 +3577,8 @@ <string name="touchpad_tutorial_action_key_button">Action key</string> <!-- Label for button finishing touchpad tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_tutorial_done_button">Done</string> + <!-- Screen title after gesture was done successfully [CHAR LIMIT=NONE] --> + <string name="touchpad_tutorial_gesture_done">Great job!</string> <!-- Touchpad back gesture action name in tutorial [CHAR LIMIT=NONE] --> <string name="touchpad_back_gesture_action_title">Go back</string> <!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] --> diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/GestureViewModelFactory.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/GestureViewModelFactory.kt deleted file mode 100644 index 504bd5fbcb42..000000000000 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/GestureViewModelFactory.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C) 2024 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.systemui.touchpad.tutorial.ui - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider - -sealed class GestureTutorialViewModel : ViewModel() - -class BackGestureTutorialViewModel : GestureTutorialViewModel() - -class HomeGestureTutorialViewModel : GestureTutorialViewModel() - -class GestureViewModelFactory : ViewModelProvider.Factory { - - @Suppress("UNCHECKED_CAST") - override fun <T : ViewModel> create(modelClass: Class<T>): T { - return when (modelClass) { - BackGestureTutorialViewModel::class.java -> BackGestureTutorialViewModel() - HomeGestureTutorialViewModel::class.java -> HomeGestureTutorialViewModel() - else -> error("Unknown ViewModel class: ${modelClass.name}") - } - as T - } -} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt new file mode 100644 index 000000000000..1fa7a0c44171 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2024 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.systemui.touchpad.tutorial.ui.gesture + +import android.view.MotionEvent +import kotlin.math.abs + +/** + * Monitor for touchpad gestures that calls [gestureDoneCallback] when gesture was successfully + * done. All tracked motion events should be passed to [processTouchpadEvent] + */ +interface TouchpadGestureMonitor { + + val gestureDistanceThresholdPx: Int + val gestureDoneCallback: () -> Unit + + fun processTouchpadEvent(event: MotionEvent) +} + +class BackGestureMonitor( + override val gestureDistanceThresholdPx: Int, + override val gestureDoneCallback: () -> Unit +) : TouchpadGestureMonitor { + + private var xStart = 0f + + override fun processTouchpadEvent(event: MotionEvent) { + val action = event.actionMasked + when (action) { + MotionEvent.ACTION_DOWN -> { + if (isThreeFingerTouchpadSwipe(event)) { + xStart = event.x + } + } + MotionEvent.ACTION_UP -> { + if (isThreeFingerTouchpadSwipe(event)) { + val distance = abs(event.x - xStart) + if (distance >= gestureDistanceThresholdPx) { + gestureDoneCallback() + } + } + } + } + } + + private fun isThreeFingerTouchpadSwipe(event: MotionEvent): Boolean { + return event.classification == MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE && + event.getAxisValue(MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT) == 3f + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt new file mode 100644 index 000000000000..4ae9c7b2426c --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGesture.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2024 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.systemui.touchpad.tutorial.ui.gesture + +enum class TouchpadGesture { + BACK, + HOME; + + fun toMonitor( + swipeDistanceThresholdPx: Int, + gestureDoneCallback: () -> Unit + ): TouchpadGestureMonitor { + return when (this) { + BACK -> BackGestureMonitor(swipeDistanceThresholdPx, gestureDoneCallback) + else -> throw IllegalArgumentException("Not implemented yet") + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt new file mode 100644 index 000000000000..dc8471c3248a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt @@ -0,0 +1,50 @@ +/* + * Copyright (C) 2024 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.systemui.touchpad.tutorial.ui.gesture + +import android.view.InputDevice +import android.view.MotionEvent + +/** + * Allows listening to touchpadGesture and calling onDone when gesture was triggered. Can have all + * motion events passed to [onMotionEvent] and will filter touchpad events accordingly + */ +class TouchpadGestureHandler( + touchpadGesture: TouchpadGesture, + swipeDistanceThresholdPx: Int, + onDone: () -> Unit +) { + + private val gestureRecognition = + touchpadGesture.toMonitor(swipeDistanceThresholdPx, gestureDoneCallback = onDone) + + fun onMotionEvent(event: MotionEvent): Boolean { + // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons + val isFromTouchpad = + event.isFromSource(InputDevice.SOURCE_MOUSE) && + event.getToolType(0) == MotionEvent.TOOL_TYPE_FINGER + val buttonClick = + event.actionMasked == MotionEvent.ACTION_DOWN && + event.isButtonPressed(MotionEvent.BUTTON_PRIMARY) + return if (isFromTouchpad && !buttonClick) { + gestureRecognition.processTouchpadEvent(event) + true + } else { + false + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt index 2460761c8592..04a47985dc29 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/BackGestureTutorialScreen.kt @@ -17,9 +17,11 @@ package com.android.systemui.touchpad.tutorial.ui.view import androidx.activity.compose.BackHandler +import androidx.annotation.StringRes import androidx.compose.foundation.Image import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -31,29 +33,67 @@ import androidx.compose.foundation.layout.width import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier +import androidx.compose.ui.input.pointer.pointerInteropFilter +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK +import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler +@OptIn(ExperimentalComposeUiApi::class) @Composable fun BackGestureTutorialScreen( onDoneButtonClicked: () -> Unit, onBack: () -> Unit, - modifier: Modifier = Modifier, ) { - BackHandler { onBack() } + BackHandler(onBack = onBack) + var gestureDone by remember { mutableStateOf(false) } + val swipeDistanceThresholdPx = + with(LocalContext.current) { + resources.getDimensionPixelSize( + com.android.internal.R.dimen.system_gestures_distance_threshold + ) + } + val gestureHandler = + remember(swipeDistanceThresholdPx) { + TouchpadGestureHandler(BACK, swipeDistanceThresholdPx, onDone = { gestureDone = true }) + } + Box( + modifier = + Modifier.fillMaxSize() + // we need to use pointerInteropFilter because some info about touchpad gestures is + // only available in MotionEvent + .pointerInteropFilter(onTouchEvent = gestureHandler::onMotionEvent) + ) { + GestureTutorialContent(gestureDone, onDoneButtonClicked) + } +} + +@Composable +private fun GestureTutorialContent(gestureDone: Boolean, onDoneButtonClicked: () -> Unit) { Column( verticalArrangement = Arrangement.Center, modifier = - modifier + Modifier.fillMaxSize() .background(color = MaterialTheme.colorScheme.surfaceContainer) .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp) - .fillMaxSize() ) { Row(modifier = Modifier.fillMaxWidth().weight(1f)) { - TutorialDescription(modifier = Modifier.weight(1f)) + TutorialDescription( + titleTextId = + if (gestureDone) R.string.touchpad_tutorial_gesture_done + else R.string.touchpad_back_gesture_action_title, + bodyTextId = R.string.touchpad_back_gesture_guidance, + modifier = Modifier.weight(1f) + ) Spacer(modifier = Modifier.width(76.dp)) TutorialAnimation(modifier = Modifier.weight(1f).padding(top = 24.dp)) } @@ -62,17 +102,15 @@ fun BackGestureTutorialScreen( } @Composable -fun TutorialDescription(modifier: Modifier = Modifier) { +fun TutorialDescription( + @StringRes titleTextId: Int, + @StringRes bodyTextId: Int, + modifier: Modifier = Modifier +) { Column(verticalArrangement = Arrangement.Top, modifier = modifier) { - Text( - text = stringResource(id = R.string.touchpad_back_gesture_action_title), - style = MaterialTheme.typography.displayLarge - ) + Text(text = stringResource(id = titleTextId), style = MaterialTheme.typography.displayLarge) Spacer(modifier = Modifier.height(16.dp)) - Text( - text = stringResource(id = R.string.touchpad_back_gesture_guidance), - style = MaterialTheme.typography.bodyLarge - ) + Text(text = stringResource(id = bodyTextId), style = MaterialTheme.typography.bodyLarge) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt index 0c5c1874db8a..93c26583e11e 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt @@ -17,6 +17,7 @@ package com.android.systemui.touchpad.tutorial.ui.view import android.os.Bundle +import android.view.WindowManager import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge @@ -25,10 +26,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.lifecycle.Lifecycle.State.STARTED import androidx.lifecycle.compose.collectAsStateWithLifecycle -import androidx.lifecycle.viewmodel.compose.viewModel import com.android.compose.theme.PlatformTheme -import com.android.systemui.touchpad.tutorial.ui.GestureViewModelFactory -import com.android.systemui.touchpad.tutorial.ui.HomeGestureTutorialViewModel import com.android.systemui.touchpad.tutorial.ui.Screen.BACK_GESTURE import com.android.systemui.touchpad.tutorial.ui.Screen.HOME_GESTURE import com.android.systemui.touchpad.tutorial.ui.Screen.TUTORIAL_SELECTION @@ -47,6 +45,8 @@ constructor( super.onCreate(savedInstanceState) enableEdgeToEdge() setContent { PlatformTheme { TouchpadTutorialScreen(vm) { finish() } } } + // required to handle 3+ fingers on touchpad + window.addPrivateFlags(WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY) } override fun onResume() { @@ -74,13 +74,8 @@ fun TouchpadTutorialScreen(vm: TouchpadTutorialViewModel, closeTutorial: () -> U BACK_GESTURE -> BackGestureTutorialScreen( onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, - onBack = { vm.goTo(TUTORIAL_SELECTION) } + onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) - HOME_GESTURE -> HomeGestureTutorialScreen() + HOME_GESTURE -> {} } } - -@Composable -fun HomeGestureTutorialScreen() { - val vm = viewModel<HomeGestureTutorialViewModel>(factory = GestureViewModelFactory()) -} diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt new file mode 100644 index 000000000000..cf0db7b51676 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt @@ -0,0 +1,160 @@ +/* + * Copyright (C) 2024 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.systemui.touchpad.tutorial.ui.gesture + +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_MOVE +import android.view.MotionEvent.ACTION_POINTER_DOWN +import android.view.MotionEvent.ACTION_POINTER_UP +import android.view.MotionEvent.ACTION_UP +import android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT +import android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE +import android.view.MotionEvent.CLASSIFICATION_TWO_FINGER_SWIPE +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class BackGestureMonitorTest : SysuiTestCase() { + + private var gestureDoneWasCalled = false + private val gestureDoneCallback = { gestureDoneWasCalled = true } + private val gestureMonitor = BackGestureMonitor(SWIPE_DISTANCE.toInt(), gestureDoneCallback) + + companion object { + const val SWIPE_DISTANCE = 100f + } + + @Test + fun triggersGestureDoneForThreeFingerGestureRight() { + val events = + listOf( + threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f), + threeFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f), + threeFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f), + threeFingerEvent(ACTION_MOVE, x = SWIPE_DISTANCE / 2, y = 0f), + threeFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f), + threeFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f), + threeFingerEvent(ACTION_UP, x = SWIPE_DISTANCE, y = 0f), + ) + + events.forEach { gestureMonitor.processTouchpadEvent(it) } + + assertThat(gestureDoneWasCalled).isTrue() + } + + @Test + fun triggersGestureDoneForThreeFingerGestureLeft() { + val events = + listOf( + threeFingerEvent(ACTION_DOWN, x = SWIPE_DISTANCE, y = 0f), + threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f), + threeFingerEvent(ACTION_POINTER_DOWN, x = SWIPE_DISTANCE, y = 0f), + threeFingerEvent(ACTION_MOVE, x = SWIPE_DISTANCE / 2, y = 0f), + threeFingerEvent(ACTION_POINTER_UP, x = 0f, y = 0f), + threeFingerEvent(ACTION_POINTER_UP, x = 0f, y = 0f), + threeFingerEvent(ACTION_UP, x = 0f, y = 0f), + ) + + events.forEach { gestureMonitor.processTouchpadEvent(it) } + + assertThat(gestureDoneWasCalled).isTrue() + } + + private fun threeFingerEvent(action: Int, x: Float, y: Float): MotionEvent { + return motionEvent( + action = action, + x = x, + y = y, + classification = CLASSIFICATION_MULTI_FINGER_SWIPE, + axisValues = mapOf(AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f) + ) + } + + @Test + fun doesntTriggerGestureDone_onThreeFingersSwipeUp() { + val events = + listOf( + threeFingerEvent(ACTION_DOWN, x = 0f, y = 0f), + threeFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f), + threeFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f), + threeFingerEvent(ACTION_MOVE, x = 0f, y = SWIPE_DISTANCE / 2), + threeFingerEvent(ACTION_POINTER_UP, x = 0f, y = SWIPE_DISTANCE), + threeFingerEvent(ACTION_POINTER_UP, x = 0f, y = SWIPE_DISTANCE), + threeFingerEvent(ACTION_UP, x = 0f, y = SWIPE_DISTANCE), + ) + + events.forEach { gestureMonitor.processTouchpadEvent(it) } + + assertThat(gestureDoneWasCalled).isFalse() + } + + @Test + fun doesntTriggerGestureDone_onTwoFingersSwipe() { + fun twoFingerEvent(action: Int, x: Float, y: Float) = + motionEvent( + action = action, + x = x, + y = y, + classification = CLASSIFICATION_TWO_FINGER_SWIPE, + axisValues = mapOf(AXIS_GESTURE_SWIPE_FINGER_COUNT to 2f) + ) + val events = + listOf( + twoFingerEvent(ACTION_DOWN, x = 0f, y = 0f), + twoFingerEvent(ACTION_MOVE, x = SWIPE_DISTANCE / 2, y = 0f), + twoFingerEvent(ACTION_UP, x = SWIPE_DISTANCE, y = 0f), + ) + + events.forEach { gestureMonitor.processTouchpadEvent(it) } + + assertThat(gestureDoneWasCalled).isFalse() + } + + @Test + fun doesntTriggerGestureDone_onFourFingersSwipe() { + fun fourFingerEvent(action: Int, x: Float, y: Float) = + motionEvent( + action = action, + x = x, + y = y, + classification = CLASSIFICATION_MULTI_FINGER_SWIPE, + axisValues = mapOf(AXIS_GESTURE_SWIPE_FINGER_COUNT to 4f) + ) + val events = + listOf( + fourFingerEvent(ACTION_DOWN, x = 0f, y = 0f), + fourFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f), + fourFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f), + fourFingerEvent(ACTION_POINTER_DOWN, x = 0f, y = 0f), + fourFingerEvent(ACTION_MOVE, x = SWIPE_DISTANCE / 2, y = 0f), + fourFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f), + fourFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f), + fourFingerEvent(ACTION_POINTER_UP, x = SWIPE_DISTANCE, y = 0f), + fourFingerEvent(ACTION_UP, x = SWIPE_DISTANCE, y = 0f), + ) + + events.forEach { gestureMonitor.processTouchpadEvent(it) } + + assertThat(gestureDoneWasCalled).isFalse() + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt new file mode 100644 index 000000000000..e632e3474b96 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/FakeMotionEvent.kt @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2024 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.systemui.touchpad.tutorial.ui.gesture + +import android.view.InputDevice.SOURCE_CLASS_POINTER +import android.view.InputDevice.SOURCE_MOUSE +import android.view.MotionEvent +import android.view.MotionEvent.CLASSIFICATION_NONE +import android.view.MotionEvent.TOOL_TYPE_FINGER +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.spy + +fun motionEvent( + action: Int, + x: Float, + y: Float, + source: Int = 0, + toolType: Int = TOOL_TYPE_FINGER, + pointerCount: Int = 1, + axisValues: Map<Int, Float> = emptyMap(), + classification: Int = CLASSIFICATION_NONE, +): MotionEvent { + val event = + MotionEvent.obtain(/* downTime= */ 0, /* eventTime= */ 0, action, x, y, /* metaState= */ 0) + event.source = source + return spy<MotionEvent>(event) { + on { getToolType(0) } doReturn toolType + on { getPointerCount() } doReturn pointerCount + axisValues.forEach { (key, value) -> on { getAxisValue(key) } doReturn value } + on { getClassification() } doReturn classification + } +} + +fun touchpadEvent( + action: Int, + x: Float, + y: Float, + pointerCount: Int = 1, + classification: Int = CLASSIFICATION_NONE, + axisValues: Map<Int, Float> = emptyMap() +): MotionEvent { + return motionEvent( + action = action, + x = x, + y = y, + source = SOURCE_MOUSE or SOURCE_CLASS_POINTER, + toolType = TOOL_TYPE_FINGER, + pointerCount = pointerCount, + classification = classification, + axisValues = axisValues + ) +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt new file mode 100644 index 000000000000..769f264f0870 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2024 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.systemui.touchpad.tutorial.ui.gesture + +import android.view.InputDevice.SOURCE_MOUSE +import android.view.InputDevice.SOURCE_TOUCHSCREEN +import android.view.MotionEvent +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.ACTION_HOVER_ENTER +import android.view.MotionEvent.ACTION_MOVE +import android.view.MotionEvent.ACTION_POINTER_DOWN +import android.view.MotionEvent.ACTION_POINTER_UP +import android.view.MotionEvent.ACTION_UP +import android.view.MotionEvent.AXIS_GESTURE_SWIPE_FINGER_COUNT +import android.view.MotionEvent.CLASSIFICATION_MULTI_FINGER_SWIPE +import android.view.MotionEvent.TOOL_TYPE_FINGER +import android.view.MotionEvent.TOOL_TYPE_MOUSE +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGesture.BACK +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class TouchpadGestureHandlerTest : SysuiTestCase() { + + private var gestureDone = false + private val handler = TouchpadGestureHandler(BACK, SWIPE_DISTANCE) { gestureDone = true } + + companion object { + const val SWIPE_DISTANCE = 100 + } + + @Test + fun handlesEventsFromTouchpad() { + val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_FINGER) + val eventHandled = handler.onMotionEvent(event) + assertThat(eventHandled).isTrue() + } + + @Test + fun ignoresEventsFromMouse() { + val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_MOUSE) + val eventHandled = handler.onMotionEvent(event) + assertThat(eventHandled).isFalse() + } + + @Test + fun ignoresEventsFromTouch() { + val event = downEvent(source = SOURCE_TOUCHSCREEN, toolType = TOOL_TYPE_FINGER) + val eventHandled = handler.onMotionEvent(event) + assertThat(eventHandled).isFalse() + } + + @Test + fun ignoresButtonClicksFromTouchpad() { + val event = downEvent(source = SOURCE_MOUSE, toolType = TOOL_TYPE_FINGER) + event.buttonState = MotionEvent.BUTTON_PRIMARY + val eventHandled = handler.onMotionEvent(event) + assertThat(eventHandled).isFalse() + } + + private fun downEvent(source: Int, toolType: Int) = + motionEvent(action = ACTION_DOWN, x = 0f, y = 0f, source = source, toolType = toolType) + + @Test + fun triggersGestureDoneForThreeFingerGesture() { + backGestureEvents().forEach { handler.onMotionEvent(it) } + + assertThat(gestureDone).isTrue() + } + + private fun backGestureEvents(): List<MotionEvent> { + // list of motion events read from device while doing back gesture + val y = 100f + return listOf( + touchpadEvent(ACTION_HOVER_ENTER, x = 759f, y = y, pointerCount = 1), + threeFingerTouchpadEvent(ACTION_DOWN, x = 759f, y = y, pointerCount = 1), + threeFingerTouchpadEvent(ACTION_POINTER_DOWN, x = 759f, y = y, pointerCount = 2), + threeFingerTouchpadEvent(ACTION_POINTER_DOWN, x = 759f, y = y, pointerCount = 3), + threeFingerTouchpadEvent(ACTION_MOVE, x = 767f, y = y, pointerCount = 3), + threeFingerTouchpadEvent(ACTION_MOVE, x = 785f, y = y, pointerCount = 3), + threeFingerTouchpadEvent(ACTION_MOVE, x = 814f, y = y, pointerCount = 3), + threeFingerTouchpadEvent(ACTION_MOVE, x = 848f, y = y, pointerCount = 3), + threeFingerTouchpadEvent(ACTION_MOVE, x = 943f, y = y, pointerCount = 3), + threeFingerTouchpadEvent(ACTION_POINTER_UP, x = 943f, y = y, pointerCount = 3), + threeFingerTouchpadEvent(ACTION_POINTER_UP, x = 943f, y = y, pointerCount = 2), + threeFingerTouchpadEvent(ACTION_UP, x = 943f, y = y, pointerCount = 1) + ) + } + + private fun threeFingerTouchpadEvent( + action: Int, + x: Float, + y: Float, + pointerCount: Int + ): MotionEvent { + return touchpadEvent( + action = action, + x = x, + y = y, + pointerCount = pointerCount, + classification = CLASSIFICATION_MULTI_FINGER_SWIPE, + axisValues = mapOf(AXIS_GESTURE_SWIPE_FINGER_COUNT to 3f) + ) + } +} |