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

Commit 35092d8d authored by Juan Sebastian Martinez's avatar Juan Sebastian Martinez
Browse files

Refactoring KeyguardQuickAffordanceHapticViewModel.

The refactor simplifies the logic to play haptics by removing the need
to rely on Flows. Now, the view model is only responsible of playing
haptics when a quick affordance toggles after being long-pressed. The
logic to play haptics when a quick affordance launches is now delegated
to the KeyguardQuickAffordanceInteractor.

Test: modified and added Unit tests
Test: manual. Verified that correct haptics play when toggling shortcuts
  in the lockscreen, as well as long-pressing to launch activities.
Test: manual. The same manual tests were verified with flexiglass on.
Flag: com.android.systemui.msdl_feedback
Bug: 394330308
Change-Id: I19a189c0b94ed0813c46da9f08297cb9777764b0
parent 4ad9d3df
Loading
Loading
Loading
Loading
+81 −51
Original line number Diff line number Diff line
@@ -16,21 +16,21 @@

package com.android.systemui.keyguard.data.quickaffordance

import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.haptics.FakeVibratorHelper
import com.android.systemui.haptics.msdl.fakeMSDLPlayer
import com.android.systemui.haptics.vibratorHelper
import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceHapticViewModelFactory
import com.android.systemui.keyguard.domain.interactor.keyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.keyguard.ui.binder.KeyguardBottomAreaVibrations
import com.android.systemui.kosmos.testScope
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.testKosmos
import com.google.android.msdl.data.model.MSDLToken
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
@@ -41,74 +41,104 @@ class KeyguardQuickAffordanceHapticViewModelTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val slotId = KeyguardQuickAffordanceSlots.SLOT_ID_BOTTOM_START
    private val configKey = "$slotId::home"
    private val keyguardQuickAffordanceInteractor = kosmos.keyguardQuickAffordanceInteractor
    private val viewModelFlow =
        MutableStateFlow(KeyguardQuickAffordanceViewModel(configKey = configKey, slotId = slotId))
    private val vibratorHelper = kosmos.vibratorHelper as FakeVibratorHelper
    private val msdlPlayer = kosmos.fakeMSDLPlayer

    private val underTest =
        kosmos.keyguardQuickAffordanceHapticViewModelFactory.create(viewModelFlow)
    private val underTest = kosmos.keyguardQuickAffordanceHapticViewModelFactory.create()

    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    @Test
    fun whenLaunchingFromTriggeredResult_hapticStateIsLaunch() =
    fun onQuickAffordanceClick_playsShadeEffect() =
        testScope.runTest {
            // GIVEN that the result from triggering the affordance launched an activity or dialog
            val hapticState by collectLastValue(underTest.quickAffordanceHapticState)
            keyguardQuickAffordanceInteractor.setLaunchingFromTriggeredResult(
                KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(true, configKey)
            )
            runCurrent()
            underTest.onQuickAffordanceClick()

            // THEN the haptic state indicates that a launch haptics must play
            assertThat(hapticState)
                .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.LAUNCH)
            assertThat(vibratorHelper.hasVibratedWithEffects(KeyguardBottomAreaVibrations.Shake))
                .isTrue()
            assertThat(msdlPlayer.tokensPlayed.isEmpty()).isTrue()
        }

    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    @Test
    fun whenNotLaunchFromTriggeredResult_hapticStateDoesNotEmit() =
    fun onQuickAffordanceClick_playsFailureToken() =
        testScope.runTest {
            // GIVEN that the result from triggering the affordance did not launch an activity or
            // dialog
            val hapticState by collectLastValue(underTest.quickAffordanceHapticState)
            keyguardQuickAffordanceInteractor.setLaunchingFromTriggeredResult(
                KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(false, configKey)
            )
            runCurrent()
            underTest.onQuickAffordanceClick()

            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.FAILURE)
            assertThat(vibratorHelper.totalVibrations).isEqualTo(0)
        }

    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    @Test
    fun onUpdateActivatedHistory_withoutLongPress_whenToggling_doesNotPlayHaptics() =
        testScope.runTest {
            // GIVEN that the isActivated state toggles without a long-press called
            assertThat(underTest.longPressed).isFalse()
            underTest.updateActivatedHistory(false)
            underTest.updateActivatedHistory(true)

            // THEN there is no haptic state to play any feedback
            assertThat(hapticState)
                .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.NO_HAPTICS)
            // THEN no haptics play
            assertThat(msdlPlayer.tokensPlayed.isEmpty()).isTrue()
            assertThat(vibratorHelper.totalVibrations).isEqualTo(0)
        }

    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    @Test
    fun onQuickAffordanceTogglesToActivated_hapticStateIsToggleOn() =
    fun onUpdateActivatedHistory_togglesToActivated_playsMSDLSwitchOnToken() =
        testScope.runTest {
            // GIVEN that an affordance toggles from deactivated to activated
            val hapticState by collectLastValue(underTest.quickAffordanceHapticState)
            toggleQuickAffordance(on = true)

            // THEN the haptic state reflects that a toggle on haptics should play
            assertThat(hapticState)
                .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.TOGGLE_ON)
            // THEN the switch on token plays
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWITCH_ON)
            assertThat(vibratorHelper.totalVibrations).isEqualTo(0)
        }

    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    @Test
    fun onQuickAffordanceTogglesToDeactivated_hapticStateIsToggleOff() =
    fun onUpdateActivatedHistory_togglesToDeactivated_playsMSDLSwitchOffToken() =
        testScope.runTest {
            // GIVEN that an affordance toggles from activated to deactivated
            val hapticState by collectLastValue(underTest.quickAffordanceHapticState)
            toggleQuickAffordance(on = false)

            // THEN the haptic state reflects that a toggle off haptics should play
            assertThat(hapticState)
                .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.TOGGLE_OFF)
            // THEN the switch off token plays
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.SWITCH_OFF)
            assertThat(vibratorHelper.totalVibrations).isEqualTo(0)
        }

    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    @Test
    fun onUpdateActivatedHistory_togglesToActivated__playsActivatedEffect() =
        testScope.runTest {
            // GIVEN that an affordance toggles from deactivated to activated
            toggleQuickAffordance(on = true)

            // THEN the activated effect plays
            assertThat(
                    vibratorHelper.hasVibratedWithEffects(KeyguardBottomAreaVibrations.Activated)
                )
                .isTrue()
            assertThat(msdlPlayer.tokensPlayed.isEmpty()).isTrue()
        }

    @DisableFlags(Flags.FLAG_MSDL_FEEDBACK)
    @Test
    fun onUpdateActivatedHistory_togglesToDeactivated_playsDeactivatedEffect() =
        testScope.runTest {
            // GIVEN that an affordance toggles from activated to deactivated
            toggleQuickAffordance(on = false)

            // THEN the deactivated effect plays
            assertThat(
                    vibratorHelper.hasVibratedWithEffects(KeyguardBottomAreaVibrations.Deactivated)
                )
                .isTrue()
            assertThat(msdlPlayer.tokensPlayed.isEmpty()).isTrue()
        }

    private fun TestScope.toggleQuickAffordance(on: Boolean) {
    private fun toggleQuickAffordance(on: Boolean) {
        underTest.onQuickAffordanceLongPress()
        underTest.updateActivatedHistory(!on)
        runCurrent()
        underTest.onQuickAffordanceLongPress()
        underTest.updateActivatedHistory(on)
        runCurrent()
    }
}
+10 −7
Original line number Diff line number Diff line
@@ -19,11 +19,13 @@ package com.android.systemui.keyguard.domain.interactor

