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 
17 package com.android.systemui.biometrics.domain.interactor
18 
19 import android.hardware.biometrics.Flags
20 import android.hardware.biometrics.PromptInfo
21 import com.android.internal.widget.LockPatternUtils
22 import com.android.systemui.biometrics.Utils
23 import com.android.systemui.biometrics.Utils.getCredentialType
24 import com.android.systemui.biometrics.Utils.isDeviceCredentialAllowed
25 import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepository
26 import com.android.systemui.biometrics.data.repository.PromptRepository
27 import com.android.systemui.biometrics.domain.model.BiometricOperationInfo
28 import com.android.systemui.biometrics.domain.model.BiometricPromptRequest
29 import com.android.systemui.biometrics.shared.model.BiometricModalities
30 import com.android.systemui.biometrics.shared.model.BiometricUserInfo
31 import com.android.systemui.biometrics.shared.model.FingerprintSensorType
32 import com.android.systemui.biometrics.shared.model.PromptKind
33 import com.android.systemui.dagger.SysUISingleton
34 import javax.inject.Inject
35 import kotlinx.coroutines.flow.Flow
36 import kotlinx.coroutines.flow.StateFlow
37 import kotlinx.coroutines.flow.combine
38 import kotlinx.coroutines.flow.distinctUntilChanged
39 import kotlinx.coroutines.flow.map
40 
41 /**
42  * Business logic for BiometricPrompt's biometric view variants (face, fingerprint, coex, etc.).
43  *
44  * This is used to cache the calling app's options that were given to the underlying authenticate
45  * APIs and should be set before any UI is shown to the user.
46  *
47  * There can be at most one request active at a given time. Use [resetPrompt] when no request is
48  * active to clear the cache.
49  *
50  * Views that use credential fallback should use [PromptCredentialInteractor] instead.
51  */
52 interface PromptSelectorInteractor {
53 
54     /** Static metadata about the current prompt. */
55     val prompt: Flow<BiometricPromptRequest.Biometric?>
56 
57     /** The kind of prompt to use (biometric, pin, pattern, etc.). */
58     val promptKind: StateFlow<PromptKind>
59 
60     /** If using a credential is allowed. */
61     val isCredentialAllowed: Flow<Boolean>
62 
63     /**
64      * The kind of credential the user may use as a fallback or [PromptKind.None] if unknown or not
65      * [isCredentialAllowed]. This is separate from [promptKind], even if [promptKind] is
66      * [PromptKind.Biometric], [credentialKind] should still be one of pin/pattern/password.
67      */
68     val credentialKind: Flow<PromptKind>
69 
70     /**
71      * If the API caller or the user's personal preferences require explicit confirmation after
72      * successful authentication.
73      */
74     val isConfirmationRequired: Flow<Boolean>
75 
76     /** Fingerprint sensor type */
77     val sensorType: Flow<FingerprintSensorType>
78 
79     /** Switch to the credential view. */
80     fun onSwitchToCredential()
81 
82     /**
83      * Update the kind of prompt (biometric prompt w/ or w/o sensor icon, pin view, pattern view,
84      * etc).
85      */
86     fun setPrompt(
87         promptInfo: PromptInfo,
88         effectiveUserId: Int,
89         requestId: Long,
90         modalities: BiometricModalities,
91         challenge: Long,
92         opPackageName: String,
93         onSwitchToCredential: Boolean,
94         isLandscape: Boolean,
95     )
96 
97     /** Unset the current authentication request. */
98     fun resetPrompt(requestId: Long)
99 }
100 
101 @SysUISingleton
102 class PromptSelectorInteractorImpl
103 @Inject
104 constructor(
105     fingerprintPropertyRepository: FingerprintPropertyRepository,
106     private val displayStateInteractor: DisplayStateInteractor,
107     private val promptRepository: PromptRepository,
108     private val lockPatternUtils: LockPatternUtils,
109 ) : PromptSelectorInteractor {
110 
111     override val prompt: Flow<BiometricPromptRequest.Biometric?> =
112         combine(
113             promptRepository.promptInfo,
114             promptRepository.challenge,
115             promptRepository.userId,
116             promptRepository.promptKind,
117             promptRepository.opPackageName,
kindnull118         ) { promptInfo, challenge, userId, kind, opPackageName ->
119             if (
120                 promptInfo == null || userId == null || challenge == null || opPackageName == null
121             ) {
122                 return@combine null
123             }
124 
125             when (kind) {
126                 is PromptKind.Biometric ->
127                     BiometricPromptRequest.Biometric(
128                         info = promptInfo,
129                         userInfo = BiometricUserInfo(userId = userId),
130                         operationInfo = BiometricOperationInfo(gatekeeperChallenge = challenge),
131                         modalities = kind.activeModalities,
132                         opPackageName = opPackageName,
133                     )
134                 else -> null
135             }
136         }
137 
138     override val promptKind: StateFlow<PromptKind> = promptRepository.promptKind
139 
140     override val isConfirmationRequired: Flow<Boolean> =
141         promptRepository.isConfirmationRequired.distinctUntilChanged()
142 
143     override val isCredentialAllowed: Flow<Boolean> =
144         promptRepository.promptInfo
infonull145             .map { info -> if (info != null) isDeviceCredentialAllowed(info) else false }
146             .distinctUntilChanged()
147 
148     override val credentialKind: Flow<PromptKind> =
isAllowednull149         combine(prompt, isCredentialAllowed) { prompt, isAllowed ->
150             if (prompt != null && isAllowed) {
151                 getCredentialType(lockPatternUtils, prompt.userInfo.deviceCredentialOwnerId)
152             } else {
153                 PromptKind.None
154             }
155         }
156 
157     override val sensorType: Flow<FingerprintSensorType> = fingerprintPropertyRepository.sensorType
158 
onSwitchToCredentialnull159     override fun onSwitchToCredential() {
160         val modalities: BiometricModalities =
161             if (promptRepository.promptKind.value.isBiometric())
162                 (promptRepository.promptKind.value as PromptKind.Biometric).activeModalities
163             else BiometricModalities()
164         setPrompt(
165             promptRepository.promptInfo.value!!,
166             promptRepository.userId.value!!,
167             promptRepository.requestId.value!!,
168             modalities,
169             promptRepository.challenge.value!!,
170             promptRepository.opPackageName.value!!,
171             onSwitchToCredential = true,
172             // isLandscape value is not important when onSwitchToCredential is true
173             isLandscape = false,
174         )
175     }
176 
setPromptnull177     override fun setPrompt(
178         promptInfo: PromptInfo,
179         effectiveUserId: Int,
180         requestId: Long,
181         modalities: BiometricModalities,
182         challenge: Long,
183         opPackageName: String,
184         onSwitchToCredential: Boolean,
185         isLandscape: Boolean,
186     ) {
187         val hasCredentialViewShown = promptKind.value.isCredential()
188         val showBpForCredential =
189             Flags.customBiometricPrompt() &&
190                 com.android.systemui.Flags.constraintBp() &&
191                 !Utils.isBiometricAllowed(promptInfo) &&
192                 isDeviceCredentialAllowed(promptInfo) &&
193                 promptInfo.contentView != null &&
194                 !promptInfo.isContentViewMoreOptionsButtonUsed
195         val showBpWithoutIconForCredential = showBpForCredential && !hasCredentialViewShown
196         var kind: PromptKind = PromptKind.None
197 
198         if (onSwitchToCredential) {
199             kind = getCredentialType(lockPatternUtils, effectiveUserId)
200         } else if (Utils.isBiometricAllowed(promptInfo) || showBpWithoutIconForCredential) {
201             // TODO(b/330908557): Subscribe to
202             // displayStateInteractor.currentRotation.value.isDefaultOrientation() for checking
203             // `isLandscape` after removing AuthContinerView.
204             kind =
205                 if (isLandscape) {
206                     val paneType =
207                         when {
208                             displayStateInteractor.isLargeScreen.value ->
209                                 PromptKind.Biometric.PaneType.ONE_PANE_LARGE_SCREEN_LANDSCAPE
210                             showBpWithoutIconForCredential ->
211                                 PromptKind.Biometric.PaneType.ONE_PANE_NO_SENSOR_LANDSCAPE
212                             else -> PromptKind.Biometric.PaneType.TWO_PANE_LANDSCAPE
213                         }
214                     PromptKind.Biometric(
215                         modalities,
216                         paneType = paneType,
217                     )
218                 } else {
219                     PromptKind.Biometric(modalities)
220                 }
221         } else if (isDeviceCredentialAllowed(promptInfo)) {
222             kind = getCredentialType(lockPatternUtils, effectiveUserId)
223         }
224 
225         promptRepository.setPrompt(
226             promptInfo = promptInfo,
227             userId = effectiveUserId,
228             requestId = requestId,
229             gatekeeperChallenge = challenge,
230             kind = kind,
231             opPackageName = opPackageName,
232         )
233     }
234 
resetPromptnull235     override fun resetPrompt(requestId: Long) {
236         promptRepository.unsetPrompt(requestId)
237     }
238 }
239