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

Commit 647ff9ab authored by harinirajan's avatar harinirajan
Browse files

Multi credential folded screen changes

Test: Manual. See go/credential-selector-ui
Bug: 310986916
Change-Id: Ic7bf1003b154eae2fa102e94f9a81d63a08829a8
parent fe58cf93
Loading
Loading
Loading
Loading
+2 −0
Original line number Original line Diff line number Diff line
@@ -33,4 +33,6 @@
  <string name="dialog_continue_button">Continue</string>
  <string name="dialog_continue_button">Continue</string>
  <!-- Content description for the sign in options button of a screen. [CHAR LIMIT=NONE] -->
  <!-- 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>
  <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>
</resources>
 No newline at end of file
+29 −0
Original line number Original line 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.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()
}
+140 −0
Original line number Original line Diff line number Diff line
/*
 * 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) }
    }
}
+75 −0
Original line number Original line Diff line number Diff line
/*
 * 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)
    }
}
+1 −1
Original line number Original line Diff line number Diff line
@@ -42,7 +42,7 @@ import com.android.credentialmanager.ui.components.DismissChip
import com.android.credentialmanager.ui.components.SignInHeader
import com.android.credentialmanager.ui.components.SignInHeader
import com.android.credentialmanager.ui.components.SignInOptionsChip
import com.android.credentialmanager.ui.components.SignInOptionsChip
import com.android.credentialmanager.ui.screens.single.SingleAccountScreen
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.annotations.ExperimentalHorologistApi
import com.google.android.horologist.compose.layout.ScalingLazyColumnState
import com.google.android.horologist.compose.layout.ScalingLazyColumnState


Loading