summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Michal Brzezinski <brzezinski@google.com> 2024-12-17 19:01:06 +0000
committer Michal Brzezinski <brzezinski@google.com> 2024-12-18 11:20:37 +0000
commitc18bf421a9714e7dc51bd705a1390a46dbe18c9e (patch)
treee1f21951779476583f29f122e5503b6e90113032
parent1b0f1a3c6d56fb9fd69ef178986ccd4055a83c48 (diff)
Extracting remaining ViewModels in touchpad tutorial
Extracting recent apps gesture and home gesture logic into ViewModels, following the same path as for back gesture in previous change. Substituting GestureTutorialScreen with GestureTutorialScreenNew Now there is some logic duplication in ViewModels so follow-up CL will try to fix that Bug: 384509663 Test: HomeGestureScreenViewModelTest Test: RecentAppsGestureScreenViewModelTest Flag: com.android.systemui.shared.new_touchpad_gestures_tutorial Change-Id: Ie1fc266fde86a983152ab44afb4bd5c7f33f7bb8
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt154
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt160
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt18
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt31
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt23
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt91
-rw-r--r--packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt95
10 files changed, 563 insertions, 97 deletions
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
new file mode 100644
index 000000000000..3c06352ace97
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt
@@ -0,0 +1,154 @@
+/*
+ * 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.content.res.mockResources
+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.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+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.res.R
+import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Error
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress
+import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
+import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture
+import com.android.systemui.touchpad.tutorial.ui.gesture.Velocity
+import com.android.systemui.touchpad.ui.gesture.fakeVelocityTracker
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class HomeGestureScreenViewModelTest : SysuiTestCase() {
+
+ companion object {
+ const val GESTURE_VELOCITY = 1f
+ const val LOW_VELOCITY_THRESHOLD = GESTURE_VELOCITY - 0.01f
+ const val TOO_HIGH_VELOCITY_THRESHOLD = GESTURE_VELOCITY + 0.01f
+ }
+
+ private val kosmos = testKosmos()
+ private val fakeConfigRepository = kosmos.fakeConfigurationRepository
+ private val fakeVelocityTracker = kosmos.fakeVelocityTracker
+ private val resources = kosmos.mockResources
+
+ private val viewModel =
+ HomeGestureScreenViewModel(kosmos.configurationInteractor, resources, fakeVelocityTracker)
+
+ @Before
+ fun before() {
+ setDistanceThreshold(threshold = SWIPE_DISTANCE - 1)
+ setVelocityThreshold(threshold = LOW_VELOCITY_THRESHOLD)
+ fakeVelocityTracker.setVelocity(Velocity(GESTURE_VELOCITY))
+ kosmos.useUnconfinedTestDispatcher()
+ }
+
+ @Test
+ fun easterEggNotTriggeredAtStart() =
+ kosmos.runTest {
+ val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered)
+ assertThat(easterEggTriggered).isFalse()
+ }
+
+ @Test
+ fun emitsProgressStateWithAnimationMarkers() =
+ kosmos.runTest {
+ assertStateAfterEvents(
+ events =
+ ThreeFingerGesture.eventsForGestureInProgress {
+ move(deltaY = -SWIPE_DISTANCE)
+ },
+ expected =
+ InProgress(
+ progress = 1f,
+ progressStartMarker = "drag with gesture",
+ progressEndMarker = "release playback realtime",
+ ),
+ )
+ }
+
+ @Test
+ fun emitsFinishedStateWithSuccessAnimation() =
+ kosmos.runTest {
+ assertStateAfterEvents(
+ events = ThreeFingerGesture.swipeUp(),
+ expected = Finished(successAnimation = R.raw.trackpad_home_success),
+ )
+ }
+
+ private fun performHomeGesture() {
+ ThreeFingerGesture.swipeUp().forEach { viewModel.handleEvent(it) }
+ }
+
+ @Test
+ fun gestureRecognitionTakesLatestDistanceThresholdIntoAccount() =
+ kosmos.runTest {
+ val state by collectLastValue(viewModel.gestureUiState)
+ performHomeGesture()
+ assertThat(state).isInstanceOf(Finished::class.java)
+
+ setDistanceThreshold(SWIPE_DISTANCE + 1)
+ performHomeGesture() // now swipe distance is not enough to trigger success
+
+ assertThat(state).isInstanceOf(Error::class.java)
+ }
+
+ @Test
+ fun gestureRecognitionTakesLatestVelocityThresholdIntoAccount() =
+ kosmos.runTest {
+ val state by collectLastValue(viewModel.gestureUiState)
+ performHomeGesture()
+ assertThat(state).isInstanceOf(Finished::class.java)
+
+ setVelocityThreshold(TOO_HIGH_VELOCITY_THRESHOLD)
+ performHomeGesture()
+
+ assertThat(state).isInstanceOf(Error::class.java)
+ }
+
+ private fun setDistanceThreshold(threshold: Float) {
+ fakeConfigRepository.setDimensionPixelSize(
+ R.dimen.touchpad_tutorial_gestures_distance_threshold,
+ (threshold).toInt(),
+ )
+ fakeConfigRepository.onAnyConfigurationChange()
+ }
+
+ private fun setVelocityThreshold(threshold: Float) {
+ whenever(resources.getDimension(R.dimen.touchpad_home_gesture_velocity_threshold))
+ .thenReturn(threshold)
+ fakeConfigRepository.onAnyConfigurationChange()
+ }
+
+ private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) {
+ val state by collectLastValue(viewModel.gestureUiState)
+ events.forEach { viewModel.handleEvent(it) }
+ assertThat(state).isEqualTo(expected)
+ }
+}
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
new file mode 100644
index 000000000000..a2d8a8b3cb0e
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.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.viewmodel
+
+import android.content.res.mockResources
+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.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.common.ui.domain.interactor.configurationInteractor
+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.res.R
+import com.android.systemui.testKosmos
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Error
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
+import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.InProgress
+import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
+import com.android.systemui.touchpad.tutorial.ui.gesture.ThreeFingerGesture
+import com.android.systemui.touchpad.tutorial.ui.gesture.Velocity
+import com.android.systemui.touchpad.ui.gesture.fakeVelocityTracker
+import com.google.common.truth.Truth.assertThat
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RecentAppsGestureScreenViewModelTest : SysuiTestCase() {
+
+ companion object {
+ const val GESTURE_VELOCITY = 1f
+ const val VELOCITY_THRESHOLD = GESTURE_VELOCITY + 0.01f
+ const val TOO_LOW_VELOCITY_THRESHOLD = GESTURE_VELOCITY - 0.01f
+ }
+
+ private val kosmos = testKosmos()
+ private val fakeConfigRepository = kosmos.fakeConfigurationRepository
+ private val fakeVelocityTracker = kosmos.fakeVelocityTracker
+ private val resources = kosmos.mockResources
+
+ private val viewModel =
+ RecentAppsGestureScreenViewModel(
+ kosmos.configurationInteractor,
+ resources,
+ fakeVelocityTracker,
+ )
+
+ @Before
+ fun before() {
+ setDistanceThreshold(threshold = SWIPE_DISTANCE - 1)
+ setVelocityThreshold(threshold = VELOCITY_THRESHOLD)
+ fakeVelocityTracker.setVelocity(Velocity(GESTURE_VELOCITY))
+ kosmos.useUnconfinedTestDispatcher()
+ }
+
+ @Test
+ fun easterEggNotTriggeredAtStart() =
+ kosmos.runTest {
+ val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered)
+ assertThat(easterEggTriggered).isFalse()
+ }
+
+ @Test
+ fun emitsProgressStateWithAnimationMarkers() =
+ kosmos.runTest {
+ assertStateAfterEvents(
+ events =
+ ThreeFingerGesture.eventsForGestureInProgress {
+ move(deltaY = -SWIPE_DISTANCE)
+ },
+ expected =
+ InProgress(
+ progress = 1f,
+ progressStartMarker = "drag with gesture",
+ progressEndMarker = "onPause",
+ ),
+ )
+ }
+
+ @Test
+ fun emitsFinishedStateWithSuccessAnimation() =
+ kosmos.runTest {
+ assertStateAfterEvents(
+ events = ThreeFingerGesture.swipeUp(),
+ expected = Finished(successAnimation = R.raw.trackpad_recent_apps_success),
+ )
+ }
+
+ private fun performRecentAppsGesture() {
+ ThreeFingerGesture.swipeUp().forEach { viewModel.handleEvent(it) }
+ }
+
+ @Test
+ fun gestureRecognitionTakesLatestDistanceThresholdIntoAccount() =
+ kosmos.runTest {
+ val state by collectLastValue(viewModel.gestureUiState)
+ performRecentAppsGesture()
+ assertThat(state).isInstanceOf(Finished::class.java)
+
+ setDistanceThreshold(SWIPE_DISTANCE + 1)
+ performRecentAppsGesture() // now swipe distance is not enough to trigger success
+
+ assertThat(state).isInstanceOf(Error::class.java)
+ }
+
+ @Test
+ fun gestureRecognitionTakesLatestVelocityThresholdIntoAccount() =
+ kosmos.runTest {
+ val state by collectLastValue(viewModel.gestureUiState)
+ performRecentAppsGesture()
+ assertThat(state).isInstanceOf(Finished::class.java)
+
+ setVelocityThreshold(TOO_LOW_VELOCITY_THRESHOLD)
+ performRecentAppsGesture()
+
+ assertThat(state).isInstanceOf(Error::class.java)
+ }
+
+ private fun setDistanceThreshold(threshold: Float) {
+ whenever(
+ resources.getDimensionPixelSize(
+ R.dimen.touchpad_tutorial_gestures_distance_threshold
+ )
+ )
+ .thenReturn(threshold.toInt())
+ fakeConfigRepository.onAnyConfigurationChange()
+ }
+
+ private fun setVelocityThreshold(threshold: Float) {
+ whenever(resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold))
+ .thenReturn(threshold)
+ fakeConfigRepository.onAnyConfigurationChange()
+ }
+
+ private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) {
+ val state by collectLastValue(viewModel.gestureUiState)
+ events.forEach { viewModel.handleEvent(it) }
+ assertThat(state).isEqualTo(expected)
+ }
+}
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 9c45ee726dbd..fbf7072cc0a0 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt
@@ -27,8 +27,11 @@ import com.android.systemui.settings.DisplayTracker
import com.android.systemui.touchpad.tutorial.domain.interactor.TouchpadGesturesInteractor
import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen
import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen
+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.HomeGestureScreenViewModel
import dagger.Binds
import dagger.Module
import dagger.Provides
@@ -47,9 +50,10 @@ interface TouchpadTutorialModule {
companion object {
@Provides
fun touchpadScreensProvider(
- backGestureScreenViewModel: BackGestureScreenViewModel
+ backGestureScreenViewModel: BackGestureScreenViewModel,
+ homeGestureScreenViewModel: HomeGestureScreenViewModel,
): TouchpadTutorialScreensProvider {
- return ScreensProvider(backGestureScreenViewModel)
+ return ScreensProvider(backGestureScreenViewModel, homeGestureScreenViewModel)
}
@SysUISingleton
@@ -62,11 +66,15 @@ interface TouchpadTutorialModule {
): TouchpadGesturesInteractor {
return TouchpadGesturesInteractor(sysUiState, displayTracker, backgroundScope, logger)
}
+
+ @Provides fun velocityTracker(): VelocityTracker = VerticalVelocityTracker()
}
}
-private class ScreensProvider(val backGestureScreenViewModel: BackGestureScreenViewModel) :
- TouchpadTutorialScreensProvider {
+private class ScreensProvider(
+ val backGestureScreenViewModel: BackGestureScreenViewModel,
+ val homeGestureScreenViewModel: HomeGestureScreenViewModel,
+) : TouchpadTutorialScreensProvider {
@Composable
override fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
BackGestureTutorialScreen(backGestureScreenViewModel, onDoneButtonClicked, onBack)
@@ -74,6 +82,6 @@ private class ScreensProvider(val backGestureScreenViewModel: BackGestureScreenV
@Composable
override fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
- HomeGestureTutorialScreen(onDoneButtonClicked, onBack)
+ HomeGestureTutorialScreen(homeGestureScreenViewModel, 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 0b71ba2d2536..804a764b5349 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
@@ -46,7 +46,7 @@ fun BackGestureTutorialScreen(
),
animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_back_edu),
)
- GestureTutorialScreenNew(
+ GestureTutorialScreen(
screenConfig = screenConfig,
gestureUiStateFlow = viewModel.gestureUiState,
motionEventConsumer = viewModel::handleEvent,
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index f4f59d5e361f..73c54af595d9 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -39,10 +39,7 @@ import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialActionSta
import com.android.systemui.inputdevice.tutorial.ui.composable.TutorialScreenConfig
import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.Finished
import com.android.systemui.touchpad.tutorial.ui.composable.GestureUiState.NotStarted
-import com.android.systemui.touchpad.tutorial.ui.gesture.EasterEggGestureMonitor
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
-import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
import kotlinx.coroutines.flow.Flow
sealed interface GestureUiState {
@@ -100,34 +97,6 @@ fun GestureUiState.toTutorialActionState(previousState: TutorialActionState): Tu
@Composable
fun GestureTutorialScreen(
screenConfig: TutorialScreenConfig,
- gestureRecognizer: GestureRecognizer,
- gestureUiStateFlow: Flow<GestureUiState>,
- onDoneButtonClicked: () -> Unit,
- onBack: () -> Unit,
-) {
- BackHandler(onBack = onBack)
- var easterEggTriggered by remember { mutableStateOf(false) }
- val gestureState by gestureUiStateFlow.collectAsStateWithLifecycle(NotStarted)
- val easterEggMonitor = EasterEggGestureMonitor { easterEggTriggered = true }
- val gestureHandler =
- remember(gestureRecognizer) { TouchpadGestureHandler(gestureRecognizer, easterEggMonitor) }
- TouchpadGesturesHandlingBox(
- { gestureHandler.onMotionEvent(it) },
- gestureState,
- easterEggTriggered,
- onEasterEggFinished = { easterEggTriggered = false },
- ) {
- var lastState: TutorialActionState by remember {
- mutableStateOf(TutorialActionState.NotStarted)
- }
- lastState = gestureState.toTutorialActionState(lastState)
- ActionTutorialContent(lastState, onDoneButtonClicked, screenConfig)
- }
-}
-
-@Composable
-fun GestureTutorialScreenNew(
- screenConfig: TutorialScreenConfig,
gestureUiStateFlow: Flow<GestureUiState>,
motionEventConsumer: (MotionEvent) -> Boolean,
easterEggTriggeredFlow: Flow<Boolean>,
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 26604ca6b845..5dcd788ea4fd 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
@@ -16,23 +16,21 @@
package com.android.systemui.touchpad.tutorial.ui.composable
-import android.content.res.Resources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
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.gesture.GestureFlowAdapter
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
-import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureRecognizer
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
+import com.android.systemui.touchpad.tutorial.ui.viewmodel.HomeGestureScreenViewModel
@Composable
-fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
+fun HomeGestureTutorialScreen(
+ viewModel: HomeGestureScreenViewModel,
+ onDoneButtonClicked: () -> Unit,
+ onBack: () -> Unit,
+) {
val screenConfig =
TutorialScreenConfig(
colors = rememberScreenColors(),
@@ -47,26 +45,15 @@ fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni
),
animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_home_edu),
)
- val recognizer = rememberHomeGestureRecognizer(LocalContext.current.resources)
- val gestureUiState: Flow<GestureUiState> =
- remember(recognizer) {
- GestureFlowAdapter(recognizer).gestureStateAsFlow.map {
- it.toGestureUiState(
- progressStartMarker = "drag with gesture",
- progressEndMarker = "release playback realtime",
- successAnimation = R.raw.trackpad_home_success,
- )
- }
- }
- GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack)
-}
-
-@Composable
-private fun rememberHomeGestureRecognizer(resources: Resources): GestureRecognizer {
- val distance =
- resources.getDimensionPixelSize(R.dimen.touchpad_tutorial_gestures_distance_threshold)
- val velocity = resources.getDimension(R.dimen.touchpad_home_gesture_velocity_threshold)
- return remember(distance) { HomeGestureRecognizer(distance, velocity) }
+ GestureTutorialScreen(
+ screenConfig = screenConfig,
+ gestureUiStateFlow = viewModel.gestureUiState,
+ motionEventConsumer = viewModel::handleEvent,
+ easterEggTriggeredFlow = viewModel.easterEggTriggered,
+ onEasterEggFinished = viewModel::onEasterEggFinished,
+ onDoneButtonClicked = onDoneButtonClicked,
+ onBack = onBack,
+ )
}
@Composable
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 6400aca57693..7ff838981950 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
@@ -16,23 +16,21 @@
package com.android.systemui.touchpad.tutorial.ui.composable
-import android.content.res.Resources
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
-import androidx.compose.ui.platform.LocalContext
import com.airbnb.lottie.compose.rememberLottieDynamicProperties
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.gesture.GestureFlowAdapter
-import com.android.systemui.touchpad.tutorial.ui.gesture.GestureRecognizer
-import com.android.systemui.touchpad.tutorial.ui.gesture.RecentAppsGestureRecognizer
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
+import com.android.systemui.touchpad.tutorial.ui.viewmodel.RecentAppsGestureScreenViewModel
@Composable
-fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
+fun RecentAppsGestureTutorialScreen(
+ viewModel: RecentAppsGestureScreenViewModel,
+ onDoneButtonClicked: () -> Unit,
+ onBack: () -> Unit,
+) {
val screenConfig =
TutorialScreenConfig(
colors = rememberScreenColors(),
@@ -48,26 +46,15 @@ fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: ()
animations =
TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_recent_apps_edu),
)
- val recognizer = rememberRecentAppsGestureRecognizer(LocalContext.current.resources)
- val gestureUiState: Flow<GestureUiState> =
- remember(recognizer) {
- GestureFlowAdapter(recognizer).gestureStateAsFlow.map {
- it.toGestureUiState(
- progressStartMarker = "drag with gesture",
- progressEndMarker = "onPause",
- successAnimation = R.raw.trackpad_recent_apps_success,
- )
- }
- }
- GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack)
-}
-
-@Composable
-private fun rememberRecentAppsGestureRecognizer(resources: Resources): GestureRecognizer {
- val distance =
- resources.getDimensionPixelSize(R.dimen.touchpad_tutorial_gestures_distance_threshold)
- val velocity = resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold)
- return remember(distance, velocity) { RecentAppsGestureRecognizer(distance, velocity) }
+ GestureTutorialScreen(
+ screenConfig = screenConfig,
+ gestureUiStateFlow = viewModel.gestureUiState,
+ motionEventConsumer = viewModel::handleEvent,
+ easterEggTriggeredFlow = viewModel.easterEggTriggered,
+ onEasterEggFinished = viewModel::onEasterEggFinished,
+ onDoneButtonClicked = onDoneButtonClicked,
+ onBack = onBack,
+ )
}
@Composable
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 1b4ee38c3470..6b4cbab3ae09 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,8 @@ 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.HomeGestureScreenViewModel
+import com.android.systemui.touchpad.tutorial.ui.viewmodel.RecentAppsGestureScreenViewModel
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.BACK_GESTURE
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.HOME_GESTURE
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.RECENT_APPS_GESTURE
@@ -53,9 +55,12 @@ constructor(
private val logger: InputDeviceTutorialLogger,
private val metricsLogger: KeyboardTouchpadTutorialMetricsLogger,
private val backGestureViewModel: BackGestureScreenViewModel,
+ private val homeGestureViewModel: HomeGestureScreenViewModel,
+ private val recentAppsGestureViewModel: RecentAppsGestureScreenViewModel,
) : ComponentActivity() {
- private val vm by viewModels<TouchpadTutorialViewModel>(factoryProducer = { viewModelFactory })
+ private val tutorialViewModel by
+ viewModels<TouchpadTutorialViewModel>(factoryProducer = { viewModelFactory })
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -63,7 +68,13 @@ constructor(
setTitle(getString(R.string.launch_touchpad_tutorial_notification_content))
setContent {
PlatformTheme {
- TouchpadTutorialScreen(vm, backGestureViewModel, closeTutorial = ::finishTutorial)
+ TouchpadTutorialScreen(
+ tutorialViewModel,
+ backGestureViewModel,
+ homeGestureViewModel,
+ recentAppsGestureViewModel,
+ closeTutorial = ::finishTutorial,
+ )
}
}
// required to handle 3+ fingers on touchpad
@@ -79,12 +90,12 @@ constructor(
override fun onResume() {
super.onResume()
- vm.onOpened()
+ tutorialViewModel.onOpened()
}
override fun onPause() {
super.onPause()
- vm.onClosed()
+ tutorialViewModel.onClosed()
}
}
@@ -92,6 +103,8 @@ constructor(
fun TouchpadTutorialScreen(
vm: TouchpadTutorialViewModel,
backGestureViewModel: BackGestureScreenViewModel,
+ homeGestureViewModel: HomeGestureScreenViewModel,
+ recentAppsGestureViewModel: RecentAppsGestureScreenViewModel,
closeTutorial: () -> Unit,
) {
val activeScreen by vm.screen.collectAsStateWithLifecycle(STARTED)
@@ -122,11 +135,13 @@ fun TouchpadTutorialScreen(
)
HOME_GESTURE ->
HomeGestureTutorialScreen(
+ homeGestureViewModel,
onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) },
onBack = { vm.goTo(TUTORIAL_SELECTION) },
)
RECENT_APPS_GESTURE ->
RecentAppsGestureTutorialScreen(
+ recentAppsGestureViewModel,
onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) },
onBack = { vm.goTo(TUTORIAL_SELECTION) },
)
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
new file mode 100644
index 000000000000..1c865f57b8c7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModel.kt
@@ -0,0 +1,91 @@
+/*
+ * 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.content.res.Resources
+import android.view.MotionEvent
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+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
+import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
+import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker
+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
+import kotlinx.coroutines.flow.map
+
+class HomeGestureScreenViewModel
+@Inject
+constructor(
+ val configurationInteractor: ConfigurationInteractor,
+ @Main val resources: Resources,
+ 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> =
+ configurationInteractor
+ .dimensionPixelSize(R.dimen.touchpad_tutorial_gestures_distance_threshold)
+ .distinctUntilChanged()
+
+ private val velocityThreshold: Flow<Float> =
+ configurationInteractor.onAnyConfigurationChange
+ .map { resources.getDimension(R.dimen.touchpad_home_gesture_velocity_threshold) }
+ .distinctUntilChanged()
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override val gestureUiState: Flow<GestureUiState> =
+ distanceThreshold
+ .combine(velocityThreshold, { distance, velocity -> distance to velocity })
+ .flatMapLatest { (distance, velocity) ->
+ val recognizer =
+ HomeGestureRecognizer(
+ gestureDistanceThresholdPx = distance,
+ velocityThresholdPxPerMs = velocity,
+ velocityTracker = velocityTracker,
+ )
+ handler = TouchpadGestureHandler(recognizer, easterEggMonitor)
+ GestureFlowAdapter(recognizer).gestureStateAsFlow
+ }
+ .map { toGestureUiState(it) }
+
+ private fun toGestureUiState(it: GestureState) =
+ it.toGestureUiState(
+ progressStartMarker = "drag with gesture",
+ progressEndMarker = "release playback realtime",
+ successAnimation = R.raw.trackpad_home_success,
+ )
+
+ override fun handleEvent(event: MotionEvent): Boolean {
+ return handler?.onMotionEvent(event) ?: false
+ }
+}
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
new file mode 100644
index 000000000000..09947a8b109e
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModel.kt
@@ -0,0 +1,95 @@
+/*
+ * 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.content.res.Resources
+import android.view.MotionEvent
+import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
+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
+import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureHandler
+import com.android.systemui.touchpad.tutorial.ui.gesture.VelocityTracker
+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
+import kotlinx.coroutines.flow.map
+
+class RecentAppsGestureScreenViewModel
+@Inject
+constructor(
+ configurationInteractor: ConfigurationInteractor,
+ @Main private val resources: Resources,
+ 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> =
+ configurationInteractor.onAnyConfigurationChange
+ .map {
+ resources.getDimensionPixelSize(
+ R.dimen.touchpad_tutorial_gestures_distance_threshold
+ )
+ }
+ .distinctUntilChanged()
+
+ private val velocityThreshold: Flow<Float> =
+ configurationInteractor.onAnyConfigurationChange
+ .map { resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold) }
+ .distinctUntilChanged()
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ override val gestureUiState: Flow<GestureUiState> =
+ distanceThreshold
+ .combine(velocityThreshold, { distance, velocity -> distance to velocity })
+ .flatMapLatest { (distance, velocity) ->
+ val recognizer =
+ RecentAppsGestureRecognizer(
+ gestureDistanceThresholdPx = distance,
+ velocityThresholdPxPerMs = velocity,
+ velocityTracker = velocityTracker,
+ )
+ handler = TouchpadGestureHandler(recognizer, easterEggMonitor)
+ GestureFlowAdapter(recognizer).gestureStateAsFlow
+ }
+ .map { toGestureUiState(it) }
+
+ private fun toGestureUiState(it: GestureState) =
+ it.toGestureUiState(
+ progressStartMarker = "drag with gesture",
+ progressEndMarker = "onPause",
+ successAnimation = R.raw.trackpad_recent_apps_success,
+ )
+
+ override fun handleEvent(event: MotionEvent): Boolean {
+ return handler?.onMotionEvent(event) ?: false
+ }
+}