From 79fa94fb78e48dde27ec86270cac80c6878b5286 Mon Sep 17 00:00:00 2001
From: shuanghao <shuanghao@google.com>
Date: Fri, 16 Feb 2024 22:19:01 +0000
Subject: [PATCH] Add auto selection support. Move entry selection logic to
 flow Engine to be shared between screens. demo: http://shortn/_GYz5GifqrK

BUG: 324276451
Test: Manual.
Change-Id: I96edee71df1dc0cf5fb52f2bcb090230beb5bf83
---
 .../credentialmanager/ktx/CredentialKtx.kt    |  3 ++-
 .../CredentialSelectorViewModel.kt            | 25 +++++++++++++++++++
 .../android/credentialmanager/FlowEngine.kt   | 12 +++++++++
 .../android/credentialmanager/ui/WearApp.kt   |  8 ++++++
 .../single/password/SinglePasswordScreen.kt   | 17 ++-----------
 5 files changed, 49 insertions(+), 16 deletions(-)

diff --git a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
index a5f227a3adc9..f1d31ba9fbc1 100644
--- a/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
+++ b/packages/CredentialManager/shared/src/com/android/credentialmanager/ktx/CredentialKtx.kt
@@ -46,8 +46,9 @@ import com.android.credentialmanager.model.CredentialType
 import com.android.credentialmanager.model.get.ProviderInfo
 import com.android.credentialmanager.model.get.RemoteEntryInfo
 import com.android.credentialmanager.TAG
+import com.android.credentialmanager.model.EntryInfo
 
