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

Commit da304d82 authored by Michał Brzeziński's avatar Michał Brzeziński Committed by Android (Google) Code Review
Browse files

Merge changes Ie1fc266f,I3e86f103 into main

* changes:
  Extracting remaining ViewModels in touchpad tutorial
  Refactoring BackGestureTutorialScreen
parents 936c43b4 c18bf421
Loading
Loading
Loading
Loading
+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)
    }
}
+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)
    }
}
+17 −5
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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)
    }
}
+15 −44
Original line number Diff line number Diff line
@@ -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(),
@@ -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