diff options
| author | 2024-12-17 19:01:06 +0000 | |
|---|---|---|
| committer | 2024-12-18 11:20:37 +0000 | |
| commit | c18bf421a9714e7dc51bd705a1390a46dbe18c9e (patch) | |
| tree | e1f21951779476583f29f122e5503b6e90113032 | |
| parent | 1b0f1a3c6d56fb9fd69ef178986ccd4055a83c48 (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
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 + } +} |