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

Commit ddedb160 authored by Harini Rajan's avatar Harini Rajan Committed by Android (Google) Code Review
Browse files

Merge changes I78a437fa,Ia3b148f4,I16205c27 into main

* changes:
  Use flowEngine.getEntrySelector across credential screens
  CredentialSelectorUiStateGetMapper Tests
  use flowengine across credential selector screens
parents 678cba88 3ec4aeb7
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -21,9 +21,6 @@
  <!-- Title of a screen prompting if the user would like to use their saved passkey.
  [CHAR LIMIT=80] -->
  <string name="use_passkey_title">Use passkey?</string>
  <!-- Title of a screen prompting if the user would like to use their saved passkey.
[CHAR LIMIT=80] -->
  <string name="use_sign_in_with_provider_title">Use your sign in for %1$s</string>
  <!-- Title of a screen prompting if the user would like to sign in with provider
  [CHAR LIMIT=80] -->
  <string name="use_password_title">Use password?</string>
@@ -35,6 +32,8 @@
  <string name="dialog_sign_in_options_button">Sign-in Options</string>
  <!-- Title for multiple credentials folded screen. [CHAR LIMIT=NONE] -->
  <string name="sign_in_options_title">Sign-in Options</string>
  <!-- Provider settings list title. [CHAR LIMIT=NONE] -->
  <string name="provider_list_title">Manage sign-ins</string>
  <!-- Title for multiple credentials screen. [CHAR LIMIT=NONE] -->
  <string name="choose_sign_in_title">Choose a sign in</string>
  <!-- Title for multiple credentials screen with only passkeys. [CHAR LIMIT=NONE] -->
+214 −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 java.time.Instant
import android.graphics.drawable.Drawable
import com.android.credentialmanager.model.get.CredentialEntryInfo
import com.android.credentialmanager.model.get.ActionEntryInfo
import com.android.credentialmanager.model.get.AuthenticationEntryInfo
import com.android.credentialmanager.model.Request
import androidx.test.filters.SmallTest
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.mockito.kotlin.mock
import org.junit.runner.RunWith
import com.android.credentialmanager.model.CredentialType
import com.google.common.truth.Truth.assertThat
import com.android.credentialmanager.ui.mappers.toGet
import com.android.credentialmanager.model.get.ProviderInfo
import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry.PerUserNameEntries

/** Unit tests for [CredentialSelectorUiStateGetMapper]. */
@SmallTest
@RunWith(AndroidJUnit4::class)
class CredentialSelectorUiStateGetMapperTest {

    private val mDrawable = mock<Drawable>()

    private val actionEntryInfo =
        ActionEntryInfo(
            providerId = "",
            entryKey = "",
            entrySubkey = "",
            pendingIntent = null,
            fillInIntent = null,
            title = "title",
            icon = mDrawable,
            subTitle = "subtitle",
        )

    private val authenticationEntryInfo =
        AuthenticationEntryInfo(
            providerId = "",
            entryKey = "",
            entrySubkey = "",
            pendingIntent = null,
            fillInIntent = null,
            title = "title",
            providerDisplayName = "",
            icon = mDrawable,
            isUnlockedAndEmpty = true,
            isLastUnlocked = true
        )

    val passkeyCredentialEntryInfo =
        createCredentialEntryInfo(credentialType = CredentialType.PASSKEY, userName = "userName")

    val unknownCredentialEntryInfo =
        createCredentialEntryInfo(credentialType = CredentialType.UNKNOWN, userName = "userName2")

    val passwordCredentialEntryInfo =
        createCredentialEntryInfo(credentialType = CredentialType.PASSWORD, userName = "userName")

    val recentlyUsedPasskeyCredential =
        createCredentialEntryInfo(credentialType =
    CredentialType.PASSKEY, lastUsedTimeMillis = 2L, userName = "userName")

    val recentlyUsedPasswordCredential =
        createCredentialEntryInfo(credentialType =
    CredentialType.PASSWORD, lastUsedTimeMillis = 2L, userName = "userName")

    val credentialList1 = listOf(
        passkeyCredentialEntryInfo,
        passwordCredentialEntryInfo
    )

    val credentialList2 = listOf(
        passkeyCredentialEntryInfo,
        passwordCredentialEntryInfo,
        recentlyUsedPasskeyCredential,
        unknownCredentialEntryInfo,
        recentlyUsedPasswordCredential
    )

