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

Commit 196ee120 authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez
Browse files

Adding MSDL haptic feedback to flexiglass lockscreen interactions.

As a followup of ag/29008354, this change adds haptic feedback
compliant with the Multi-sensory Design Language to authentication CUJs
in flexiglass and the lockscreen scene.

Test: atest SceneContainerStartableTest
Flag: com.android.systemui.msdl_feedback
Bug: 352770621
Bug: 352768515
Bug: 352767358
Bug: 352766822
Bug: 352765846
Bug: 352764953
Change-Id: I8fa80e0be9868128cf1c120cda5943b06c058179
parent 7eedb2d0
Loading
Loading
Loading
Loading
+196 −0
Original line number Diff line number Diff line
@@ -21,6 +21,8 @@ package com.android.systemui.scene.domain.startable
import android.app.StatusBarManager
import android.hardware.face.FaceManager
import android.os.PowerManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.view.Display
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,6 +30,8 @@ import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.internal.logging.uiEventLoggerFake
import com.android.internal.policy.IKeyguardDismissCallback
import com.android.keyguard.AuthInteractionProperties
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
@@ -48,6 +52,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
import com.android.systemui.deviceentry.shared.model.SuccessFaceAuthenticationStatus
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.haptics.msdl.fakeMSDLPlayer
import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.keyevent.data.repository.fakeKeyEventRepository
import com.android.systemui.keyguard.data.repository.biometricSettingsRepository
@@ -95,6 +100,7 @@ import com.android.systemui.statusbar.policy.data.repository.fakeDeviceProvision
import com.android.systemui.statusbar.sysuiStatusBarStateController
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.google.android.msdl.data.model.MSDLToken
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
@@ -137,6 +143,8 @@ class SceneContainerStartableTest : SysuiTestCase() {
    private val powerInteractor = kosmos.powerInteractor
    private val fakeTrustRepository = kosmos.fakeTrustRepository
    private val uiEventLoggerFake = kosmos.uiEventLoggerFake
    private val msdlPlayer = kosmos.fakeMSDLPlayer
    private val authInteractionProperties = AuthInteractionProperties()

    private lateinit var underTest: SceneContainerStartable

@@ -654,6 +662,7 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playSuccessHaptics_onSuccessfulLockscreenAuth_udfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -680,6 +689,31 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playSuccessMSDLHaptics_onSuccessfulLockscreenAuth_udfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val playSuccessHaptic by
                collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)

            setupBiometricAuth(hasUdfps = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()

            underTest.start()
            unlockWithFingerprintAuth()

            assertThat(playSuccessHaptic).isNotNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK)
            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)

            updateFingerprintAuthStatus(isSuccess = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playSuccessHaptics_onSuccessfulLockscreenAuth_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -707,6 +741,32 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playSuccessMSDLHaptics_onSuccessfulLockscreenAuth_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val playSuccessHaptic by
                collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)

            setupBiometricAuth(hasSfps = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()

            underTest.start()
            allowHapticsOnSfps()
            unlockWithFingerprintAuth()

            assertThat(playSuccessHaptic).isNotNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.UNLOCK)
            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)

            updateFingerprintAuthStatus(isSuccess = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playErrorHaptics_onFailedLockscreenAuth_udfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -727,6 +787,27 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playMSDLErrorHaptics_onFailedLockscreenAuth_udfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic)

            setupBiometricAuth(hasUdfps = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()

            underTest.start()
            updateFingerprintAuthStatus(isSuccess = false)

            assertThat(playErrorHaptic).isNotNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE)
            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playErrorHaptics_onFailedLockscreenAuth_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -747,6 +828,27 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun playMSDLErrorHaptics_onFailedLockscreenAuth_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic)

            setupBiometricAuth(hasSfps = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()

            underTest.start()
            updateFingerprintAuthStatus(isSuccess = false)

            assertThat(playErrorHaptic).isNotNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE)
            assertThat(msdlPlayer.latestPropertiesPlayed).isEqualTo(authInteractionProperties)
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun skipsSuccessHaptics_whenPowerButtonDown_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -774,6 +876,32 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun skipsMSDLSuccessHaptics_whenPowerButtonDown_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val playSuccessHaptic by
                collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)

            setupBiometricAuth(hasSfps = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()

            underTest.start()
            allowHapticsOnSfps(isPowerButtonDown = true)
            unlockWithFingerprintAuth()

            assertThat(playSuccessHaptic).isNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(msdlPlayer.latestTokenPlayed).isNull()
            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()

            updateFingerprintAuthStatus(isSuccess = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun skipsSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -801,6 +929,32 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun skipsMSDLSuccessHaptics_whenPowerButtonRecentlyPressed_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val playSuccessHaptic by
                collectLastValue(deviceEntryHapticsInteractor.playSuccessHaptic)

            setupBiometricAuth(hasSfps = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()

            underTest.start()
            allowHapticsOnSfps(lastPowerPress = 50)
            unlockWithFingerprintAuth()

            assertThat(playSuccessHaptic).isNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(msdlPlayer.latestTokenPlayed).isNull()
            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()

            updateFingerprintAuthStatus(isSuccess = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Gone)
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun skipsErrorHaptics_whenPowerButtonDown_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -822,6 +976,28 @@ class SceneContainerStartableTest : SysuiTestCase() {
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun skipsMSDLErrorHaptics_whenPowerButtonDown_sfps() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic)

            setupBiometricAuth(hasSfps = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()

            underTest.start()
            kosmos.fakeKeyEventRepository.setPowerButtonDown(true)
            updateFingerprintAuthStatus(isSuccess = false)

            assertThat(playErrorHaptic).isNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(msdlPlayer.latestTokenPlayed).isNull()
            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
        }

    @Test
    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun skipsFaceErrorHaptics_nonSfps_coEx() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
@@ -841,6 +1017,26 @@ class SceneContainerStartableTest : SysuiTestCase() {
            verify(vibratorHelper, never()).vibrateAuthSuccess(anyString())
        }

    @Test
    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    fun skipsMSDLFaceErrorHaptics_nonSfps_coEx() =
        testScope.runTest {
            val currentSceneKey by collectLastValue(sceneInteractor.currentScene)
            val playErrorHaptic by collectLastValue(deviceEntryHapticsInteractor.playErrorHaptic)

            setupBiometricAuth(hasUdfps = true, hasFace = true)
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isFalse()

            underTest.start()
            updateFaceAuthStatus(isSuccess = false)

            assertThat(playErrorHaptic).isNull()
            assertThat(currentSceneKey).isEqualTo(Scenes.Lockscreen)
            assertThat(msdlPlayer.latestTokenPlayed).isNull()
            assertThat(msdlPlayer.latestPropertiesPlayed).isNull()
        }

    @Test
    fun hydrateSystemUiState() =
        testScope.runTest {
+27 −6
Original line number Diff line number Diff line
@@ -22,7 +22,9 @@ import android.app.StatusBarManager
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
import com.android.internal.logging.UiEventLogger
import com.android.keyguard.AuthInteractionProperties
import com.android.systemui.CoreStartable
import com.android.systemui.Flags
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
@@ -73,6 +75,8 @@ import com.android.systemui.util.kotlin.pairwise
import com.android.systemui.util.kotlin.sample
import com.android.systemui.util.printSection
import com.android.systemui.util.println
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.domain.MSDLPlayer
import dagger.Lazy
import java.io.PrintWriter
import java.util.Optional
@@ -139,10 +143,13 @@ constructor(
    private val statusBarStateController: SysuiStatusBarStateController,
    private val alternateBouncerInteractor: AlternateBouncerInteractor,
    private val vibratorHelper: VibratorHelper,
    private val msdlPlayer: MSDLPlayer,
) : CoreStartable {
    private val centralSurfaces: CentralSurfaces?
        get() = centralSurfacesOptLazy.get().getOrNull()

    private val authInteractionProperties = AuthInteractionProperties()

    override fun start() {
        if (SceneContainerFlag.isEnabled) {
            sceneLogger.logFrameworkEnabled(isEnabled = true)
@@ -541,16 +548,29 @@ constructor(
                            deviceEntryHapticsInteractor.playSuccessHaptic
                                .sample(sceneInteractor.currentScene)
                                .collect { currentScene ->
                                    if (Flags.msdlFeedback()) {
                                        msdlPlayer.playToken(
                                            MSDLToken.UNLOCK,
                                            authInteractionProperties,
                                        )
                                    } else {
                                        vibratorHelper.vibrateAuthSuccess(
                                            "$TAG, $currentScene device-entry::success"
                                        )
                                    }
                                }
                        }

                        launch {
                            deviceEntryHapticsInteractor.playErrorHaptic
                                .sample(sceneInteractor.currentScene)
                                .collect { currentScene ->
                                    if (Flags.msdlFeedback()) {
                                        msdlPlayer.playToken(
                                            MSDLToken.FAILURE,
                                            authInteractionProperties,
                                        )
                                    } else {
                                        vibratorHelper.vibrateAuthError(
                                            "$TAG, $currentScene device-entry::error"
                                        )
@@ -561,6 +581,7 @@ constructor(
                }
            }
        }
    }

    /** Keeps [SysUiState] up-to-date */
    private fun hydrateSystemUiState() {
+2 −2
Original line number Diff line number Diff line
@@ -29,7 +29,7 @@ import com.android.internal.logging.MetricsLogger
import com.android.internal.widget.LockPatternUtils
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.haptics.msdl.FakeMSDLPlayer
import com.android.systemui.haptics.msdl.fakeMSDLPlayer
import com.android.systemui.haptics.msdl.msdlPlayer
import com.android.systemui.shade.ShadeController
import com.android.systemui.statusbar.policy.ConfigurationController
@@ -72,7 +72,7 @@ class EmergencyButtonControllerTest : SysuiTestCase() {
    val mainExecutor = FakeExecutor(fakeSystemClock)
    val backgroundExecutor = FakeExecutor(fakeSystemClock)
    private val kosmos = testKosmos()
    private val msdlPlayer: FakeMSDLPlayer = kosmos.msdlPlayer
    private val msdlPlayer = kosmos.fakeMSDLPlayer

    lateinit var underTest: EmergencyButtonController

+3 −1
Original line number Diff line number Diff line
@@ -17,5 +17,7 @@
package com.android.systemui.haptics.msdl

import com.android.systemui.kosmos.Kosmos
import com.google.android.msdl.domain.MSDLPlayer

val Kosmos.msdlPlayer by Kosmos.Fixture { FakeMSDLPlayer() }
var Kosmos.msdlPlayer: MSDLPlayer by Kosmos.Fixture { fakeMSDLPlayer }
val Kosmos.fakeMSDLPlayer by Kosmos.Fixture { FakeMSDLPlayer() }
+2 −2
Original line number Diff line number Diff line
@@ -36,7 +36,7 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.globalactions.domain.interactor.globalActionsInteractor
import com.android.systemui.haptics.msdl.msdlPlayer
import com.android.systemui.haptics.msdl.fakeMSDLPlayer
import com.android.systemui.haptics.qs.qsLongPressEffect
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
@@ -155,5 +155,5 @@ class KosmosJavaAdapter() {
    val scrimController by lazy { kosmos.scrimController }
    val scrimStartable by lazy { kosmos.scrimStartable }
    val sceneContainerOcclusionInteractor by lazy { kosmos.sceneContainerOcclusionInteractor }
    val msdlPlayer by lazy { kosmos.msdlPlayer }
    val msdlPlayer by lazy { kosmos.fakeMSDLPlayer }
}
Loading