import android.app.admin.DevicePolicyManager
import android.os.UserHandle
import android.platform.test.annotations.EnableFlags
import android.view.accessibility.AccessibilityManager
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.common.shared.model.ContentDescription
@@ -34,6 +36,7 @@ import com.android.systemui.dock.DockManager
import com.android.systemui.dock.DockManagerFake
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.haptics.msdl.fakeMSDLPlayer
import com.android.systemui.keyguard.data.quickaffordance.BuiltInKeyguardQuickAffordanceKeys
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceConfig
import com.android.systemui.keyguard.data.quickaffordance.FakeKeyguardQuickAffordanceProviderClientFactory
@@ -65,6 +68,7 @@ import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.util.settings.fakeSettings
import com.google.android.msdl.data.model.MSDLToken
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.runCurrent
@@ -84,6 +88,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
    private val kosmos = testKosmos()
    private val testScope = kosmos.testScope
    private val settings = kosmos.fakeSettings
    private val msdlPlayer = kosmos.fakeMSDLPlayer

    @Mock private lateinit var lockPatternUtils: LockPatternUtils
    @Mock private lateinit var keyguardStateController: KeyguardStateController
@@ -198,6 +203,7 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
                appContext = context,
                accessibilityManager = accessibilityManager,
                sceneInteractor = { kosmos.sceneInteractor },
                msdlPlayer = msdlPlayer,
            )
        kosmos.keyguardQuickAffordanceInteractor = underTest

