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 }