Loading packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt 0 → 100644 +116 −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.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() } } packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt +18 −23 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() } packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +69 −25 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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) { Loading @@ -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 { Loading @@ -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() } } } } } Loading Loading @@ -178,7 +211,7 @@ constructor( com.android.internal.R.color.materialColorOnPrimaryFixed } else { com.android.internal.R.color.materialColorOnSurface }, } ) ) Loading Loading @@ -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 = Loading @@ -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))) } Loading Loading @@ -268,7 +302,7 @@ constructor( Size( view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), ), ) ) } Loading Loading @@ -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) { Loading @@ -312,12 +347,13 @@ constructor( slotId = viewModel.slotId, ) ) vibratorHelper?.vibrate( vibratorHelper?.playFeedback( if (viewModel.isActivated) { KeyguardBottomAreaVibrations.Activated } else { KeyguardBottomAreaVibrations.Deactivated } }, msdlPlayer, ) } Loading @@ -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) } } } packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt 0 → 100644 +96 −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.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 } } packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt 0 → 100644 +35 −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.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, ) } } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/data/quickaffordance/KeyguardQuickAffordanceHapticViewModelTest.kt 0 → 100644 +116 −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.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() } }
packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBottomAreaVibrations.kt +18 −23 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() }
packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +69 −25 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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) { Loading @@ -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 { Loading @@ -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() } } } } } Loading Loading @@ -178,7 +211,7 @@ constructor( com.android.internal.R.color.materialColorOnPrimaryFixed } else { com.android.internal.R.color.materialColorOnSurface }, } ) ) Loading Loading @@ -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 = Loading @@ -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))) } Loading Loading @@ -268,7 +302,7 @@ constructor( Size( view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width), view.resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height), ), ) ) } Loading Loading @@ -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) { Loading @@ -312,12 +347,13 @@ constructor( slotId = viewModel.slotId, ) ) vibratorHelper?.vibrate( vibratorHelper?.playFeedback( if (viewModel.isActivated) { KeyguardBottomAreaVibrations.Activated } else { KeyguardBottomAreaVibrations.Deactivated } }, msdlPlayer, ) } Loading @@ -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) } } }
packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordanceHapticViewModel.kt 0 → 100644 +96 −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.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 } }
packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardQuickAffordanceHapticViewModelKosmos.kt 0 → 100644 +35 −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.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, ) } }