@@ -782,8 +788,9 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
            assertThat(launchingAffordance).isFalse()
        }

    @EnableFlags(Flags.FLAG_MSDL_FEEDBACK)
    @Test
    fun onQuickAffordanceTriggered_updatesLaunchingFromTriggeredResult() =
    fun onQuickAffordanceTriggered_onLaunched_playsMSDLLongPress() =
        testScope.runTest {
            // WHEN selecting and triggering a quick affordance at a slot
            val key = homeControls.key
@@ -796,12 +803,8 @@ class KeyguardQuickAffordanceInteractorTest : SysuiTestCase() {
            runCurrent()
            underTest.onQuickAffordanceTriggered(encodedKey, expandable = null, slot)

            // THEN the latest triggered result shows that an action launched for the same key and
            // slot
            val launchingFromTriggeredResult by
                collectLastValue(underTest.launchingFromTriggeredResult)
            assertThat(launchingFromTriggeredResult?.launched).isEqualTo(actionLaunched)
            assertThat(launchingFromTriggeredResult?.configKey).isEqualTo(encodedKey)
            // THEN long-press token plays since the action launched.
            assertThat(msdlPlayer.latestTokenPlayed).isEqualTo(MSDLToken.LONG_PRESS)
        }

    companion object {
+13 −33
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import com.android.app.tracing.coroutines.withContextTraced as withContext
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.internal.widget.LockPatternUtils
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.Flags.msdlFeedback
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
import com.android.systemui.dagger.SysUISingleton
@@ -57,11 +58,12 @@ import com.android.systemui.shared.customization.data.content.CustomizationProvi
import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants.KEYGUARD_QUICK_AFFORDANCE_ID_NONE
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.policy.KeyguardStateController
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.domain.MSDLPlayer
import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
@@ -93,6 +95,7 @@ constructor(
    @Background private val backgroundDispatcher: CoroutineDispatcher,
    @ShadeDisplayAware private val appContext: Context,
    private val sceneInteractor: Lazy<SceneInteractor>,
    private val msdlPlayer: MSDLPlayer,
) {
    /**
     * Whether a quick affordance is being launched. Quick Affordances are interactive lockscreen UI
@@ -100,14 +103,6 @@ constructor(
     */
    val launchingAffordance: StateFlow<Boolean> = repository.get().launchingAffordance.asStateFlow()

    /**
     * Whether a [KeyguardQuickAffordanceConfig.OnTriggeredResult] indicated that the system
     * launched an activity or showed a dialog.
     */
    private val _launchingFromTriggeredResult =
        MutableStateFlow<KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult?>(null)
    val launchingFromTriggeredResult = _launchingFromTriggeredResult.asStateFlow()

    /**
     * Whether the UI should use the long press gesture to activate quick affordances.
     *
@@ -199,42 +194,27 @@ constructor(

        when (val result = config.onTriggered(expandable)) {
            is KeyguardQuickAffordanceConfig.OnTriggeredResult.StartActivity -> {
                setLaunchingFromTriggeredResult(
                    KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(
                        launched = true,
                        configKey,
                    )
                )
                launchQuickAffordance(
                    intent = result.intent,
                    canShowWhileLocked = result.canShowWhileLocked,
                    expandable = expandable,
                )
                if (msdlFeedback()) {
                    msdlPlayer.playToken(MSDLToken.LONG_PRESS)
                }
            }
            is KeyguardQuickAffordanceConfig.OnTriggeredResult.Handled -> {
                setLaunchingFromTriggeredResult(
                    KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(
                        result.actionLaunched,
                        configKey,
                    )
                )
                if (result.actionLaunched && msdlFeedback()) {
                    msdlPlayer.playToken(MSDLToken.LONG_PRESS)
                }
            }
            is KeyguardQuickAffordanceConfig.OnTriggeredResult.ShowDialog -> {
                setLaunchingFromTriggeredResult(
                    KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult(
                        launched = true,
                        configKey,
                    )
                )
                showDialog(result.dialog, result.expandable)
                if (msdlFeedback()) {
                    msdlPlayer.playToken(MSDLToken.LONG_PRESS)
                }
                showDialog(result.dialog, result.expandable)
            }
        }

    fun setLaunchingFromTriggeredResult(
        launchingResult: KeyguardQuickAffordanceConfig.LaunchingFromTriggeredResult?
    ) {
        _launchingFromTriggeredResult.value = launchingResult
    }

    /**
+9 −60
Original line number Diff line number Diff line
@@ -20,7 +20,6 @@ package com.android.systemui.keyguard.ui.binder
import android.annotation.SuppressLint
import android.content.res.ColorStateList
import android.graphics.drawable.Animatable2
import android.os.VibrationEffect
import android.util.Size
import android.view.View
import android.view.ViewGroup
@@ -34,7 +33,6 @@ import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.Flags
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.common.shared.model.Icon
@@ -48,13 +46,11 @@ import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.doOnEnd
import com.google.android.msdl.data.model.MSDLToken
import com.google.android.msdl.domain.MSDLPlayer
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map

/** This is only for a SINGLE Quick affordance */
@@ -93,12 +89,8 @@ constructor(
    ): Binding {
        val button = view as ImageView
        val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
        val hapticsViewModel =
            if (Flags.msdlFeedback()) {
                hapticsViewModelFactory.create(viewModel)
            } else {
                null
            }
        val hapticsViewModel = hapticsViewModelFactory.create()

        val disposableHandle =
            view.repeatWhenAttached {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -107,9 +99,9 @@ constructor(
                            updateButton(
                                view = button,
                                viewModel = buttonModel,
                                hapticsViewModel,
                                messageDisplayer = messageDisplayer,
                            )
                            hapticsViewModel?.updateActivatedHistory(buttonModel.isActivated)
                        }
                    }

@@ -125,32 +117,6 @@ constructor(
                            }
                        }
                    }

                    if (Flags.msdlFeedback()) {
                        launch {
                            hapticsViewModel
                                ?.quickAffordanceHapticState
                                ?.filter {
                                    it !=
                                        KeyguardQuickAffordanceHapticViewModel.HapticState
                                            .NO_HAPTICS
                                }
                                ?.collect { state ->
                                    when (state) {
                                        KeyguardQuickAffordanceHapticViewModel.HapticState
                                            .TOGGLE_ON -> msdlPlayer.playToken(MSDLToken.SWITCH_ON)
                                        KeyguardQuickAffordanceHapticViewModel.HapticState
                                            .TOGGLE_OFF ->
                                            msdlPlayer.playToken(MSDLToken.SWITCH_OFF)
                                        KeyguardQuickAffordanceHapticViewModel.HapticState.LAUNCH ->
                                            msdlPlayer.playToken(MSDLToken.LONG_PRESS)
                                        KeyguardQuickAffordanceHapticViewModel.HapticState
                                            .NO_HAPTICS -> Unit
                                    }
                                    hapticsViewModel.resetLaunchingFromTriggeredResult()
                                }
                        }
                    }
                }
            }

