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