Multi credential folded screen changes
Test: Manual. See go/credential-selector-ui
Bug: 310986916
Change-Id: Ic7bf1003b154eae2fa102e94f9a81d63a08829a8
diff --git a/packages/CredentialManager/wear/res/values/strings.xml b/packages/CredentialManager/wear/res/values/strings.xml
index be7e448..4a92936 100644
--- a/packages/CredentialManager/wear/res/values/strings.xml
+++ b/packages/CredentialManager/wear/res/values/strings.xml
@@ -33,4 +33,6 @@
<string name="dialog_continue_button">Continue</string>
<!-- Content description for the sign in options button of a screen. [CHAR LIMIT=NONE] -->
<string name="dialog_sign_in_options_button">Sign-in Options</string>
+ <!-- Title for multiple credentials flattened screen. [CHAR LIMIT=NONE] -->
+ <string name="choose_sign_in_title">Choose a sign in</string>
</resources>
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt
new file mode 100644
index 0000000..98a9e93
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/UiState.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.ui.screens
+
+import androidx.activity.result.IntentSenderRequest
+
+sealed class UiState {
+ data object CredentialScreen : UiState()
+
+ data class CredentialSelected(
+ val intentSenderRequest: IntentSenderRequest?
+ ) : UiState()
+
+ data object Cancel : UiState()
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
new file mode 100644
index 0000000..6ba2011
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldScreen.kt
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.ui.screens.multiple
+
+import com.android.credentialmanager.ui.screens.UiState
+import android.graphics.drawable.Drawable
+import androidx.activity.compose.rememberLauncherForActivityResult
+import com.android.credentialmanager.R
+import androidx.compose.ui.res.stringResource
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.padding
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.SideEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.unit.dp
+import androidx.hilt.navigation.compose.hiltViewModel
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import androidx.navigation.NavHostController
+import androidx.navigation.compose.rememberNavController
+import com.android.credentialmanager.CredentialSelectorUiState
+import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.ui.components.DismissChip
+import com.android.credentialmanager.ui.components.CredentialsScreenChip
+import com.android.credentialmanager.ui.components.SignInHeader
+import com.android.credentialmanager.ui.components.SignInOptionsChip
+import com.google.android.horologist.annotations.ExperimentalHorologistApi
+import com.google.android.horologist.compose.layout.ScalingLazyColumn
+import com.google.android.horologist.compose.layout.ScalingLazyColumnState
+
+/**
+ * Screen that shows multiple credentials to select from.
+ *
+ * @param credentialSelectorUiState The app bar view model.
+ * @param screenIcon The view model corresponding to the home page.
+ * @param columnState ScalingLazyColumn configuration to be be applied
+ * @param modifier styling for composable
+ * @param viewModel ViewModel that updates ui state for this screen
+ * @param navController handles navigation events from this screen
+ */
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun MultiCredentialsFoldScreen(
+ credentialSelectorUiState: CredentialSelectorUiState.Get.MultipleEntry,
+ screenIcon: Drawable?,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier = Modifier,
+ viewModel: MultiCredentialsFoldViewModel = hiltViewModel(),
+ navController: NavHostController = rememberNavController(),
+) {
+ val uiState by viewModel.uiState.collectAsStateWithLifecycle()
+
+ when (val state = uiState) {
+ UiState.CredentialScreen -> {
+ MultiCredentialsFoldScreen(
+ state = credentialSelectorUiState,
+ onSignInOptionsClicked = viewModel::onSignInOptionsClicked,
+ onCredentialClicked = viewModel::onCredentialClicked,
+ onCancelClicked = viewModel::onCancelClicked,
+ screenIcon = screenIcon,
+ columnState = columnState,
+ modifier = modifier
+ )
+ }
+
+ is UiState.CredentialSelected -> {
+ val launcher = rememberLauncherForActivityResult(
+ StartBalIntentSenderForResultContract()
+ ) {
+ viewModel.onInfoRetrieved(it.resultCode, null)
+ }
+
+ SideEffect {
+ state.intentSenderRequest?.let {
+ launcher.launch(it)
+ }
+ }
+ }
+
+ UiState.Cancel -> {
+ navController.popBackStack()
+ }
+ }
+}
+
+@OptIn(ExperimentalHorologistApi::class)
+@Composable
+fun MultiCredentialsFoldScreen(
+ state: CredentialSelectorUiState.Get.MultipleEntry,
+ onSignInOptionsClicked: () -> Unit,
+ onCredentialClicked: (entryInfo: CredentialEntryInfo) -> Unit,
+ onCancelClicked: () -> Unit,
+ screenIcon: Drawable?,
+ columnState: ScalingLazyColumnState,
+ modifier: Modifier,
+) {
+ ScalingLazyColumn(
+ columnState = columnState,
+ modifier = modifier.fillMaxSize(),
+ ) {
+ item {
+ SignInHeader(
+ icon = screenIcon,
+ title = stringResource(R.string.choose_sign_in_title),
+ modifier = Modifier
+ .padding(top = 6.dp),
+ )
+ }
+
+ state.accounts.forEach {
+ it.sortedCredentialEntryList.forEach { credential: CredentialEntryInfo ->
+ item {
+ CredentialsScreenChip(
+ label = credential.userName,
+ onClick = { onCredentialClicked(credential) },
+ secondaryLabel = credential.credentialTypeDisplayName,
+ icon = credential.icon,
+ )
+ }
+ }
+ }
+ item { SignInOptionsChip(onSignInOptionsClicked) }
+ item { DismissChip(onCancelClicked) }
+ }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt
new file mode 100644
index 0000000..627a63d
--- /dev/null
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/multiple/MultiCredentialsFoldViewModel.kt
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.credentialmanager.ui.screens.multiple
+
+import android.content.Intent
+import android.credentials.selection.ProviderPendingIntentResponse
+import android.credentials.selection.UserSelectionDialogResult
+import androidx.lifecycle.ViewModel
+import com.android.credentialmanager.client.CredentialManagerClient
+import com.android.credentialmanager.ktx.getIntentSenderRequest
+import com.android.credentialmanager.model.Request
+import com.android.credentialmanager.model.get.CredentialEntryInfo
+import com.android.credentialmanager.ui.screens.UiState
+import dagger.hilt.android.lifecycle.HiltViewModel
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import javax.inject.Inject
+
+/** ViewModel for [MultiCredentialsFoldScreen].*/
+@HiltViewModel
+class MultiCredentialsFoldViewModel @Inject constructor(
+ private val credentialManagerClient: CredentialManagerClient,
+) : ViewModel() {
+
+ private lateinit var requestGet: Request.Get
+ private lateinit var entryInfo: CredentialEntryInfo
+
+ private val _uiState =
+ MutableStateFlow<UiState>(UiState.CredentialScreen)
+ val uiState: StateFlow<UiState> = _uiState
+
+ fun onCredentialClicked(entryInfo: CredentialEntryInfo) {
+ this.entryInfo = entryInfo
+ _uiState.value = UiState.CredentialSelected(
+ intentSenderRequest = entryInfo.getIntentSenderRequest()
+ )
+ }
+
+ fun onSignInOptionsClicked() {
+ // TODO(b/322797032) Implement navigation route for single credential screen to multiple
+ // credentials
+ }
+
+ fun onCancelClicked() {
+ _uiState.value = UiState.Cancel
+ }
+
+ fun onInfoRetrieved(
+ resultCode: Int? = null,
+ resultData: Intent? = null,
+ ) {
+ val userSelectionDialogResult = UserSelectionDialogResult(
+ requestGet.token,
+ entryInfo.providerId,
+ entryInfo.entryKey,
+ entryInfo.entrySubkey,
+ if (resultCode != null) ProviderPendingIntentResponse(resultCode, resultData) else null
+ )
+ credentialManagerClient.sendResult(userSelectionDialogResult)
+ }
+}
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
index 92d8a39..1697e0f 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreen.kt
@@ -42,7 +42,7 @@
import com.android.credentialmanager.ui.components.SignInHeader
import com.android.credentialmanager.ui.components.SignInOptionsChip
import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
-import com.android.credentialmanager.ui.screens.single.UiState
+import com.android.credentialmanager.ui.screens.UiState
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
index 35c39f6..37ffaca 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/passkey/SinglePasskeyScreenViewModel.kt
@@ -26,7 +26,7 @@
import com.android.credentialmanager.client.CredentialManagerClient
import com.android.credentialmanager.model.get.CredentialEntryInfo
import dagger.hilt.android.lifecycle.HiltViewModel
-import com.android.credentialmanager.ui.screens.single.UiState
+import com.android.credentialmanager.ui.screens.UiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
index c33a863..8c2c081 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderScreen.kt
@@ -40,7 +40,7 @@
import com.android.credentialmanager.ui.components.SignInHeader
import com.android.credentialmanager.ui.components.SignInOptionsChip
import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
-import com.android.credentialmanager.ui.screens.single.UiState
+import com.android.credentialmanager.ui.screens.UiState
import com.google.android.horologist.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt
index 2f9e2b1..7ba45e5 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/signInWithProvider/SignInWithProviderViewModel.kt
@@ -26,7 +26,7 @@
import com.android.credentialmanager.client.CredentialManagerClient
import com.android.credentialmanager.model.get.CredentialEntryInfo
import dagger.hilt.android.lifecycle.HiltViewModel
-import com.android.credentialmanager.ui.screens.single.UiState
+import com.android.credentialmanager.ui.screens.UiState
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import javax.inject.Inject