diff options
| author | 2024-12-19 22:53:02 +0000 | |
|---|---|---|
| committer | 2024-12-20 18:57:19 +0000 | |
| commit | 936c1902879ee8e4488831dcbd20b80b10e28d71 (patch) | |
| tree | 688a520cf2bf699f196b5f9e7fa73cde74a2a72b | |
| parent | c18bf421a9714e7dc51bd705a1390a46dbe18c9e (diff) | |
1/n refactoring ViewModels for touchpad tutorial
Extracting handling of easter egg to separate ViewModel to not repeat it in all gesture view models
1. Simplifying TouchpadGestureHandler so that it accepts any gesture recognizers and is not directly dependent on easter egg monitor
2. EasterEggMonitor now extends GestureRecognizer
3. Easter egg handling logic is extracted to separate ViewModel
Bug: 384509663
Test: EasterEggGestureViewModelTest
Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial
Change-Id: I46dd032a992f27252dd95510b77ccaf30f78096d
19 files changed, 308 insertions, 126 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGesture.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGesture.kt new file mode 100644 index 000000000000..68b5772bd7ac --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGesture.kt @@ -0,0 +1,76 @@ +/* + * 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.graphics.PointF +import android.view.MotionEvent +import kotlin.math.cos +import kotlin.math.sin + +/** Test helper to generate circular gestures or full EasterEgg gesture */ +object EasterEggGesture { + + fun motionEventsForGesture(): List<MotionEvent> { + val gesturePath = generateCircularGesturePoints(circlesCount = 3) + val events = + TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { p -> move(p.x, p.y) } } + return events + } + + /** + * Generates list of points that would make up clockwise circular motion with given [radius]. + * [circlesCount] determines how many full circles gesture should perform. [radiusNoiseFraction] + * can introduce noise to mimic real-world gesture which is not perfect - shape will be still + * circular but radius at any given point can be deviate from given radius by + * [radiusNoiseFraction]. + */ + fun generateCircularGesturePoints( + circlesCount: Int, + radiusNoiseFraction: Double? = null, + radius: Float = 100f, + ): List<PointF> { + val pointsPerCircle = 50 + val angleStep = 360 / pointsPerCircle + val angleBuffer = 20 // buffer to make sure we're doing a bit more than 360 degree + val totalAngle = circlesCount * (360 + angleBuffer) + // Because all gestures in tests should start at (DEFAULT_X, DEFAULT_Y) we need to shift + // circle center x coordinate by radius + val centerX = -radius + val centerY = 0f + + val randomNoise: (Double) -> Double = + if (radiusNoiseFraction == null) { + { 0.0 } + } else { + { radianAngle -> sin(radianAngle * 2) * radiusNoiseFraction } + } + + val events = mutableListOf<PointF>() + var currentAngle = 0f + // as cos(0) == 1 and sin(0) == 0 we start gesture at position of (radius, 0) and go + // clockwise - first Y increases and X decreases + while (currentAngle < totalAngle) { + val radianAngle = Math.toRadians(currentAngle.toDouble()) + val radiusWithNoise = radius * (1 + randomNoise(radianAngle).toFloat()) + val x = centerX + radiusWithNoise * cos(radianAngle).toFloat() + val y = centerY + radiusWithNoise * sin(radianAngle).toFloat() + events.add(PointF(x, y)) + currentAngle += angleStep + } + return events + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt index ff0cec5e06e9..e1f590c7f55b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt @@ -16,14 +16,14 @@ package com.android.systemui.touchpad.tutorial.ui.gesture +import android.graphics.PointF import android.view.MotionEvent 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.MultiFingerGesture.Companion.SWIPE_DISTANCE +import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGesture.generateCircularGesturePoints import com.google.common.truth.Truth.assertThat -import kotlin.math.cos -import kotlin.math.sin +import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -31,14 +31,14 @@ import org.junit.runner.RunWith @RunWith(AndroidJUnit4::class) class EasterEggGestureTest : SysuiTestCase() { - private data class Point(val x: Float, val y: Float) - private var triggered = false - private val handler = - TouchpadGestureHandler( - BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()), - EasterEggGestureMonitor(callback = { triggered = true }), - ) + private val gestureRecognizer = EasterEggGestureMonitor() + private val handler = TouchpadGestureHandler(gestureRecognizer) + + @Before + fun setup() { + gestureRecognizer.addGestureStateCallback { triggered = it == GestureState.Finished } + } @Test fun easterEggTriggeredAfterThreeCircles() { @@ -103,52 +103,9 @@ class EasterEggGestureTest : SysuiTestCase() { assertThat(triggered).isEqualTo(wasTriggered) } - private fun assertStateAfterTwoFingerGesture(gesturePath: List<Point>, wasTriggered: Boolean) { + private fun assertStateAfterTwoFingerGesture(gesturePath: List<PointF>, wasTriggered: Boolean) { val events = - TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { (x, y) -> move(x, y) } } + TwoFingerGesture.eventsForFullGesture { gesturePath.forEach { p -> move(p.x, p.y) } } assertStateAfterEvents(events = events, wasTriggered = wasTriggered) } - - /** - * Generates list of points that would make up clockwise circular motion with given [radius]. - * [circlesCount] determines how many full circles gesture should perform. [radiusNoiseFraction] - * can introduce noise to mimic real-world gesture which is not perfect - shape will be still - * circular but radius at any given point can be deviate from given radius by - * [radiusNoiseFraction]. - */ - private fun generateCircularGesturePoints( - circlesCount: Int, - radiusNoiseFraction: Double? = null, - radius: Float = 100f, - ): List<Point> { - val pointsPerCircle = 50 - val angleStep = 360 / pointsPerCircle - val angleBuffer = 20 // buffer to make sure we're doing a bit more than 360 degree - val totalAngle = circlesCount * (360 + angleBuffer) - // Because all gestures in tests should start at (DEFAULT_X, DEFAULT_Y) we need to shift - // circle center x coordinate by radius - val centerX = -radius - val centerY = 0f - - val events = mutableListOf<Point>() - val randomNoise: (Double) -> Double = - if (radiusNoiseFraction == null) { - { 0.0 } - } else { - { radianAngle -> sin(radianAngle * 2) * radiusNoiseFraction } - } - - var currentAngle = 0f - // as cos(0) == 1 and sin(0) == 0 we start gesture at position of (radius, 0) and go - // clockwise - first Y increases and X decreases - while (currentAngle < totalAngle) { - val radianAngle = Math.toRadians(currentAngle.toDouble()) - val radiusWithNoise = radius * (1 + randomNoise(radianAngle).toFloat()) - val x = centerX + radiusWithNoise * cos(radianAngle).toFloat() - val y = centerY + radiusWithNoise * sin(radianAngle).toFloat() - events.add(Point(x, y)) - currentAngle += angleStep - } - return events - } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt index c302b40fc4d7..8eb79ebc3bb1 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt @@ -38,7 +38,7 @@ class TouchpadGestureHandlerTest : SysuiTestCase() { private var gestureState: GestureState = GestureState.NotStarted private val gestureRecognizer = BackGestureRecognizer(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()) - private val handler = TouchpadGestureHandler(gestureRecognizer, EasterEggGestureMonitor {}) + private val handler = TouchpadGestureHandler(gestureRecognizer) @Before fun before() { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt index f90e14caca75..79c1f9fcf517 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt @@ -54,13 +54,6 @@ class BackGestureScreenViewModelTest : SysuiTestCase() { } @Test - fun easterEggNotTriggeredAtStart() = - kosmos.runTest { - val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) - assertThat(easterEggTriggered).isFalse() - } - - @Test fun emitsProgressStateWithLeftProgressAnimation() = kosmos.runTest { assertProgressWhileMovingFingers( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt new file mode 100644 index 000000000000..4af374287c62 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModelTest.kt @@ -0,0 +1,78 @@ +/* + * 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.viewmodel + +import android.view.MotionEvent +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGesture +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class EasterEggGestureViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val viewModel = EasterEggGestureViewModel() + + @Before + fun before() { + kosmos.useUnconfinedTestDispatcher() + } + + @Test + fun easterEggNotTriggeredAtStart() = + kosmos.runTest { + val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) + assertThat(easterEggTriggered).isFalse() + } + + @Test + fun emitsTrueOnEasterEggTriggered() = + kosmos.runTest { + assertStateAfterEvents( + events = EasterEggGesture.motionEventsForGesture(), + expected = true, + ) + } + + @Test + fun emitsFalseOnEasterEggCallbackExecuted() = + kosmos.runTest { + val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) + EasterEggGesture.motionEventsForGesture().forEach { viewModel.accept(it) } + + assertThat(easterEggTriggered).isEqualTo(true) + viewModel.onEasterEggFinished() + assertThat(easterEggTriggered).isEqualTo(false) + } + + private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: Boolean) { + val state by collectLastValue(viewModel.easterEggTriggered) + events.forEach { viewModel.accept(it) } + assertThat(state).isEqualTo(expected) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt index 3c06352ace97..4dfd01a91f17 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt @@ -70,13 +70,6 @@ class HomeGestureScreenViewModelTest : SysuiTestCase() { } @Test - fun easterEggNotTriggeredAtStart() = - kosmos.runTest { - val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) - assertThat(easterEggTriggered).isFalse() - } - - @Test fun emitsProgressStateWithAnimationMarkers() = kosmos.runTest { assertStateAfterEvents( diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt index a2d8a8b3cb0e..66bf778a754b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt @@ -74,13 +74,6 @@ class RecentAppsGestureScreenViewModelTest : SysuiTestCase() { } @Test - fun easterEggNotTriggeredAtStart() = - kosmos.runTest { - val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) - assertThat(easterEggTriggered).isFalse() - } - - @Test fun emitsProgressStateWithAnimationMarkers() = kosmos.runTest { assertStateAfterEvents( diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt index fbf7072cc0a0..a6c066500054 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt @@ -31,6 +31,7 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker import com.android.systemui.touchpad.tutorial.ui.view.TouchpadTutorialActivity import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureScreenViewModel import dagger.Binds import dagger.Module @@ -53,7 +54,11 @@ interface TouchpadTutorialModule { backGestureScreenViewModel: BackGestureScreenViewModel, homeGestureScreenViewModel: HomeGestureScreenViewModel, ): TouchpadTutorialScreensProvider { - return ScreensProvider(backGestureScreenViewModel, homeGestureScreenViewModel) + return ScreensProvider( + backGestureScreenViewModel, + homeGestureScreenViewModel, + EasterEggGestureViewModel(), + ) } @SysUISingleton @@ -74,14 +79,25 @@ interface TouchpadTutorialModule { private class ScreensProvider( val backGestureScreenViewModel: BackGestureScreenViewModel, val homeGestureScreenViewModel: HomeGestureScreenViewModel, + val easterEggGestureViewModel: EasterEggGestureViewModel, ) : TouchpadTutorialScreensProvider { @Composable override fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { - BackGestureTutorialScreen(backGestureScreenViewModel, onDoneButtonClicked, onBack) + BackGestureTutorialScreen( + backGestureScreenViewModel, + easterEggGestureViewModel, + onDoneButtonClicked, + onBack, + ) } @Composable override fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { - HomeGestureTutorialScreen(homeGestureScreenViewModel, onDoneButtonClicked, onBack) + HomeGestureTutorialScreen( + homeGestureScreenViewModel, + easterEggGestureViewModel, + onDoneButtonClicked, + onBack, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt index 804a764b5349..ae32b7a6175c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt @@ -25,10 +25,12 @@ import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenCon import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel @Composable fun BackGestureTutorialScreen( viewModel: BackGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { @@ -49,9 +51,12 @@ fun BackGestureTutorialScreen( GestureTutorialScreen( screenConfig = screenConfig, gestureUiStateFlow = viewModel.gestureUiState, - motionEventConsumer = viewModel::handleEvent, - easterEggTriggeredFlow = viewModel.easterEggTriggered, - onEasterEggFinished = viewModel::onEasterEggFinished, + motionEventConsumer = { + easterEggGestureViewModel.accept(it) + viewModel.handleEvent(it) + }, + easterEggTriggeredFlow = easterEggGestureViewModel.easterEggTriggered, + onEasterEggFinished = easterEggGestureViewModel::onEasterEggFinished, onDoneButtonClicked = onDoneButtonClicked, onBack = onBack, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt index 5dcd788ea4fd..4f1f40dc4c05 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt @@ -23,11 +23,13 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureScreenViewModel @Composable fun HomeGestureTutorialScreen( viewModel: HomeGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { @@ -48,9 +50,12 @@ fun HomeGestureTutorialScreen( GestureTutorialScreen( screenConfig = screenConfig, gestureUiStateFlow = viewModel.gestureUiState, - motionEventConsumer = viewModel::handleEvent, - easterEggTriggeredFlow = viewModel.easterEggTriggered, - onEasterEggFinished = viewModel::onEasterEggFinished, + motionEventConsumer = { + easterEggGestureViewModel.accept(it) + viewModel.handleEvent(it) + }, + easterEggTriggeredFlow = easterEggGestureViewModel.easterEggTriggered, + onEasterEggFinished = easterEggGestureViewModel::onEasterEggFinished, onDoneButtonClicked = onDoneButtonClicked, onBack = onBack, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt index 7ff838981950..6c9e26c4b7ea 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt @@ -23,11 +23,13 @@ import com.android.compose.theme.LocalAndroidColorScheme import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig import com.android.systemui.inputdevice.tutorial.ui.composable.rememberColorFilterProperty import com.android.systemui.res.R +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.RecentAppsGestureScreenViewModel @Composable fun RecentAppsGestureTutorialScreen( viewModel: RecentAppsGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { @@ -49,9 +51,12 @@ fun RecentAppsGestureTutorialScreen( GestureTutorialScreen( screenConfig = screenConfig, gestureUiStateFlow = viewModel.gestureUiState, - motionEventConsumer = viewModel::handleEvent, - easterEggTriggeredFlow = viewModel.easterEggTriggered, - onEasterEggFinished = viewModel::onEasterEggFinished, + motionEventConsumer = { + easterEggGestureViewModel.accept(it) + viewModel.handleEvent(it) + }, + easterEggTriggeredFlow = easterEggGestureViewModel.easterEggTriggered, + onEasterEggFinished = easterEggGestureViewModel::onEasterEggFinished, onDoneButtonClicked = onDoneButtonClicked, onBack = onBack, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureMonitor.kt index 7483840d1933..7befb570b647 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureMonitor.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureMonitor.kt @@ -25,10 +25,12 @@ import kotlin.math.sqrt /** * Monitor recognizing easter egg gesture, that is at least [CIRCLES_COUNT_THRESHOLD] circles - * clockwise within one gesture. It tries to be on the safer side of not triggering gesture if we're - * not sure if full circle was done. + * clockwise within one two-fingers gesture. It tries to be on the safer side of not triggering + * gesture if we're not sure if full circle was done. */ -class EasterEggGestureMonitor(private val callback: () -> Unit) { +class EasterEggGestureMonitor : GestureRecognizer { + + private var gestureStateChangedCallback: (GestureState) -> Unit = {} private var last: Point = Point(0f, 0f) private var cumulativeAngle: Float = 0f @@ -39,7 +41,16 @@ class EasterEggGestureMonitor(private val callback: () -> Unit) { private val points = mutableListOf<Point>() - fun processTouchpadEvent(event: MotionEvent) { + override fun addGestureStateCallback(callback: (GestureState) -> Unit) { + gestureStateChangedCallback = callback + } + + override fun clearGestureStateCallback() { + gestureStateChangedCallback = {} + } + + override fun accept(event: MotionEvent) { + if (!isTwoFingerSwipe(event)) return when (event.action) { MotionEvent.ACTION_DOWN -> { reset() @@ -75,7 +86,7 @@ class EasterEggGestureMonitor(private val callback: () -> Unit) { // without checking if gesture is circular we can have gesture doing arches back and // forth that finally reaches full circle angle if (circleCount >= CIRCLES_COUNT_THRESHOLD && wasGestureCircular(points)) { - callback() + gestureStateChangedCallback(GestureState.Finished) } reset() } 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 index dd275bd11d1e..f417c4c84f3c 100644 --- 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 @@ -24,10 +24,7 @@ import java.util.function.Consumer * 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( - private val gestureRecognizer: Consumer<MotionEvent>, - private val easterEggGestureMonitor: EasterEggGestureMonitor, -) { +class TouchpadGestureHandler(private vararg val eventConsumers: Consumer<MotionEvent>) { fun onMotionEvent(event: MotionEvent): Boolean { // events from touchpad have SOURCE_MOUSE and not SOURCE_TOUCHPAD because of legacy reasons @@ -38,10 +35,7 @@ class TouchpadGestureHandler( event.actionMasked == MotionEvent.ACTION_DOWN && event.isButtonPressed(MotionEvent.BUTTON_PRIMARY) return if (isFromTouchpad && !buttonClick) { - if (isTwoFingerSwipe(event)) { - easterEggGestureMonitor.processTouchpadEvent(event) - } - gestureRecognizer.accept(event) + eventConsumers.forEach { it.accept(event) } true } else { false 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 6b4cbab3ae09..cefe382a299c 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 @@ -39,6 +39,7 @@ import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialS import com.android.systemui.touchpad.tutorial.ui.composable.RecentAppsGestureTutorialScreen import com.android.systemui.touchpad.tutorial.ui.composable.TutorialSelectionScreen import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel +import com.android.systemui.touchpad.tutorial.ui.viewmodel.EasterEggGestureViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureScreenViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.RecentAppsGestureScreenViewModel import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.BACK_GESTURE @@ -73,6 +74,7 @@ constructor( backGestureViewModel, homeGestureViewModel, recentAppsGestureViewModel, + EasterEggGestureViewModel(), closeTutorial = ::finishTutorial, ) } @@ -105,6 +107,7 @@ fun TouchpadTutorialScreen( backGestureViewModel: BackGestureScreenViewModel, homeGestureViewModel: HomeGestureScreenViewModel, recentAppsGestureViewModel: RecentAppsGestureScreenViewModel, + easterEggGestureViewModel: EasterEggGestureViewModel, closeTutorial: () -> Unit, ) { val activeScreen by vm.screen.collectAsStateWithLifecycle(STARTED) @@ -130,18 +133,21 @@ fun TouchpadTutorialScreen( BACK_GESTURE -> BackGestureTutorialScreen( backGestureViewModel, + easterEggGestureViewModel, onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) HOME_GESTURE -> HomeGestureTutorialScreen( homeGestureViewModel, + easterEggGestureViewModel, onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) RECENT_APPS_GESTURE -> RecentAppsGestureTutorialScreen( recentAppsGestureViewModel, + easterEggGestureViewModel, onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) }, onBack = { vm.goTo(TUTORIAL_SELECTION) }, ) diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt index 0154c910be91..b5ed25b8a0da 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModel.kt @@ -22,7 +22,6 @@ import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureRecognizer -import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureDirection import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState @@ -32,7 +31,6 @@ import com.android.systemui.util.kotlin.pairwiseBy import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -40,9 +38,6 @@ class BackGestureScreenViewModel @Inject constructor(configurationInteractor: ConfigurationInteractor) : TouchpadTutorialScreenViewModel { - private val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered.value = true } - override val easterEggTriggered = MutableStateFlow(false) - private var handler: TouchpadGestureHandler? = null private val distanceThreshold: Flow<Int> = @@ -55,7 +50,7 @@ constructor(configurationInteractor: ConfigurationInteractor) : TouchpadTutorial distanceThreshold .flatMapLatest { val recognizer = BackGestureRecognizer(gestureDistanceThresholdPx = it) - handler = TouchpadGestureHandler(recognizer, easterEggMonitor) + handler = TouchpadGestureHandler(recognizer) GestureFlowAdapter(recognizer).gestureStateAsFlow } .pairwiseBy(GestureState.NotStarted) { previous, current -> diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt new file mode 100644 index 000000000000..e539f0aa8abf --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/EasterEggGestureViewModel.kt @@ -0,0 +1,71 @@ +/* + * 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.viewmodel + +import android.view.MotionEvent +import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter +import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState +import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler +import java.util.function.Consumer +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.receiveAsFlow + +class EasterEggGestureViewModel( + easterEggMonitor: EasterEggGestureMonitor = EasterEggGestureMonitor() +) : Consumer<MotionEvent> { + + private val handler = TouchpadGestureHandler(easterEggMonitor) + + private val gestureDone = + GestureFlowAdapter(easterEggMonitor).gestureStateAsFlow.filter { + it == GestureState.Finished + } + + private val easterEggFinished = Channel<Unit>() + + val easterEggTriggered = + merge( + gestureDone.map { Event.GestureFinished }, + easterEggFinished.receiveAsFlow().map { Event.StateRestarted }, + ) + .map { + when (it) { + Event.GestureFinished -> true + Event.StateRestarted -> false + } + } + .onStart { emit(false) } + + override fun accept(event: MotionEvent) { + handler.onMotionEvent(event) + } + + fun onEasterEggFinished() { + easterEggFinished.trySend(Unit) + } + + private sealed interface Event { + data object GestureFinished : Event + + data object StateRestarted : Event + } +} diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt index 1c865f57b8c7..569cc936bd0b 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt @@ -23,7 +23,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState -import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer @@ -33,7 +32,6 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -47,9 +45,6 @@ constructor( val velocityTracker: VelocityTracker = VerticalVelocityTracker(), ) : TouchpadTutorialScreenViewModel { - private val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered.value = true } - override val easterEggTriggered = MutableStateFlow(false) - private var handler: TouchpadGestureHandler? = null private val distanceThreshold: Flow<Int> = @@ -73,7 +68,7 @@ constructor( velocityThresholdPxPerMs = velocity, velocityTracker = velocityTracker, ) - handler = TouchpadGestureHandler(recognizer, easterEggMonitor) + handler = TouchpadGestureHandler(recognizer) GestureFlowAdapter(recognizer).gestureStateAsFlow } .map { toGestureUiState(it) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt index 09947a8b109e..989a60878733 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt @@ -23,7 +23,6 @@ import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.res.R import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import com.android.systemui.touchpad.tutorial.ui.composable.toGestureUiState -import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor import com.android.systemui.touchpad.tutorial.ui.gesture.GestureFlowAdapter import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer @@ -33,7 +32,6 @@ import com.android.systemui.touchpad.tutorial.ui.gesture.VerticalVelocityTracker import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest @@ -47,9 +45,6 @@ constructor( private val velocityTracker: VelocityTracker = VerticalVelocityTracker(), ) : TouchpadTutorialScreenViewModel { - private val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered.value = true } - override val easterEggTriggered = MutableStateFlow(false) - private var handler: TouchpadGestureHandler? = null private val distanceThreshold: Flow<Int> = @@ -77,7 +72,7 @@ constructor( velocityThresholdPxPerMs = velocity, velocityTracker = velocityTracker, ) - handler = TouchpadGestureHandler(recognizer, easterEggMonitor) + handler = TouchpadGestureHandler(recognizer) GestureFlowAdapter(recognizer).gestureStateAsFlow } .map { toGestureUiState(it) } diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt index 500f6a0238c3..31e953d6643c 100644 --- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialScreenViewModel.kt @@ -19,15 +19,9 @@ package com.android.systemui.touchpad.tutorial.ui.viewmodel import android.view.MotionEvent import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.MutableStateFlow interface TouchpadTutorialScreenViewModel { val gestureUiState: Flow<GestureUiState> - val easterEggTriggered: MutableStateFlow<Boolean> - - fun onEasterEggFinished() { - easterEggTriggered.value = false - } fun handleEvent(event: MotionEvent): Boolean } |