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.createflow
18
19 import android.credentials.flags.Flags.selectorUiImprovementsEnabled
20 import android.hardware.biometrics.BiometricPrompt
21 import android.os.CancellationSignal
22 import android.text.TextUtils
23 import androidx.activity.compose.ManagedActivityResultLauncher
24 import androidx.activity.result.ActivityResult
25 import androidx.activity.result.IntentSenderRequest
26 import androidx.compose.foundation.layout.Arrangement
27 import androidx.compose.foundation.layout.Column
28 import androidx.compose.foundation.layout.Row
29 import androidx.compose.foundation.layout.fillMaxWidth
30 import androidx.compose.foundation.layout.padding
31 import androidx.compose.foundation.layout.wrapContentHeight
32 import androidx.compose.material3.Divider
33 import androidx.compose.material.icons.Icons
34 import androidx.compose.material.icons.outlined.NewReleases
35 import androidx.compose.material.icons.filled.Add
36 import androidx.compose.material.icons.filled.ArrowBack
37 import androidx.compose.material.icons.filled.Close
38 import androidx.compose.material.icons.outlined.QrCodeScanner
39 import androidx.compose.runtime.Composable
40 import androidx.compose.runtime.LaunchedEffect
41 import androidx.compose.ui.Modifier
42 import androidx.compose.ui.graphics.Color
43 import androidx.compose.ui.graphics.asImageBitmap
44 import androidx.compose.ui.platform.LocalContext
45 import androidx.compose.ui.res.stringResource
46 import androidx.compose.ui.unit.Dp
47 import androidx.compose.ui.unit.dp
48 import androidx.core.graphics.drawable.toBitmap
49 import com.android.compose.theme.LocalAndroidColorScheme
50 import com.android.credentialmanager.CredentialSelectorViewModel
51 import com.android.credentialmanager.R
52 import com.android.credentialmanager.common.BiometricError
53 import com.android.credentialmanager.common.BiometricFlowType
54 import com.android.credentialmanager.common.BiometricPromptState
55 import com.android.credentialmanager.model.EntryInfo
56 import com.android.credentialmanager.model.CredentialType
57 import com.android.credentialmanager.common.ProviderActivityState
58 import com.android.credentialmanager.common.material.ModalBottomSheetDefaults
59 import com.android.credentialmanager.common.runBiometricFlowForCreate
60 import com.android.credentialmanager.common.ui.ActionButton
61 import com.android.credentialmanager.common.ui.BodyMediumText
62 import com.android.credentialmanager.common.ui.BodySmallText
63 import com.android.credentialmanager.common.ui.ConfirmButton
64 import com.android.credentialmanager.common.ui.CredentialContainerCard
65 import com.android.credentialmanager.common.ui.CtaButtonRow
66 import com.android.credentialmanager.common.ui.Entry
67 import com.android.credentialmanager.common.ui.HeadlineIcon
68 import com.android.credentialmanager.common.ui.LargeLabelTextOnSurfaceVariant
69 import com.android.credentialmanager.common.ui.ModalBottomSheet
70 import com.android.credentialmanager.common.ui.MoreOptionTopAppBar
71 import com.android.credentialmanager.common.ui.SheetContainerCard
72 import com.android.credentialmanager.common.ui.HeadlineText
73 import com.android.credentialmanager.logging.CreateCredentialEvent
74 import com.android.credentialmanager.model.creation.CreateOptionInfo
75 import com.android.credentialmanager.model.creation.RemoteInfo
76 import com.android.internal.logging.UiEventLogger.UiEventEnum
77
78 @Composable
79 fun CreateCredentialScreen(
80 viewModel: CredentialSelectorViewModel,
81 createCredentialUiState: CreateCredentialUiState,
82 providerActivityLauncher: ManagedActivityResultLauncher<IntentSenderRequest, ActivityResult>
83 ) {
84 ModalBottomSheet(
85 sheetContent = {
86 // Hide the sheet content as opposed to the whole bottom sheet to maintain the scrim
87 // background color even when the content should be hidden while waiting for
88 // results from the provider app.
89 when (viewModel.uiState.providerActivityState) {
90 ProviderActivityState.NOT_APPLICABLE -> {
91 when (createCredentialUiState.currentScreenState) {
92 CreateScreenState.CREATION_OPTION_SELECTION -> CreationSelectionCard(
93 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
94 enabledProviderList = createCredentialUiState.enabledProviders,
95 providerInfo = createCredentialUiState
96 .activeEntry?.activeProvider!!,
97 createOptionInfo =
98 createCredentialUiState.activeEntry.activeEntryInfo
99 as CreateOptionInfo,
100 onOptionSelected = viewModel::createFlowOnEntrySelected,
101 onConfirm = viewModel::createFlowOnConfirmEntrySelected,
102 onMoreOptionsSelected =
103 viewModel::createFlowOnMoreOptionsSelectedOnCreationSelection,
104 onLog = { viewModel.logUiEvent(it) },
105 )
106 CreateScreenState.BIOMETRIC_SELECTION ->
107 BiometricSelectionPage(
108 biometricEntry = createCredentialUiState
109 .activeEntry?.activeEntryInfo,
110 onCancelFlowAndFinish = viewModel::onUserCancel,
111 onIllegalScreenStateAndFinish = viewModel::onIllegalUiState,
112 onMoreOptionSelected =
113 viewModel::createFlowOnMoreOptionsOnlySelectedOnCreationSelection,
114 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
115 enabledProviderInfo = createCredentialUiState
116 .activeEntry?.activeProvider!!,
117 onBiometricEntrySelected =
118 viewModel::createFlowOnEntrySelected,
119 fallbackToOriginalFlow =
120 viewModel::fallbackFromBiometricToNormalFlow,
121 getBiometricPromptState =
122 viewModel::getBiometricPromptStateStatus,
123 onBiometricPromptStateChange =
124 viewModel::onBiometricPromptStateChange,
125 getBiometricCancellationSignal =
126 viewModel::getBiometricCancellationSignal,
127 onLog = { viewModel.logUiEvent(it) },
128 )
129 CreateScreenState.MORE_OPTIONS_SELECTION_ONLY -> MoreOptionsSelectionCard(
130 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
131 enabledProviderList = createCredentialUiState.enabledProviders,
132 disabledProviderList = createCredentialUiState.disabledProviders,
133 sortedCreateOptionsPairs =
134 createCredentialUiState.sortedCreateOptionsPairs,
135 onBackCreationSelectionButtonSelected =
136 viewModel::createFlowOnBackCreationSelectionButtonSelected,
137 onOptionSelected =
138 viewModel::createFlowOnEntrySelectedFromMoreOptionScreen,
139 onDisabledProvidersSelected =
140 viewModel::createFlowOnLaunchSettings,
141 onRemoteEntrySelected = viewModel::createFlowOnEntrySelected,
142 onLog = { viewModel.logUiEvent(it) },
143 customTopAppBar = { MoreOptionTopAppBar(
144 text = stringResource(
145 R.string.save_credential_to_title,
146 when (createCredentialUiState.requestDisplayInfo
147 .type) {
148 CredentialType.PASSKEY ->
149 stringResource(R.string.passkey)
150 CredentialType.PASSWORD ->
151 stringResource(R.string.password)
152 CredentialType.UNKNOWN -> stringResource(
153 R.string.sign_in_info)
154 }
155 ),
156 onNavigationIconClicked = viewModel::onUserCancel,
157 bottomPadding = 16.dp,
158 navigationIcon = Icons.Filled.Close,
159 navigationIconContentDescription = stringResource(
160 R.string.accessibility_close_button
161 )
162 )}
163 )
164 CreateScreenState.MORE_OPTIONS_SELECTION -> MoreOptionsSelectionCard(
165 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
166 enabledProviderList = createCredentialUiState.enabledProviders,
167 disabledProviderList = createCredentialUiState.disabledProviders,
168 sortedCreateOptionsPairs =
169 createCredentialUiState.sortedCreateOptionsPairs,
170 onBackCreationSelectionButtonSelected =
171 viewModel::createFlowOnBackCreationSelectionButtonSelected,
172 onOptionSelected =
173 viewModel::createFlowOnEntrySelectedFromMoreOptionScreen,
174 onDisabledProvidersSelected =
175 viewModel::createFlowOnLaunchSettings,
176 onRemoteEntrySelected = viewModel::createFlowOnEntrySelected,
177 onLog = { viewModel.logUiEvent(it) },
178 )
179 CreateScreenState.DEFAULT_PROVIDER_CONFIRMATION -> {
180 if (createCredentialUiState.activeEntry == null) {
181 viewModel.onIllegalUiState("Expect active entry to be non-null" +
182 " upon default provider dialog.")
183 } else {
184 NonDefaultUsageConfirmationCard(
185 selectedEntry = createCredentialUiState.activeEntry,
186 onIllegalScreenState = viewModel::onIllegalUiState,
187 onLaunchSettings =
188 viewModel::createFlowOnLaunchSettings,
189 onUseOnceSelected = viewModel::createFlowOnUseOnceSelected,
190 onLog = { viewModel.logUiEvent(it) },
191 )
192 }
193 }
194 CreateScreenState.EXTERNAL_ONLY_SELECTION -> ExternalOnlySelectionCard(
195 requestDisplayInfo = createCredentialUiState.requestDisplayInfo,
196 activeRemoteEntry =
197 createCredentialUiState.activeEntry?.activeEntryInfo!!,
198 onOptionSelected = viewModel::createFlowOnEntrySelected,
199 onConfirm = viewModel::createFlowOnConfirmEntrySelected,
200 onLog = { viewModel.logUiEvent(it) },
201 )
202 }
203 }
204 ProviderActivityState.READY_TO_LAUNCH -> {
205 // This is a native bug from ModalBottomSheet. For now, use the temporary
206 // solution of not having an empty state.
207 if (viewModel.uiState.isAutoSelectFlow) {
208 Divider(
209 thickness = Dp.Hairline, color = ModalBottomSheetDefaults.scrimColor
210 )
211 }
212 // Launch only once per providerActivityState change so that the provider
213 // UI will not be accidentally launched twice.
214 LaunchedEffect(viewModel.uiState.providerActivityState) {
215 viewModel.launchProviderUi(providerActivityLauncher)
216 }
217 viewModel.uiMetrics.log(
218 CreateCredentialEvent
219 .CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_READY_TO_LAUNCH)
220 }
221 ProviderActivityState.PENDING -> {
222 if (viewModel.uiState.isAutoSelectFlow) {
223 Divider(
224 thickness = Dp.Hairline, color = ModalBottomSheetDefaults.scrimColor
225 )
226 }
227 // Hide our content when the provider activity is active.
228 viewModel.uiMetrics.log(
229 CreateCredentialEvent.CREDMAN_CREATE_CRED_PROVIDER_ACTIVITY_PENDING)
230 }
231 }
232 },
233 onDismiss = viewModel::onUserCancel,
234 isInitialRender = viewModel.uiState.isInitialRender,
235 isAutoSelectFlow = viewModel.uiState.isAutoSelectFlow,
236 onInitialRenderComplete = viewModel::onInitialRenderComplete,
237 )
238 }
239
240 @Composable
MoreOptionsSelectionCardnull241 fun MoreOptionsSelectionCard(
242 requestDisplayInfo: RequestDisplayInfo,
243 enabledProviderList: List<EnabledProviderInfo>,
244 disabledProviderList: List<DisabledProviderInfo>?,
245 sortedCreateOptionsPairs: List<Pair<CreateOptionInfo, EnabledProviderInfo>>,
246 onBackCreationSelectionButtonSelected: () -> Unit,
247 onOptionSelected: (ActiveEntry) -> Unit,
248 onDisabledProvidersSelected: () -> Unit,
249 onRemoteEntrySelected: (EntryInfo) -> Unit,
250 onLog: @Composable (UiEventEnum) -> Unit,
251 customTopAppBar: (@Composable() () -> Unit)? = null
252 ) {
253 SheetContainerCard(topAppBar = {
254 if (customTopAppBar != null) {
255 customTopAppBar()
256 } else {
257 MoreOptionTopAppBar(
258 text = stringResource(
259 R.string.save_credential_to_title,
260 when (requestDisplayInfo.type) {
261 CredentialType.PASSKEY ->
262 stringResource(R.string.passkey)
263 CredentialType.PASSWORD ->
264 stringResource(R.string.password)
265 CredentialType.UNKNOWN -> stringResource(R.string.sign_in_info)
266 }
267 ),
268 onNavigationIconClicked = onBackCreationSelectionButtonSelected,
269 bottomPadding = 16.dp,
270 navigationIcon = Icons.Filled.ArrowBack,
271 navigationIconContentDescription = stringResource(
272 R.string.accessibility_back_arrow_button
273 )
274 )
275 }
276 }) {
277 // bottom padding already
278 item {
279 CredentialContainerCard {
280 Column(verticalArrangement = Arrangement.spacedBy(2.dp)) {
281 sortedCreateOptionsPairs.forEach { entry ->
282 MoreOptionsInfoRow(
283 requestDisplayInfo = requestDisplayInfo,
284 providerInfo = entry.second,
285 createOptionInfo = entry.first,
286 onOptionSelected = {
287 onOptionSelected(
288 ActiveEntry(
289 entry.second,
290 entry.first
291 )
292 )
293 }
294 )
295 }
296 MoreOptionsDisabledProvidersRow(
297 disabledProviders = disabledProviderList,
298 onDisabledProvidersSelected =
299 onDisabledProvidersSelected,
300 )
301 enabledProviderList.forEach {
302 if (it.remoteEntry != null) {
303 RemoteEntryRow(
304 remoteInfo = it.remoteEntry!!,
305 onRemoteEntrySelected = onRemoteEntrySelected,
306 )
307 return@forEach
308 }
309 }
310 }
311 }
312 }
313 }
314 onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_MORE_OPTIONS_SELECTION)
315 }
316
317 @Composable
NonDefaultUsageConfirmationCardnull318 fun NonDefaultUsageConfirmationCard(
319 selectedEntry: ActiveEntry,
320 onIllegalScreenState: (String) -> Unit,
321 onLaunchSettings: () -> Unit,
322 onUseOnceSelected: () -> Unit,
323 onLog: @Composable (UiEventEnum) -> Unit,
324 ) {
325 val entryInfo = selectedEntry.activeEntryInfo
326 if (entryInfo !is CreateOptionInfo) {
327 onIllegalScreenState("Encountered unexpected type of entry during the default provider" +
328 " dialog: ${entryInfo::class}")
329 return
330 }
331 SheetContainerCard {
332 item { HeadlineIcon(imageVector = Icons.Outlined.NewReleases) }
333 item { Divider(thickness = 16.dp, color = Color.Transparent) }
334 item {
335 HeadlineText(
336 text = stringResource(
337 R.string.use_provider_for_all_title, selectedEntry.activeProvider.displayName)
338 )
339 }
340 item { Divider(thickness = 24.dp, color = Color.Transparent) }
341 item {
342 Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
343 BodyMediumText(text = stringResource(
344 R.string.use_provider_for_all_description, entryInfo.userProviderDisplayName))
345 }
346 }
347 item { Divider(thickness = 24.dp, color = Color.Transparent) }
348 item {
349 CtaButtonRow(
350 leftButton = {
351 ActionButton(
352 stringResource(R.string.settings),
353 onClick = onLaunchSettings,
354 )
355 },
356 rightButton = {
357 ConfirmButton(
358 stringResource(R.string.use_once),
359 onClick = onUseOnceSelected,
360 )
361 },
362 )
363 }
364 }
365 onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_MORE_OPTIONS_ROW_INTRO)
366 }
367
368 @Composable
CreationSelectionCardnull369 fun CreationSelectionCard(
370 requestDisplayInfo: RequestDisplayInfo,
371 enabledProviderList: List<EnabledProviderInfo>,
372 providerInfo: EnabledProviderInfo,
373 createOptionInfo: CreateOptionInfo,
374 onOptionSelected: (EntryInfo) -> Unit,
375 onConfirm: () -> Unit,
376 onMoreOptionsSelected: () -> Unit,
377 onLog: @Composable (UiEventEnum) -> Unit,
378 ) {
379 SheetContainerCard {
380 item {
381 HeadlineIcon(
382 bitmap = providerInfo.icon.toBitmap().asImageBitmap(),
383 tint = Color.Unspecified,
384 )
385 }
386 item { Divider(thickness = 4.dp, color = Color.Transparent) }
387 item { LargeLabelTextOnSurfaceVariant(text = providerInfo.displayName) }
388 item { Divider(thickness = 16.dp, color = Color.Transparent) }
389 item {
390 HeadlineText(
391 text = stringResource(
392 getCreateTitleResCode(requestDisplayInfo),
393 requestDisplayInfo.appName)
394 )
395 }
396 item { Divider(thickness = 24.dp, color = Color.Transparent) }
397
398 val footerDescription = createOptionInfo.footerDescription
399 if (selectorUiImprovementsEnabled()) {
400 if (!footerDescription.isNullOrBlank()) {
401 item {
402 Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
403 BodyMediumText(text = footerDescription)
404 }
405 }
406 item { Divider(thickness = 24.dp, color = Color.Transparent) }
407 }
408 }
409 item {
410 CredentialContainerCard {
411 PrimaryCreateOptionRow(
412 requestDisplayInfo = requestDisplayInfo,
413 entryInfo = createOptionInfo,
414 onOptionSelected = onOptionSelected
415 )
416 }
417 }
418 item { Divider(thickness = 24.dp, color = Color.Transparent) }
419 var createOptionsSize = 0
420 var remoteEntry: RemoteInfo? = null
421 enabledProviderList.forEach { enabledProvider ->
422 if (enabledProvider.remoteEntry != null) {
423 remoteEntry = enabledProvider.remoteEntry
424 }
425 createOptionsSize += enabledProvider.sortedCreateOptions.size
426 }
427 val shouldShowMoreOptionsButton = createOptionsSize > 1 || remoteEntry != null
428 item {
429 CtaButtonRow(
430 leftButton = if (shouldShowMoreOptionsButton) {
431 {
432 ActionButton(
433 stringResource(R.string.string_more_options),
434 onMoreOptionsSelected
435 )
436 }
437 } else null,
438 rightButton = {
439 ConfirmButton(
440 stringResource(R.string.string_continue),
441 onClick = onConfirm
442 )
443 },
444 )
445 }
446 if (!selectorUiImprovementsEnabled()) {
447 if (footerDescription != null && footerDescription.length > 0) {
448 item {
449 Divider(
450 thickness = 1.dp,
451 color = LocalAndroidColorScheme.current.outlineVariant,
452 modifier = Modifier.padding(vertical = 16.dp)
453 )
454 }
455 item {
456 Row(modifier = Modifier.fillMaxWidth().wrapContentHeight()) {
457 BodySmallText(text = footerDescription)
458 }
459 }
460 }
461 }
462 }
463 onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_CREATION_OPTION_SELECTION)
464 }
465
466 @Composable
ExternalOnlySelectionCardnull467 fun ExternalOnlySelectionCard(
468 requestDisplayInfo: RequestDisplayInfo,
469 activeRemoteEntry: EntryInfo,
470 onOptionSelected: (EntryInfo) -> Unit,
471 onConfirm: () -> Unit,
472 onLog: @Composable (UiEventEnum) -> Unit,
473 ) {
474 SheetContainerCard {
475 item { HeadlineIcon(imageVector = Icons.Outlined.QrCodeScanner) }
476 item { Divider(thickness = 16.dp, color = Color.Transparent) }
477 item {
478 HeadlineText(
479 text = stringResource(
480 when (requestDisplayInfo.type) {
481 CredentialType.PASSKEY -> R.string.create_passkey_in_other_device_title
482 CredentialType.PASSWORD -> R.string.save_password_on_other_device_title
483 else -> R.string.save_sign_in_on_other_device_title
484 }
485 )
486 )
487 }
488 item { Divider(thickness = 24.dp, color = Color.Transparent) }
489 item {
490 CredentialContainerCard {
491 PrimaryCreateOptionRow(
492 requestDisplayInfo = requestDisplayInfo,
493 entryInfo = activeRemoteEntry,
494 onOptionSelected = onOptionSelected
495 )
496 }
497 }
498 item { Divider(thickness = 24.dp, color = Color.Transparent) }
499 item {
500 CtaButtonRow(
501 rightButton = {
502 ConfirmButton(
503 stringResource(R.string.string_continue),
504 onClick = onConfirm
505 )
506 },
507 )
508 }
509 }
510 onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_EXTERNAL_ONLY_SELECTION)
511 }
512
513 @Composable
PrimaryCreateOptionRownull514 fun PrimaryCreateOptionRow(
515 requestDisplayInfo: RequestDisplayInfo,
516 entryInfo: EntryInfo,
517 onOptionSelected: (EntryInfo) -> Unit
518 ) {
519 Entry(
520 onClick = { onOptionSelected(entryInfo) },
521 iconImageBitmap = ((entryInfo as? CreateOptionInfo)?.profileIcon
522 ?: requestDisplayInfo.typeIcon)
523 .toBitmap().asImageBitmap(),
524 shouldApplyIconImageBitmapTint = !(entryInfo is CreateOptionInfo &&
525 entryInfo.profileIcon != null),
526 entryHeadlineText = requestDisplayInfo.title,
527 entrySecondLineText = when (requestDisplayInfo.type) {
528 CredentialType.PASSKEY -> {
529 if (!TextUtils.isEmpty(requestDisplayInfo.subtitle)) {
530 requestDisplayInfo.subtitle + " • " + stringResource(
531 R.string.passkey_before_subtitle
532 )
533 } else {
534 stringResource(R.string.passkey_before_subtitle)
535 }
536 }
537 // Set passwordValue instead
538 CredentialType.PASSWORD -> null
539 CredentialType.UNKNOWN -> requestDisplayInfo.subtitle
540 },
541 passwordValue =
542 if (requestDisplayInfo.type == CredentialType.PASSWORD)
543 // This subtitle would never be null for create password
544 requestDisplayInfo.subtitle ?: ""
545 else null,
546 enforceOneLine = true,
547 )
548 }
549
550 @Composable
MoreOptionsInfoRownull551 fun MoreOptionsInfoRow(
552 requestDisplayInfo: RequestDisplayInfo,
553 providerInfo: EnabledProviderInfo,
554 createOptionInfo: CreateOptionInfo,
555 onOptionSelected: () -> Unit
556 ) {
557 Entry(
558 onClick = onOptionSelected,
559 iconImageBitmap = providerInfo.icon.toBitmap().asImageBitmap(),
560 entryHeadlineText = providerInfo.displayName,
561 entrySecondLineText = createOptionInfo.userProviderDisplayName,
562 entryThirdLineText =
563 if (requestDisplayInfo.type == CredentialType.PASSKEY ||
564 requestDisplayInfo.type == CredentialType.PASSWORD) {
565 val passwordCount = createOptionInfo.passwordCount
566 val passkeyCount = createOptionInfo.passkeyCount
567 if (passwordCount != null && passkeyCount != null) {
568 stringResource(
569 R.string.more_options_usage_passwords_passkeys,
570 passwordCount,
571 passkeyCount
572 )
573 } else if (passwordCount != null) {
574 stringResource(
575 R.string.more_options_usage_passwords,
576 passwordCount
577 )
578 } else if (passkeyCount != null) {
579 stringResource(
580 R.string.more_options_usage_passkeys,
581 passkeyCount
582 )
583 } else {
584 null
585 }
586 } else {
587 val totalCredentialCount = createOptionInfo.totalCredentialCount
588 if (totalCredentialCount != null) {
589 stringResource(
590 R.string.more_options_usage_credentials,
591 totalCredentialCount
592 )
593 } else {
594 null
595 }
596 },
597 )
598 }
599
600 @Composable
MoreOptionsDisabledProvidersRownull601 fun MoreOptionsDisabledProvidersRow(
602 disabledProviders: List<ProviderInfo>?,
603 onDisabledProvidersSelected: () -> Unit,
604 ) {
605 if (disabledProviders != null && disabledProviders.isNotEmpty()) {
606 Entry(
607 onClick = onDisabledProvidersSelected,
608 iconImageVector = Icons.Filled.Add,
609 entryHeadlineText = stringResource(R.string.other_password_manager),
610 entrySecondLineText = disabledProviders.joinToString(separator = " • ") {
611 it.displayName
612 },
613 )
614 }
615 }
616
617 @Composable
RemoteEntryRownull618 fun RemoteEntryRow(
619 remoteInfo: RemoteInfo,
620 onRemoteEntrySelected: (RemoteInfo) -> Unit,
621 ) {
622 Entry(
623 onClick = { onRemoteEntrySelected(remoteInfo) },
624 iconImageVector = Icons.Outlined.QrCodeScanner,
625 entryHeadlineText = stringResource(R.string.another_device),
626 )
627 }
628
629 @Composable
630 internal fun BiometricSelectionPage(
631 biometricEntry: EntryInfo?,
632 onMoreOptionSelected: () -> Unit,
633 requestDisplayInfo: RequestDisplayInfo,
634 enabledProviderInfo: EnabledProviderInfo,
635 onBiometricEntrySelected: (
636 EntryInfo,
637 BiometricPrompt.AuthenticationResult?,
638 BiometricError?
639 ) -> Unit,
640 onCancelFlowAndFinish: () -> Unit,
641 onIllegalScreenStateAndFinish: (String) -> Unit,
642 fallbackToOriginalFlow: (BiometricFlowType) -> Unit,
643 getBiometricPromptState: () -> BiometricPromptState,
644 onBiometricPromptStateChange: (BiometricPromptState) -> Unit,
645 getBiometricCancellationSignal: () -> CancellationSignal,
646 onLog: @Composable (UiEventEnum) -> Unit
647 ) {
648 if (biometricEntry == null) {
649 fallbackToOriginalFlow(BiometricFlowType.CREATE)
650 return
651 }
652 val biometricFlowCalled = runBiometricFlowForCreate(
653 biometricEntry = biometricEntry,
654 context = LocalContext.current,
655 openMoreOptionsPage = onMoreOptionSelected,
656 sendDataToProvider = onBiometricEntrySelected,
657 onCancelFlowAndFinish = onCancelFlowAndFinish,
658 getBiometricPromptState = getBiometricPromptState,
659 onBiometricPromptStateChange = onBiometricPromptStateChange,
660 createRequestDisplayInfo = requestDisplayInfo,
661 createProviderInfo = enabledProviderInfo,
662 onBiometricFailureFallback = fallbackToOriginalFlow,
663 onIllegalStateAndFinish = onIllegalScreenStateAndFinish,
664 getBiometricCancellationSignal = getBiometricCancellationSignal
665 )
666 if (biometricFlowCalled) {
667 onLog(CreateCredentialEvent.CREDMAN_CREATE_CRED_BIOMETRIC_FLOW_LAUNCHED)
668 }
669 }
670