    @Test
    fun `On primary screen, just one account returns SingleEntry`() {
        val getCredentialUiState = Request.Get(
            token = null,
            resultReceiver = null,
            finalResponseReceiver = null,
            providerInfos = listOf(createProviderInfo(credentialList1))).toGet(isPrimary = true)

        assertThat(getCredentialUiState).isEqualTo(
            CredentialSelectorUiState.Get.SingleEntry(passkeyCredentialEntryInfo)
        ) // prefer passkey over password for selected credential
    }

    @Test
    fun `On primary screen, multiple accounts returns SingleEntryPerAccount`() {
        val getCredentialUiState = Request.Get(
            token = null,
            resultReceiver = null,
            finalResponseReceiver = null,
            providerInfos = listOf(createProviderInfo(listOf(passkeyCredentialEntryInfo,
                unknownCredentialEntryInfo)))).toGet(isPrimary = true)

        assertThat(getCredentialUiState).isEqualTo(
            CredentialSelectorUiState.Get.SingleEntryPerAccount(
                sortedEntries = listOf(
                    passkeyCredentialEntryInfo, // userName
                    unknownCredentialEntryInfo // userName2
                ),
                authenticationEntryList = listOf(authenticationEntryInfo)
            )) // prefer passkey from account 1, then unknown from account 2
    }

    @Test
    fun `On secondary screen, a MultipleEntry is returned`() {
        val getCredentialUiState = Request.Get(
            token = null,
            resultReceiver = null,
            finalResponseReceiver = null,
            providerInfos = listOf(createProviderInfo(credentialList1))).toGet(isPrimary = false)

        assertThat(getCredentialUiState).isEqualTo(
            CredentialSelectorUiState.Get.MultipleEntry(
                listOf(PerUserNameEntries("userName", listOf(
                    passkeyCredentialEntryInfo,
                    passwordCredentialEntryInfo))
                ),
                listOf(actionEntryInfo),
                listOf(authenticationEntryInfo)
            ))
    }

    @Test
    fun `Returned multiple entry is sorted by credentialType and lastUsedTimeMillis`() {
        val getCredentialUiState = Request.Get(
            token = null,
            resultReceiver = null,
            finalResponseReceiver = null,
            providerInfos = listOf(createProviderInfo(credentialList1),
                createProviderInfo(credentialList2))).toGet(isPrimary = false)

        assertThat(getCredentialUiState).isEqualTo(
            CredentialSelectorUiState.Get.MultipleEntry(
                listOf(
                    PerUserNameEntries("userName",
                        listOf(
                            recentlyUsedPasskeyCredential, // from provider 2
                            passkeyCredentialEntryInfo, // from provider 1 or 2
                            passkeyCredentialEntryInfo, // from provider 1 or 2
                            recentlyUsedPasswordCredential, // from provider 2
                            passwordCredentialEntryInfo, // from provider 1 or 2
                            passwordCredentialEntryInfo, // from provider 1 or 2
                        )),
                    PerUserNameEntries("userName2", listOf(unknownCredentialEntryInfo)),
                ),
                listOf(actionEntryInfo, actionEntryInfo),
                listOf(authenticationEntryInfo, authenticationEntryInfo)
            )
        )
    }

    fun createCredentialEntryInfo(
        userName: String,
        credentialType: CredentialType = CredentialType.PASSKEY,
        lastUsedTimeMillis: Long = 0L
    ): CredentialEntryInfo =
        CredentialEntryInfo(
            providerId = "",
            entryKey = "",
            entrySubkey = "",
            pendingIntent = null,
            fillInIntent = null,
            credentialType = credentialType,
            rawCredentialType = "",
            credentialTypeDisplayName = "",
            providerDisplayName = "",
            userName = userName,
            displayName = "",
            icon = mDrawable,
            shouldTintIcon = false,
            lastUsedTimeMillis = Instant.ofEpochMilli(lastUsedTimeMillis),
            isAutoSelectable = true,
            entryGroupId = "",
            isDefaultIconPreferredAsSingleProvider = false,
            affiliatedDomain = "",
        )

