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
18 
19 import android.content.ComponentName
20 import android.content.Context
21 import android.content.pm.PackageInfo
22 import android.content.pm.PackageManager
23 import android.credentials.GetCredentialRequest
24 import android.credentials.selection.CreateCredentialProviderData
25 import android.credentials.selection.DisabledProviderData
26 import android.credentials.selection.Entry
27 import android.credentials.selection.GetCredentialProviderData
28 import android.credentials.selection.RequestInfo
29 import android.graphics.drawable.Drawable
30 import android.text.TextUtils
31 import android.util.Log
32 import com.android.credentialmanager.common.Constants
33 import com.android.credentialmanager.model.CredentialType
34 import com.android.credentialmanager.createflow.ActiveEntry
35 import com.android.credentialmanager.createflow.CreateCredentialUiState
36 import com.android.credentialmanager.model.creation.CreateOptionInfo
37 import com.android.credentialmanager.createflow.CreateScreenState
38 import com.android.credentialmanager.createflow.DisabledProviderInfo
39 import com.android.credentialmanager.createflow.EnabledProviderInfo
40 import com.android.credentialmanager.model.creation.RemoteInfo
41 import com.android.credentialmanager.createflow.RequestDisplayInfo
42 import com.android.credentialmanager.model.get.ProviderInfo
43 import com.android.credentialmanager.ktx.toProviderList
44 import androidx.credentials.CreateCredentialRequest
45 import androidx.credentials.CreateCustomCredentialRequest
46 import androidx.credentials.CreatePasswordRequest
47 import androidx.credentials.CreatePublicKeyCredentialRequest
48 import androidx.credentials.CredentialOption
49 import androidx.credentials.PasswordCredential
50 import androidx.credentials.PublicKeyCredential
51 import androidx.credentials.provider.CreateEntry
52 import androidx.credentials.provider.RemoteEntry
53 import org.json.JSONObject
54 import android.credentials.flags.Flags
55 import com.android.credentialmanager.createflow.isBiometricFlow
56 import com.android.credentialmanager.createflow.isFlowAutoSelectable
57 import com.android.credentialmanager.getflow.TopBrandingContent
58 import com.android.credentialmanager.ktx.retrieveEntryBiometricRequest
59 import java.time.Instant
60 
61 fun getAppLabel(
62     pm: PackageManager,
63     appPackageName: String
64 ): String? {
65     return try {
66         val pkgInfo = if (Flags.instantAppsEnabled()) {
67             getPackageInfo(pm, appPackageName)
68         } else {
69             pm.getPackageInfo(appPackageName, PackageManager.PackageInfoFlags.of(0))
70         }
71         val applicationInfo = checkNotNull(pkgInfo.applicationInfo)
72         applicationInfo.loadSafeLabel(
73             pm, 0f,
74             TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
75         ).toString()
76     } catch (e: Exception) {
77         Log.e(Constants.LOG_TAG, "Caller app not found", e)
78         null
79     }
80 }
81 
getServiceLabelAndIconnull82 private fun getServiceLabelAndIcon(
83     pm: PackageManager,
84     providerFlattenedComponentName: String
85 ): Pair<String, Drawable>? {
86     var providerLabel: String? = null
87     var providerIcon: Drawable? = null
88     val component = ComponentName.unflattenFromString(providerFlattenedComponentName)
89     if (component == null) {
90         // Test data has only package name not component name.
91         // For test data usage only.
92         try {
93             val pkgInfo = if (Flags.instantAppsEnabled()) {
94                 getPackageInfo(pm, providerFlattenedComponentName)
95             } else {
96                 pm.getPackageInfo(
97                         providerFlattenedComponentName,
98                         PackageManager.PackageInfoFlags.of(0)
99                 )
100             }
101             val applicationInfo = checkNotNull(pkgInfo.applicationInfo)
102             providerLabel =
103                 applicationInfo.loadSafeLabel(
104                     pm, 0f,
105                     TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
106                 ).toString()
107             providerIcon = applicationInfo.loadIcon(pm)
108         } catch (e: Exception) {
109             Log.e(Constants.LOG_TAG, "Provider package info not found", e)
110         }
111     } else {
112         try {
113             val si = pm.getServiceInfo(component, PackageManager.ComponentInfoFlags.of(0))
114             providerLabel = si.loadSafeLabel(
115                 pm, 0f,
116                 TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
117             ).toString()
118             providerIcon = si.loadIcon(pm)
119         } catch (e: PackageManager.NameNotFoundException) {
120             Log.e(Constants.LOG_TAG, "Provider service info not found", e)
121             // Added for mdoc use case where the provider may not need to register a service and
122             // instead only relies on the registration api.
123             try {
124                 val pkgInfo = if (Flags.instantAppsEnabled()) {
125                     getPackageInfo(pm, providerFlattenedComponentName)
126                 } else {
127                     pm.getPackageInfo(
128                             component.packageName,
129                             PackageManager.PackageInfoFlags.of(0)
130                     )
131                 }
132                 val applicationInfo = checkNotNull(pkgInfo.applicationInfo)
133                 providerLabel =
134                     applicationInfo.loadSafeLabel(
135                         pm, 0f,
136                         TextUtils.SAFE_STRING_FLAG_FIRST_LINE or TextUtils.SAFE_STRING_FLAG_TRIM
137                     ).toString()
138                 providerIcon = applicationInfo.loadIcon(pm)
139             } catch (e: Exception) {
140                 Log.e(Constants.LOG_TAG, "Provider package info not found", e)
141             }
142         }
143     }
144     return if (providerLabel == null || providerIcon == null) {
145         Log.d(
146             Constants.LOG_TAG,
147             "Failed to load provider label/icon for provider $providerFlattenedComponentName"
148         )
149         null
150     } else {
151         Pair(providerLabel, providerIcon)
152     }
153 }
154 
getPackageInfonull155 private fun getPackageInfo(
156     pm: PackageManager,
157     packageName: String
158 ): PackageInfo {
159     val packageManagerFlags = PackageManager.MATCH_INSTANT
160 
161     return pm.getPackageInfo(
162        packageName,
163        PackageManager.PackageInfoFlags.of(
164                (packageManagerFlags).toLong())
165     )
166 }
167 
168 /** Utility functions for converting CredentialManager data structures to or from UI formats. */
169 class GetFlowUtils {
170     companion object {
extractTypePriorityMapnull171         fun extractTypePriorityMap(request: GetCredentialRequest): Map<String, Int> {
172             val typePriorityMap = mutableMapOf<String, Int>()
173             request.credentialOptions.forEach {option ->
174                 // TODO(b/280085288) - use jetpack conversion method when exposed, rather than
175                 // parsing from the raw Bundle
176                 val priority = option.candidateQueryData.getInt(
177                         "androidx.credentials.BUNDLE_KEY_TYPE_PRIORITY_VALUE",
178                         when (option.type) {
179                             PasswordCredential.TYPE_PASSWORD_CREDENTIAL ->
180                                 CredentialOption.PRIORITY_PASSWORD_OR_SIMILAR
181                             PublicKeyCredential.TYPE_PUBLIC_KEY_CREDENTIAL -> 100
182                             else -> CredentialOption.PRIORITY_DEFAULT
183                         }
184                 )
185                 typePriorityMap[option.type] = priority
186             }
187             return typePriorityMap
188         }
189 
190         // Returns the list (potentially empty) of enabled provider.
toProviderListnull191         fun toProviderList(
192             providerDataList: List<GetCredentialProviderData>,
193             context: Context,
194         ): List<ProviderInfo> = providerDataList.toProviderList(context)
195         fun toRequestDisplayInfo(
196             requestInfo: RequestInfo?,
197             context: Context,
198             originName: String?,
199         ): com.android.credentialmanager.getflow.RequestDisplayInfo? {
200             val getCredentialRequest = requestInfo?.getCredentialRequest ?: return null
201             val preferImmediatelyAvailableCredentials = getCredentialRequest.data.getBoolean(
202                 "androidx.credentials.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS")
203             val preferUiBrandingComponentName =
204                 getCredentialRequest.data.getParcelable(
205                     "androidx.credentials.BUNDLE_KEY_PREFER_UI_BRANDING_COMPONENT_NAME",
206                     ComponentName::class.java
207                 )
208             val preferTopBrandingContent: TopBrandingContent? =
209                 if (!requestInfo.hasPermissionToOverrideDefault() ||
210                     preferUiBrandingComponentName == null) null
211                 else {
212                     val (displayName, icon) = getServiceLabelAndIcon(
213                         context.packageManager, preferUiBrandingComponentName.flattenToString())
214                         ?: Pair(null, null)
215                     if (displayName != null && icon != null) {
216                         TopBrandingContent(icon, displayName)
217                     } else {
218                         null
219                     }
220                 }
221 
222             val typePriorityMap = extractTypePriorityMap(getCredentialRequest)
223 
224             return com.android.credentialmanager.getflow.RequestDisplayInfo(
225                 appName = originName?.ifEmpty { null }
226                     ?: getAppLabel(context.packageManager, requestInfo.packageName)
227                     ?: return null,
228                 preferImmediatelyAvailableCredentials = preferImmediatelyAvailableCredentials,
229                 preferIdentityDocUi = getCredentialRequest.data.getBoolean(
230                     // TODO(b/276777444): replace with direct library constant reference once
231                     // exposed.
232                     "androidx.credentials.BUNDLE_KEY_PREFER_IDENTITY_DOC_UI"),
233                 preferTopBrandingContent = preferTopBrandingContent,
234                 typePriorityMap = typePriorityMap,
235             )
236         }
237     }
238 }
239 
240 class CreateFlowUtils {
241     companion object {
242 
243         private const val CREATE_ENTRY_PREFIX = "androidx.credentials.provider.createEntry."
244 
245         /**
246          * Note: caller required handle empty list due to parsing error.
247          */
toEnabledProviderListnull248         fun toEnabledProviderList(
249             providerDataList: List<CreateCredentialProviderData>,
250             context: Context,
251         ): List<EnabledProviderInfo> {
252             val providerList: MutableList<EnabledProviderInfo> = mutableListOf()
253             providerDataList.forEach {
254                 val providerLabelAndIcon = getServiceLabelAndIcon(
255                     context.packageManager,
256                     it.providerFlattenedComponentName
257                 ) ?: return@forEach
258                 val (providerLabel, providerIcon) = providerLabelAndIcon
259                 providerList.add(EnabledProviderInfo(
260                     id = it.providerFlattenedComponentName,
261                     displayName = providerLabel,
262                     icon = providerIcon,
263                     sortedCreateOptions = toSortedCreationOptionInfoList(
264                         it.providerFlattenedComponentName, it.saveEntries, context
265                     ),
266                     remoteEntry = toRemoteInfo(it.providerFlattenedComponentName, it.remoteEntry),
267                 ))
268             }
269             return providerList
270         }
271 
272         /**
273          * Note: caller required handle empty list due to parsing error.
274          */
toDisabledProviderListnull275         fun toDisabledProviderList(
276             providerDataList: List<DisabledProviderData>?,
277             context: Context,
278         ): List<DisabledProviderInfo> {
279             val providerList: MutableList<DisabledProviderInfo> = mutableListOf()
280             providerDataList?.forEach {
281                 val providerLabelAndIcon = getServiceLabelAndIcon(
282                     context.packageManager,
283                     it.providerFlattenedComponentName
284                 ) ?: return@forEach
285                 val (providerLabel, providerIcon) = providerLabelAndIcon
286                 providerList.add(DisabledProviderInfo(
287                     icon = providerIcon,
288                     id = it.providerFlattenedComponentName,
289                     displayName = providerLabel,
290                 ))
291             }
292             return providerList
293         }
294 
toRequestDisplayInfonull295         fun toRequestDisplayInfo(
296             requestInfo: RequestInfo?,
297             context: Context,
298             originName: String?,
299         ): RequestDisplayInfo? {
300             if (requestInfo == null) {
301                 return null
302             }
303             val appLabel = originName?.ifEmpty { null }
304                 ?: getAppLabel(context.packageManager, requestInfo.packageName)
305                 ?: return null
306             val createCredentialRequest = requestInfo.createCredentialRequest ?: return null
307             val createCredentialRequestJetpack = CreateCredentialRequest.createFrom(
308                 createCredentialRequest.type,
309                 createCredentialRequest.credentialData,
310                 createCredentialRequest.candidateQueryData,
311                 createCredentialRequest.isSystemProviderRequired,
312                 createCredentialRequest.origin,
313             )
314             val appPreferredDefaultProviderId: String? =
315                 if (!requestInfo.hasPermissionToOverrideDefault()) null
316                 else createCredentialRequestJetpack?.displayInfo?.preferDefaultProvider
317             val typeDisplayIcon = createCredentialRequestJetpack?.displayInfo?.credentialTypeIcon
318                     ?.loadDrawable(context)
319             return when (createCredentialRequestJetpack) {
320                 is CreatePasswordRequest -> RequestDisplayInfo(
321                     createCredentialRequestJetpack.id,
322                     createCredentialRequestJetpack.password,
323                     CredentialType.PASSWORD,
324                     appLabel,
325                     context.getDrawable(R.drawable.ic_password_24) ?: return null,
326                     preferImmediatelyAvailableCredentials =
327                     createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
328                     appPreferredDefaultProviderId = appPreferredDefaultProviderId,
329                     userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
330                     // The jetpack library requires a fix to parse this value correctly for
331                     // the password type. For now, directly parse it ourselves.
332                     isAutoSelectRequest = createCredentialRequest.credentialData.getBoolean(
333                         Constants.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false),
334                 )
335                 is CreatePublicKeyCredentialRequest -> {
336                     newRequestDisplayInfoFromPasskeyJson(
337                         requestJson = createCredentialRequestJetpack.requestJson,
338                         appLabel = appLabel,
339                         preferImmediatelyAvailableCredentials =
340                         createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
341                         appPreferredDefaultProviderId = appPreferredDefaultProviderId,
342                         userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
343                         // The jetpack library requires a fix to parse this value correctly for
344                         // the passkey type. For now, directly parse it ourselves.
345                         isAutoSelectRequest = createCredentialRequest.credentialData.getBoolean(
346                             Constants.BUNDLE_KEY_PREFER_IMMEDIATELY_AVAILABLE_CREDENTIALS, false),
347                         typeIcon = context.getDrawable(R.drawable.ic_passkey_24) ?: return null,
348                     )
349                 }
350                 is CreateCustomCredentialRequest -> {
351                     // TODO: directly use the display info once made public
352                     val displayInfo = CreateCredentialRequest.DisplayInfo.createFrom(
353                             createCredentialRequest.credentialData)
354                         ?: return null
355                     RequestDisplayInfo(
356                         title = displayInfo.userId.toString(),
357                         subtitle = displayInfo.userDisplayName?.toString(),
358                         type = CredentialType.UNKNOWN,
359                         appName = appLabel,
360                         typeIcon = typeDisplayIcon
361                             ?: context.getDrawable(R.drawable.ic_other_sign_in_24) ?: return null,
362                         preferImmediatelyAvailableCredentials =
363                         createCredentialRequestJetpack.preferImmediatelyAvailableCredentials,
364                         appPreferredDefaultProviderId = appPreferredDefaultProviderId,
365                         userSetDefaultProviderIds = requestInfo.defaultProviderIds.toSet(),
366                         isAutoSelectRequest = createCredentialRequestJetpack.isAutoSelectAllowed,
367                     )
368                 }
369                 else -> null
370             }
371         }
372 
toCreateCredentialUiStatenull373         fun toCreateCredentialUiState(
374             enabledProviders: List<EnabledProviderInfo>,
375             disabledProviders: List<DisabledProviderInfo>?,
376             defaultProviderIdPreferredByApp: String?,
377             defaultProviderIdsSetByUser: Set<String>,
378             requestDisplayInfo: RequestDisplayInfo,
379         ): CreateCredentialUiState? {
380             var remoteEntry: RemoteInfo? = null
381             var remoteEntryProvider: EnabledProviderInfo? = null
382             var defaultProviderPreferredByApp: EnabledProviderInfo? = null
383             var defaultProviderSetByUser: EnabledProviderInfo? = null
384             var createOptionsPairs:
385                 MutableList<Pair<CreateOptionInfo, EnabledProviderInfo>> = mutableListOf()
386             enabledProviders.forEach { enabledProvider ->
387                 if (defaultProviderIdPreferredByApp != null) {
388                     if (enabledProvider.id == defaultProviderIdPreferredByApp) {
389                         defaultProviderPreferredByApp = enabledProvider
390                     }
391                 }
392                 if (enabledProvider.sortedCreateOptions.isNotEmpty() &&
393                     defaultProviderIdsSetByUser.contains(enabledProvider.id)) {
394                     if (defaultProviderSetByUser == null) {
395                         defaultProviderSetByUser = enabledProvider
396                     } else {
397                         val newLastUsedTime = enabledProvider.sortedCreateOptions.firstOrNull()
398                           ?.lastUsedTime
399                         val curLastUsedTime = defaultProviderSetByUser?.sortedCreateOptions
400                         ?.firstOrNull()?.lastUsedTime ?: Instant.MIN
401                         if (newLastUsedTime != null) {
402                             if (curLastUsedTime == null || newLastUsedTime > curLastUsedTime) {
403                                 defaultProviderSetByUser = enabledProvider
404                             }
405                         }
406                     }
407                 }
408                 if (enabledProvider.sortedCreateOptions.isNotEmpty()) {
409                     enabledProvider.sortedCreateOptions.forEach {
410                         createOptionsPairs.add(Pair(it, enabledProvider))
411                     }
412                 }
413                 val currRemoteEntry = enabledProvider.remoteEntry
414                 if (currRemoteEntry != null) {
415                     if (remoteEntry != null) {
416                         // There can only be at most one remote entry
417                         Log.d(Constants.LOG_TAG, "Found more than one remote entry.")
418                         return null
419                     }
420                     remoteEntry = currRemoteEntry
421                     remoteEntryProvider = enabledProvider
422                 }
423             }
424             val defaultProvider = defaultProviderPreferredByApp ?: defaultProviderSetByUser
425             val sortedCreateOptionsPairs = createOptionsPairs.sortedWith(
426                 compareByDescending { it.first.lastUsedTime }
427             )
428             val activeEntry = toActiveEntry(
429                 defaultProvider = defaultProvider,
430                 sortedCreateOptionsPairs = sortedCreateOptionsPairs,
431                 remoteEntry = remoteEntry,
432                 remoteEntryProvider = remoteEntryProvider,
433             )
434             val isBiometricFlow = if (activeEntry == null) false else isBiometricFlow(activeEntry,
435                 isFlowAutoSelectable(
436                     requestDisplayInfo = requestDisplayInfo,
437                     activeEntry = activeEntry,
438                     sortedCreateOptionsPairs = sortedCreateOptionsPairs
439                 )
440             )
441             val initialScreenState = toCreateScreenState(
442                 createOptionSize = createOptionsPairs.size,
443                 remoteEntry = remoteEntry,
444                 isBiometricFlow = isBiometricFlow
445             )
446             return CreateCredentialUiState(
447                 enabledProviders = enabledProviders,
448                 disabledProviders = disabledProviders,
449                 currentScreenState = initialScreenState,
450                 requestDisplayInfo = requestDisplayInfo,
451                 sortedCreateOptionsPairs = sortedCreateOptionsPairs,
452                 activeEntry = activeEntry,
453                 remoteEntry = remoteEntry,
454                 foundCandidateFromUserDefaultProvider = defaultProviderSetByUser != null,
455             )
456         }
457 
toCreateScreenStatenull458         fun toCreateScreenState(
459             createOptionSize: Int,
460             remoteEntry: RemoteInfo?,
461             isBiometricFlow: Boolean,
462         ): CreateScreenState {
463             return if (createOptionSize == 0 && remoteEntry != null) {
464                 CreateScreenState.EXTERNAL_ONLY_SELECTION
465             } else if (isBiometricFlow) {
466                 CreateScreenState.BIOMETRIC_SELECTION
467             } else {
468                 CreateScreenState.CREATION_OPTION_SELECTION
469             }
470         }
471 
toActiveEntrynull472         private fun toActiveEntry(
473             defaultProvider: EnabledProviderInfo?,
474             sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
475             remoteEntry: RemoteInfo?,
476             remoteEntryProvider: EnabledProviderInfo?,
477         ): ActiveEntry? {
478             return if (
479                 sortedCreateOptionsPairs.isEmpty() && remoteEntry != null &&
480                 remoteEntryProvider != null
481             ) {
482                 ActiveEntry(remoteEntryProvider, remoteEntry)
483             } else if (defaultProvider != null &&
484                 defaultProvider.sortedCreateOptions.isNotEmpty()) {
485                 ActiveEntry(defaultProvider, defaultProvider.sortedCreateOptions.first())
486             } else if (sortedCreateOptionsPairs.isNotEmpty()) {
487                 val (topEntry, topEntryProvider) = sortedCreateOptionsPairs.first()
488                 ActiveEntry(topEntryProvider, topEntry)
489             } else null
490         }
491 
492         /**
493          * Note: caller required handle empty list due to parsing error.
494          */
toSortedCreationOptionInfoListnull495         private fun toSortedCreationOptionInfoList(
496             providerId: String,
497             creationEntries: List<Entry>,
498             context: Context,
499         ): List<CreateOptionInfo> {
500             val result: MutableList<CreateOptionInfo> = mutableListOf()
501             creationEntries.forEach {
502                 val createEntry = CreateEntry.fromSlice(it.slice) ?: return@forEach
503                 result.add(
504                     CreateOptionInfo(
505                     providerId = providerId,
506                     entryKey = it.key,
507                     entrySubkey = it.subkey,
508                     pendingIntent = createEntry.pendingIntent,
509                     fillInIntent = it.frameworkExtrasIntent,
510                     userProviderDisplayName = createEntry.accountName.toString(),
511                     profileIcon = createEntry.icon?.loadDrawable(context),
512                     passwordCount = createEntry.getPasswordCredentialCount(),
513                     passkeyCount = createEntry.getPublicKeyCredentialCount(),
514                     totalCredentialCount = createEntry.getTotalCredentialCount(),
515                     lastUsedTime = createEntry.lastUsedTime ?: Instant.MIN,
516                     footerDescription = createEntry.description?.toString(),
517                     // TODO(b/281065680): replace with official library constant once available
518                     allowAutoSelect =
519                     it.slice.items.firstOrNull {
520                         it.hasHint("androidx.credentials.provider.createEntry.SLICE_HINT_AUTO_" +
521                             "SELECT_ALLOWED")
522                     }?.text == "true",
523                     biometricRequest = retrieveEntryBiometricRequest(it,
524                         CREATE_ENTRY_PREFIX),
525                 )
526                 )
527             }
528             return result.sortedWith(
529                 compareByDescending { it.lastUsedTime }
530             )
531         }
532 
toRemoteInfonull533         private fun toRemoteInfo(
534             providerId: String,
535             remoteEntry: Entry?,
536         ): RemoteInfo? {
537             return if (remoteEntry != null) {
538                 val structuredRemoteEntry = RemoteEntry.fromSlice(remoteEntry.slice)
539                     ?: return null
540                 RemoteInfo(
541                     providerId = providerId,
542                     entryKey = remoteEntry.key,
543                     entrySubkey = remoteEntry.subkey,
544                     pendingIntent = structuredRemoteEntry.pendingIntent,
545                     fillInIntent = remoteEntry.frameworkExtrasIntent,
546                 )
547             } else null
548         }
549 
newRequestDisplayInfoFromPasskeyJsonnull550         private fun newRequestDisplayInfoFromPasskeyJson(
551             requestJson: String,
552             appLabel: String,
553             typeIcon: Drawable,
554             preferImmediatelyAvailableCredentials: Boolean,
555             appPreferredDefaultProviderId: String?,
556             userSetDefaultProviderIds: Set<String>,
557             isAutoSelectRequest: Boolean
558         ): RequestDisplayInfo? {
559             val json = JSONObject(requestJson)
560             var passkeyUsername = ""
561             var passkeyDisplayName = ""
562             if (json.has("user")) {
563                 val user: JSONObject = json.getJSONObject("user")
564                 passkeyUsername = user.getString("name")
565                 passkeyDisplayName = user.getString("displayName")
566             }
567             val (username, displayname) = userAndDisplayNameForPasskey(
568                 passkeyUsername = passkeyUsername,
569                 passkeyDisplayName = passkeyDisplayName,
570             )
571             return RequestDisplayInfo(
572                 username,
573                 displayname,
574                 CredentialType.PASSKEY,
575                 appLabel,
576                 typeIcon,
577                 preferImmediatelyAvailableCredentials,
578                 appPreferredDefaultProviderId,
579                 userSetDefaultProviderIds,
580                 isAutoSelectRequest,
581             )
582         }
583     }
584 }
585 
586 /**
587  * Returns the actual username and display name for the UI display purpose for the passkey use case.
588  *
589  * Passkey has some special requirements:
590  * 1) display-name on top (turned into UI username) if one is available, username on second line.
591  * 2) username on top if display-name is not available.
592  * 3) don't show username on second line if username == display-name
593  */
userAndDisplayNameForPasskeynull594 fun userAndDisplayNameForPasskey(
595     passkeyUsername: String,
596     passkeyDisplayName: String,
597 ): Pair<String, String> {
598     if (!TextUtils.isEmpty(passkeyUsername) && !TextUtils.isEmpty(passkeyDisplayName)) {
599         if (passkeyUsername == passkeyDisplayName) {
600             return Pair(passkeyUsername, "")
601         } else {
602             return Pair(passkeyDisplayName, passkeyUsername)
603         }
604     } else if (!TextUtils.isEmpty(passkeyUsername)) {
605         return Pair(passkeyUsername, passkeyDisplayName)
606     } else if (!TextUtils.isEmpty(passkeyDisplayName)) {
607         return Pair(passkeyDisplayName, passkeyUsername)
608     } else {
609         return Pair(passkeyDisplayName, passkeyUsername)
610     }
611 }
612