• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
<lambda>null2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.android.systemui.deviceentry.domain.interactor
17 
18 import com.android.keyguard.logging.BiometricUnlockLogger
19 import com.android.systemui.Flags
20 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
21 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
22 import com.android.systemui.dagger.SysUISingleton
23 import com.android.systemui.dump.DumpManager
24 import com.android.systemui.keyevent.domain.interactor.KeyEventInteractor
25 import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
26 import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
27 import com.android.systemui.power.domain.interactor.PowerInteractor
28 import com.android.systemui.power.shared.model.WakeSleepReason
29 import com.android.systemui.util.kotlin.FlowDumperImpl
30 import com.android.systemui.util.kotlin.sample
31 import com.android.systemui.util.time.SystemClock
32 import javax.inject.Inject
33 import kotlinx.coroutines.flow.Flow
34 import kotlinx.coroutines.flow.combine
35 import kotlinx.coroutines.flow.combineTransform
36 import kotlinx.coroutines.flow.distinctUntilChanged
37 import kotlinx.coroutines.flow.filter
38 import kotlinx.coroutines.flow.map
39 import kotlinx.coroutines.flow.merge
40 import kotlinx.coroutines.flow.onStart
41 
42 /**
43  * Business logic for device entry haptic events. Determines whether the haptic should play. In
44  * particular, there are extra guards for whether device entry error and successes haptics should
45  * play when the physical fingerprint sensor is located on the power button.
46  */
47 @SysUISingleton
48 class DeviceEntryHapticsInteractor
49 @Inject
50 constructor(
51     biometricSettingsRepository: BiometricSettingsRepository,
52     deviceEntryBiometricAuthInteractor: DeviceEntryBiometricAuthInteractor,
53     deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
54     deviceEntrySourceInteractor: DeviceEntrySourceInteractor,
55     fingerprintPropertyRepository: FingerprintPropertyRepository,
56     keyEventInteractor: KeyEventInteractor,
57     private val logger: BiometricUnlockLogger,
58     powerInteractor: PowerInteractor,
59     keyguardInteractor: KeyguardInteractor,
60     private val systemClock: SystemClock,
61     dumpManager: DumpManager,
62 ) : FlowDumperImpl(dumpManager) {
63     private val powerButtonSideFpsEnrolled =
64         combineTransform(
65                 fingerprintPropertyRepository.sensorType,
66                 biometricSettingsRepository.isFingerprintEnrolledAndEnabled,
67             ) { sensorType, enrolledAndEnabled ->
68                 if (sensorType == FingerprintSensorType.POWER_BUTTON) {
69                     emit(enrolledAndEnabled)
70                 } else {
71                     emit(false)
72                 }
73             }
74             .distinctUntilChanged()
75     private val powerButtonDown: Flow<Boolean> = keyEventInteractor.isPowerButtonDown
76     private val lastPowerButtonWakeup: Flow<Long> =
77         powerInteractor.detailedWakefulness
78             .filter { it.isAwakeFrom(WakeSleepReason.POWER_BUTTON) }
79             .map { systemClock.uptimeMillis() }
80             .onStart {
81                 // If the power button hasn't been pressed, we still want this to evaluate to true:
82                 // `uptimeMillis() - lastPowerButtonWakeup > recentPowerButtonPressThresholdMs`
83                 emit(recentPowerButtonPressThresholdMs * -1L - 1L)
84             }
85 
86     private val playSuccessHapticOnDeviceEntryFromBiometricSource: Flow<Unit> =
87         deviceEntrySourceInteractor.deviceEntryFromBiometricSource
88             .sample(
89                 combine(
90                     powerButtonSideFpsEnrolled,
91                     powerButtonDown,
92                     lastPowerButtonWakeup,
93                     ::Triple,
94                 )
95             )
96             .filter { (sideFpsEnrolled, powerButtonDown, lastPowerButtonWakeup) ->
97                 val sideFpsAllowsHaptic =
98                     !powerButtonDown &&
99                         systemClock.uptimeMillis() - lastPowerButtonWakeup >
100                             recentPowerButtonPressThresholdMs
101                 val allowHaptic = !sideFpsEnrolled || sideFpsAllowsHaptic
102                 if (!allowHaptic) {
103                     logger.d("Skip success haptic. Recent power button press or button is down.")
104                 }
105                 allowHaptic
106             }
107             // map to Unit
108             .map {}
109 
110     private val playSuccessHapticOnDeviceEntryFromDeviceEntryIcon: Flow<Unit> =
111         deviceEntrySourceInteractor.attemptEnterDeviceFromDeviceEntryIcon
112             .map { keyguardInteractor.isKeyguardDismissible.value }
113             .filter { it } // only play if the keyguard is dismissible
114             // map to Unit
115             .map {}
116 
117     /**
118      * Indicates when success haptics should play when the device is entered. When entering via a
119      * biometric sources, this always occurs on successful fingerprint authentications. It also
120      * occurs on successful face authentication but only if the lockscreen is bypassed.
121      */
122     val playSuccessHapticOnDeviceEntry: Flow<Unit> =
123         if (Flags.msdlFeedback()) {
124             merge(
125                     playSuccessHapticOnDeviceEntryFromBiometricSource,
126                     playSuccessHapticOnDeviceEntryFromDeviceEntryIcon,
127                 )
128                 .dumpWhileCollecting("playSuccessHaptic")
129         } else {
130             playSuccessHapticOnDeviceEntryFromBiometricSource.dumpWhileCollecting(
131                 "playSuccessHaptic"
132             )
133         }
134 
135     private val playErrorHapticForBiometricFailure: Flow<Unit> =
136         merge(
137                 deviceEntryFingerprintAuthInteractor.fingerprintFailure,
138                 deviceEntryBiometricAuthInteractor.faceOnlyFaceFailure,
139             )
140             // map to Unit
141             .map {}
142             .dumpWhileCollecting("playErrorHapticForBiometricFailure")
143 
144     val playErrorHaptic: Flow<Unit> =
145         playErrorHapticForBiometricFailure
146             .sample(combine(powerButtonSideFpsEnrolled, powerButtonDown, ::Pair))
147             .filter { (sideFpsEnrolled, powerButtonDown) ->
148                 val allowHaptic = !sideFpsEnrolled || !powerButtonDown
149                 if (!allowHaptic) {
150                     logger.d("Skip error haptic. Power button is down.")
151                 }
152                 allowHaptic
153             }
154             // map to Unit
155             .map {}
156             .dumpWhileCollecting("playErrorHaptic")
157 
158     private val recentPowerButtonPressThresholdMs = 400L
159 }
160