diff options
6 files changed, 131 insertions, 148 deletions
diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt index 0df40d7adba5..283dc7d6fe08 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorActivity.kt @@ -25,7 +25,6 @@ import androidx.wear.compose.material.MaterialTheme import com.android.credentialmanager.ui.WearApp import com.google.android.horologist.annotations.ExperimentalHorologistApi import dagger.hilt.android.AndroidEntryPoint -import kotlin.system.exitProcess @AndroidEntryPoint(ComponentActivity::class) class CredentialSelectorActivity : Hilt_CredentialSelectorActivity() { @@ -40,7 +39,7 @@ class CredentialSelectorActivity : Hilt_CredentialSelectorActivity() { MaterialTheme { WearApp( viewModel = viewModel, - onCloseApp = { exitProcess(0) }, + onCloseApp = { finish() }, ) } } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt index 2fc98e27bea1..366e3a7970c7 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt @@ -19,11 +19,14 @@ package com.android.credentialmanager import android.content.Intent import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.android.credentialmanager.CredentialSelectorUiState.Get import com.android.credentialmanager.model.Request import com.android.credentialmanager.client.CredentialManagerClient +import com.android.credentialmanager.model.EntryInfo 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 dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted @@ -35,10 +38,20 @@ import javax.inject.Inject @HiltViewModel class CredentialSelectorViewModel @Inject constructor( private val credentialManagerClient: CredentialManagerClient, -) : ViewModel() { - private val isPrimaryScreen = MutableStateFlow(false) - val uiState: StateFlow<CredentialSelectorUiState> = credentialManagerClient.requests - .combine(isPrimaryScreen) { request, isPrimary -> +) : FlowEngine, ViewModel() { + private val isPrimaryScreen = MutableStateFlow(true) + private val shouldClose = MutableStateFlow(false) + val uiState: StateFlow<CredentialSelectorUiState> = + combine( + credentialManagerClient.requests, + isPrimaryScreen, + shouldClose + ) { request, isPrimary, shouldClose -> + if (shouldClose) { + Log.d(TAG, "Request finished, closing ") + return@combine CredentialSelectorUiState.Close + } + when (request) { null -> CredentialSelectorUiState.Idle is Request.Cancel -> CredentialSelectorUiState.Cancel(request.appName) @@ -56,6 +69,41 @@ class CredentialSelectorViewModel @Inject constructor( fun updateRequest(intent: Intent) { credentialManagerClient.updateRequest(intent = intent) } + + override fun back() { + Log.d(TAG, "OnBackPressed") + when (uiState.value) { + is Get.MultipleEntry -> isPrimaryScreen.value = true + else -> { + shouldClose.value = true + // TODO("b/300422310 - [Wear] Implement UI for cancellation request with message") + } + } + } + + override fun cancel() { + shouldClose.value = true + // TODO("b/300422310 - [Wear] Implement UI for cancellation request with message") + } + + override fun openSecondaryScreen() { + isPrimaryScreen.value = false + } + + override fun sendSelectionResult( + entryInfo: EntryInfo, + resultCode: Int?, + resultData: Intent?, + isAutoSelected: Boolean, + ) { + val result = credentialManagerClient.sendEntrySelectionResult( + entryInfo = entryInfo, + resultCode = resultCode, + resultData = resultData, + isAutoSelected = isAutoSelected + ) + shouldClose.value = result + } } 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 new file mode 100644 index 000000000000..e4216446772b --- /dev/null +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/FlowEngine.kt @@ -0,0 +1,45 @@ +/* + * 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 + +import android.content.Intent +import com.android.credentialmanager.model.EntryInfo + +/** Engine of the credential selecting flow. */ +interface FlowEngine { + /** Back from previous stage. */ + fun back() + /** Cancels the selection flow. */ + fun cancel() + /** Opens secondary screen. */ + fun openSecondaryScreen() + /** + * Sends [entryInfo] as long as result after launching [EntryInfo.pendingIntent] with + * [EntryInfo.fillInIntent]. + * + * @param entryInfo: selected entry. + * @param resultCode: result code received after launch. + * @param resultData: data received after launch + * @param isAutoSelected: whether the entry is auto selected or by user. + */ + fun sendSelectionResult( + entryInfo: EntryInfo, + resultCode: Int? = null, + resultData: Intent? = null, + isAutoSelected: Boolean = false, + ) +}
\ 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 f7158e89a5cd..332b81634df7 100644 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt +++ b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt @@ -18,8 +18,11 @@ package com.android.credentialmanager.ui +import android.util.Log +import androidx.activity.compose.BackHandler import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.navigation.NavController import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState @@ -29,6 +32,8 @@ import com.android.credentialmanager.CredentialSelectorUiState import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry import com.android.credentialmanager.CredentialSelectorViewModel +import com.android.credentialmanager.FlowEngine +import com.android.credentialmanager.TAG import com.android.credentialmanager.ui.screens.LoadingScreen import com.android.credentialmanager.ui.screens.single.passkey.SinglePasskeyScreen import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen @@ -44,6 +49,7 @@ import com.android.credentialmanager.ui.screens.multiple.MultiCredentialsFoldScr @Composable fun WearApp( viewModel: CredentialSelectorViewModel, + flowEngine: FlowEngine = viewModel, onCloseApp: () -> Unit, ) { val navController = rememberSwipeDismissableNavController() @@ -52,7 +58,6 @@ fun WearApp( rememberSwipeDismissableNavHostState(swipeToDismissBoxState = swipeToDismissBoxState) val uiState by viewModel.uiState.collectAsStateWithLifecycle() - WearNavScaffold( startDestination = Screen.Loading.route, navController = navController, @@ -61,11 +66,11 @@ fun WearApp( composable(Screen.Loading.route) { LoadingScreen() } - scrollable(Screen.SinglePasswordScreen.route) { SinglePasswordScreen( - credentialSelectorUiState = viewModel.uiState.value as SingleEntry, + entry = (remember { uiState } as SingleEntry).entry, columnState = it.columnState, + flowEngine = flowEngine, ) } @@ -88,10 +93,13 @@ fun WearApp( credentialSelectorUiState = viewModel.uiState.value as MultipleEntry, screenIcon = null, columnState = it.columnState, - ) + ) } } - + BackHandler(true) { + viewModel.back() + } + Log.d(TAG, "uiState change, state: $uiState") when (val state = uiState) { CredentialSelectorUiState.Idle -> { if (navController.currentDestination?.route != Screen.Loading.route) { @@ -142,7 +150,7 @@ private fun handleGetNavigation( } } - is CredentialSelectorUiState.Get.MultipleEntry -> { + is MultipleEntry -> { navController.navigateToMultipleCredentialsFoldScreen() } 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 4c7f583fcb09..1f1a296dca9b 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,22 +18,19 @@ 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 -import androidx.compose.runtime.SideEffect -import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource 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.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 @@ -41,71 +38,30 @@ 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.model.get.CredentialEntryInfo -import com.android.credentialmanager.ui.screens.single.UiState import com.google.android.horologist.annotations.ExperimentalHorologistApi import com.google.android.horologist.compose.layout.ScalingLazyColumnState /** * Screen that shows sign in with provider credential. * - * @param credentialSelectorUiState The app bar view model. + * @param entry The password entry. * @param columnState ScalingLazyColumn configuration to be be applied to SingleAccountScreen * @param modifier styling for composable - * @param viewModel ViewModel that updates ui state for this screen - * @param navController handles navigation events from this screen + * @param flowEngine [FlowEngine] that updates ui state for this screen */ @OptIn(ExperimentalHorologistApi::class) @Composable fun SinglePasswordScreen( - credentialSelectorUiState: CredentialSelectorUiState.Get.SingleEntry, - columnState: ScalingLazyColumnState, - modifier: Modifier = Modifier, - viewModel: SinglePasswordScreenViewModel = hiltViewModel(), - navController: NavHostController = rememberNavController(), -) { - viewModel.initialize(credentialSelectorUiState.entry) - - val uiState by viewModel.uiState.collectAsStateWithLifecycle() - - when (val state = uiState) { - UiState.CredentialScreen -> { - SinglePasswordScreen( - credentialSelectorUiState.entry, - columnState, - modifier, - viewModel - ) - } - - is UiState.CredentialSelected -> { - val launcher = rememberLauncherForActivityResult( - StartBalIntentSenderForResultContract() - ) { - viewModel.onPasswordInfoRetrieved(it.resultCode, null) - } - - SideEffect { - state.intentSenderRequest?.let { - launcher.launch(it) - } - } - } - - UiState.Cancel -> { - // TODO(b/322797032) add valid navigation path here for going back - navController.popBackStack() - } - } -} - -@OptIn(ExperimentalHorologistApi::class) -@Composable -private fun SinglePasswordScreen( entry: CredentialEntryInfo, columnState: ScalingLazyColumnState, modifier: Modifier = Modifier, - viewModel: SinglePasswordScreenViewModel, + flowEngine: FlowEngine, ) { + val launcher = rememberLauncherForActivityResult( + StartBalIntentSenderForResultContract() + ) { + flowEngine.sendSelectionResult(entry, it.resultCode, it.data) + } SingleAccountScreen( headerContent = { SignInHeader( @@ -124,9 +80,13 @@ private fun SinglePasswordScreen( ) { item { Column { - ContinueChip(viewModel::onContinueClick) - SignInOptionsChip(viewModel::onSignInOptionsClick) - DismissChip(viewModel::onDismissClick) + ContinueChip { + entry.getIntentSenderRequest()?.let { + launcher.launch(it) + } ?: Log.w(TAG, "Cannot parse IntentSenderRequest") + } + SignInOptionsChip{ flowEngine.openSecondaryScreen() } + DismissChip { flowEngine.cancel() } } } } diff --git a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt b/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt deleted file mode 100644 index 8debecbac599..000000000000 --- a/packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt +++ /dev/null @@ -1,77 +0,0 @@ -/* - * 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.0N - * - * 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.single.password - -import android.content.Intent -import android.credentials.selection.UserSelectionDialogResult -import android.credentials.selection.ProviderPendingIntentResponse -import androidx.annotation.MainThread -import androidx.lifecycle.ViewModel -import com.android.credentialmanager.ktx.getIntentSenderRequest -import com.android.credentialmanager.model.Request -import com.android.credentialmanager.client.CredentialManagerClient -import com.android.credentialmanager.model.get.CredentialEntryInfo -import com.android.credentialmanager.ui.screens.single.UiState -import dagger.hilt.android.lifecycle.HiltViewModel -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow -import javax.inject.Inject - -@HiltViewModel -class SinglePasswordScreenViewModel @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 - - @MainThread - fun initialize(entryInfo: CredentialEntryInfo) { - this.entryInfo = entryInfo - } - - fun onDismissClick() { - _uiState.value = UiState.Cancel - } - - fun onContinueClick() { - _uiState.value = UiState.CredentialSelected( - intentSenderRequest = entryInfo.getIntentSenderRequest() - ) - } - - fun onSignInOptionsClick() { - } - - fun onPasswordInfoRetrieved( - 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) - } -} |