    fun createProviderInfo(credentials: List<CredentialEntryInfo> = listOf()): ProviderInfo =
        ProviderInfo(
            id = "providerInfo",
            icon = mDrawable,
            displayName = "displayName",
            credentialEntryList = credentials,
            authenticationEntryList = listOf(authenticationEntryInfo),
            remoteEntry = null,
            actionEntryList = listOf(actionEntryInfo)
        )
}
+4 −1
Original line number Diff line number Diff line
@@ -141,7 +141,10 @@ sealed class CredentialSelectorUiState {
    data object Idle : CredentialSelectorUiState()
    sealed class Get : CredentialSelectorUiState() {
        data class SingleEntry(val entry: CredentialEntryInfo) : Get()
        data class SingleEntryPerAccount(val sortedEntries: List<CredentialEntryInfo>) : Get()
        data class SingleEntryPerAccount(
            val sortedEntries: List<CredentialEntryInfo>,
            val authenticationEntryList: List<AuthenticationEntryInfo>,
            ) : Get()
        data class MultipleEntry(
            val accounts: List<PerUserNameEntries>,
            val actionEntryList: List<ActionEntryInfo>,
+51 −38
Original line number Diff line number Diff line
@@ -29,6 +29,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.SingleEntryPerAccount
import com.android.credentialmanager.CredentialSelectorUiState.Get.SingleEntry
import com.android.credentialmanager.CredentialSelectorUiState.Get.MultipleEntry
import com.android.credentialmanager.CredentialSelectorViewModel
@@ -45,6 +46,8 @@ 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
import com.android.credentialmanager.ui.screens.multiple.MultiCredentialsFlattenScreen


@OptIn(ExperimentalHorologistApi::class)
@Composable
@@ -78,23 +81,33 @@ fun WearApp(

        scrollable(Screen.SinglePasskeyScreen.route) {
            SinglePasskeyScreen(
                credentialSelectorUiState = viewModel.uiState.value as SingleEntry,
                entry = (remember { uiState } as SingleEntry).entry,
                columnState = it.columnState,
                flowEngine = flowEngine,
            )
        }

        scrollable(Screen.SignInWithProviderScreen.route) {
            SignInWithProviderScreen(
                credentialSelectorUiState = viewModel.uiState.value as SingleEntry,
                entry = (remember { uiState } as SingleEntry).entry,
                columnState = it.columnState,
                flowEngine = flowEngine,
            )
        }

        scrollable(Screen.MultipleCredentialsScreenFold.route) {
            MultiCredentialsFoldScreen(
                credentialSelectorUiState = viewModel.uiState.value as MultipleEntry,
                screenIcon = null,
                credentialSelectorUiState = (remember { uiState } as SingleEntryPerAccount),
                columnState = it.columnState,
                flowEngine = flowEngine,
            )
        }

        scrollable(Screen.MultipleCredentialsScreenFlatten.route) {
            MultiCredentialsFlattenScreen(
                credentialSelectorUiState = (remember { uiState } as MultipleEntry),
                columnState = it.columnState,
                flowEngine = flowEngine,
            )
        }
    }
@@ -108,6 +121,7 @@ fun WearApp(
                    navController.navigateToLoading()
                }
            }

            is CredentialSelectorUiState.Get -> {
                handleGetNavigation(
                    navController = navController,
@@ -157,13 +171,12 @@ private fun handleGetNavigation(
            }
        }

        is MultipleEntry -> {
            is SingleEntryPerAccount -> {
                navController.navigateToMultipleCredentialsFoldScreen()
            }

        else -> {
            // TODO: b/301206470 - Implement other get flows
            onCloseApp()
            is MultipleEntry -> {
                navController.navigateToMultipleCredentialsFlattenScreen()
            }
        }
    }
+5 −2
Original line number Diff line number Diff line
@@ -32,11 +32,14 @@ fun Request.Get.toGet(isPrimary: Boolean): CredentialSelectorUiState.Get {
    return if (isPrimary) {
        if (accounts.size == 1) {
            CredentialSelectorUiState.Get.SingleEntry(
                accounts[0].value.minWith(comparator)
                entry = accounts[0].value.minWith(comparator)
            )
        } else {
            CredentialSelectorUiState.Get.SingleEntryPerAccount(
                accounts.map { it.value.minWith(comparator) }.sortedWith(comparator)
                sortedEntries = accounts.map {
                    it.value.minWith(comparator)
                }.sortedWith(comparator),
                authenticationEntryList = providerInfos.flatMap { it.authenticationEntryList }
            )
        }
    } else {
Loading