summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Juan Sebastian Martinez <juansmartinez@google.com> 2024-12-03 16:14:44 -0800
committer Juan Sebastian Martinez <juansmartinez@google.com> 2024-12-17 15:19:49 -0800
commit96cb65407515987eead787cea70d8a9778d737d7 (patch)
tree29da98058bd4ee9091a548ead5d5424848347e2d
parent7415da587004876bef28332721e9fb0a2c5d8ab1 (diff)
Adding MSDL haptics to interactions with lockscreen shortcuts.
The KeyguardQuickAffordanceHapticViewModel is introduced to make use of data flows about interactions with quick affordances. With these, the view-model produces a Flow of haptic states, which can then be collected to play the corresponding haptics using the MSDL player in SysUI. Test: KeyguardQuickAffordanceHapticViewModelTest Test: manual. Verified failure haptics when tapping on a shortcut. Test: manual. Verified long-press haptics when launching from a shortcut. Test: manual. Verified switch on haptics when long-pressing a shortcut turns a control on. Test: manual. Verified switch off haptics when long-pressing a shortcut turns a control off. Flag: com.android.systemui.msdl_feedback Bug: 361320572 Change-Id: I9a4268251a4d5380cae3023ba23ccf30a6560ad5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt116
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt94
-rw-r--r--packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt96
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt35
5 files changed, 334 insertions, 48 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt
new file mode 100644
index 000000000000..18946f9d7e07
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt
@@ -0,0 +1,116 @@
+/*
+ * 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.keyguard.data.quickaffordance
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+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.kosmos.testScope
+import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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
+
+@SmallTest
+@OptIn(ExperimentalCoroutinesApi::class)
+@RunWith(AndroidJUnit4::class)
+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 underTest =
+ kosmos.keyguardQuickAffordanceHapticViewModelFactory.create(viewModelFlow)
+
+ @Test
+ fun whenLaunchingFromTriggeredResult_hapticStateIsLaunch() =
+ 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()
+
+ // THEN the haptic state indicates that a launch haptics must play
+ assertThat(hapticState)
+ .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.LAUNCH)
+ }
+
+ @Test
+ fun whenNotLaunchFromTriggeredResult_hapticStateDoesNotEmit() =
+ 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()
+
+ // THEN there is no haptic state to play any feedback
+ assertThat(hapticState)
+ .isEqualTo(KeyguardQuickAffordanceHapticViewModel.HapticState.NO_HAPTICS)
+ }
+
+ @Test
+ fun onQuickAffordanceTogglesToActivated_hapticStateIsToggleOn() =
+ 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)
+ }
+
+ @Test
+ fun onQuickAffordanceTogglesToDeactivated_hapticStateIsToggleOff() =
+ 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)
+ }
+
+ private fun TestScope.toggleQuickAffordance(on: Boolean) {
+ underTest.updateActivatedHistory(!on)
+ runCurrent()
+ underTest.updateActivatedHistory(on)
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt
index e7803c5e964c..a4a5ba691965 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt
@@ -17,12 +17,23 @@
package com.android.systemui.keyguard.ui.binder
import android.os.VibrationEffect
+import com.android.systemui.Flags
import kotlin.time.Duration.Companion.milliseconds
object KeyguardBottomAreaVibrations {
- val ShakeAnimationDuration = 300.milliseconds
- const val ShakeAnimationCycles = 5f
+ val ShakeAnimationDuration =
+ if (Flags.msdlFeedback()) {
+ 285.milliseconds
+ } else {
+ 300.milliseconds
+ }
+ val ShakeAnimationCycles =
+ if (Flags.msdlFeedback()) {
+ 3f
+ } else {
+ 5f
+ }
private const val SmallVibrationScale = 0.3f
private const val BigVibrationScale = 0.6f
@@ -32,7 +43,7 @@ object KeyguardBottomAreaVibrations {
.apply {
val vibrationDelayMs =
(ShakeAnimationDuration.inWholeMilliseconds / (ShakeAnimationCycles * 2))
- .toInt()
+ .toInt()
val vibrationCount = ShakeAnimationCycles.toInt() * 2
repeat(vibrationCount) {
@@ -47,29 +58,13 @@ object KeyguardBottomAreaVibrations {
val Activated =
VibrationEffect.startComposition()
- .addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_TICK,
- BigVibrationScale,
- 0,
- )
- .addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_QUICK_RISE,
- 0.1f,
- 0,
- )
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, BigVibrationScale, 0)
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_RISE, 0.1f, 0)
.compose()
val Deactivated =
VibrationEffect.startComposition()
- .addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_TICK,
- BigVibrationScale,
- 0,
- )
- .addPrimitive(
- VibrationEffect.Composition.PRIMITIVE_QUICK_FALL,
- 0.1f,
- 0,
- )
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_TICK, BigVibrationScale, 0)
+ .addPrimitive(VibrationEffect.Composition.PRIMITIVE_QUICK_FALL, 0.1f, 0)
.compose()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index 8725cdd273df..8a2e3dd791c2 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -20,6 +20,7 @@ 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
@@ -33,25 +34,27 @@ 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
import com.android.systemui.common.ui.binder.IconViewBinder
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
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.CoroutineDispatcher
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
-import com.android.app.tracing.coroutines.launchTraced as launch
/** This is only for a SINGLE Quick affordance */
@SysUISingleton
@@ -60,8 +63,9 @@ class KeyguardQuickAffordanceViewBinder
constructor(
private val falsingManager: FalsingManager?,
private val vibratorHelper: VibratorHelper?,
+ private val msdlPlayer: MSDLPlayer,
private val logger: KeyguardQuickAffordancesLogger,
- @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+ private val hapticsViewModelFactory: KeyguardQuickAffordanceHapticViewModel.Factory,
) {
private val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
@@ -88,6 +92,12 @@ constructor(
): Binding {
val button = view as ImageView
val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
+ val hapticsViewModel =
+ if (Flags.msdlFeedback()) {
+ hapticsViewModelFactory.create(viewModel)
+ } else {
+ null
+ }
val disposableHandle =
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
@@ -98,15 +108,12 @@ constructor(
viewModel = buttonModel,
messageDisplayer = messageDisplayer,
)
+ hapticsViewModel?.updateActivatedHistory(buttonModel.isActivated)
}
}
launch {
- updateButtonAlpha(
- view = button,
- viewModel = viewModel,
- alphaFlow = alpha,
- )
+ updateButtonAlpha(view = button, viewModel = viewModel, alphaFlow = alpha)
}
launch {
@@ -117,6 +124,32 @@ 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()
+ }
+ }
+ }
}
}
@@ -178,7 +211,7 @@ constructor(
com.android.internal.R.color.materialColorOnPrimaryFixed
} else {
com.android.internal.R.color.materialColorOnSurface
- },
+ }
)
)
@@ -221,12 +254,7 @@ constructor(
.getDimensionPixelSize(R.dimen.keyguard_affordance_shake_amplitude)
.toFloat()
val shakeAnimator =
- ObjectAnimator.ofFloat(
- view,
- "translationX",
- -amplitude / 2,
- amplitude / 2,
- )
+ ObjectAnimator.ofFloat(view, "translationX", -amplitude / 2, amplitude / 2)
shakeAnimator.duration =
KeyguardBottomAreaVibrations.ShakeAnimationDuration.inWholeMilliseconds
shakeAnimator.interpolator =
@@ -234,11 +262,17 @@ constructor(
shakeAnimator.doOnEnd { view.translationX = 0f }
shakeAnimator.start()
- vibratorHelper?.vibrate(KeyguardBottomAreaVibrations.Shake)
+ vibratorHelper?.playFeedback(KeyguardBottomAreaVibrations.Shake, msdlPlayer)
logger.logQuickAffordanceTapped(viewModel.configKey)
}
view.onLongClickListener =
- OnLongClickListener(falsingManager, viewModel, vibratorHelper, onTouchListener)
+ OnLongClickListener(
+ falsingManager,
+ viewModel,
+ vibratorHelper,
+ onTouchListener,
+ msdlPlayer,
+ )
} else {
view.setOnClickListener(OnClickListener(viewModel, checkNotNull(falsingManager)))
}
@@ -268,7 +302,7 @@ constructor(
Size(
view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width),
view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height),
- ),
+ )
)
}
@@ -297,7 +331,8 @@ constructor(
private val falsingManager: FalsingManager?,
private val viewModel: KeyguardQuickAffordanceViewModel,
private val vibratorHelper: VibratorHelper?,
- private val onTouchListener: KeyguardQuickAffordanceOnTouchListener
+ private val onTouchListener: KeyguardQuickAffordanceOnTouchListener,
+ private val msdlPlayer: MSDLPlayer,
) : View.OnLongClickListener {
override fun onLongClick(view: View): Boolean {
if (falsingManager?.isFalseLongTap(FalsingManager.MODERATE_PENALTY) == true) {
@@ -312,12 +347,13 @@ constructor(
slotId = viewModel.slotId,
)
)
- vibratorHelper?.vibrate(
+ vibratorHelper?.playFeedback(
if (viewModel.isActivated) {
KeyguardBottomAreaVibrations.Activated
} else {
KeyguardBottomAreaVibrations.Deactivated
- }
+ },
+ msdlPlayer,
)
}
@@ -328,7 +364,15 @@ constructor(
override fun onLongClickUseDefaultHapticFeedback(view: View) = false
}
- private data class ConfigurationBasedDimensions(
- val buttonSizePx: Size,
- )
+ 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)
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt
new file mode 100644
index 000000000000..890628c31c55
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
+
+class KeyguardQuickAffordanceHapticViewModel
+@AssistedInject
+constructor(
+ @Assisted quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel>,
+ private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
+) {
+
+ private val activatedHistory = MutableStateFlow(ActivatedHistory(false))
+
+ private val launchingHapticState: Flow<HapticState> =
+ combine(
+ quickAffordanceViewModel.map { it.configKey },
+ quickAffordanceInteractor.launchingFromTriggeredResult,
+ ) { key, launchingResult ->
+ val validKey = key != null && key == launchingResult?.configKey
+ if (validKey && launchingResult?.launched == true) {
+ HapticState.LAUNCH
+ } else {
+ HapticState.NO_HAPTICS
+ }
+ }
+ .distinctUntilChanged()
+
+ private val toggleHapticState: Flow<HapticState> =
+ activatedHistory
+ .map { history ->
+ when {
+ history.previousValue == false && history.currentValue -> HapticState.TOGGLE_ON
+ history.previousValue == true && !history.currentValue -> HapticState.TOGGLE_OFF
+ else -> HapticState.NO_HAPTICS
+ }
+ }
+ .distinctUntilChanged()
+
+ val quickAffordanceHapticState =
+ merge(launchingHapticState, toggleHapticState).distinctUntilChanged()
+
+ fun resetLaunchingFromTriggeredResult() =
+ quickAffordanceInteractor.setLaunchingFromTriggeredResult(null)
+
+ fun updateActivatedHistory(isActivated: Boolean) {
+ activatedHistory.value =
+ ActivatedHistory(
+ currentValue = isActivated,
+ previousValue = activatedHistory.value.currentValue,
+ )
+ }
+
+ enum class HapticState {
+ TOGGLE_ON,
+ TOGGLE_OFF,
+ LAUNCH,
+ NO_HAPTICS,
+ }
+
+ private data class ActivatedHistory(
+ val currentValue: Boolean,
+ val previousValue: Boolean? = null,
+ )
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel>
+ ): KeyguardQuickAffordanceHapticViewModel
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt
new file mode 100644
index 000000000000..d857157137b6
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * 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.keyguard.domain.interactor
+
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceHapticViewModel
+import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.flow.Flow
+
+val Kosmos.keyguardQuickAffordanceHapticViewModelFactory by
+ Kosmos.Fixture {
+ object : KeyguardQuickAffordanceHapticViewModel.Factory {
+ override fun create(
+ quickAffordanceViewModel: Flow<KeyguardQuickAffordanceViewModel>
+ ): KeyguardQuickAffordanceHapticViewModel =
+ KeyguardQuickAffordanceHapticViewModel(
+ quickAffordanceViewModel,
+ keyguardQuickAffordanceInteractor,
+ )
+ }
+ }