1 /*
<lambda>null2  * Copyright (C) 2024 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 
17 package com.android.systemui.deviceentry.domain.interactor
18 
19 import android.content.res.Resources
20 import com.android.systemui.biometrics.domain.interactor.FingerprintPropertyInteractor
21 import com.android.systemui.dagger.SysUISingleton
22 import com.android.systemui.dagger.qualifiers.Main
23 import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
24 import com.android.systemui.deviceentry.shared.model.FaceFailureMessage
25 import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage
26 import com.android.systemui.deviceentry.shared.model.FaceMessage
27 import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage
28 import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
29 import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage
30 import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
31 import com.android.systemui.deviceentry.shared.model.FingerprintMessage
32 import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
33 import com.android.systemui.keyguard.domain.interactor.DevicePostureInteractor
34 import com.android.systemui.keyguard.shared.model.DevicePosture
35 import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
36 import com.android.systemui.res.R
37 import com.android.systemui.util.kotlin.sample
38 import javax.inject.Inject
39 import kotlinx.coroutines.ExperimentalCoroutinesApi
40 import kotlinx.coroutines.flow.Flow
41 import kotlinx.coroutines.flow.combine
42 import kotlinx.coroutines.flow.filter
43 import kotlinx.coroutines.flow.filterIsInstance
44 import kotlinx.coroutines.flow.filterNot
45 import kotlinx.coroutines.flow.flatMapLatest
46 import kotlinx.coroutines.flow.flowOf
47 import kotlinx.coroutines.flow.map
48 import kotlinx.coroutines.flow.merge
49 
50 /**
51  * BiometricMessage business logic. Filters biometric error/fail/success events for authentication
52  * events that should never surface a message to the user at the current device state.
53  */
54 @ExperimentalCoroutinesApi
55 @SysUISingleton
56 class BiometricMessageInteractor
57 @Inject
58 constructor(
59     @Main private val resources: Resources,
60     fingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
61     fingerprintPropertyInteractor: FingerprintPropertyInteractor,
62     faceAuthInteractor: DeviceEntryFaceAuthInteractor,
63     private val biometricSettingsInteractor: DeviceEntryBiometricSettingsInteractor,
64     faceHelpMessageDeferralInteractor: FaceHelpMessageDeferralInteractor,
65     devicePostureInteractor: DevicePostureInteractor,
66 ) {
67     private val faceHelp: Flow<HelpFaceAuthenticationStatus> =
68         faceAuthInteractor.authenticationStatus.filterIsInstance<HelpFaceAuthenticationStatus>()
69     private val faceError: Flow<ErrorFaceAuthenticationStatus> =
70         faceAuthInteractor.authenticationStatus.filterIsInstance<ErrorFaceAuthenticationStatus>()
71     private val faceFailure: Flow<FailedFaceAuthenticationStatus> =
72         faceAuthInteractor.authenticationStatus.filterIsInstance<FailedFaceAuthenticationStatus>()
73 
74     /**
75      * The acquisition message ids to show message when both fingerprint and face are enrolled and
76      * enabled for device entry.
77      */
78     private val coExFaceAcquisitionMsgIdsToShowDefault: Set<Int> =
79         resources.getIntArray(R.array.config_face_help_msgs_when_fingerprint_enrolled).toSet()
80 
81     /**
82      * The acquisition message ids to show message when both fingerprint and face are enrolled and
83      * enabled for device entry and the device is unfolded.
84      */
85     private val coExFaceAcquisitionMsgIdsToShowUnfolded: Set<Int> =
86         resources
87             .getIntArray(R.array.config_face_help_msgs_when_fingerprint_enrolled_unfolded)
88             .toSet()
89 
90     private fun ErrorFingerprintAuthenticationStatus.shouldSuppressError(): Boolean {
91         return isCancellationError() || isPowerPressedError()
92     }
93 
94     private fun ErrorFaceAuthenticationStatus.shouldSuppressError(): Boolean {
95         return isCancellationError() || isUnableToProcessError()
96     }
97 
98     private val fingerprintErrorMessage: Flow<FingerprintMessage> =
99         fingerprintAuthInteractor.fingerprintError
100             .filterNot { it.shouldSuppressError() }
101             .sample(biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed, ::Pair)
102             .filter { (errorStatus, fingerprintAuthAllowed) ->
103                 fingerprintAuthAllowed || errorStatus.isLockoutError()
104             }
105             .map { (errorStatus, _) ->
106                 when {
107                     errorStatus.isLockoutError() -> FingerprintLockoutMessage(errorStatus.msg)
108                     else -> FingerprintMessage(errorStatus.msg)
109                 }
110             }
111 
112     private val fingerprintHelpMessage: Flow<FingerprintMessage> =
113         fingerprintAuthInteractor.fingerprintHelp
114             .sample(biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed, ::Pair)
115             .filter { (_, fingerprintAuthAllowed) -> fingerprintAuthAllowed }
116             .map { (helpStatus, _) -> FingerprintMessage(helpStatus.msg) }
117 
118     private val fingerprintFailMessage: Flow<FingerprintMessage> =
119         fingerprintPropertyInteractor.isUdfps.flatMapLatest { isUdfps ->
120             fingerprintAuthInteractor.fingerprintFailure
121                 .sample(biometricSettingsInteractor.fingerprintAuthCurrentlyAllowed)
122                 .filter { fingerprintAuthAllowed -> fingerprintAuthAllowed }
123                 .map {
124                     FingerprintFailureMessage(
125                         if (isUdfps) {
126                             resources.getString(
127                                 com.android.internal.R.string.fingerprint_udfps_error_not_match
128                             )
129                         } else {
130                             resources.getString(
131                                 com.android.internal.R.string.fingerprint_error_not_match
132                             )
133                         }
134                     )
135                 }
136         }
137 
138     val coExFaceAcquisitionMsgIdsToShow: Flow<Set<Int>> =
139         devicePostureInteractor.posture.map { devicePosture ->
140             when (devicePosture) {
141                 DevicePosture.OPENED -> coExFaceAcquisitionMsgIdsToShowUnfolded
142                 DevicePosture.UNKNOWN, // Devices without posture support (non-foldable) use UNKNOWN
143                 DevicePosture.CLOSED,
144                 DevicePosture.HALF_OPENED,
145                 DevicePosture.FLIPPED -> coExFaceAcquisitionMsgIdsToShowDefault
146             }
147         }
148 
149     val fingerprintMessage: Flow<FingerprintMessage> =
150         merge(
151             fingerprintErrorMessage,
152             fingerprintFailMessage,
153             fingerprintHelpMessage,
154         )
155 
156     private val filterConditionForFaceHelpMessages:
157         Flow<(HelpFaceAuthenticationStatus) -> Boolean> =
158         combine(
159                 biometricSettingsInteractor.isFingerprintAuthEnrolledAndEnabled,
160                 biometricSettingsInteractor.faceAuthCurrentlyAllowed,
161                 ::Pair
162             )
163             .flatMapLatest { (fingerprintEnrolled, faceAuthCurrentlyAllowed) ->
164                 if (fingerprintEnrolled && faceAuthCurrentlyAllowed) {
165                     // Show only some face help messages if fingerprint is also enrolled
166                     coExFaceAcquisitionMsgIdsToShow.map { msgIdsToShow ->
167                         { helpStatus: HelpFaceAuthenticationStatus ->
168                             msgIdsToShow.contains(helpStatus.msgId)
169                         }
170                     }
171                 } else if (faceAuthCurrentlyAllowed) {
172                     // Show all face help messages if only face is enrolled and currently allowed
173                     flowOf { _: HelpFaceAuthenticationStatus -> true }
174                 } else {
175                     flowOf { _: HelpFaceAuthenticationStatus -> false }
176                 }
177             }
178 
179     private val faceHelpMessage: Flow<FaceMessage> =
180         faceHelp
181             .filterNot {
182                 // Message deferred to potentially show at face timeout error instead
183                 faceHelpMessageDeferralInteractor.shouldDefer(it.msgId)
184             }
185             .sample(filterConditionForFaceHelpMessages, ::Pair)
186             .filter { (helpMessage, filterCondition) -> filterCondition(helpMessage) }
187             .map { (status, _) -> FaceMessage(status.msg) }
188 
189     private val faceFailureMessage: Flow<FaceMessage> =
190         faceFailure
191             .sample(biometricSettingsInteractor.faceAuthCurrentlyAllowed)
192             .filter { faceAuthCurrentlyAllowed -> faceAuthCurrentlyAllowed }
193             .map { FaceFailureMessage(resources.getString(R.string.keyguard_face_failed)) }
194 
195     private val faceErrorMessage: Flow<FaceMessage> =
196         faceError
197             .filterNot { it.shouldSuppressError() }
198             .sample(biometricSettingsInteractor.faceAuthCurrentlyAllowed, ::Pair)
199             .filter { (errorStatus, faceAuthCurrentlyAllowed) ->
200                 faceAuthCurrentlyAllowed || errorStatus.isLockoutError()
201             }
202             .map { (status, _) ->
203                 when {
204                     status.isTimeoutError() -> {
205                         val deferredMessage = faceHelpMessageDeferralInteractor.getDeferredMessage()
206                         if (deferredMessage != null) {
207                             FaceMessage(deferredMessage.toString())
208                         } else {
209                             FaceTimeoutMessage(status.msg)
210                         }
211                     }
212                     status.isLockoutError() -> FaceLockoutMessage(status.msg)
213                     else -> FaceMessage(status.msg)
214                 }
215             }
216 
217     val faceMessage: Flow<FaceMessage> =
218         merge(
219             faceHelpMessage,
220             faceFailureMessage,
221             faceErrorMessage,
222         )
223 }
224