@@ -170,8 +136,10 @@ constructor(
    private fun updateButton(
        view: ImageView,
        viewModel: KeyguardQuickAffordanceViewModel,
        hapticsViewModel: KeyguardQuickAffordanceHapticViewModel,
        messageDisplayer: (Int) -> Unit,
    ) {
        hapticsViewModel.updateActivatedHistory(viewModel.isActivated)
        logger.logUpdate(viewModel)
        if (!viewModel.isVisible) {
            view.isInvisible = true
@@ -263,16 +231,15 @@ constructor(
                    shakeAnimator.doOnEnd { view.translationX = 0f }
                    shakeAnimator.start()

                    vibratorHelper?.playFeedback(KeyguardBottomAreaVibrations.Shake, msdlPlayer)
                    hapticsViewModel.onQuickAffordanceClick()
                    logger.logQuickAffordanceTapped(viewModel.configKey)
                }
                view.onLongClickListener =
                    OnLongClickListener(
                        falsingManager,
                        viewModel,
                        vibratorHelper,
                        hapticsViewModel,
                        onTouchListener,
                        msdlPlayer,
                    )
            } else {
                view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
@@ -332,9 +299,8 @@ constructor(
    private class OnLongClickListener(
        private val falsingManager: FalsingManager?,
        private val viewModel: KeyguardQuickAffordanceViewModel,
        private val vibratorHelper: VibratorHelper?,
        private val hapticsViewModel: KeyguardQuickAffordanceHapticViewModel,
        private val onTouchListener: KeyguardQuickAffordanceOnTouchListener,
        private val msdlPlayer: MSDLPlayer,
    ) : View.OnLongClickListener {
        override fun onLongClick(view: View): Boolean {
            if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) {
@@ -342,6 +308,7 @@ constructor(
            }

            if (viewModel.configKey != null) {
                hapticsViewModel.onQuickAffordanceLongPress()
                viewModel.onClicked(
                    KeyguardQuickAffordanceViewModel.OnClickedParameters(
                        configKey = viewModel.configKey,
@@ -349,14 +316,6 @@ constructor(
                        slotId = viewModel.slotId,
                    )
                )
                vibratorHelper?.playFeedback(
                    if (viewModel.isActivated) {
                        KeyguardBottomAreaVibrations.Activated
                    } else {
                        KeyguardBottomAreaVibrations.Deactivated
                    },
                    msdlPlayer,
                )
            }

            onTouchListener.cancel()
@@ -368,13 +327,3 @@ constructor(

    private data class ConfigurationBasedDimensions(val buttonSizePx: Size)
}

private fun VibratorHelper.playFeedback(effect: VibrationEffect, msdlPlayer: MSDLPlayer) {
    if (!Flags.msdlFeedback()) {
        vibrate(effect)
    } else {
        if (effect == KeyguardBottomAreaVibrations.Shake) {
            msdlPlayer.playToken(MSDLToken.FAILURE)
        }
    }
}
+43 −59

File changed.

Preview size limit exceeded, changes collapsed.

Loading