1 /*
<lambda>null2 * Copyright (C) 2022 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.credentialmanager.getflow
18
19 import android.credentials.flags.Flags.selectorUiImprovementsEnabled
20 import android.credentials.flags.Flags.credmanBiometricApiEnabled
21 import android.graphics.drawable.Drawable
22 import androidx.credentials.CredentialOption
23 import com.android.credentialmanager.R
24 import com.android.credentialmanager.model.CredentialType
25 import com.android.credentialmanager.model.get.ProviderInfo
26 import com.android.credentialmanager.model.EntryInfo
27 import com.android.credentialmanager.model.get.AuthenticationEntryInfo
28 import com.android.credentialmanager.model.get.CredentialEntryInfo
29 import com.android.credentialmanager.model.get.RemoteEntryInfo
30 import com.android.internal.util.Preconditions
31 import java.time.Instant
32
33 data class GetCredentialUiState(
34 val isRequestForAllOptions: Boolean,
35 val providerInfoList: List<ProviderInfo>,
36 val requestDisplayInfo: RequestDisplayInfo,
37 val providerDisplayInfo: ProviderDisplayInfo =
38 toProviderDisplayInfo(providerInfoList, requestDisplayInfo.typePriorityMap),
39 val currentScreenState: GetScreenState = toGetScreenState(
40 providerDisplayInfo, isRequestForAllOptions),
41 val activeEntry: EntryInfo? = toActiveEntry(providerDisplayInfo),
42 val isNoAccount: Boolean = false,
43 )
44
45 /**
46 * Checks if this get flow is a biometric selection flow by ensuring that the first account has a
47 * single credential entry to display. The presently agreed upon condition validates this flow for
48 * a single account. In the case when there's a single credential, this flow matches the auto
49 * select criteria, but with the possibility that the two flows (autoselect and biometric) may
50 * collide. In those collision cases, the auto select flow is supported over the biometric flow.
51 * If there is a single account but more than one credential, and the first ranked credential has
52 * the biometric bit flipped on, we will use the biometric flow. If all conditions are valid, this
53 * responds with the entry utilized by the biometricFlow, or null otherwise.
54 */
55 internal fun findBiometricFlowEntry(
56 providerDisplayInfo: ProviderDisplayInfo,
57 isAutoSelectFlow: Boolean
58 ): CredentialEntryInfo? {
59 if (!credmanBiometricApiEnabled()) {
60 return null
61 }
62 if (isAutoSelectFlow) {
63 // For this to be true, it must be the case that there is a single entry and a single
64 // account. If that is the case, and auto-select is enabled along side the one-tap flow, we
65 // always favor that over the one tap flow.
66 return null
67 }
68 // The flow through an authentication entry, even if only a singular entry exists, is deemed
69 // as not being eligible for the single tap flow given that it adds any number of credentials
70 // once unlocked; essentially, this entry contains additional complexities behind it, making it
71 // invalid.
72 if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) {
73 return null
74 }
75 val singleAccountEntryList = getCredentialEntryListIffSingleAccount(
76 providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null
77
78 val firstEntry = singleAccountEntryList.firstOrNull()
79 return if (firstEntry?.biometricRequest != null) firstEntry else null
80 }
81
82 /**
83 * A utility method that will procure the credential entry list if and only if the credential entry
84 * list is for a singular account use case. This can be used for various flows that condition on
85 * a singular account.
86 */
getCredentialEntryListIffSingleAccountnull87 internal fun getCredentialEntryListIffSingleAccount(
88 sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>
89 ): List<CredentialEntryInfo>? {
90 if (sortedUserNameToCredentialEntryList.size != 1) {
91 return null
92 }
93 val entryList = sortedUserNameToCredentialEntryList.firstOrNull() ?: return null
94 val sortedEntryList = entryList.sortedCredentialEntryList
95 return sortedEntryList
96 }
97
hasContentToDisplaynull98 internal fun hasContentToDisplay(state: GetCredentialUiState): Boolean {
99 return state.providerDisplayInfo.sortedUserNameToCredentialEntryList.isNotEmpty() ||
100 state.providerDisplayInfo.authenticationEntryList.isNotEmpty() ||
101 (state.providerDisplayInfo.remoteEntry != null &&
102 !state.requestDisplayInfo.preferImmediatelyAvailableCredentials)
103 }
104
findAutoSelectEntrynull105 internal fun findAutoSelectEntry(providerDisplayInfo: ProviderDisplayInfo): CredentialEntryInfo? {
106 if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) {
107 return null
108 }
109 val entryList = getCredentialEntryListIffSingleAccount(
110 providerDisplayInfo.sortedUserNameToCredentialEntryList) ?: return null
111 if (entryList.size != 1) {
112 return null
113 }
114 val entry = entryList.firstOrNull() ?: return null
115 if (entry.isAutoSelectable) {
116 return entry
117 }
118 return null
119 }
120
121 /** Display-centric data structure derived from the [ProviderInfo]. This abstraction is not grouping
122 * by the provider id but instead focuses on structures convenient for display purposes. */
123 data class ProviderDisplayInfo(
124 /**
125 * The credential entries grouped by userName, derived from all entries of the [providerInfoList].
126 * Note that the list order matters to the display order.
127 */
128 val sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>,
129 val authenticationEntryList: List<AuthenticationEntryInfo>,
130 val remoteEntry: RemoteEntryInfo?
131 )
132
133 data class RequestDisplayInfo(
134 val appName: String,
135 val preferImmediatelyAvailableCredentials: Boolean,
136 val preferIdentityDocUi: Boolean,
137 // A top level branding icon + display name preferred by the app.
138 val preferTopBrandingContent: TopBrandingContent?,
139 // Map of credential type -> priority.
140 val typePriorityMap: Map<String, Int>,
141 )
142
143 data class TopBrandingContent(
144 val icon: Drawable,
145 val displayName: String,
146 )
147
148 /**
149 * @property userName the userName that groups all the entries in this list
150 * @property sortedCredentialEntryList the credential entries associated with the [userName] sorted
151 * by last used timestamps and then by credential types
152 */
153 data class PerUserNameCredentialEntryList(
154 val userName: String,
155 val sortedCredentialEntryList: List<CredentialEntryInfo>,
156 )
157
158 /** The name of the current screen. */
159 enum class GetScreenState {
160 /** The primary credential selection page. */
161 PRIMARY_SELECTION,
162
163 /** The single tap biometric selection page. */
164 BIOMETRIC_SELECTION,
165
166 /**
167 * The secondary credential selection page, where all sign-in options are listed.
168 *
169 * This state is expected to go back to PRIMARY_SELECTION on back navigation
170 */
171 ALL_SIGN_IN_OPTIONS,
172
173 /** The snackbar only page when there's no account but only a remoteEntry. */
174 REMOTE_ONLY,
175
176 /** The snackbar when there are only auth entries and all of them turn out to be empty. */
177 UNLOCKED_AUTH_ENTRIES_ONLY,
178
179 /**
180 * The secondary credential selection page, where all sign-in options are listed.
181 *
182 * This state has no option for the user to navigate back to PRIMARY_SELECTION, and
183 * instead can be terminated independently.
184 */
185 ALL_SIGN_IN_OPTIONS_ONLY,
186 }
187
188
189 /**
190 * IMPORTANT: new invocation should be mindful that this method will throw if more than 1 remote
191 * entry exists
192 *
193 * @hide
194 */
toProviderDisplayInfonull195 fun toProviderDisplayInfo(
196 providerInfoList: List<ProviderInfo>,
197 typePriorityMap: Map<String, Int>,
198 ): ProviderDisplayInfo {
199 val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
200 val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
201 val remoteEntryList = mutableListOf<RemoteEntryInfo>()
202 providerInfoList.forEach { providerInfo ->
203 authenticationEntryList.addAll(providerInfo.authenticationEntryList)
204 providerInfo.remoteEntry?.let {
205 remoteEntryList.add(it)
206 }
207 // There can only be at most one remote entry
208 Preconditions.checkState(remoteEntryList.size <= 1)
209
210 providerInfo.credentialEntryList.forEach {
211 userNameToCredentialEntryMap.compute(
212 if (selectorUiImprovementsEnabled()) it.entryGroupId else it.userName
213 ) { _, v ->
214 if (v == null) {
215 mutableListOf(it)
216 } else {
217 v.add(it)
218 v
219 }
220 }
221 }
222 }
223
224 // Compose sortedUserNameToCredentialEntryList
225 val comparator = CredentialEntryInfoComparatorByTypeThenTimestamp(typePriorityMap)
226 // Sort per username
227 userNameToCredentialEntryMap.values.forEach {
228 it.sortWith(comparator)
229 }
230 // Transform to list of PerUserNameCredentialEntryLists and then sort the outer list (of
231 // entries grouped by username / entryGroupId) based on the latest timestamp within that
232 // PerUserNameCredentialEntryList
233 val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
234 PerUserNameCredentialEntryList(it.key, it.value)
235 }.sortedWith(
236 compareByDescending {
237 it.sortedCredentialEntryList.maxByOrNull{ entry ->
238 entry.lastUsedTimeMillis ?: Instant.MIN
239 }?.lastUsedTimeMillis ?: Instant.MIN
240 }
241 )
242
243 return ProviderDisplayInfo(
244 sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
245 authenticationEntryList = authenticationEntryList,
246 remoteEntry = remoteEntryList.getOrNull(0),
247 )
248 }
249
250 /**
251 * This generates the res code for the large display title text for the selector. For example, it
252 * retrieves the resource for strings like: "Use your saved passkey for *rpName*".
253 * TODO(b/330396140) : Validate approach and add dynamic auth strings
254 */
generateDisplayTitleTextResCodenull255 internal fun generateDisplayTitleTextResCode(
256 singleEntryType: CredentialType,
257 authenticationEntryList: List<AuthenticationEntryInfo> = emptyList()
258 ): Int =
259 if (singleEntryType == CredentialType.PASSKEY)
260 R.string.get_dialog_title_use_passkey_for
261 else if (singleEntryType == CredentialType.PASSWORD)
262 R.string.get_dialog_title_use_password_for
263 else if (authenticationEntryList.isNotEmpty())
264 R.string.get_dialog_title_unlock_options_for
265 else R.string.get_dialog_title_use_sign_in_for
266
267 fun toActiveEntry(
268 providerDisplayInfo: ProviderDisplayInfo,
269 ): EntryInfo? {
270 val sortedUserNameToCredentialEntryList =
271 providerDisplayInfo.sortedUserNameToCredentialEntryList
272 val authenticationEntryList = providerDisplayInfo.authenticationEntryList
273 var activeEntry: EntryInfo? = null
274 if (sortedUserNameToCredentialEntryList
275 .size == 1 && authenticationEntryList.isEmpty()
276 ) {
277 activeEntry = sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList.first()
278 } else if (
279 sortedUserNameToCredentialEntryList
280 .isEmpty() && authenticationEntryList.size == 1
281 ) {
282 activeEntry = authenticationEntryList.first()
283 }
284 return activeEntry
285 }
286
toGetScreenStatenull287 private fun toGetScreenState(
288 providerDisplayInfo: ProviderDisplayInfo,
289 isRequestForAllOptions: Boolean
290 ): GetScreenState {
291 return if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() &&
292 providerDisplayInfo.remoteEntry == null &&
293 providerDisplayInfo.authenticationEntryList.all { it.isUnlockedAndEmpty })
294 GetScreenState.UNLOCKED_AUTH_ENTRIES_ONLY
295 else if (isRequestForAllOptions)
296 GetScreenState.ALL_SIGN_IN_OPTIONS_ONLY
297 else if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() &&
298 providerDisplayInfo.authenticationEntryList.isEmpty() &&
299 providerDisplayInfo.remoteEntry != null)
300 GetScreenState.REMOTE_ONLY
301 else if (isBiometricFlow(providerDisplayInfo, isFlowAutoSelectable(providerDisplayInfo)))
302 GetScreenState.BIOMETRIC_SELECTION
303 else GetScreenState.PRIMARY_SELECTION
304 }
305
306 /**
307 * Determines if the flow is a biometric flow by taking into account autoselect criteria.
308 */
isBiometricFlownull309 internal fun isBiometricFlow(providerDisplayInfo: ProviderDisplayInfo, isAutoSelectFlow: Boolean) =
310 findBiometricFlowEntry(providerDisplayInfo, isAutoSelectFlow) != null
311
312 /**
313 * Determines if the flow is an autoselect flow.
314 */
315 internal fun isFlowAutoSelectable(providerDisplayInfo: ProviderDisplayInfo) =
316 findAutoSelectEntry(providerDisplayInfo) != null
317
318 internal class CredentialEntryInfoComparatorByTypeThenTimestamp(
319 val typePriorityMap: Map<String, Int>,
320 ) : Comparator<CredentialEntryInfo> {
321 override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
322 // First rank by priorities of each credential type.
323 if (p0.rawCredentialType != p1.rawCredentialType) {
324 val p0Priority = typePriorityMap.getOrDefault(
325 p0.rawCredentialType, CredentialOption.PRIORITY_DEFAULT
326 )
327 val p1Priority = typePriorityMap.getOrDefault(
328 p1.rawCredentialType, CredentialOption.PRIORITY_DEFAULT
329 )
330 if (p0Priority < p1Priority) {
331 return -1
332 } else if (p1Priority < p0Priority) {
333 return 1
334 }
335 }
336 // Then rank by last used timestamps.
337 val p0LastUsedTimeMillis = p0.lastUsedTimeMillis
338 val p1LastUsedTimeMillis = p1.lastUsedTimeMillis
339 // Then order by last used timestamp
340 if (p0LastUsedTimeMillis != null && p1LastUsedTimeMillis != null) {
341 if (p0LastUsedTimeMillis < p1LastUsedTimeMillis) {
342 return 1
343 } else if (p0LastUsedTimeMillis > p1LastUsedTimeMillis) {
344 return -1
345 }
346 } else if (p0LastUsedTimeMillis != null) {
347 return -1
348 } else if (p1LastUsedTimeMillis != null) {
349 return 1
350 }
351 return 0
352 }
353 }