1 /*
2  * 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
18 
19 import android.app.slice.Slice
20 import android.content.ComponentName
21 import android.content.Context
22 import android.content.pm.PackageManager
23 import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
24 import android.credentials.ui.AuthenticationEntry
25 import android.credentials.ui.CreateCredentialProviderData
26 import android.credentials.ui.DisabledProviderData
27 import android.credentials.ui.Entry
28 import android.credentials.ui.GetCredentialProviderData
29 import android.credentials.ui.RequestInfo
30 import android.graphics.drawable.Drawable
31 import android.text.TextUtils
32 import android.util.Log
33 import com.android.credentialmanager.common.Constants
34 import com.android.credentialmanager.common.CredentialType
35 import com.android.credentialmanager.createflow.ActiveEntry
36 import com.android.credentialmanager.createflow.CreateCredentialUiState
37 import com.android.credentialmanager.createflow.CreateOptionInfo
38 import com.android.credentialmanager.createflow.CreateScreenState
39 import com.android.credentialmanager.createflow.DisabledProviderInfo
40 import com.android.credentialmanager.createflow.EnabledProviderInfo
41 import com.android.credentialmanager.createflow.RemoteInfo
42 import com.android.credentialmanager.createflow.RequestDisplayInfo
43 import com.android.credentialmanager.getflow.ActionEntryInfo
44 import com.android.credentialmanager.getflow.AuthenticationEntryInfo
45 import com.android.credentialmanager.getflow.CredentialEntryInfo
46 import com.android.credentialmanager.getflow.ProviderInfo
47 import com.android.credentialmanager.getflow.RemoteEntryInfo
48 import com.android.credentialmanager.getflow.TopBrandingContent
49 import androidx.credentials.CreateCredentialRequest
50 import androidx.credentials.CreateCustomCredentialRequest
51 import androidx.credentials.CreatePasswordRequest
52 import androidx.credentials.CreatePublicKeyCredentialRequest
53 import androidx.credentials.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
54 import androidx.credentials.provider.Action
55 import androidx.credentials.provider.AuthenticationAction
56 import androidx.credentials.provider.CreateEntry
57 import androidx.credentials.provider.CredentialEntry
58 import androidx.credentials.provider.CustomCredentialEntry
59 import androidx.credentials.provider.PasswordCredentialEntry
60 import androidx.credentials.provider.PublicKeyCredentialEntry
61 import androidx.credentials.provider.RemoteEntry
62 import org.json.JSONObject
63 import java.time.Instant
64 
65 fun getAppLabel(
66     pm: PackageManager,
67     appPackageName: String
68 ): String? {
69     return try {
70         val pkgInfo = pm.getPackageInfo(appPackageName, PackageManager.PackageInfoFlags.of(0))
71         pkgInfo.applicationInfo.loadSafeLabel(
72             pm, 0f,
73             TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
74         ).toString()
75     } catch (e: PackageManager.NameNotFoundException) {
76         Log.e(Constants.LOG_TAG, "Caller app not found", e)
77         null
78     }
79 }
80 
81 private fun getServiceLabelAndIcon(
82     pm: PackageManager,
83     providerFlattenedComponentName: String
84 ): Pair<String, Drawable>? {
85     var providerLabel: String? = null
86     var providerIcon: Drawable? = null
87     val component = ComponentName.unflattenFromString(providerFlattenedComponentName)
88     if (component == null) {
89         // Test data has only package name not component name.
90         // For test data usage only.
91         try {
92             val pkgInfo = pm.getPackageInfo(
93                 providerFlattenedComponentName,
94                 PackageManager.PackageInfoFlags.of(0)
95             )
96             providerLabel =
97                 pkgInfo.applicationInfo.loadSafeLabel(
98                     pm, 0f,
99                     TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
100                 ).toString()
101             providerIcon = pkgInfo.applicationInfo.loadIcon(pm)
102         } catch (e: PackageManager.NameNotFoundException) {
103             Log.e(Constants.LOG_TAG, "Provider package info not found", e)
104         }
105     } else {
106         try {
107             val si = pm.getServiceInfo(component, PackageManager.ComponentInfoFlags.of(0))
108             providerLabel = si.loadSafeLabel(
109                 pm, 0f,
110                 TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
111             ).toString()
112             providerIcon = si.loadIcon(pm)
113         } catch (e: PackageManager.NameNotFoundException) {
114             Log.e(Constants.LOG_TAG, "Provider service info not found", e)
115             // Added for mdoc use case where the provider may not need to register a service and
116             // instead only relies on the registration api.
117             try {
118                 val pkgInfo = pm.getPackageInfo(
119                     component.packageName,
120                     PackageManager.PackageInfoFlags.of(0)
121                 )
122                 providerLabel =
123                     pkgInfo.applicationInfo.loadSafeLabel(
124                         pm, 0f,
125                         TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
126                     ).toString()
127                 providerIcon = pkgInfo.applicationInfo.loadIcon(pm)
128             } catch (e: PackageManager.NameNotFoundException) {
129                 Log.e(Constants.LOG_TAG, "Provider package info not found", e)
130             }
131         }
132     }
133     return if (providerLabel == null || providerIcon == null) {
134         Log.d(
135             Constants.LOG_TAG,
136             "Failed to load provider label/icon for provider $providerFlattenedComponentName"
137         )
138         null
139     } else {
140         Pair(providerLabel, providerIcon)
141     }
142 }
143 
144 /** Utility functions for converting CredentialManager data structures to or from UI formats. */
145 class GetFlowUtils {
146     companion object {
147         // Returns the list (potentially empty) of enabled provider.
148         fun toProviderList(
149             providerDataList: List<GetCredentialProviderData>,
150             context: Context,
151         ): List<ProviderInfo> {
152             val providerList: MutableList<ProviderInfo> = mutableListOf()
153             providerDataList.forEach {
154                 val providerLabelAndIcon = getServiceLabelAndIcon(
155                     context.packageManager,
156                     it.providerFlattenedComponentName
157                 ) ?: return@forEach
158                 val (providerLabel, providerIcon) = providerLabelAndIcon
159                 providerList.add(
160                     ProviderInfo(
161                         id = it.providerFlattenedComponentName,
162                         icon = providerIcon,
163                         displayName = providerLabel,
164                         credentialEntryList = getCredentialOptionInfoList(
165                             providerId = it.providerFlattenedComponentName,
166                             providerLabel = providerLabel,
167                             credentialEntries = it.credentialEntries,
168                             context = context
169                         ),
170                         authenticationEntryList = getAuthenticationEntryList(
171                             it.providerFlattenedComponentName,
172                             providerLabel,
173                             providerIcon,
174                             it.authenticationEntries),
175                         remoteEntry = getRemoteEntry(
176                             it.providerFlattenedComponentName,
177                             it.remoteEntry
178                         ),
179                         actionEntryList = getActionEntryList(
180                             it.providerFlattenedComponentName, it.actionChips, providerIcon
181                         ),
182                     )
183                 )
184             }
185             return providerList
186         }
187 
188         fun toRequestDisplayInfo(
189             requestInfo: RequestInfo?,
190             context: Context,
191             originName: String?,
192         ): com.android.credentialmanager.getflow.RequestDisplayInfo? {
193             val getCredentialRequest = requestInfo?.getCredentialRequest ?: return null
194             val preferImmediatelyAvailableCredentials = getCredentialRequest.data.getBoolean(
195                 "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS")
196             val preferUiBrandingComponentName =
197                 getCredentialRequest.data.getParcelable(
198                     "androidx.credentials.BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME",
199                     ComponentName::class.java
200                 )
201             val preferTopBrandingContent: TopBrandingContent? =
202                 if (!requestInfo.hasPermissionToOverrideDefault() ||
203                     preferUiBrandingComponentName == null) null
204                 else {
205                     val (displayName, icon) = getServiceLabelAndIcon(
206                         context.packageManager, preferUiBrandingComponentName.flattenToString())
207                         ?: Pair(null, null)
208                     if (displayName != null && icon != null) {
209                         TopBrandingContent(icon, displayName)
210                     } else {
211                         null
212                     }
213                 }
214             return com.android.credentialmanager.getflow.RequestDisplayInfo(
215                 appName = originName?.ifEmpty { null }
216                     ?: getAppLabel(context.packageManager, requestInfo.appPackageName)
217                     ?: return null,
218                 preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
219                 preferIdentityDocUi = getCredentialRequest.data.getBoolean(
220                     // TODO(b/276777444): replace with direct library constant reference once
221                     // exposed.
222                     "androidx.credentials.BUNDLE_KEY_PREFER_IDENTITY_DOC_UI"),
223                 preferTopBrandingContent = preferTopBrandingContent,
224             )
225         }
226 
227 
228         /**
229          * Note: caller required handle empty list due to parsing error.
230          */
231         private fun getCredentialOptionInfoList(
232             providerId: String,
233             providerLabel: String,
234             credentialEntries: List<Entry>,
235             context: Context,
236         ): List<CredentialEntryInfo> {
237             val result: MutableList<CredentialEntryInfo> = mutableListOf()
238             credentialEntries.forEach {
239                 val credentialEntry = parseCredentialEntryFromSlice(it.slice)
240                 when (credentialEntry) {
241                     is PasswordCredentialEntry -> {
242                         result.add(CredentialEntryInfo(
243                             providerId = providerId,
244                             providerDisplayName = providerLabel,
245                             entryKey = it.key,
246                             entrySubkey = it.subkey,
247                             pendingIntent = credentialEntry.pendingIntent,
248                             fillInIntent = it.frameworkExtrasIntent,
249                             credentialType = CredentialType.PASSWORD,
250                             credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
251                             userName = credentialEntry.username.toString(),
252                             displayName = credentialEntry.displayName?.toString(),
253                             icon = credentialEntry.icon.loadDrawable(context),
254                             shouldTintIcon = credentialEntry.isDefaultIcon,
255                             lastUsedTimeMillis = credentialEntry.lastUsedTime,
256                             isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
257                                 credentialEntry.autoSelectAllowedFromOption,
258                         ))
259                     }
260                     is PublicKeyCredentialEntry -> {
261                         result.add(CredentialEntryInfo(
262                             providerId = providerId,
263                             providerDisplayName = providerLabel,
264                             entryKey = it.key,
265                             entrySubkey = it.subkey,
266                             pendingIntent = credentialEntry.pendingIntent,
267                             fillInIntent = it.frameworkExtrasIntent,
268                             credentialType = CredentialType.PASSKEY,
269                             credentialTypeDisplayName = credentialEntry.typeDisplayName.toString(),
270                             userName = credentialEntry.username.toString(),
271                             displayName = credentialEntry.displayName?.toString(),
272                             icon = credentialEntry.icon.loadDrawable(context),
273                             shouldTintIcon = credentialEntry.isDefaultIcon,
274                             lastUsedTimeMillis = credentialEntry.lastUsedTime,
275                             isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
276                                 credentialEntry.autoSelectAllowedFromOption,
277                         ))
278                     }
279                     is CustomCredentialEntry -> {
280                         result.add(CredentialEntryInfo(
281                             providerId = providerId,
282                             providerDisplayName = providerLabel,
283                             entryKey = it.key,
284                             entrySubkey = it.subkey,
285                             pendingIntent = credentialEntry.pendingIntent,
286                             fillInIntent = it.frameworkExtrasIntent,
287                             credentialType = CredentialType.UNKNOWN,
288                             credentialTypeDisplayName =
289                             credentialEntry.typeDisplayName?.toString().orEmpty(),
290                             userName = credentialEntry.title.toString(),
291                             displayName = credentialEntry.subtitle?.toString(),
292                             icon = credentialEntry.icon.loadDrawable(context),
293                             shouldTintIcon = credentialEntry.isDefaultIcon,
294                             lastUsedTimeMillis = credentialEntry.lastUsedTime,
295                             isAutoSelectable = credentialEntry.isAutoSelectAllowed &&
296                                 credentialEntry.autoSelectAllowedFromOption,
297                         ))
298                     }
299                     else -> Log.d(
300                         Constants.LOG_TAG,
301                         "Encountered unrecognized credential entry ${it.slice.spec?.type}"
302                     )
303                 }
304             }
305             return result
306         }
307 
308         private fun parseCredentialEntryFromSlice(slice: Slice): CredentialEntry? {
309             try {
310                 when (slice.spec?.type) {
311                     TYPE_PASSWORD_CREDENTIAL -> return PasswordCredentialEntry.fromSlice(slice)!!
312                     TYPE_PUBLIC_KEY_CREDENTIAL -> return PublicKeyCredentialEntry.fromSlice(slice)!!
313                     else -> return CustomCredentialEntry.fromSlice(slice)!!
314                 }
315             } catch (e: Exception) {
316                 // Try CustomCredentialEntry.fromSlice one last time in case the cause was a failed
317                 // password / passkey parsing attempt.
318                 return CustomCredentialEntry.fromSlice(slice)
319             }
320         }
321 
322         /**
323          * Note: caller required handle empty list due to parsing error.
324          */
325         private fun getAuthenticationEntryList(
326             providerId: String,
327             providerDisplayName: String,
328             providerIcon: Drawable,
329             authEntryList: List<AuthenticationEntry>,
330         ): List<AuthenticationEntryInfo> {
331             val result: MutableList<AuthenticationEntryInfo> = mutableListOf()
332             authEntryList.forEach { entry ->
333                 val structuredAuthEntry =
334                     AuthenticationAction.fromSlice(entry.slice) ?: return@forEach
335 
336                 val title: String =
337                     structuredAuthEntry.title.toString().ifEmpty { providerDisplayName }
338 
339                 result.add(AuthenticationEntryInfo(
340                     providerId = providerId,
341                     entryKey = entry.key,
342                     entrySubkey = entry.subkey,
343                     pendingIntent = structuredAuthEntry.pendingIntent,
344                     fillInIntent = entry.frameworkExtrasIntent,
345                     title = title,
346                     providerDisplayName = providerDisplayName,
347                     icon = providerIcon,
348                     isUnlockedAndEmpty = entry.status != AuthenticationEntry.STATUS_LOCKED,
349                     isLastUnlocked =
350                     entry.status == AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT
351                 ))
352             }
353             return result
354         }
355 
356         private fun getRemoteEntry(providerId: String, remoteEntry: Entry?): RemoteEntryInfo? {
357             if (remoteEntry == null) {
358                 return null
359             }
360             val structuredRemoteEntry = RemoteEntry.fromSlice(remoteEntry.slice)
361                 ?: return null
362             return RemoteEntryInfo(
363                 providerId = providerId,
364                 entryKey = remoteEntry.key,
365                 entrySubkey = remoteEntry.subkey,
366                 pendingIntent = structuredRemoteEntry.pendingIntent,
367                 fillInIntent = remoteEntry.frameworkExtrasIntent,
368             )
369         }
370 
371         /**
372          * Note: caller required handle empty list due to parsing error.
373          */
374         private fun getActionEntryList(
375             providerId: String,
376             actionEntries: List<Entry>,
377             providerIcon: Drawable,
378         ): List<ActionEntryInfo> {
379             val result: MutableList<ActionEntryInfo> = mutableListOf()
380             actionEntries.forEach {
381                 val actionEntryUi = Action.fromSlice(it.slice) ?: return@forEach
382                 result.add(ActionEntryInfo(
383                     providerId = providerId,
384                     entryKey = it.key,
385                     entrySubkey = it.subkey,
386                     pendingIntent = actionEntryUi.pendingIntent,
387                     fillInIntent = it.frameworkExtrasIntent,
388                     title = actionEntryUi.title.toString(),
389                     icon = providerIcon,
390                     subTitle = actionEntryUi.subtitle?.toString(),
391                 ))
392             }
393             return result
394         }
395     }
396 }
397 
398 class CreateFlowUtils {
399     companion object {
400         /**
401          * Note: caller required handle empty list due to parsing error.
402          */
403         fun toEnabledProviderList(
404             providerDataList: List<CreateCredentialProviderData>,
405             context: Context,
406         ): List<EnabledProviderInfo> {
407             val providerList: MutableList<EnabledProviderInfo> = mutableListOf()
408             providerDataList.forEach {
409                 val providerLabelAndIcon = getServiceLabelAndIcon(
410                     context.packageManager,
411                     it.providerFlattenedComponentName
412                 ) ?: return@forEach
413                 val (providerLabel, providerIcon) = providerLabelAndIcon
414                 providerList.add(EnabledProviderInfo(
415                     id = it.providerFlattenedComponentName,
416                     displayName = providerLabel,
417                     icon = providerIcon,
418                     sortedCreateOptions = toSortedCreationOptionInfoList(
419                         it.providerFlattenedComponentName, it.saveEntries, context
420                     ),
421                     remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry),
422                 ))
423             }
424             return providerList
425         }
426 
427         /**
428          * Note: caller required handle empty list due to parsing error.
429          */
430         fun toDisabledProviderList(
431             providerDataList: List<DisabledProviderData>?,
432             context: Context,
433         ): List<DisabledProviderInfo> {
434             val providerList: MutableList<DisabledProviderInfo> = mutableListOf()
435             providerDataList?.forEach {
436                 val providerLabelAndIcon = getServiceLabelAndIcon(
437                     context.packageManager,
438                     it.providerFlattenedComponentName
439                 ) ?: return@forEach
440                 val (providerLabel, providerIcon) = providerLabelAndIcon
441                 providerList.add(DisabledProviderInfo(
442                     icon = providerIcon,
443                     id = it.providerFlattenedComponentName,
444                     displayName = providerLabel,
445                 ))
446             }
447             return providerList
448         }
449 
450         fun toRequestDisplayInfo(
451             requestInfo: RequestInfo?,
452             context: Context,
453             originName: String?,
454         ): RequestDisplayInfo? {
455             if (requestInfo == null) {
456                 return null
457             }
458             val appLabel = originName?.ifEmpty { null }
459                 ?: getAppLabel(context.packageManager, requestInfo.appPackageName)
460                 ?: return null
461             val createCredentialRequest = requestInfo.createCredentialRequest ?: return null
462             val createCredentialRequestJetpack = CreateCredentialRequest.createFrom(
463                 createCredentialRequest.type,
464                 createCredentialRequest.credentialData,
465                 createCredentialRequest.candidateQueryData,
466                 createCredentialRequest.isSystemProviderRequired,
467                 createCredentialRequest.origin,
468             )
469             val appPreferredDefaultProviderId: String? =
470                 if (!requestInfo.hasPermissionToOverrideDefault()) null
471                 else createCredentialRequestJetpack?.displayInfo?.preferDefaultProvider
472             return when (createCredentialRequestJetpack) {
473                 is CreatePasswordRequest -> RequestDisplayInfo(
474                     createCredentialRequestJetpack.id,
475                     createCredentialRequestJetpack.password,
476                     CredentialType.PASSWORD,
477                     appLabel,
478                     context.getDrawable(R.drawable.ic_password_24) ?: return null,
479                     preferImmediatelyAvailableCredentials =
480                     createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
481                     appPreferredDefaultProviderId = appPreferredDefaultProviderId,
482                     userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
483                     // The jetpack library requires a fix to parse this value correctly for
484                     // the password type. For now, directly parse it ourselves.
485                     isAutoSelectRequest = createCredentialRequest.credentialData.getBoolean(
486                         Constants.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false),
487                 )
488                 is CreatePublicKeyCredentialRequest -> {
489                     newRequestDisplayInfoFromPasskeyJson(
490                         requestJson = createCredentialRequestJetpack.requestJson,
491                         appLabel = appLabel,
492                         context = context,
493                         preferImmediatelyAvailableCredentials =
494                         createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
495                         appPreferredDefaultProviderId = appPreferredDefaultProviderId,
496                         userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
497                         // The jetpack library requires a fix to parse this value correctly for
498                         // the passkey type. For now, directly parse it ourselves.
499                         isAutoSelectRequest = createCredentialRequest.credentialData.getBoolean(
500                             Constants.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false),
501                     )
502                 }
503                 is CreateCustomCredentialRequest -> {
504                     // TODO: directly use the display info once made public
505                     val displayInfo = CreateCredentialRequest.DisplayInfo
506                         .parseFromCredentialDataBundle(createCredentialRequest.credentialData)
507                         ?: return null
508                     RequestDisplayInfo(
509                         title = displayInfo.userId.toString(),
510                         subtitle = displayInfo.userDisplayName?.toString(),
511                         type = CredentialType.UNKNOWN,
512                         appName = appLabel,
513                         typeIcon = displayInfo.credentialTypeIcon?.loadDrawable(context)
514                             ?: context.getDrawable(R.drawable.ic_other_sign_in_24) ?: return null,
515                         preferImmediatelyAvailableCredentials =
516                         createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
517                         appPreferredDefaultProviderId = appPreferredDefaultProviderId,
518                         userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
519                         isAutoSelectRequest = createCredentialRequestJetpack.isAutoSelectAllowed,
520                     )
521                 }
522                 else -> null
523             }
524         }
525 
526         fun toCreateCredentialUiState(
527             enabledProviders: List<EnabledProviderInfo>,
528             disabledProviders: List<DisabledProviderInfo>?,
529             defaultProviderIdPreferredByApp: String?,
530             defaultProviderIdsSetByUser: Set<String>,
531             requestDisplayInfo: RequestDisplayInfo,
532             isOnPasskeyIntroStateAlready: Boolean,
533             isPasskeyFirstUse: Boolean,
534         ): CreateCredentialUiState? {
535             var remoteEntry: RemoteInfo? = null
536             var remoteEntryProvider: EnabledProviderInfo? = null
537             var defaultProviderPreferredByApp: EnabledProviderInfo? = null
538             var defaultProviderSetByUser: EnabledProviderInfo? = null
539             var createOptionsPairs:
540                 MutableList<Pair<CreateOptionInfo, EnabledProviderInfo>> = mutableListOf()
541             enabledProviders.forEach { enabledProvider ->
542                 if (defaultProviderIdPreferredByApp != null) {
543                     if (enabledProvider.id == defaultProviderIdPreferredByApp) {
544                         defaultProviderPreferredByApp = enabledProvider
545                     }
546                 }
547                 if (enabledProvider.sortedCreateOptions.isNotEmpty() &&
548                     defaultProviderIdsSetByUser.contains(enabledProvider.id)) {
549                     if (defaultProviderSetByUser == null) {
550                         defaultProviderSetByUser = enabledProvider
551                     } else {
552                         val newLastUsedTime = enabledProvider.sortedCreateOptions.firstOrNull()
553                           ?.lastUsedTime
554                         val curLastUsedTime = defaultProviderSetByUser?.sortedCreateOptions
555                         ?.firstOrNull()?.lastUsedTime ?: Instant.MIN
556                         if (newLastUsedTime != null) {
557                             if (curLastUsedTime == null || newLastUsedTime > curLastUsedTime) {
558                                 defaultProviderSetByUser = enabledProvider
559                             }
560                         }
561                     }
562                 }
563                 if (enabledProvider.sortedCreateOptions.isNotEmpty()) {
564                     enabledProvider.sortedCreateOptions.forEach {
565                         createOptionsPairs.add(Pair(it, enabledProvider))
566                     }
567                 }
568                 val currRemoteEntry = enabledProvider.remoteEntry
569                 if (currRemoteEntry != null) {
570                     if (remoteEntry != null) {
571                         // There can only be at most one remote entry
572                         Log.d(Constants.LOG_TAG, "Found more than one remote entry.")
573                         return null
574                     }
575                     remoteEntry = currRemoteEntry
576                     remoteEntryProvider = enabledProvider
577                 }
578             }
579             val defaultProvider = defaultProviderPreferredByApp ?: defaultProviderSetByUser
580             val initialScreenState = toCreateScreenState(
581                 createOptionSize = createOptionsPairs.size,
582                 isOnPasskeyIntroStateAlready = isOnPasskeyIntroStateAlready,
583                 requestDisplayInfo = requestDisplayInfo,
584                 remoteEntry = remoteEntry,
585                 isPasskeyFirstUse = isPasskeyFirstUse
586             ) ?: return null
587             val sortedCreateOptionsPairs = createOptionsPairs.sortedWith(
588                 compareByDescending { it.first.lastUsedTime }
589             )
590             return CreateCredentialUiState(
591                 enabledProviders = enabledProviders,
592                 disabledProviders = disabledProviders,
593                 currentScreenState = initialScreenState,
594                 requestDisplayInfo = requestDisplayInfo,
595                 sortedCreateOptionsPairs = sortedCreateOptionsPairs,
596                 activeEntry = toActiveEntry(
597                     defaultProvider = defaultProvider,
598                     sortedCreateOptionsPairs = sortedCreateOptionsPairs,
599                     remoteEntry = remoteEntry,
600                     remoteEntryProvider = remoteEntryProvider,
601                 ),
602                 remoteEntry = remoteEntry,
603                 foundCandidateFromUserDefaultProvider = defaultProviderSetByUser != null,
604             )
605         }
606 
607         fun toCreateScreenState(
608             createOptionSize: Int,
609             isOnPasskeyIntroStateAlready: Boolean,
610             requestDisplayInfo: RequestDisplayInfo,
611             remoteEntry: RemoteInfo?,
612             isPasskeyFirstUse: Boolean,
613         ): CreateScreenState? {
614             return if (isPasskeyFirstUse && requestDisplayInfo.type == CredentialType.PASSKEY &&
615                 !isOnPasskeyIntroStateAlready) {
616                 CreateScreenState.PASSKEY_INTRO
617             } else if (createOptionSize == 0 && remoteEntry != null) {
618                 CreateScreenState.EXTERNAL_ONLY_SELECTION
619             } else {
620                 CreateScreenState.CREATION_OPTION_SELECTION
621             }
622         }
623 
624         private fun toActiveEntry(
625             defaultProvider: EnabledProviderInfo?,
626             sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
627             remoteEntry: RemoteInfo?,
628             remoteEntryProvider: EnabledProviderInfo?,
629         ): ActiveEntry? {
630             return if (
631                 sortedCreateOptionsPairs.isEmpty() && remoteEntry != null &&
632                 remoteEntryProvider != null
633             ) {
634                 ActiveEntry(remoteEntryProvider, remoteEntry)
635             } else if (defaultProvider != null &&
636                 defaultProvider.sortedCreateOptions.isNotEmpty()) {
637                 ActiveEntry(defaultProvider, defaultProvider.sortedCreateOptions.first())
638             } else if (sortedCreateOptionsPairs.isNotEmpty()) {
639                 val (topEntry, topEntryProvider) = sortedCreateOptionsPairs.first()
640                 ActiveEntry(topEntryProvider, topEntry)
641             } else null
642         }
643 
644         /**
645          * Note: caller required handle empty list due to parsing error.
646          */
647         private fun toSortedCreationOptionInfoList(
648             providerId: String,
649             creationEntries: List<Entry>,
650             context: Context,
651         ): List<CreateOptionInfo> {
652             val result: MutableList<CreateOptionInfo> = mutableListOf()
653             creationEntries.forEach {
654                 val createEntry = CreateEntry.fromSlice(it.slice) ?: return@forEach
655                 result.add(CreateOptionInfo(
656                     providerId = providerId,
657                     entryKey = it.key,
658                     entrySubkey = it.subkey,
659                     pendingIntent = createEntry.pendingIntent,
660                     fillInIntent = it.frameworkExtrasIntent,
661                     userProviderDisplayName = createEntry.accountName.toString(),
662                     profileIcon = createEntry.icon?.loadDrawable(context),
663                     passwordCount = createEntry.getPasswordCredentialCount(),
664                     passkeyCount = createEntry.getPublicKeyCredentialCount(),
665                     totalCredentialCount = createEntry.getTotalCredentialCount(),
666                     lastUsedTime = createEntry.lastUsedTime ?: Instant.MIN,
667                     footerDescription = createEntry.description?.toString(),
668                     // TODO(b/281065680): replace with official library constant once available
669                     allowAutoSelect =
670                     it.slice.items.firstOrNull {
671                         it.hasHint("androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_" +
672                             "SELECT_ALLOWED")
673                     }?.text == "true",
674                 ))
675             }
676             return result.sortedWith(
677                 compareByDescending { it.lastUsedTime }
678             )
679         }
680 
681         private fun toRemoteInfo(
682             providerId: String,
683             remoteEntry: Entry?,
684         ): RemoteInfo? {
685             return if (remoteEntry != null) {
686                 val structuredRemoteEntry = RemoteEntry.fromSlice(remoteEntry.slice)
687                     ?: return null
688                 RemoteInfo(
689                     providerId = providerId,
690                     entryKey = remoteEntry.key,
691                     entrySubkey = remoteEntry.subkey,
692                     pendingIntent = structuredRemoteEntry.pendingIntent,
693                     fillInIntent = remoteEntry.frameworkExtrasIntent,
694                 )
695             } else null
696         }
697 
698         private fun newRequestDisplayInfoFromPasskeyJson(
699             requestJson: String,
700             appLabel: String,
701             context: Context,
702             preferImmediatelyAvailableCredentials: Boolean,
703             appPreferredDefaultProviderId: String?,
704             userSetDefaultProviderIds: Set<String>,
705             isAutoSelectRequest: Boolean
706         ): RequestDisplayInfo? {
707             val json = JSONObject(requestJson)
708             var passkeyUsername = ""
709             var passkeyDisplayName = ""
710             if (json.has("user")) {
711                 val user: JSONObject = json.getJSONObject("user")
712                 passkeyUsername = user.getString("name")
713                 passkeyDisplayName = user.getString("displayName")
714             }
715             val (username, displayname) = userAndDisplayNameForPasskey(
716                 passkeyUsername = passkeyUsername,
717                 passkeyDisplayName = passkeyDisplayName,
718             )
719             return RequestDisplayInfo(
720                 username,
721                 displayname,
722                 CredentialType.PASSKEY,
723                 appLabel,
724                 context.getDrawable(R.drawable.ic_passkey_24) ?: return null,
725                 preferImmediatelyAvailableCredentials,
726                 appPreferredDefaultProviderId,
727                 userSetDefaultProviderIds,
728                 isAutoSelectRequest,
729             )
730         }
731     }
732 }
733 
734 /**
735  * Returns the actual username and display name for the UI display purpose for the passkey use case.
736  *
737  * Passkey has some special requirements:
738  * 1) display-name on top (turned into UI username) if one is available, username on second line.
739  * 2) username on top if display-name is not available.
740  * 3) don't show username on second line if username == display-name
741  */
742 fun userAndDisplayNameForPasskey(
743     passkeyUsername: String,
744     passkeyDisplayName: String,
745 ): Pair<String, String> {
746     if (!TextUtils.isEmpty(passkeyUsername) && !TextUtils.isEmpty(passkeyDisplayName)) {
747         if (passkeyUsername == passkeyDisplayName) {
748             return Pair(passkeyUsername, "")
749         } else {
750             return Pair(passkeyDisplayName, passkeyUsername)
751         }
752     } else if (!TextUtils.isEmpty(passkeyUsername)) {
753         return Pair(passkeyUsername, passkeyDisplayName)
754     } else if (!TextUtils.isEmpty(passkeyDisplayName)) {
755         return Pair(passkeyDisplayName, passkeyUsername)
756     } else {
757         return Pair(passkeyDisplayName, passkeyUsername)
758     }
759 }
760