diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 870d9832ca0b2..a3ebf1efbb511 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -80,6 +80,8 @@ "Close sheet" "Go back to the previous page" + + "Close the Credential Manager action suggestion appearing at the bottom of the screen" diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index f801ba61e0736..94aa8e81ff609 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -44,7 +44,6 @@ import com.android.credentialmanager.createflow.CreateCredentialUiState import com.android.credentialmanager.createflow.EnabledProviderInfo import com.android.credentialmanager.createflow.RemoteInfo import com.android.credentialmanager.getflow.GetCredentialUiState -import com.android.credentialmanager.getflow.GetScreenState import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest.Companion.toBundle import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL @@ -124,11 +123,9 @@ class CredentialManagerRepo( val providerEnabledList = GetFlowUtils.toProviderList( // TODO: handle runtime cast error providerEnabledList as List, context) - // TODO: covert from real requestInfo - val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo("the app") + val requestDisplayInfo = GetFlowUtils.toRequestDisplayInfo(requestInfo) return GetCredentialUiState( providerEnabledList, - GetScreenState.PRIMARY_SELECTION, requestDisplayInfo, ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 0d7e81984eadb..f514b02f8abe2 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -88,6 +88,14 @@ class GetFlowUtils { } } + fun toRequestDisplayInfo( + requestInfo: RequestInfo, + ): com.android.credentialmanager.getflow.RequestDisplayInfo { + return com.android.credentialmanager.getflow.RequestDisplayInfo( + appDomainName = requestInfo.appPackageName + ) + } + /* From service data structure to UI credential entry list representation. */ private fun getCredentialOptionInfoList( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index d6d71220e943f..7a015a841fae7 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -32,15 +32,19 @@ import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.ArrowBack +import androidx.compose.material.icons.filled.Close import androidx.compose.material3.Divider import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Snackbar +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment @@ -65,6 +69,7 @@ import com.android.credentialmanager.common.ui.ContainerCard import com.android.credentialmanager.common.ui.TransparentBackgroundEntry import com.android.credentialmanager.jetpack.developer.PublicKeyCredential import com.android.credentialmanager.ui.theme.EntryShape +import com.android.credentialmanager.ui.theme.LocalAndroidColorScheme @Composable fun GetCredentialScreen( @@ -75,39 +80,50 @@ fun GetCredentialScreen( initialValue = ModalBottomSheetValue.Expanded, skipHalfExpanded = true ) - ModalBottomSheetLayout( - sheetBackgroundColor = MaterialTheme.colorScheme.surface, - modifier = Modifier.background(Color.Transparent), - sheetState = state, - sheetContent = { - val uiState = viewModel.uiState - if (!uiState.hidden) { - when (uiState.currentScreenState) { - GetScreenState.PRIMARY_SELECTION -> PrimarySelectionCard( - requestDisplayInfo = uiState.requestDisplayInfo, - providerDisplayInfo = uiState.providerDisplayInfo, - onEntrySelected = viewModel::onEntrySelected, - onCancel = viewModel::onCancel, - onMoreOptionSelected = viewModel::onMoreOptionSelected, - ) - GetScreenState.ALL_SIGN_IN_OPTIONS -> AllSignInOptionCard( - providerInfoList = uiState.providerInfoList, - providerDisplayInfo = uiState.providerDisplayInfo, - onEntrySelected = viewModel::onEntrySelected, - onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen, - ) + val uiState = viewModel.uiState + if (uiState.currentScreenState != GetScreenState.REMOTE_ONLY) { + ModalBottomSheetLayout( + sheetBackgroundColor = MaterialTheme.colorScheme.surface, + modifier = Modifier.background(Color.Transparent), + sheetState = state, + sheetContent = { + // TODO: hide UI at top level + if (!uiState.hidden) { + if (uiState.currentScreenState == GetScreenState.PRIMARY_SELECTION) { + PrimarySelectionCard( + requestDisplayInfo = uiState.requestDisplayInfo, + providerDisplayInfo = uiState.providerDisplayInfo, + onEntrySelected = viewModel::onEntrySelected, + onCancel = viewModel::onCancel, + onMoreOptionSelected = viewModel::onMoreOptionSelected, + ) + } else { + AllSignInOptionCard( + providerInfoList = uiState.providerInfoList, + providerDisplayInfo = uiState.providerDisplayInfo, + onEntrySelected = viewModel::onEntrySelected, + onBackButtonClicked = viewModel::onBackToPrimarySelectionScreen, + onCancel = viewModel::onCancel, + isNoAccount = uiState.isNoAccount, + ) + } + } else if (uiState.selectedEntry != null && !uiState.providerActivityPending) { + viewModel.launchProviderUi(providerActivityLauncher) } - } else if (uiState.selectedEntry != null && !uiState.providerActivityPending) { - viewModel.launchProviderUi(providerActivityLauncher) + }, + scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.8f), + sheetShape = EntryShape.TopRoundedCorner, + ) {} + LaunchedEffect(state.currentValue) { + if (state.currentValue == ModalBottomSheetValue.Hidden) { + viewModel.onCancel() } - }, - scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = 0.8f), - sheetShape = EntryShape.TopRoundedCorner, - ) {} - LaunchedEffect(state.currentValue) { - if (state.currentValue == ModalBottomSheetValue.Hidden) { - viewModel.onCancel() } + } else { + SnackBarScreen( + onClick = viewModel::onMoreOptionOnSnackBarSelected, + onCancel = viewModel::onCancel, + ) } } @@ -195,6 +211,8 @@ fun AllSignInOptionCard( providerDisplayInfo: ProviderDisplayInfo, onEntrySelected: (EntryInfo) -> Unit, onBackButtonClicked: () -> Unit, + onCancel: () -> Unit, + isNoAccount: Boolean, ) { val sortedUserNameToCredentialEntryList = providerDisplayInfo.sortedUserNameToCredentialEntryList @@ -212,7 +230,7 @@ fun AllSignInOptionCard( ) }, navigationIcon = { - IconButton(onClick = onBackButtonClicked) { + IconButton(onClick = if (isNoAccount) onCancel else onBackButtonClicked) { Icon( Icons.Filled.ArrowBack, contentDescription = stringResource( @@ -518,3 +536,37 @@ fun SignInAnotherWayRow(onSelect: () -> Unit) { } ) } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SnackBarScreen( + onClick: (Boolean) -> Unit, + onCancel: () -> Unit, +) { + // TODO: Change the height, width and position according to the design + Snackbar ( + modifier = Modifier.padding(horizontal = 80.dp).padding(top = 700.dp), + shape = EntryShape.FullMediumRoundedCorner, + containerColor = LocalAndroidColorScheme.current.colorBackground, + contentColor = LocalAndroidColorScheme.current.colorAccentPrimaryVariant, + ) { + Row( + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + TextButton( + onClick = {onClick(true)}, + ) { + Text(text = stringResource(R.string.get_dialog_use_saved_passkey_for)) + } + IconButton(onClick = onCancel) { + Icon( + Icons.Filled.Close, + contentDescription = stringResource( + R.string.accessibility_close_button + ) + ) + } + } + } +} \ No newline at end of file diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt index 7b80124d618c8..fcf49db6935ad 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialViewModel.kt @@ -36,12 +36,13 @@ import com.android.internal.util.Preconditions data class GetCredentialUiState( val providerInfoList: List, - val currentScreenState: GetScreenState, val requestDisplayInfo: RequestDisplayInfo, + val currentScreenState: GetScreenState = toGetScreenState(providerInfoList), val providerDisplayInfo: ProviderDisplayInfo = toProviderDisplayInfo(providerInfoList), val selectedEntry: EntryInfo? = null, val hidden: Boolean = false, val providerActivityPending: Boolean = false, + val isNoAccount: Boolean = false, ) class GetCredentialViewModel( @@ -127,6 +128,14 @@ class GetCredentialViewModel( ) } + fun onMoreOptionOnSnackBarSelected(isNoAccount: Boolean) { + Log.d("Account Selector", "More Option on snackBar selected") + uiState = uiState.copy( + currentScreenState = GetScreenState.ALL_SIGN_IN_OPTIONS, + isNoAccount = isNoAccount, + ) + } + fun onBackToPrimarySelectionScreen() { uiState = uiState.copy( currentScreenState = GetScreenState.PRIMARY_SELECTION @@ -192,6 +201,24 @@ private fun toProviderDisplayInfo( ) } +private fun toGetScreenState( + providerInfoList: List +): GetScreenState { + var noLocalAccount = true + var remoteInfo: RemoteEntryInfo? = null + providerInfoList.forEach{providerInfo -> if ( + providerInfo.credentialEntryList.isNotEmpty() || providerInfo.authenticationEntry != null + ) { noLocalAccount = false } + // TODO: handle the error situation that if multiple remoteInfos exists + if (providerInfo.remoteEntry != null) { + remoteInfo = providerInfo.remoteEntry + } + } + + return if (noLocalAccount && remoteInfo != null) + GetScreenState.REMOTE_ONLY else GetScreenState.PRIMARY_SELECTION +} + internal class CredentialEntryInfoComparator : Comparator { override fun compare(p0: CredentialEntryInfo, p1: CredentialEntryInfo): Int { // First order by last used timestamp diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index 0c3baffd8864f..3a2a73801d8ee 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -120,4 +120,6 @@ enum class GetScreenState { PRIMARY_SELECTION, /** The secondary credential selection page, where all sign-in options are listed. */ ALL_SIGN_IN_OPTIONS, + /** The snackbar only page when there's no account but only a remoteEntry. */ + REMOTE_ONLY, }