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.app.PendingIntent
20 import android.content.Intent
21 import android.graphics.drawable.Drawable
22 import com.android.credentialmanager.common.BaseEntry
23 import com.android.credentialmanager.common.CredentialType
24 import com.android.internal.util.Preconditions
25
26 import java.time.Instant
27
28 data class GetCredentialUiState(
29 val providerInfoList: List<ProviderInfo>,
30 val requestDisplayInfo: RequestDisplayInfo,
31 val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList),
32 val currentScreenState: GetScreenState = toGetScreenState(providerDisplayInfo),
33 val activeEntry: BaseEntry? = toActiveEntry(providerDisplayInfo),
34 val isNoAccount: Boolean = false,
35 )
36
37 internal fun hasContentToDisplay(state: GetCredentialUiState): Boolean {
38 return state.providerDisplayInfo.sortedUserNameToCredentialEntryList.isNotEmpty() ||
39 state.providerDisplayInfo.authenticationEntryList.isNotEmpty() ||
40 (state.providerDisplayInfo.remoteEntry != null &&
41 !state.requestDisplayInfo.preferImmediatelyAvailableCredentials)
42 }
43
findAutoSelectEntrynull44 internal fun findAutoSelectEntry(providerDisplayInfo: ProviderDisplayInfo): CredentialEntryInfo? {
45 if (providerDisplayInfo.authenticationEntryList.isNotEmpty()) {
46 return null
47 }
48 if (providerDisplayInfo.sortedUserNameToCredentialEntryList.size == 1) {
49 val entryList = providerDisplayInfo.sortedUserNameToCredentialEntryList.firstOrNull()
50 ?: return null
51 if (entryList.sortedCredentialEntryList.size == 1) {
52 val entry = entryList.sortedCredentialEntryList.firstOrNull() ?: return null
53 if (entry.isAutoSelectable) {
54 return entry
55 }
56 }
57 }
58 return null
59 }
60
61 data class ProviderInfo(
62 /**
63 * Unique id (component name) of this provider.
64 * Not for display purpose - [displayName] should be used for ui rendering.
65 */
66 val id: String,
67 val icon: Drawable,
68 val displayName: String,
69 val credentialEntryList: List<CredentialEntryInfo>,
70 val authenticationEntryList: List<AuthenticationEntryInfo>,
71 val remoteEntry: RemoteEntryInfo?,
72 val actionEntryList: List<ActionEntryInfo>,
73 )
74
75 /** Display-centric data structure derived from the [ProviderInfo]. This abstraction is not grouping
76 * by the provider id but instead focuses on structures convenient for display purposes. */
77 data class ProviderDisplayInfo(
78 /**
79 * The credential entries grouped by userName, derived from all entries of the [providerInfoList].
80 * Note that the list order matters to the display order.
81 */
82 val sortedUserNameToCredentialEntryList: List<PerUserNameCredentialEntryList>,
83 val authenticationEntryList: List<AuthenticationEntryInfo>,
84 val remoteEntry: RemoteEntryInfo?
85 )
86
87 class CredentialEntryInfo(
88 providerId: String,
89 entryKey: String,
90 entrySubkey: String,
91 pendingIntent: PendingIntent?,
92 fillInIntent: Intent?,
93 /** Type of this credential used for sorting. Not localized so must not be directly displayed. */
94 val credentialType: CredentialType,
95 /** Localized type value of this credential used for display purpose. */
96 val credentialTypeDisplayName: String,
97 val providerDisplayName: String,
98 val userName: String,
99 val displayName: String?,
100 val icon: Drawable?,
101 val shouldTintIcon: Boolean,
102 val lastUsedTimeMillis: Instant?,
103 val isAutoSelectable: Boolean,
104 ) : BaseEntry(
105 providerId,
106 entryKey,
107 entrySubkey,
108 pendingIntent,
109 fillInIntent,
110 shouldTerminateUiUponSuccessfulProviderResult = true,
111 )
112
113 class AuthenticationEntryInfo(
114 providerId: String,
115 entryKey: String,
116 entrySubkey: String,
117 pendingIntent: PendingIntent?,
118 fillInIntent: Intent?,
119 val title: String,
120 val providerDisplayName: String,
121 val icon: Drawable,
122 // The entry had been unlocked and turned out to be empty. Used to determine whether to
123 // show "Tap to unlock" or "No sign-in info" for this entry.
124 val isUnlockedAndEmpty: Boolean,
125 // True if the entry was the last one unlocked. Used to show the no sign-in info snackbar.
126 val isLastUnlocked: Boolean,
127 ) : BaseEntry(
128 providerId,
129 entryKey, entrySubkey,
130 pendingIntent,
131 fillInIntent,
132 shouldTerminateUiUponSuccessfulProviderResult = false,
133 )
134
135 class RemoteEntryInfo(
136 providerId: String,
137 entryKey: String,
138 entrySubkey: String,
139 pendingIntent: PendingIntent?,
140 fillInIntent: Intent?,
141 ) : BaseEntry(
142 providerId,
143 entryKey,
144 entrySubkey,
145 pendingIntent,
146 fillInIntent,
147 shouldTerminateUiUponSuccessfulProviderResult = true,
148 )
149
150 class ActionEntryInfo(
151 providerId: String,
152 entryKey: String,
153 entrySubkey: String,
154 pendingIntent: PendingIntent?,
155 fillInIntent: Intent?,
156 val title: String,
157 val icon: Drawable,
158 val subTitle: String?,
159 ) : BaseEntry(
160 providerId,
161 entryKey,
162 entrySubkey,
163 pendingIntent,
164 fillInIntent,
165 shouldTerminateUiUponSuccessfulProviderResult = true,
166 )
167
168 data class RequestDisplayInfo(
169 val appName: String,
170 val preferImmediatelyAvailableCredentials: Boolean,
171 val preferIdentityDocUi: Boolean,
172 // A top level branding icon + display name preferred by the app.
173 val preferTopBrandingContent: TopBrandingContent?,
174 )
175
176 data class TopBrandingContent(
177 val icon: Drawable,
178 val displayName: String,
179 )
180
181 /**
182 * @property userName the userName that groups all the entries in this list
183 * @property sortedCredentialEntryList the credential entries associated with the [userName] sorted
184 * by last used timestamps and then by credential types
185 */
186 data class PerUserNameCredentialEntryList(
187 val userName: String,
188 val sortedCredentialEntryList: List<CredentialEntryInfo>,
189 )
190
191 /** The name of the current screen. */
192 enum class GetScreenState {
193 /** The primary credential selection page. */
194 PRIMARY_SELECTION,
195
196 /** The secondary credential selection page, where all sign-in options are listed. */
197 ALL_SIGN_IN_OPTIONS,
198
199 /** The snackbar only page when there's no account but only a remoteEntry. */
200 REMOTE_ONLY,
201
202 /** The snackbar when there are only auth entries and all of them turn out to be empty. */
203 UNLOCKED_AUTH_ENTRIES_ONLY,
204 }
205
206 // IMPORTANT: new invocation should be mindful that this method will throw if more than 1 remote
207 // entry exists
toProviderDisplayInfonull208 private fun toProviderDisplayInfo(
209 providerInfoList: List<ProviderInfo>
210 ): ProviderDisplayInfo {
211 val userNameToCredentialEntryMap = mutableMapOf<String, MutableList<CredentialEntryInfo>>()
212 val authenticationEntryList = mutableListOf<AuthenticationEntryInfo>()
213 val remoteEntryList = mutableListOf<RemoteEntryInfo>()
214 providerInfoList.forEach { providerInfo ->
215 authenticationEntryList.addAll(providerInfo.authenticationEntryList)
216 if (providerInfo.remoteEntry != null) {
217 remoteEntryList.add(providerInfo.remoteEntry)
218 }
219 // There can only be at most one remote entry
220 Preconditions.checkState(remoteEntryList.size <= 1)
221
222 providerInfo.credentialEntryList.forEach {
223 userNameToCredentialEntryMap.compute(
224 it.userName
225 ) { _, v ->
226 if (v == null) {
227 mutableListOf(it)
228 } else {
229 v.add(it)
230 v
231 }
232 }
233 }
234 }
235
236 // Compose sortedUserNameToCredentialEntryList
237 val comparator = CredentialEntryInfoComparatorByTypeThenTimestamp()
238 // Sort per username
239 userNameToCredentialEntryMap.values.forEach {
240 it.sortWith(comparator)
241 }
242 // Transform to list of PerUserNameCredentialEntryLists and then sort across usernames
243 val sortedUserNameToCredentialEntryList = userNameToCredentialEntryMap.map {
244 PerUserNameCredentialEntryList(it.key, it.value)
245 }.sortedWith(
246 compareByDescending { it.sortedCredentialEntryList.first().lastUsedTimeMillis }
247 )
248
249 return ProviderDisplayInfo(
250 sortedUserNameToCredentialEntryList = sortedUserNameToCredentialEntryList,
251 authenticationEntryList = authenticationEntryList,
252 remoteEntry = remoteEntryList.getOrNull(0),
253 )
254 }
255
toActiveEntrynull256 private fun toActiveEntry(
257 providerDisplayInfo: ProviderDisplayInfo,
258 ): BaseEntry? {
259 val sortedUserNameToCredentialEntryList =
260 providerDisplayInfo.sortedUserNameToCredentialEntryList
261 val authenticationEntryList = providerDisplayInfo.authenticationEntryList
262 var activeEntry: BaseEntry? = null
263 if (sortedUserNameToCredentialEntryList
264 .size == 1 && authenticationEntryList.isEmpty()
265 ) {
266 activeEntry = sortedUserNameToCredentialEntryList.first().sortedCredentialEntryList.first()
267 } else if (
268 sortedUserNameToCredentialEntryList
269 .isEmpty() && authenticationEntryList.size == 1
270 ) {
271 activeEntry = authenticationEntryList.first()
272 }
273 return activeEntry
274 }
275
toGetScreenStatenull276 private fun toGetScreenState(
277 providerDisplayInfo: ProviderDisplayInfo
278 ): GetScreenState {
279 return if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() &&
280 providerDisplayInfo.remoteEntry == null &&
281 providerDisplayInfo.authenticationEntryList.all { it.isUnlockedAndEmpty })
282 GetScreenState.UNLOCKED_AUTH_ENTRIES_ONLY
283 else if (providerDisplayInfo.sortedUserNameToCredentialEntryList.isEmpty() &&
284 providerDisplayInfo.authenticationEntryList.isEmpty() &&
285 providerDisplayInfo.remoteEntry != null)
286 GetScreenState.REMOTE_ONLY
287 else GetScreenState.PRIMARY_SELECTION
288 }
289
290 internal class CredentialEntryInfoComparatorByTypeThenTimestamp : Comparator<CredentialEntryInfo> {
comparenull291 override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int {
292 // First prefer passkey type for its security benefits
293 if (p0.credentialType != p1.credentialType) {
294 if (CredentialType.PASSKEY == p0.credentialType) {
295 return -1
296 } else if (CredentialType.PASSKEY == p1.credentialType) {
297 return 1
298 }
299 }
300
301 // Then order by last used timestamp
302 if (p0.lastUsedTimeMillis != null && p1.lastUsedTimeMillis != null) {
303 if (p0.lastUsedTimeMillis < p1.lastUsedTimeMillis) {
304 return 1
305 } else if (p0.lastUsedTimeMillis > p1.lastUsedTimeMillis) {
306 return -1
307 }
308 } else if (p0.lastUsedTimeMillis != null) {
309 return -1
310 } else if (p1.lastUsedTimeMillis != null) {
311 return 1
312 }
313 return 0
314 }
315 }