Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c18bf421 authored by Michal Brzezinski's avatar Michal Brzezinski
Browse files

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
parent 1b0f1a3c
Loading
Loading
Loading
Loading
+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)
    }
}
+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)
    }
}
+13 −5
Original line number Diff line number Diff line
@@ -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)
    }
}
+1 −1
Original line number Diff line number Diff line
@@ -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,
+0 −31
Original line number Diff line number Diff line
@@ -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 {
@@ -99,34 +96,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,
Loading