Loading packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt 0 → 100644 +144 −0 Original line number Diff line number Diff line /* * 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.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.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class BackGestureScreenViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val fakeConfigRepository = kosmos.fakeConfigurationRepository private val viewModel = BackGestureScreenViewModel(kosmos.configurationInteractor) @Before fun before() { setThresholdResource(threshold = SWIPE_DISTANCE - 1) kosmos.useUnconfinedTestDispatcher() } @Test fun easterEggNotTriggeredAtStart() = kosmos.runTest { val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) assertThat(easterEggTriggered).isFalse() } @Test fun emitsProgressStateWithLeftProgressAnimation() = kosmos.runTest { assertProgressWhileMovingFingers( deltaX = -SWIPE_DISTANCE, expected = InProgress( progress = 1f, progressStartMarker = "gesture to L", progressEndMarker = "end progress L", ), ) } @Test fun emitsProgressStateWithRightProgressAnimation() = kosmos.runTest { assertProgressWhileMovingFingers( deltaX = SWIPE_DISTANCE, expected = InProgress( progress = 1f, progressStartMarker = "gesture to R", progressEndMarker = "end progress R", ), ) } @Test fun emitsFinishedStateWithLeftSuccessAnimation() = kosmos.runTest { assertStateAfterEvents( events = ThreeFingerGesture.swipeLeft(), expected = Finished(successAnimation = R.raw.trackpad_back_success_left), ) } @Test fun emitsFinishedStateWithRightSuccessAnimation() = kosmos.runTest { assertStateAfterEvents( events = ThreeFingerGesture.swipeRight(), expected = Finished(successAnimation = R.raw.trackpad_back_success_right), ) } @Test fun gestureRecognitionTakesLatestDistanceThresholdIntoAccount() = kosmos.runTest { fun performBackGesture() = ThreeFingerGesture.swipeLeft().forEach { viewModel.handleEvent(it) } val state by collectLastValue(viewModel.gestureUiState) performBackGesture() assertThat(state).isInstanceOf(Finished::class.java) setThresholdResource(SWIPE_DISTANCE + 1) performBackGesture() // now swipe distance is not enough to trigger success assertThat(state).isInstanceOf(Error::class.java) } private fun setThresholdResource(threshold: Float) { fakeConfigRepository.setDimensionPixelSize( R.dimen.touchpad_tutorial_gestures_distance_threshold, (threshold).toInt(), ) fakeConfigRepository.onAnyConfigurationChange() } private fun Kosmos.assertProgressWhileMovingFingers(deltaX: Float, expected: GestureUiState) { assertStateAfterEvents( events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) }, expected = expected, ) } private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) { val state by collectLastValue(viewModel.gestureUiState) events.forEach { viewModel.handleEvent(it) } assertThat(state).isEqualTo(expected) } } packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt 0 → 100644 +154 −0 Original line number Diff line number Diff line /* * 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) } } packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt 0 → 100644 +160 −0 Original line number Diff line number Diff line /* * 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) } } packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt +17 −5 Original line number Diff line number Diff line Loading @@ -27,7 +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 Loading @@ -45,8 +49,11 @@ interface TouchpadTutorialModule { companion object { @Provides fun touchpadScreensProvider(): TouchpadTutorialScreensProvider { return ScreensProvider fun touchpadScreensProvider( backGestureScreenViewModel: BackGestureScreenViewModel, homeGestureScreenViewModel: HomeGestureScreenViewModel, ): TouchpadTutorialScreensProvider { return ScreensProvider(backGestureScreenViewModel, homeGestureScreenViewModel) } @SysUISingleton Loading @@ -59,17 +66,22 @@ interface TouchpadTutorialModule { ): TouchpadGesturesInteractor { return TouchpadGesturesInteractor(sysUiState, displayTracker, backgroundScope, logger) } @Provides fun velocityTracker(): VelocityTracker = VerticalVelocityTracker() } } private object ScreensProvider : TouchpadTutorialScreensProvider { private class ScreensProvider( val backGestureScreenViewModel: BackGestureScreenViewModel, val homeGestureScreenViewModel: HomeGestureScreenViewModel, ) : TouchpadTutorialScreensProvider { @Composable override fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { BackGestureTutorialScreen(onDoneButtonClicked, onBack) BackGestureTutorialScreen(backGestureScreenViewModel, onDoneButtonClicked, onBack) } @Composable override fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { HomeGestureTutorialScreen(onDoneButtonClicked, onBack) HomeGestureTutorialScreen(homeGestureScreenViewModel, onDoneButtonClicked, onBack) } } packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +15 −44 Original line number Diff line number Diff line Loading @@ -16,26 +16,22 @@ package com.android.systemui.touchpad.tutorial.ui.composable import android.content.res.Resources import androidx.compose.material3.MaterialTheme 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.BackGestureRecognizer 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.GestureRecognizer import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.util.kotlin.pairwiseBy import kotlinx.coroutines.flow.Flow import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel @Composable fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { fun BackGestureTutorialScreen( viewModel: BackGestureScreenViewModel, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { val screenConfig = TutorialScreenConfig( colors = rememberScreenColors(), Loading @@ -50,41 +46,16 @@ fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni ), animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_back_edu), ) val recognizer = rememberBackGestureRecognizer(LocalContext.current.resources) val gestureUiState: Flow<GestureUiState> = remember(recognizer) { GestureFlowAdapter(recognizer).gestureStateAsFlow.pairwiseBy(GestureState.NotStarted) { previous, current -> val (startMarker, endMarker) = getMarkers(current) current.toGestureUiState( progressStartMarker = startMarker, progressEndMarker = endMarker, successAnimation = successAnimation(previous), GestureTutorialScreen( screenConfig = screenConfig, gestureUiStateFlow = viewModel.gestureUiState, motionEventConsumer = viewModel::handleEvent, easterEggTriggeredFlow = viewModel.easterEggTriggered, onEasterEggFinished = viewModel::onEasterEggFinished, onDoneButtonClicked = onDoneButtonClicked, onBack = onBack, ) } } GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack) } @Composable private fun rememberBackGestureRecognizer(resources: Resources): GestureRecognizer { val distance = resources.getDimensionPixelSize(R.dimen.touchpad_tutorial_gestures_distance_threshold) return remember(distance) { BackGestureRecognizer(distance) } } private fun getMarkers(it: GestureState): Pair<String, String> { return if (it is GestureState.InProgress && it.direction == GestureDirection.LEFT) { "gesture to L" to "end progress L" } else "gesture to R" to "end progress R" } private fun successAnimation(previous: GestureState): Int { return if (previous is GestureState.InProgress && previous.direction == GestureDirection.LEFT) { R.raw.trackpad_back_success_left } else R.raw.trackpad_back_success_right } @Composable private fun rememberScreenColors(): TutorialScreenConfig.Colors { Loading Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/BackGestureScreenViewModelTest.kt 0 → 100644 +144 −0 Original line number Diff line number Diff line /* * 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.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.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class BackGestureScreenViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val fakeConfigRepository = kosmos.fakeConfigurationRepository private val viewModel = BackGestureScreenViewModel(kosmos.configurationInteractor) @Before fun before() { setThresholdResource(threshold = SWIPE_DISTANCE - 1) kosmos.useUnconfinedTestDispatcher() } @Test fun easterEggNotTriggeredAtStart() = kosmos.runTest { val easterEggTriggered by collectLastValue(viewModel.easterEggTriggered) assertThat(easterEggTriggered).isFalse() } @Test fun emitsProgressStateWithLeftProgressAnimation() = kosmos.runTest { assertProgressWhileMovingFingers( deltaX = -SWIPE_DISTANCE, expected = InProgress( progress = 1f, progressStartMarker = "gesture to L", progressEndMarker = "end progress L", ), ) } @Test fun emitsProgressStateWithRightProgressAnimation() = kosmos.runTest { assertProgressWhileMovingFingers( deltaX = SWIPE_DISTANCE, expected = InProgress( progress = 1f, progressStartMarker = "gesture to R", progressEndMarker = "end progress R", ), ) } @Test fun emitsFinishedStateWithLeftSuccessAnimation() = kosmos.runTest { assertStateAfterEvents( events = ThreeFingerGesture.swipeLeft(), expected = Finished(successAnimation = R.raw.trackpad_back_success_left), ) } @Test fun emitsFinishedStateWithRightSuccessAnimation() = kosmos.runTest { assertStateAfterEvents( events = ThreeFingerGesture.swipeRight(), expected = Finished(successAnimation = R.raw.trackpad_back_success_right), ) } @Test fun gestureRecognitionTakesLatestDistanceThresholdIntoAccount() = kosmos.runTest { fun performBackGesture() = ThreeFingerGesture.swipeLeft().forEach { viewModel.handleEvent(it) } val state by collectLastValue(viewModel.gestureUiState) performBackGesture() assertThat(state).isInstanceOf(Finished::class.java) setThresholdResource(SWIPE_DISTANCE + 1) performBackGesture() // now swipe distance is not enough to trigger success assertThat(state).isInstanceOf(Error::class.java) } private fun setThresholdResource(threshold: Float) { fakeConfigRepository.setDimensionPixelSize( R.dimen.touchpad_tutorial_gestures_distance_threshold, (threshold).toInt(), ) fakeConfigRepository.onAnyConfigurationChange() } private fun Kosmos.assertProgressWhileMovingFingers(deltaX: Float, expected: GestureUiState) { assertStateAfterEvents( events = ThreeFingerGesture.eventsForGestureInProgress { move(deltaX = deltaX) }, expected = expected, ) } private fun Kosmos.assertStateAfterEvents(events: List<MotionEvent>, expected: GestureUiState) { val state by collectLastValue(viewModel.gestureUiState) events.forEach { viewModel.handleEvent(it) } assertThat(state).isEqualTo(expected) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/HomeGestureScreenViewModelTest.kt 0 → 100644 +154 −0 Original line number Diff line number Diff line /* * 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) } }
packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/RecentAppsGestureScreenViewModelTest.kt 0 → 100644 +160 −0 Original line number Diff line number Diff line /* * 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) } }
packages/SystemUI/src/com/android/systemui/touchpad/tutorial/TouchpadTutorialModule.kt +17 −5 Original line number Diff line number Diff line Loading @@ -27,7 +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 Loading @@ -45,8 +49,11 @@ interface TouchpadTutorialModule { companion object { @Provides fun touchpadScreensProvider(): TouchpadTutorialScreensProvider { return ScreensProvider fun touchpadScreensProvider( backGestureScreenViewModel: BackGestureScreenViewModel, homeGestureScreenViewModel: HomeGestureScreenViewModel, ): TouchpadTutorialScreensProvider { return ScreensProvider(backGestureScreenViewModel, homeGestureScreenViewModel) } @SysUISingleton Loading @@ -59,17 +66,22 @@ interface TouchpadTutorialModule { ): TouchpadGesturesInteractor { return TouchpadGesturesInteractor(sysUiState, displayTracker, backgroundScope, logger) } @Provides fun velocityTracker(): VelocityTracker = VerticalVelocityTracker() } } private object ScreensProvider : TouchpadTutorialScreensProvider { private class ScreensProvider( val backGestureScreenViewModel: BackGestureScreenViewModel, val homeGestureScreenViewModel: HomeGestureScreenViewModel, ) : TouchpadTutorialScreensProvider { @Composable override fun BackGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { BackGestureTutorialScreen(onDoneButtonClicked, onBack) BackGestureTutorialScreen(backGestureScreenViewModel, onDoneButtonClicked, onBack) } @Composable override fun HomeGesture(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { HomeGestureTutorialScreen(onDoneButtonClicked, onBack) HomeGestureTutorialScreen(homeGestureScreenViewModel, onDoneButtonClicked, onBack) } }
packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt +15 −44 Original line number Diff line number Diff line Loading @@ -16,26 +16,22 @@ package com.android.systemui.touchpad.tutorial.ui.composable import android.content.res.Resources import androidx.compose.material3.MaterialTheme 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.BackGestureRecognizer 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.GestureRecognizer import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState import com.android.systemui.util.kotlin.pairwiseBy import kotlinx.coroutines.flow.Flow import com.android.systemui.touchpad.tutorial.ui.viewmodel.BackGestureScreenViewModel @Composable fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) { fun BackGestureTutorialScreen( viewModel: BackGestureScreenViewModel, onDoneButtonClicked: () -> Unit, onBack: () -> Unit, ) { val screenConfig = TutorialScreenConfig( colors = rememberScreenColors(), Loading @@ -50,41 +46,16 @@ fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Uni ), animations = TutorialScreenConfig.Animations(educationResId = R.raw.trackpad_back_edu), ) val recognizer = rememberBackGestureRecognizer(LocalContext.current.resources) val gestureUiState: Flow<GestureUiState> = remember(recognizer) { GestureFlowAdapter(recognizer).gestureStateAsFlow.pairwiseBy(GestureState.NotStarted) { previous, current -> val (startMarker, endMarker) = getMarkers(current) current.toGestureUiState( progressStartMarker = startMarker, progressEndMarker = endMarker, successAnimation = successAnimation(previous), GestureTutorialScreen( screenConfig = screenConfig, gestureUiStateFlow = viewModel.gestureUiState, motionEventConsumer = viewModel::handleEvent, easterEggTriggeredFlow = viewModel.easterEggTriggered, onEasterEggFinished = viewModel::onEasterEggFinished, onDoneButtonClicked = onDoneButtonClicked, onBack = onBack, ) } } GestureTutorialScreen(screenConfig, recognizer, gestureUiState, onDoneButtonClicked, onBack) } @Composable private fun rememberBackGestureRecognizer(resources: Resources): GestureRecognizer { val distance = resources.getDimensionPixelSize(R.dimen.touchpad_tutorial_gestures_distance_threshold) return remember(distance) { BackGestureRecognizer(distance) } } private fun getMarkers(it: GestureState): Pair<String, String> { return if (it is GestureState.InProgress && it.direction == GestureDirection.LEFT) { "gesture to L" to "end progress L" } else "gesture to R" to "end progress R" } private fun successAnimation(previous: GestureState): Int { return if (previous is GestureState.InProgress && previous.direction == GestureDirection.LEFT) { R.raw.trackpad_back_success_left } else R.raw.trackpad_back_success_right } @Composable private fun rememberScreenColors(): TutorialScreenConfig.Colors { Loading