-fun CredentialEntryInfo.getIntentSenderRequest(
+fun EntryInfo.getIntentSenderRequest(
     isAutoSelected: Boolean = false
 ): IntentSenderRequest? {
     val entryIntent = fillInIntent?.putExtra(IS_AUTO_SELECTED_KEY, isAutoSelected)
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
index 88bdc3339db4..880038553d63 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt
@@ -28,10 +28,14 @@ import com.android.credentialmanager.model.get.ActionEntryInfo
 import com.android.credentialmanager.model.get.CredentialEntryInfo
 import com.android.credentialmanager.ui.mappers.toGet
 import android.util.Log
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.compose.runtime.Composable
 import com.android.credentialmanager.CredentialSelectorUiState.Cancel
 import com.android.credentialmanager.CredentialSelectorUiState.Close
 import com.android.credentialmanager.CredentialSelectorUiState.Create
 import com.android.credentialmanager.CredentialSelectorUiState.Idle
+import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
+import com.android.credentialmanager.ktx.getIntentSenderRequest
 import dagger.hilt.android.lifecycle.HiltViewModel
 import kotlinx.coroutines.flow.MutableStateFlow
 import kotlinx.coroutines.flow.SharingStarted
@@ -46,6 +50,8 @@ class CredentialSelectorViewModel @Inject constructor(
 ) : FlowEngine, ViewModel() {
     private val isPrimaryScreen = MutableStateFlow(true)
     private val shouldClose = MutableStateFlow(false)
+    private lateinit var selectedEntry: EntryInfo
+    private var isAutoSelected: Boolean = false
     val uiState: StateFlow<CredentialSelectorUiState> =
         combine(
             credentialManagerClient.requests,
@@ -107,6 +113,25 @@ class CredentialSelectorViewModel @Inject constructor(
         )
         shouldClose.value = result
     }
+
+    @Composable
+    override fun getEntrySelector(): (entry: EntryInfo, isAutoSelected: Boolean) -> Unit {
+        val launcher = rememberLauncherForActivityResult(
+            StartBalIntentSenderForResultContract()
+        ) {
+            sendSelectionResult(entryInfo = selectedEntry,
+                resultCode = it.resultCode,
+                resultData = it.data,
+                isAutoSelected = isAutoSelected)
+        }
+        return { selected, autoSelect ->
+            selectedEntry = selected
+            isAutoSelected = autoSelect
+            selected.getIntentSenderRequest()?.let {
+                launcher.launch(it)
+            } ?: Log.w(TAG, "Cannot parse IntentSenderRequest")
+        }
+    }
 }
 
 sealed class CredentialSelectorUiState {
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt
index e4216446772b..2e80a7c672f4 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt
@@ -17,6 +17,8 @@
 package com.android.credentialmanager
 
 import android.content.Intent
+import androidx.activity.result.IntentSenderRequest
+import androidx.compose.runtime.Composable
 import com.android.credentialmanager.model.EntryInfo
 
 /** Engine of the credential selecting flow. */
@@ -42,4 +44,14 @@ interface FlowEngine {
         resultData: Intent? = null,
         isAutoSelected: Boolean = false,
     )
+
+    /**
+     * Helper function to get an entry selector.
+     *
+     * @return selector fun consumes selected [EntryInfo]. Once invoked, [IntentSenderRequest] would
+     * be launched and invocation of [sendSelectionResult] would happen right after launching result
+     * coming back.
+     */
+    @Composable
+    fun getEntrySelector(): (entry: EntryInfo, isAutoSelected: Boolean) -> Unit
 }
\ No newline at end of file
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
index f9a5887158eb..405de1d3acc6 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt
@@ -43,6 +43,7 @@ import com.google.android.horologist.compose.navscaffold.WearNavScaffold
 import com.google.android.horologist.compose.navscaffold.composable
 import com.google.android.horologist.compose.navscaffold.scrollable
 import com.android.credentialmanager.model.CredentialType
+import com.android.credentialmanager.model.EntryInfo
 import com.android.credentialmanager.ui.screens.multiple.MultiCredentialsFoldScreen
 
 @OptIn(ExperimentalHorologistApi::class)
@@ -56,6 +57,7 @@ fun WearApp(
     val swipeToDismissBoxState = rememberSwipeToDismissBoxState()
     val navHostState =
         rememberSwipeDismissableNavHostState(swipeToDismissBoxState = swipeToDismissBoxState)
+    val selectEntry = flowEngine.getEntrySelector()
 
     val uiState by viewModel.uiState.collectAsStateWithLifecycle()
     WearNavScaffold(
@@ -111,6 +113,7 @@ fun WearApp(
                 navController = navController,
                 state = state,
                 onCloseApp = onCloseApp,
+                selectEntry = selectEntry
             )
         }
 
@@ -133,9 +136,14 @@ private fun handleGetNavigation(
     navController: NavController,
     state: CredentialSelectorUiState.Get,
     onCloseApp: () -> Unit,
+    selectEntry: (entry: EntryInfo, isAutoSelected: Boolean) -> Unit,
 ) {
     when (state) {
         is SingleEntry -> {
+            if (state.entry.isAutoSelectable) {
+                selectEntry(state.entry, true)
+                return
+            }
             when (state.entry.credentialType) {
                 CredentialType.UNKNOWN -> {
                     navController.navigateToSignInWithProviderScreen()
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
index 1f1a296dca9b..2ca8ef13c0cf 100644
--- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
+++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt
@@ -18,8 +18,6 @@
 
 package com.android.credentialmanager.ui.screens.single.password
 
-import android.util.Log
-import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.padding
 import androidx.compose.runtime.Composable
@@ -28,9 +26,6 @@ import androidx.compose.ui.res.stringResource
 import androidx.compose.ui.unit.dp
 import com.android.credentialmanager.FlowEngine
 import com.android.credentialmanager.R
-import com.android.credentialmanager.TAG
-import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract
-import com.android.credentialmanager.ktx.getIntentSenderRequest
 import com.android.credentialmanager.ui.components.PasswordRow
 import com.android.credentialmanager.ui.components.ContinueChip
 import com.android.credentialmanager.ui.components.DismissChip
@@ -57,11 +52,7 @@ fun SinglePasswordScreen(
     modifier: Modifier = Modifier,
     flowEngine: FlowEngine,
 ) {
-    val launcher = rememberLauncherForActivityResult(
-        StartBalIntentSenderForResultContract()
-    ) {
-        flowEngine.sendSelectionResult(entry, it.resultCode, it.data)
-    }
+    val selectEntry = flowEngine.getEntrySelector()
     SingleAccountScreen(
         headerContent = {
             SignInHeader(
@@ -80,11 +71,7 @@ fun SinglePasswordScreen(
     ) {
         item {
             Column {
-                ContinueChip {
-                    entry.getIntentSenderRequest()?.let {
-                        launcher.launch(it)
-                    } ?: Log.w(TAG, "Cannot parse IntentSenderRequest")
-                }
+                ContinueChip { selectEntry(entry, false) }
                 SignInOptionsChip{ flowEngine.openSecondaryScreen() }
                 DismissChip { flowEngine.cancel() }
             }
-- 
GitLab