Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit e83bd36c authored by shuanghao's avatar shuanghao
Browse files

Single login screen refactoring:

1. introduce flowEngine.
2. make use of CredentialManager::sendEntrySelectionResult api.
3. git rid of Screen ViewModel since there is no dynamic content in this screen.

BUG: 301206470
Test: Manual.
Change-Id: I24e7511ab436b0079454e117f347f13524a30fc6
parent 9dd2a059
Loading
Loading
Loading
Loading
+1 −2
Original line number Diff line number Diff line
@@ -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() },
                )
            }
        }
+52 −4
Original line number Diff line number Diff line
@@ -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 {
+45 −0
Original line number Diff line number Diff line
/*
 * 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
+14 −6
Original line number Diff line number Diff line
@@ -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,
            )
        }

@@ -91,7 +96,10 @@ fun WearApp(
            )
        }
    }

    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()
        }

+19 −59
Original line number Diff line number Diff line
@@ -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,
    entry: CredentialEntryInfo,
    columnState: ScalingLazyColumnState,
    modifier: Modifier = Modifier,
    viewModel: SinglePasswordScreenViewModel = hiltViewModel(),
    navController: NavHostController = rememberNavController(),
    flowEngine: FlowEngine,
) {
    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()
        }
    }
        flowEngine.sendSelectionResult(entry, it.resultCode, it.data)
    }

@OptIn(ExperimentalHorologistApi::class)
@Composable
private fun SinglePasswordScreen(
    entry: CredentialEntryInfo,
    columnState: ScalingLazyColumnState,
    modifier: Modifier = Modifier,
    viewModel: SinglePasswordScreenViewModel,
) {
    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() }
            }
        }
    }
Loading