diff options
| author | 2023-10-17 13:17:59 +0000 | |
|---|---|---|
| committer | 2023-10-17 13:17:59 +0000 | |
| commit | c1918796fa1cd4848fdbad8ed5d40151fc23e33b (patch) | |
| tree | f79889153caf957c9654bba86c925681fd480a0e | |
| parent | b27d7344c0aa362af3f9c63266f9d5751919d6a8 (diff) | |
| parent | abecab9c8680cb12937bb0e5098cc972fe3f20b6 (diff) | |
Merge "Don't play SFPS success/error haptics if power button is down" into main
9 files changed, 479 insertions, 176 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt index e7f835f7b858..c3aaef76cb2f 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/DeviceEntryModule.kt @@ -1,5 +1,6 @@ package com.android.systemui.deviceentry +import com.android.systemui.deviceentry.data.repository.DeviceEntryHapticsRepositoryModule import com.android.systemui.deviceentry.data.repository.DeviceEntryRepositoryModule import dagger.Module @@ -7,6 +8,7 @@ import dagger.Module includes = [ DeviceEntryRepositoryModule::class, + DeviceEntryHapticsRepositoryModule::class, ], ) object DeviceEntryModule diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsRepository.kt new file mode 100644 index 000000000000..1458404446e6 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsRepository.kt @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2023 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.deviceentry.data.repository + +import com.android.systemui.dagger.SysUISingleton +import dagger.Binds +import dagger.Module +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.asStateFlow + +/** Interface for classes that can access device-entry haptics application state. */ +interface DeviceEntryHapticsRepository { + /** + * Whether a successful biometric haptic has been requested. Has not yet been handled if true. + */ + val successHapticRequest: Flow<Boolean> + + /** Whether an error biometric haptic has been requested. Has not yet been handled if true. */ + val errorHapticRequest: Flow<Boolean> + + fun requestSuccessHaptic() + fun handleSuccessHaptic() + fun requestErrorHaptic() + fun handleErrorHaptic() +} + +/** Encapsulates application state for device entry haptics. */ +@SysUISingleton +class DeviceEntryHapticsRepositoryImpl @Inject constructor() : DeviceEntryHapticsRepository { + private val _successHapticRequest = MutableStateFlow(false) + override val successHapticRequest: Flow<Boolean> = _successHapticRequest.asStateFlow() + + private val _errorHapticRequest = MutableStateFlow(false) + override val errorHapticRequest: Flow<Boolean> = _errorHapticRequest.asStateFlow() + + override fun requestSuccessHaptic() { + _successHapticRequest.value = true + } + + override fun handleSuccessHaptic() { + _successHapticRequest.value = false + } + + override fun requestErrorHaptic() { + _errorHapticRequest.value = true + } + + override fun handleErrorHaptic() { + _errorHapticRequest.value = false + } +} + +@Module +interface DeviceEntryHapticsRepositoryModule { + @Binds fun repository(impl: DeviceEntryHapticsRepositoryImpl): DeviceEntryHapticsRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt new file mode 100644 index 000000000000..53d6f737af8d --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryHapticsInteractor.kt @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2023 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.deviceentry.domain.interactor + +import com.android.keyguard.logging.BiometricUnlockLogger +import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.data.repository.DeviceEntryHapticsRepository +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.util.kotlin.sample +import com.android.systemui.util.time.SystemClock +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.combineTransform +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart + +/** + * Business logic for device entry haptic events. Determines whether the haptic should play. In + * particular, there are extra guards for whether device entry error and successes hatpics should + * play when the physical fingerprint sensor is located on the power button. + */ +@ExperimentalCoroutinesApi +@SysUISingleton +class DeviceEntryHapticsInteractor +@Inject +constructor( + private val repository: DeviceEntryHapticsRepository, + fingerprintPropertyRepository: FingerprintPropertyRepository, + biometricSettingsRepository: BiometricSettingsRepository, + keyEventInteractor: KeyEventInteractor, + powerInteractor: PowerInteractor, + private val systemClock: SystemClock, + private val logger: BiometricUnlockLogger, +) { + private val powerButtonSideFpsEnrolled = + combineTransform( + fingerprintPropertyRepository.sensorType, + biometricSettingsRepository.isFingerprintEnrolledAndEnabled, + ) { sensorType, enrolledAndEnabled -> + if (sensorType == FingerprintSensorType.POWER_BUTTON) { + emit(enrolledAndEnabled) + } else { + emit(false) + } + } + .distinctUntilChanged() + private val powerButtonDown: Flow<Boolean> = keyEventInteractor.isPowerButtonDown + private val lastPowerButtonWakeup: Flow<Long> = + powerInteractor.detailedWakefulness + .filter { it.isAwakeFrom(WakeSleepReason.POWER_BUTTON) } + .map { systemClock.uptimeMillis() } + .onStart { + // If the power button hasn't been pressed, we still want this to evaluate to true: + // `uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs` + emit(recentPowerButtonPressThresholdMs * -1L - 1L) + } + + val playSuccessHaptic: Flow<Boolean> = + repository.successHapticRequest + .filter { it } + .sample( + combine( + powerButtonSideFpsEnrolled, + powerButtonDown, + lastPowerButtonWakeup, + ::Triple + ) + ) + .map { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) -> + val sideFpsAllowsHaptic = + !powerButtonDown && + systemClock.uptimeMillis() - lastPowerButtonWakeup > + recentPowerButtonPressThresholdMs + val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic + if (!allowHaptic) { + logger.d("Skip success haptic. Recent power button press or button is down.") + handleSuccessHaptic() // immediately handle, don't vibrate + } + allowHaptic + } + val playErrorHaptic: Flow<Boolean> = + repository.errorHapticRequest + .filter { it } + .sample(combine(powerButtonSideFpsEnrolled, powerButtonDown, ::Pair)) + .map { (sideFpsEnrolled, powerButtonDown) -> + val allowHaptic = !sideFpsEnrolled || !powerButtonDown + if (!allowHaptic) { + logger.d("Skip error haptic. Power button is down.") + handleErrorHaptic() // immediately handle, don't vibrate + } + allowHaptic + } + + fun vibrateSuccess() { + repository.requestSuccessHaptic() + } + + fun vibrateError() { + repository.requestErrorHaptic() + } + + fun handleSuccessHaptic() { + repository.handleSuccessHaptic() + } + + fun handleErrorHaptic() { + repository.handleErrorHaptic() + } + + private val recentPowerButtonPressThresholdMs = 400L +} diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt index a511713eddd3..119ade48d4f7 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewConfigurator.kt @@ -28,6 +28,7 @@ import com.android.keyguard.LockIconViewController import com.android.keyguard.dagger.KeyguardStatusViewComponent import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.KeyguardBlueprintViewBinder @@ -44,6 +45,7 @@ import com.android.systemui.res.R import com.android.systemui.shade.NotificationShadeWindowView import com.android.systemui.shade.domain.interactor.ShadeInteractor import com.android.systemui.statusbar.KeyguardIndicationController +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator import javax.inject.Inject @@ -72,7 +74,9 @@ constructor( private val keyguardIndicationController: KeyguardIndicationController, private val lockIconViewController: LockIconViewController, private val shadeInteractor: ShadeInteractor, - private val interactionJankMonitor: InteractionJankMonitor + private val interactionJankMonitor: InteractionJankMonitor, + private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, + private val vibratorHelper: VibratorHelper, ) : CoreStartable { private var rootViewHandle: DisposableHandle? = null @@ -143,6 +147,8 @@ constructor( shadeInteractor, { keyguardStatusViewController!!.getClockController() }, interactionJankMonitor, + deviceEntryHapticsInteractor, + vibratorHelper, ) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt index c72e6ce0b7d6..7ccabfe76a85 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardRootViewBinder.kt @@ -17,6 +17,7 @@ package com.android.systemui.keyguard.ui.binder import android.annotation.DrawableRes +import android.view.HapticFeedbackConstants import android.view.View import android.view.View.OnLayoutChangeListener import android.view.ViewGroup @@ -29,6 +30,7 @@ import com.android.keyguard.KeyguardClockSwitch.MISSING_CLOCK_ID import com.android.systemui.common.shared.model.Icon import com.android.systemui.common.shared.model.Text import com.android.systemui.common.shared.model.TintedIcon +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.shared.model.TransitionState @@ -38,6 +40,7 @@ import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.ClockController import com.android.systemui.res.R import com.android.systemui.shade.domain.interactor.ShadeInteractor +import com.android.systemui.statusbar.VibratorHelper import com.android.systemui.statusbar.policy.KeyguardStateController import com.android.systemui.temporarydisplay.ViewPriority import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator @@ -45,6 +48,7 @@ import com.android.systemui.temporarydisplay.chipbar.ChipbarInfo import javax.inject.Provider import kotlinx.coroutines.DisposableHandle import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.filter import kotlinx.coroutines.launch /** Bind occludingAppDeviceEntryMessageViewModel to run whenever the keyguard view is attached. */ @@ -62,6 +66,8 @@ object KeyguardRootViewBinder { shadeInteractor: ShadeInteractor, clockControllerProvider: Provider<ClockController>?, interactionJankMonitor: InteractionJankMonitor?, + deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor?, + vibratorHelper: VibratorHelper?, ): DisposableHandle { var onLayoutChangeListener: OnLayoutChange? = null val childViews = mutableMapOf<Int, View?>() @@ -177,6 +183,44 @@ object KeyguardRootViewBinder { } } } + + if (deviceEntryHapticsInteractor != null && vibratorHelper != null) { + launch { + deviceEntryHapticsInteractor.playSuccessHaptic + .filter { it } + .collect { + if ( + featureFlags.isEnabled(Flags.ONE_WAY_HAPTICS_API_MIGRATION) + ) { + vibratorHelper.performHapticFeedback( + view, + HapticFeedbackConstants.CONFIRM, + ) + } else { + vibratorHelper.vibrateAuthSuccess("device-entry::success") + } + deviceEntryHapticsInteractor.handleSuccessHaptic() + } + } + + launch { + deviceEntryHapticsInteractor.playErrorHaptic + .filter { it } + .collect { + if ( + featureFlags.isEnabled(Flags.ONE_WAY_HAPTICS_API_MIGRATION) + ) { + vibratorHelper.performHapticFeedback( + view, + HapticFeedbackConstants.REJECT, + ) + } else { + vibratorHelper.vibrateAuthSuccess("device-entry::error") + } + deviceEntryHapticsInteractor.handleErrorHaptic() + } + } + } } } viewModel.clockControllerProvider = clockControllerProvider diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt index 5a4bbef587af..692984a90a14 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt @@ -46,6 +46,7 @@ import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor import com.android.systemui.flags.FeatureFlags import com.android.systemui.flags.Flags import com.android.systemui.keyguard.ui.binder.KeyguardPreviewClockViewBinder @@ -114,6 +115,7 @@ constructor( private val chipbarCoordinator: ChipbarCoordinator, private val keyguardStateController: KeyguardStateController, private val shadeInteractor: ShadeInteractor, + private val deviceEntryHapticsInteractor: DeviceEntryHapticsInteractor, ) { val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN) @@ -339,6 +341,8 @@ constructor( shadeInteractor, null, // clock provider only needed for burn in null, // jank monitor not required for preview mode + null, // device entry haptics not required for preview mode + null, // device entry haptics not required for preview mode ) ) rootView.addView( diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java index fb45a6706e4d..8129b83a22d9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/BiometricUnlockController.java @@ -17,8 +17,6 @@ package com.android.systemui.statusbar.phone; import static android.app.StatusBarManager.SESSION_KEYGUARD; -import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; -import static com.android.systemui.keyguard.WakefulnessLifecycle.UNKNOWN_LAST_WAKE_TIME; import android.annotation.IntDef; import android.content.res.Resources; @@ -30,7 +28,6 @@ import android.metrics.LogMaker; import android.os.Handler; import android.os.PowerManager; import android.os.Trace; -import android.view.HapticFeedbackConstants; import androidx.annotation.Nullable; @@ -47,16 +44,17 @@ import com.android.keyguard.KeyguardUpdateMonitorCallback; import com.android.keyguard.KeyguardViewController; import com.android.keyguard.logging.BiometricUnlockLogger; import com.android.systemui.Dumpable; -import com.android.systemui.res.R; import com.android.systemui.biometrics.AuthController; import com.android.systemui.dagger.SysUISingleton; import com.android.systemui.dagger.qualifiers.Main; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FeatureFlags; import com.android.systemui.keyguard.KeyguardViewMediator; import com.android.systemui.keyguard.WakefulnessLifecycle; import com.android.systemui.log.SessionTracker; import com.android.systemui.plugins.statusbar.StatusBarStateController; +import com.android.systemui.res.R; import com.android.systemui.statusbar.NotificationMediaManager; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.VibratorHelper; @@ -73,12 +71,14 @@ import java.util.Set; import javax.inject.Inject; +import kotlinx.coroutines.ExperimentalCoroutinesApi; + /** * Controller which coordinates all the biometric unlocking actions with the UI. */ +@ExperimentalCoroutinesApi @SysUISingleton public class BiometricUnlockController extends KeyguardUpdateMonitorCallback implements Dumpable { - private static final long RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS = 400L; private static final long BIOMETRIC_WAKELOCK_TIMEOUT_MS = 15 * 1000; private static final String BIOMETRIC_WAKE_LOCK_NAME = "wake-and-unlock:wakelock"; private static final UiEventLogger UI_EVENT_LOGGER = new UiEventLoggerImpl(); @@ -175,6 +175,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp private final BiometricUnlockLogger mLogger; private final SystemClock mSystemClock; private final boolean mOrderUnlockAndWake; + private final DeviceEntryHapticsInteractor mHapticsInteractor; private long mLastFpFailureUptimeMillis; private int mNumConsecutiveFpFailures; @@ -284,7 +285,8 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp ScreenOffAnimationController screenOffAnimationController, VibratorHelper vibrator, SystemClock systemClock, - FeatureFlags featureFlags + FeatureFlags featureFlags, + DeviceEntryHapticsInteractor hapticsInteractor ) { mPowerManager = powerManager; mUpdateMonitor = keyguardUpdateMonitor; @@ -314,6 +316,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp mFeatureFlags = featureFlags; mOrderUnlockAndWake = resources.getBoolean( com.android.internal.R.bool.config_orderUnlockAndWake); + mHapticsInteractor = hapticsInteractor; dumpManager.registerDumpable(this); } @@ -434,7 +437,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp if (mode == MODE_WAKE_AND_UNLOCK || mode == MODE_WAKE_AND_UNLOCK_PULSING || mode == MODE_UNLOCK_COLLAPSING || mode == MODE_WAKE_AND_UNLOCK_FROM_DREAM || mode == MODE_DISMISS_BOUNCER) { - vibrateSuccess(biometricSourceType); + mHapticsInteractor.vibrateSuccess(); onBiometricUnlockedWithKeyguardDismissal(biometricSourceType); } startWakeAndUnlock(mode); @@ -722,7 +725,7 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp && !mUpdateMonitor.getCachedIsUnlockWithFingerprintPossible( KeyguardUpdateMonitor.getCurrentUser())) || (biometricSourceType == BiometricSourceType.FINGERPRINT)) { - vibrateError(biometricSourceType); + mHapticsInteractor.vibrateError(); } cleanup(); @@ -749,45 +752,6 @@ public class BiometricUnlockController extends KeyguardUpdateMonitorCallback imp cleanup(); } - // these haptics are for device-entry only - private void vibrateSuccess(BiometricSourceType type) { - if (mAuthController.isSfpsEnrolled(KeyguardUpdateMonitor.getCurrentUser()) - && lastWakeupFromPowerButtonWithinHapticThreshold()) { - mLogger.d("Skip auth success haptic. Power button was recently pressed."); - return; - } - if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { - mVibratorHelper.performHapticFeedback( - mKeyguardViewController.getViewRootImpl().getView(), - HapticFeedbackConstants.CONFIRM - ); - } else { - mVibratorHelper.vibrateAuthSuccess( - getClass().getSimpleName() + ", type =" + type + "device-entry::success"); - } - } - - private boolean lastWakeupFromPowerButtonWithinHapticThreshold() { - final boolean lastWakeupFromPowerButton = mWakefulnessLifecycle.getLastWakeReason() - == PowerManager.WAKE_REASON_POWER_BUTTON; - return lastWakeupFromPowerButton - && mWakefulnessLifecycle.getLastWakeTime() != UNKNOWN_LAST_WAKE_TIME - && mSystemClock.uptimeMillis() - mWakefulnessLifecycle.getLastWakeTime() - < RECENT_POWER_BUTTON_PRESS_THRESHOLD_MS; - } - - private void vibrateError(BiometricSourceType type) { - if (mFeatureFlags.isEnabled(ONE_WAY_HAPTICS_API_MIGRATION)) { - mVibratorHelper.performHapticFeedback( - mKeyguardViewController.getViewRootImpl().getView(), - HapticFeedbackConstants.REJECT - ); - } else { - mVibratorHelper.vibrateAuthError( - getClass().getSimpleName() + ", type =" + type + "device-entry::error"); - } - } - private void cleanup() { releaseBiometricWakeLock(); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt new file mode 100644 index 000000000000..9b8e581d1ba4 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryHapticsInteractorTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2023 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.deviceentry.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.keyguard.logging.BiometricUnlockLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.biometrics.data.repository.FakeFingerprintPropertyRepository +import com.android.systemui.biometrics.shared.model.FingerprintSensorType +import com.android.systemui.biometrics.shared.model.SensorStrength +import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor +import com.android.systemui.keyevent.data.repository.FakeKeyEventRepository +import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor +import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository +import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.power.data.repository.FakePowerRepository +import com.android.systemui.power.domain.interactor.PowerInteractor +import com.android.systemui.power.shared.model.WakeSleepReason +import com.android.systemui.power.shared.model.WakefulnessState +import com.android.systemui.statusbar.phone.ScreenOffAnimationController +import com.android.systemui.util.time.FakeSystemClock +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +class DeviceEntryHapticsInteractorTest : SysuiTestCase() { + + private lateinit var repository: DeviceEntryHapticsRepository + private lateinit var fingerprintPropertyRepository: FakeFingerprintPropertyRepository + private lateinit var biometricSettingsRepository: FakeBiometricSettingsRepository + private lateinit var keyEventRepository: FakeKeyEventRepository + private lateinit var powerRepository: FakePowerRepository + private lateinit var systemClock: FakeSystemClock + private lateinit var underTest: DeviceEntryHapticsInteractor + + @Before + fun setUp() { + repository = DeviceEntryHapticsRepositoryImpl() + fingerprintPropertyRepository = FakeFingerprintPropertyRepository() + biometricSettingsRepository = FakeBiometricSettingsRepository() + keyEventRepository = FakeKeyEventRepository() + powerRepository = FakePowerRepository() + systemClock = FakeSystemClock() + underTest = + DeviceEntryHapticsInteractor( + repository = repository, + fingerprintPropertyRepository = fingerprintPropertyRepository, + biometricSettingsRepository = biometricSettingsRepository, + keyEventInteractor = KeyEventInteractor(keyEventRepository), + powerInteractor = + PowerInteractor( + powerRepository, + mock(FalsingCollector::class.java), + mock(ScreenOffAnimationController::class.java), + mock(StatusBarStateController::class.java), + ), + systemClock = systemClock, + logger = mock(BiometricUnlockLogger::class.java), + ) + } + + @Test + fun nonPowerButtonFPS_vibrateSuccess() = runTest { + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC) + underTest.vibrateSuccess() + assertThat(playSuccessHaptic).isTrue() + } + + @Test + fun powerButtonFPS_vibrateSuccess() = runTest { + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + keyEventRepository.setPowerButtonDown(false) + + // It's been 10 seconds since the last power button wakeup + setAwakeFromPowerButton() + runCurrent() + systemClock.setUptimeMillis(systemClock.uptimeMillis() + 10000) + + underTest.vibrateSuccess() + assertThat(playSuccessHaptic).isTrue() + } + + @Test + fun powerButtonFPS_powerDown_doNotVibrateSuccess() = runTest { + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + keyEventRepository.setPowerButtonDown(true) // power button is currently DOWN + + // It's been 10 seconds since the last power button wakeup + setAwakeFromPowerButton() + runCurrent() + systemClock.setUptimeMillis(systemClock.uptimeMillis() + 10000) + + underTest.vibrateSuccess() + assertThat(playSuccessHaptic).isFalse() + } + + @Test + fun powerButtonFPS_powerButtonRecentlyPressed_doNotVibrateSuccess() = runTest { + val playSuccessHaptic by collectLastValue(underTest.playSuccessHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + keyEventRepository.setPowerButtonDown(false) + + // It's only been 50ms since the last power button wakeup + setAwakeFromPowerButton() + runCurrent() + systemClock.setUptimeMillis(systemClock.uptimeMillis() + 50) + + underTest.vibrateSuccess() + assertThat(playSuccessHaptic).isFalse() + } + + @Test + fun nonPowerButtonFPS_vibrateError() = runTest { + val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) + setFingerprintSensorType(FingerprintSensorType.UDFPS_ULTRASONIC) + underTest.vibrateError() + assertThat(playErrorHaptic).isTrue() + } + + @Test + fun powerButtonFPS_vibrateError() = runTest { + val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + underTest.vibrateError() + assertThat(playErrorHaptic).isTrue() + } + + @Test + fun powerButtonFPS_powerDown_doNotVibrateError() = runTest { + val playErrorHaptic by collectLastValue(underTest.playErrorHaptic) + setPowerButtonFingerprintProperty() + setFingerprintEnrolled() + keyEventRepository.setPowerButtonDown(true) + underTest.vibrateError() + assertThat(playErrorHaptic).isFalse() + } + + private fun setFingerprintSensorType(fingerprintSensorType: FingerprintSensorType) { + fingerprintPropertyRepository.setProperties( + sensorId = 0, + strength = SensorStrength.STRONG, + sensorType = fingerprintSensorType, + sensorLocations = mapOf(), + ) + } + + private fun setPowerButtonFingerprintProperty() { + setFingerprintSensorType(FingerprintSensorType.POWER_BUTTON) + } + + private fun setFingerprintEnrolled() { + biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true) + } + + private fun setAwakeFromPowerButton() { + powerRepository.updateWakefulness( + WakefulnessState.AWAKE, + WakeSleepReason.POWER_BUTTON, + WakeSleepReason.POWER_BUTTON, + powerButtonLaunchGestureTriggered = false, + ) + } +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java index 700de5305778..8344cd143c5d 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/BiometricsUnlockControllerTest.java @@ -18,7 +18,9 @@ package com.android.systemui.statusbar.phone; import static com.android.systemui.flags.Flags.ONE_WAY_HAPTICS_API_MIGRATION; import static com.android.systemui.statusbar.phone.BiometricUnlockController.MODE_WAKE_AND_UNLOCK; + import static com.google.common.truth.Truth.assertThat; + import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -38,7 +40,6 @@ import android.test.suitebuilder.annotation.SmallTest; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.testing.TestableResources; -import android.view.HapticFeedbackConstants; import android.view.ViewRootImpl; import com.android.internal.logging.MetricsLogger; @@ -47,6 +48,7 @@ import com.android.keyguard.KeyguardUpdateMonitor; import com.android.keyguard.logging.BiometricUnlockLogger; import com.android.systemui.SysuiTestCase; import com.android.systemui.biometrics.AuthController; +import com.android.systemui.deviceentry.domain.interactor.DeviceEntryHapticsInteractor; import com.android.systemui.dump.DumpManager; import com.android.systemui.flags.FakeFeatureFlags; import com.android.systemui.keyguard.KeyguardViewMediator; @@ -122,6 +124,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { private BiometricUnlockLogger mLogger; @Mock private ViewRootImpl mViewRootImpl; + @Mock + private DeviceEntryHapticsInteractor mDeviceEntryHapticsInteractor; private final FakeSystemClock mSystemClock = new FakeSystemClock(); private FakeFeatureFlags mFeatureFlags; private BiometricUnlockController mBiometricUnlockController; @@ -158,7 +162,8 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { mAuthController, mStatusBarStateController, mSessionTracker, mLatencyTracker, mScreenOffAnimationController, mVibratorHelper, mSystemClock, - mFeatureFlags + mFeatureFlags, + mDeviceEntryHapticsInteractor ); biometricUnlockController.setKeyguardViewController(mStatusBarKeyguardViewManager); biometricUnlockController.addListener(mBiometricUnlockEventsListener); @@ -462,145 +467,23 @@ public class BiometricsUnlockControllerTest extends SysuiTestCase { } @Test - public void onSideFingerprintSuccess_recentPowerButtonPress_noHaptic() { - // GIVEN side fingerprint enrolled, last wake reason was power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - - // GIVEN last wake time just occurred - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - - // WHEN biometric fingerprint succeeds - givenFingerprintModeUnlockCollapsing(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, - true); - - // THEN DO NOT vibrate the device - verify(mVibratorHelper, never()).vibrateAuthSuccess(anyString()); - } - - @Test - public void onSideFingerprintSuccess_oldPowerButtonPress_playHaptic() { - // GIVEN side fingerprint enrolled, last wake reason was power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - - // GIVEN last wake time was 500ms ago - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - mSystemClock.advanceTime(500); - - // WHEN biometric fingerprint succeeds - givenFingerprintModeUnlockCollapsing(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, - true); - - // THEN vibrate the device - verify(mVibratorHelper).vibrateAuthSuccess(anyString()); - } - - @Test - public void onSideFingerprintSuccess_oldPowerButtonPress_playOneWayHaptic() { - // GIVEN oneway haptics is enabled - mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); - // GIVEN side fingerprint enrolled, last wake reason was power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - - // GIVEN last wake time was 500ms ago - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - mSystemClock.advanceTime(500); - + public void onFingerprintSuccess_requestSuccessHaptic() { // WHEN biometric fingerprint succeeds givenFingerprintModeUnlockCollapsing(); mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, true); - // THEN vibrate the device - verify(mVibratorHelper).performHapticFeedback( - any(), - eq(HapticFeedbackConstants.CONFIRM) - ); - } - - @Test - public void onSideFingerprintSuccess_recentGestureWakeUp_playHaptic() { - // GIVEN side fingerprint enrolled, wakeup just happened - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - - // GIVEN last wake reason was from a gesture - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_GESTURE); - - // WHEN biometric fingerprint succeeds - givenFingerprintModeUnlockCollapsing(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, - true); - - // THEN vibrate the device - verify(mVibratorHelper).vibrateAuthSuccess(anyString()); - } - - @Test - public void onSideFingerprintSuccess_recentGestureWakeUp_playOnewayHaptic() { - //GIVEN oneway haptics is enabled - mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); - // GIVEN side fingerprint enrolled, wakeup just happened - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - - // GIVEN last wake reason was from a gesture - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_GESTURE); - - // WHEN biometric fingerprint succeeds - givenFingerprintModeUnlockCollapsing(); - mBiometricUnlockController.startWakeAndUnlock(BiometricSourceType.FINGERPRINT, - true); - - // THEN vibrate the device - verify(mVibratorHelper).performHapticFeedback( - any(), - eq(HapticFeedbackConstants.CONFIRM) - ); - } - - @Test - public void onSideFingerprintFail_alwaysPlaysHaptic() { - // GIVEN side fingerprint enrolled, last wake reason was recent power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - - // WHEN biometric fingerprint fails - mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); - // THEN always vibrate the device - verify(mVibratorHelper).vibrateAuthError(anyString()); + verify(mDeviceEntryHapticsInteractor).vibrateSuccess(); } @Test - public void onSideFingerprintFail_alwaysPlaysOneWayHaptic() { - // GIVEN oneway haptics is enabled - mFeatureFlags.set(ONE_WAY_HAPTICS_API_MIGRATION, true); - // GIVEN side fingerprint enrolled, last wake reason was recent power button - when(mAuthController.isSfpsEnrolled(anyInt())).thenReturn(true); - when(mWakefulnessLifecycle.getLastWakeReason()) - .thenReturn(PowerManager.WAKE_REASON_POWER_BUTTON); - when(mWakefulnessLifecycle.getLastWakeTime()).thenReturn(mSystemClock.uptimeMillis()); - + public void onFingerprintFail_requestErrorHaptic() { // WHEN biometric fingerprint fails mBiometricUnlockController.onBiometricAuthFailed(BiometricSourceType.FINGERPRINT); // THEN always vibrate the device - verify(mVibratorHelper).performHapticFeedback( - any(), - eq(HapticFeedbackConstants.REJECT) - ); + verify(mDeviceEntryHapticsInteractor).vibrateError(); } @Test |