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

Commit c1918796 authored by Beverly Tai's avatar Beverly Tai Committed by Android (Google) Code Review
Browse files

Merge "Don't play SFPS success/error haptics if power button is down" into main

parents b27d7344 abecab9c
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
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
+72 −0
Original line number Diff line number Diff line
/*
 * 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
}
+133 −0
Original line number Diff line number Diff line
/*
 * 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
}
+7 −1
Original line number Diff line number Diff line
@@ -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,
            )
    }

+44 −0
Original line number Diff line number Diff line
@@ -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
Loading