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