Loading packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +19 −7 Original line number Diff line number Diff line Loading @@ -21,11 +21,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.credentialmanager.model.Request import com.android.credentialmanager.client.CredentialManagerClient import com.android.credentialmanager.model.get.ActionEntryInfo import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.ui.mappers.toGet import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import javax.inject.Inject Loading @@ -33,15 +36,15 @@ import javax.inject.Inject class CredentialSelectorViewModel @Inject constructor( private val credentialManagerClient: CredentialManagerClient, ) : ViewModel() { private val isPrimaryScreen = MutableStateFlow(false) val uiState: StateFlow<CredentialSelectorUiState> = credentialManagerClient.requests .map { request -> .combine(isPrimaryScreen) { request, isPrimary -> when (request) { null -> CredentialSelectorUiState.Idle is Request.Cancel -> CredentialSelectorUiState.Cancel(request.appName) is Request.Close -> CredentialSelectorUiState.Close is Request.Create -> CredentialSelectorUiState.Create is Request.Get -> request.toGet() is Request.Get -> request.toGet(isPrimary) } } .stateIn( Loading @@ -57,9 +60,18 @@ class CredentialSelectorViewModel @Inject constructor( sealed class CredentialSelectorUiState { data object Idle : CredentialSelectorUiState() sealed class Get : CredentialSelectorUiState() { data object SingleProviderSinglePasskey : Get() data object SingleProviderSinglePassword : Get() sealed class Get() : CredentialSelectorUiState() { data class SingleEntry(val entry: CredentialEntryInfo) : Get() data class SingleEntryPerAccount(val sortedEntries: List<CredentialEntryInfo>) : Get() data class MultipleEntry( val accounts: List<PerUserNameEntries>, val actionEntryList: List<ActionEntryInfo>, ) : Get() { data class PerUserNameEntries( val userName: String, val sortedCredentialEntryList: List<CredentialEntryInfo>, ) } // TODO: b/301206470 add the remaining states } Loading packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt +3 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.android.credentialmanager.CredentialSelectorUiState import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.ui.screens.LoadingScreen import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen Loading Loading @@ -57,6 +58,7 @@ fun WearApp( scrollable(Screen.SinglePasswordScreen.route) { SinglePasswordScreen( state = viewModel.uiState.value as SingleEntry, columnState = it.columnState, onCloseApp = onCloseApp, ) Loading Loading @@ -100,7 +102,7 @@ private fun handleGetNavigation( onCloseApp: () -> Unit, ) { when (state) { is CredentialSelectorUiState.Get.SingleProviderSinglePassword -> { is SingleEntry -> { navController.navigateToSinglePasswordScreen() } Loading packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt +35 −8 Original line number Diff line number Diff line Loading @@ -18,18 +18,45 @@ package com.android.credentialmanager.ui.mappers import com.android.credentialmanager.model.Request import com.android.credentialmanager.CredentialSelectorUiState import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry.PerUserNameEntries import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.get.CredentialEntryInfo fun Request.Get.toGet(): CredentialSelectorUiState.Get { fun Request.Get.toGet(isPrimary: Boolean): CredentialSelectorUiState.Get { // TODO: b/301206470 returning a hard coded state for MVP if (true) return CredentialSelectorUiState.Get.SingleProviderSinglePassword return if (providerInfos.size == 1) { if (providerInfos.first().credentialEntryList.size == 1) { CredentialSelectorUiState.Get.SingleProviderSinglePassword if (true) return CredentialSelectorUiState.Get.SingleEntry( providerInfos .flatMap { it.credentialEntryList } .first { it.credentialType == CredentialType.PASSWORD } ) val accounts = providerInfos .flatMap { it.credentialEntryList } .groupBy { it.userName} .entries .toList() return if (isPrimary) { if (accounts.size == 1) { CredentialSelectorUiState.Get.SingleEntry( accounts[0].value.minWith(comparator) ) } else { TODO() // b/301206470 - Implement other get flows CredentialSelectorUiState.Get.SingleEntryPerAccount( accounts.map { it.value.minWith(comparator) }.sortedWith(comparator) ) } } else { TODO() // b/301206470 - Implement other get flows CredentialSelectorUiState.Get.MultipleEntry( accounts = accounts.map { PerUserNameEntries( it.key, it.value.sortedWith(comparator) ) }, actionEntryList = providerInfos.flatMap { it.actionEntryList }, ) } } val comparator = compareBy<CredentialEntryInfo> { entryInfo -> // Passkey type always go first entryInfo.credentialType.let{ if (it == CredentialType.PASSKEY) 0 else 1 } } .thenByDescending{ it.lastUsedTimeMillis } packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt +3 −1 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.R import com.android.credentialmanager.TAG import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract Loading @@ -44,12 +45,13 @@ import com.google.android.horologist.compose.tools.WearPreview @Composable fun SinglePasswordScreen( state: SingleEntry, columnState: ScalingLazyColumnState, onCloseApp: () -> Unit, modifier: Modifier = Modifier, viewModel: SinglePasswordScreenViewModel = hiltViewModel(), ) { viewModel.initialize() viewModel.initialize(state.entry) val uiState by viewModel.uiState.collectAsStateWithLifecycle() Loading packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt +6 −28 Original line number Diff line number Diff line Loading @@ -19,12 +19,9 @@ package com.android.credentialmanager.ui.screens.single.password import android.content.Intent import android.credentials.ui.ProviderPendingIntentResponse import android.credentials.ui.UserSelectionDialogResult import android.util.Log import androidx.activity.result.IntentSenderRequest import androidx.annotation.MainThread import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.credentialmanager.TAG import com.android.credentialmanager.ktx.getIntentSenderRequest import com.android.credentialmanager.model.Request import com.android.credentialmanager.client.CredentialManagerClient Loading @@ -33,7 +30,6 @@ import com.android.credentialmanager.ui.model.PasswordUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel Loading @@ -51,33 +47,15 @@ class SinglePasswordScreenViewModel @Inject constructor( val uiState: StateFlow<SinglePasswordScreenUiState> = _uiState @MainThread fun initialize() { fun initialize(entryInfo: CredentialEntryInfo) { if (initializeCalled) return initializeCalled = true viewModelScope.launch { val request = credentialManagerClient.requests.value Log.d(TAG, "request: $request, client instance: $credentialManagerClient") if (request !is Request.Get) { _uiState.value = SinglePasswordScreenUiState.Error } else { requestGet = request if (requestGet.providerInfos.all { it.credentialEntryList.isEmpty() }) { Log.d(TAG, "Empty passwordEntries") _uiState.value = SinglePasswordScreenUiState.Error } else { entryInfo = requestGet.providerInfos.first().credentialEntryList.first() _uiState.value = SinglePasswordScreenUiState.Loaded( PasswordUiModel( email = entryInfo.userName, ) ) } } } } fun onCancelClick() { _uiState.value = SinglePasswordScreenUiState.Cancel Loading Loading
packages/CredentialManager/wear/src/com/android/credentialmanager/CredentialSelectorViewModel.kt +19 −7 Original line number Diff line number Diff line Loading @@ -21,11 +21,14 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.credentialmanager.model.Request import com.android.credentialmanager.client.CredentialManagerClient import com.android.credentialmanager.model.get.ActionEntryInfo import com.android.credentialmanager.model.get.CredentialEntryInfo import com.android.credentialmanager.ui.mappers.toGet import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import javax.inject.Inject Loading @@ -33,15 +36,15 @@ import javax.inject.Inject class CredentialSelectorViewModel @Inject constructor( private val credentialManagerClient: CredentialManagerClient, ) : ViewModel() { private val isPrimaryScreen = MutableStateFlow(false) val uiState: StateFlow<CredentialSelectorUiState> = credentialManagerClient.requests .map { request -> .combine(isPrimaryScreen) { request, isPrimary -> when (request) { null -> CredentialSelectorUiState.Idle is Request.Cancel -> CredentialSelectorUiState.Cancel(request.appName) is Request.Close -> CredentialSelectorUiState.Close is Request.Create -> CredentialSelectorUiState.Create is Request.Get -> request.toGet() is Request.Get -> request.toGet(isPrimary) } } .stateIn( Loading @@ -57,9 +60,18 @@ class CredentialSelectorViewModel @Inject constructor( sealed class CredentialSelectorUiState { data object Idle : CredentialSelectorUiState() sealed class Get : CredentialSelectorUiState() { data object SingleProviderSinglePasskey : Get() data object SingleProviderSinglePassword : Get() sealed class Get() : CredentialSelectorUiState() { data class SingleEntry(val entry: CredentialEntryInfo) : Get() data class SingleEntryPerAccount(val sortedEntries: List<CredentialEntryInfo>) : Get() data class MultipleEntry( val accounts: List<PerUserNameEntries>, val actionEntryList: List<ActionEntryInfo>, ) : Get() { data class PerUserNameEntries( val userName: String, val sortedCredentialEntryList: List<CredentialEntryInfo>, ) } // TODO: b/301206470 add the remaining states } Loading
packages/CredentialManager/wear/src/com/android/credentialmanager/ui/WearApp.kt +3 −1 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import androidx.wear.compose.foundation.rememberSwipeToDismissBoxState import androidx.wear.compose.navigation.rememberSwipeDismissableNavController import androidx.wear.compose.navigation.rememberSwipeDismissableNavHostState import com.android.credentialmanager.CredentialSelectorUiState import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.CredentialSelectorViewModel import com.android.credentialmanager.ui.screens.LoadingScreen import com.android.credentialmanager.ui.screens.single.password.SinglePasswordScreen Loading Loading @@ -57,6 +58,7 @@ fun WearApp( scrollable(Screen.SinglePasswordScreen.route) { SinglePasswordScreen( state = viewModel.uiState.value as SingleEntry, columnState = it.columnState, onCloseApp = onCloseApp, ) Loading Loading @@ -100,7 +102,7 @@ private fun handleGetNavigation( onCloseApp: () -> Unit, ) { when (state) { is CredentialSelectorUiState.Get.SingleProviderSinglePassword -> { is SingleEntry -> { navController.navigateToSinglePasswordScreen() } Loading
packages/CredentialManager/wear/src/com/android/credentialmanager/ui/mappers/CredentialSelectorUiStateGetMapper.kt +35 −8 Original line number Diff line number Diff line Loading @@ -18,18 +18,45 @@ package com.android.credentialmanager.ui.mappers import com.android.credentialmanager.model.Request import com.android.credentialmanager.CredentialSelectorUiState import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry.PerUserNameEntries import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.get.CredentialEntryInfo fun Request.Get.toGet(): CredentialSelectorUiState.Get { fun Request.Get.toGet(isPrimary: Boolean): CredentialSelectorUiState.Get { // TODO: b/301206470 returning a hard coded state for MVP if (true) return CredentialSelectorUiState.Get.SingleProviderSinglePassword return if (providerInfos.size == 1) { if (providerInfos.first().credentialEntryList.size == 1) { CredentialSelectorUiState.Get.SingleProviderSinglePassword if (true) return CredentialSelectorUiState.Get.SingleEntry( providerInfos .flatMap { it.credentialEntryList } .first { it.credentialType == CredentialType.PASSWORD } ) val accounts = providerInfos .flatMap { it.credentialEntryList } .groupBy { it.userName} .entries .toList() return if (isPrimary) { if (accounts.size == 1) { CredentialSelectorUiState.Get.SingleEntry( accounts[0].value.minWith(comparator) ) } else { TODO() // b/301206470 - Implement other get flows CredentialSelectorUiState.Get.SingleEntryPerAccount( accounts.map { it.value.minWith(comparator) }.sortedWith(comparator) ) } } else { TODO() // b/301206470 - Implement other get flows CredentialSelectorUiState.Get.MultipleEntry( accounts = accounts.map { PerUserNameEntries( it.key, it.value.sortedWith(comparator) ) }, actionEntryList = providerInfos.flatMap { it.actionEntryList }, ) } } val comparator = compareBy<CredentialEntryInfo> { entryInfo -> // Passkey type always go first entryInfo.credentialType.let{ if (it == CredentialType.PASSKEY) 0 else 1 } } .thenByDescending{ it.lastUsedTimeMillis }
packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreen.kt +3 −1 Original line number Diff line number Diff line Loading @@ -29,6 +29,7 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import androidx.hilt.navigation.compose.hiltViewModel import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry import com.android.credentialmanager.R import com.android.credentialmanager.TAG import com.android.credentialmanager.activity.StartBalIntentSenderForResultContract Loading @@ -44,12 +45,13 @@ import com.google.android.horologist.compose.tools.WearPreview @Composable fun SinglePasswordScreen( state: SingleEntry, columnState: ScalingLazyColumnState, onCloseApp: () -> Unit, modifier: Modifier = Modifier, viewModel: SinglePasswordScreenViewModel = hiltViewModel(), ) { viewModel.initialize() viewModel.initialize(state.entry) val uiState by viewModel.uiState.collectAsStateWithLifecycle() Loading
packages/CredentialManager/wear/src/com/android/credentialmanager/ui/screens/single/password/SinglePasswordScreenViewModel.kt +6 −28 Original line number Diff line number Diff line Loading @@ -19,12 +19,9 @@ package com.android.credentialmanager.ui.screens.single.password import android.content.Intent import android.credentials.ui.ProviderPendingIntentResponse import android.credentials.ui.UserSelectionDialogResult import android.util.Log import androidx.activity.result.IntentSenderRequest import androidx.annotation.MainThread import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.android.credentialmanager.TAG import com.android.credentialmanager.ktx.getIntentSenderRequest import com.android.credentialmanager.model.Request import com.android.credentialmanager.client.CredentialManagerClient Loading @@ -33,7 +30,6 @@ import com.android.credentialmanager.ui.model.PasswordUiModel import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch import javax.inject.Inject @HiltViewModel Loading @@ -51,33 +47,15 @@ class SinglePasswordScreenViewModel @Inject constructor( val uiState: StateFlow<SinglePasswordScreenUiState> = _uiState @MainThread fun initialize() { fun initialize(entryInfo: CredentialEntryInfo) { if (initializeCalled) return initializeCalled = true viewModelScope.launch { val request = credentialManagerClient.requests.value Log.d(TAG, "request: $request, client instance: $credentialManagerClient") if (request !is Request.Get) { _uiState.value = SinglePasswordScreenUiState.Error } else { requestGet = request if (requestGet.providerInfos.all { it.credentialEntryList.isEmpty() }) { Log.d(TAG, "Empty passwordEntries") _uiState.value = SinglePasswordScreenUiState.Error } else { entryInfo = requestGet.providerInfos.first().credentialEntryList.first() _uiState.value = SinglePasswordScreenUiState.Loaded( PasswordUiModel( email = entryInfo.userName, ) ) } } } } fun onCancelClick() { _uiState.value = SinglePasswordScreenUiState.Cancel Loading