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

Commit e1309aad authored by Helen Qin's avatar Helen Qin Committed by Android (Google) Code Review
Browse files

Merge "Get flow fundamental E2E."

parents ebdae6cc b4e2a704
Loading
Loading
Loading
Loading
+2 −1
Original line number Diff line number Diff line
@@ -53,7 +53,8 @@ public class Entry implements Parcelable {
    /** Below are only available for get flows. */
    public static final String HINT_NOTE = "HINT_NOTE";
    public static final String HINT_USER_NAME = "HINT_USER_NAME";
    public static final String HINT_CREDENTIAL_TYPE = "HINT_CREDENTIAL_TYPE";
    public static final String HINT_CREDENTIAL_TYPE_DISPLAY_NAME =
            "HINT_CREDENTIAL_TYPE_DISPLAY_NAME";
    public static final String HINT_PASSKEY_USER_DISPLAY_NAME = "HINT_PASSKEY_USER_DISPLAY_NAME";
    public static final String HINT_PASSWORD_VALUE = "HINT_PASSWORD_VALUE";

+22 −0
Original line number Diff line number Diff line
@@ -29,4 +29,26 @@
  <string name="createOptionInfo_icon_description">CreateOptionInfo credentialType icon</string>
  <!-- Spoken content description of an element which will close the sheet when clicked. -->
  <string name="close_sheet">"Close sheet"</string>
  <!-- Spoken content description of the back arrow button. -->
  <string name="accessibility_back_arrow_button">"Go back to the previous page"</string>

  <!-- Strings for the get flow. -->
  <!-- This appears as the title of the modal bottom sheet asking for user confirmation to use the single previously saved passkey to sign in to the app. [CHAR LIMIT=200] -->
  <string name="get_dialog_title_use_passkey_for">Use your saved passkey for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
  <!-- This appears as the title of the dialog asking for user confirmation to use the single previously saved credential to sign in to the app. [CHAR LIMIT=200] -->
  <string name="get_dialog_title_use_sign_in_for">Use your saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g>?</string>
  <!-- This appears as the title of the dialog asking for user to make a choice from various previously saved credentials to sign in to the app. [CHAR LIMIT=200] -->
  <string name="get_dialog_title_choose_sign_in_for">Choose a saved sign-in for <xliff:g id="app_name" example="YouTube">%1$s</xliff:g></string>
  <!-- Appears as an option row for viewing all the available sign-in options. [CHAR LIMIT=80] -->
  <string name="get_dialog_use_saved_passkey_for">Sign in another way</string>
  <!-- Button label to close the dialog when the user does not want to use any sign-in. [CHAR LIMIT=40] -->
  <string name="get_dialog_button_label_no_thanks">No thanks</string>
  <!-- Button label to continue with the selected sign-in. [CHAR LIMIT=40] -->
  <string name="get_dialog_button_label_continue">Continue</string>
  <!-- Separator for sign-in type and username in a sign-in entry. -->
  <string name="get_dialog_sign_in_type_username_separator" translatable="false">" - "</string>
  <!-- Modal bottom sheet title for displaying all the available sign-in options. [CHAR LIMIT=80] -->
  <string name="get_dialog_title_sign_in_options">Sign-in options</string>
  <!-- Column heading for displaying sign-ins for a specific username. [CHAR LIMIT=20] -->
  <string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string>
</resources>
 No newline at end of file
+81 −38
Original line number Diff line number Diff line
@@ -16,11 +16,14 @@

package com.android.credentialmanager

import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.app.slice.Slice
import android.app.slice.SliceSpec
import android.content.Context
import android.content.Intent
import android.credentials.CreateCredentialRequest
import android.credentials.GetCredentialOption
import android.credentials.GetCredentialRequest
import android.credentials.ui.Constants
import android.credentials.ui.Entry
import android.credentials.ui.CreateCredentialProviderData
@@ -40,7 +43,7 @@ import com.android.credentialmanager.createflow.ProviderInfo
import com.android.credentialmanager.createflow.RequestDisplayInfo
import com.android.credentialmanager.getflow.GetCredentialUiState
import com.android.credentialmanager.getflow.GetScreenState
import com.android.credentialmanager.jetpack.provider.CredentialEntryUi.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL

// Consider repo per screen, similar to view model?
class CredentialManagerRepo(
@@ -56,7 +59,7 @@ class CredentialManagerRepo(
    requestInfo = intent.extras?.getParcelable(
      RequestInfo.EXTRA_REQUEST_INFO,
      RequestInfo::class.java
    ) ?: testRequestInfo()
    ) ?: testCreateRequestInfo()

    providerList = when (requestInfo.type) {
      RequestInfo.TYPE_CREATE ->
@@ -104,16 +107,11 @@ class CredentialManagerRepo(
    // TODO: handle runtime cast error
    providerList as List<GetCredentialProviderData>, context)
    // TODO: covert from real requestInfo
    val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo(
      "Elisa Beckett",
      "beckett-bakert@gmail.com",
      TYPE_PUBLIC_KEY_CREDENTIAL,
      "tribank")
    val requestDisplayInfo = com.android.credentialmanager.getflow.RequestDisplayInfo("tribank")
    return GetCredentialUiState(
      providerList,
      GetScreenState.CREDENTIAL_SELECTION,
      GetScreenState.PRIMARY_SELECTION,
      requestDisplayInfo,
      providerList.first()
    )
  }

@@ -165,9 +163,9 @@ class CredentialManagerRepo(
        .Builder("com.google/com.google.CredentialManagerService")
        .setSaveEntries(
          listOf<Entry>(
            newEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
            newCreateEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
              20, 7, 27, 10000),
            newEntry("key1", "subkey-2", "elisa.work@google.com",
            newCreateEntry("key1", "subkey-2", "elisa.work@google.com",
              20, 7, 27, 11000),
          )
        )
@@ -177,9 +175,9 @@ class CredentialManagerRepo(
        .Builder("com.dashlane/com.dashlane.CredentialManagerService")
        .setSaveEntries(
          listOf<Entry>(
            newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
            newCreateEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
              20, 7, 27, 30000),
            newEntry("key1", "subkey-4", "elisa.work@dashlane.com",
            newCreateEntry("key1", "subkey-4", "elisa.work@dashlane.com",
              20, 7, 27, 31000),
          )
        )
@@ -192,37 +190,69 @@ class CredentialManagerRepo(
      GetCredentialProviderData.Builder("com.google/com.google.CredentialManagerService")
        .setCredentialEntries(
          listOf<Entry>(
            newEntry("key1", "subkey-1", "elisa.beckett@gmail.com",
              20, 7, 27, 10000),
            newEntry("key1", "subkey-2", "elisa.work@google.com",
              20, 7, 27, 11000),
          )
        ).setActionChips(
          listOf<Entry>(
            newEntry("key2", "subkey-1", "Go to Settings",
              20, 7, 27, 20000),
            newEntry("key2", "subkey-2", "Switch Account",
              20, 7, 27, 21000),
            newGetEntry(
              "key1", "subkey-1", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
              "elisa.bakery@gmail.com", "Elisa Beckett", 300L
            ),
            newGetEntry(
              "key1", "subkey-2", TYPE_PASSWORD_CREDENTIAL, "Password",
              "elisa.bakery@gmail.com", null, 300L
            ),
            newGetEntry(
              "key1", "subkey-3", TYPE_PASSWORD_CREDENTIAL, "Password",
              "elisa.family@outlook.com", null, 100L
            ),
          )
        ).build(),
      GetCredentialProviderData.Builder("com.dashlane/com.dashlane.CredentialManagerService")
        .setCredentialEntries(
          listOf<Entry>(
            newEntry("key1", "subkey-3", "elisa.beckett@dashlane.com",
              20, 7, 27, 30000),
            newEntry("key1", "subkey-4", "elisa.work@dashlane.com",
              20, 7, 27, 31000),
          )
        ).setActionChips(
          listOf<Entry>(
            newEntry("key2", "subkey-3", "Manage Accounts",
              20, 7, 27, 40000),
            newGetEntry(
              "key1", "subkey-1", TYPE_PASSWORD_CREDENTIAL, "Password",
              "elisa.family@outlook.com", null, 600L
            ),
            newGetEntry(
              "key1", "subkey-2", TYPE_PUBLIC_KEY_CREDENTIAL, "Passkey",
              "elisa.family@outlook.com", null, 100L
            ),
          )
        ).build(),
    )
  }

  private fun newEntry(
  private fun newGetEntry(
    key: String,
    subkey: String,
    credentialType: String,
    credentialTypeDisplayName: String,
    userName: String,
    userDisplayName: String?,
    lastUsedTimeMillis: Long?,
  ): Entry {
    val slice = Slice.Builder(
      Entry.CREDENTIAL_MANAGER_ENTRY_URI, SliceSpec(credentialType, 1)
    ).addText(
      credentialTypeDisplayName, null, listOf(Entry.HINT_CREDENTIAL_TYPE_DISPLAY_NAME)
    ).addText(
      userName, null, listOf(Entry.HINT_USER_NAME)
    ).addIcon(
      Icon.createWithResource(context, R.drawable.ic_passkey),
      null,
      listOf(Entry.HINT_PROFILE_ICON))
    if (userDisplayName != null) {
      slice.addText(userDisplayName, null, listOf(Entry.HINT_PASSKEY_USER_DISPLAY_NAME))
    }
    if (lastUsedTimeMillis != null) {
      slice.addLong(lastUsedTimeMillis, null, listOf(Entry.HINT_LAST_USED_TIME_MILLIS))
    }
    return Entry(
      key,
      subkey,
      slice.build()
    )
  }

  private fun newCreateEntry(
    key: String,
    subkey: String,
    providerDisplayName: String,
@@ -259,12 +289,11 @@ class CredentialManagerRepo(
    )
  }

  private fun testRequestInfo(): RequestInfo {
  private fun testCreateRequestInfo(): RequestInfo {
    val data = Bundle()
    return RequestInfo.newCreateRequestInfo(
      Binder(),
      CreateCredentialRequest(
        // TODO: use the jetpack type and utils once defined.
        TYPE_PUBLIC_KEY_CREDENTIAL,
        data
      ),
@@ -272,4 +301,18 @@ class CredentialManagerRepo(
      "tribank.us"
    )
  }

  private fun testGetRequestInfo(): RequestInfo {
    val data = Bundle()
    return RequestInfo.newGetRequestInfo(
      Binder(),
      GetCredentialRequest.Builder()
        .addGetCredentialOption(
          GetCredentialOption(TYPE_PUBLIC_KEY_CREDENTIAL, Bundle())
        )
        .build(),
      /*isFirstUsage=*/false,
      "tribank.us"
    )
  }
}
+42 −11
Original line number Diff line number Diff line
@@ -21,8 +21,11 @@ import android.credentials.ui.Entry
import android.credentials.ui.GetCredentialProviderData
import android.credentials.ui.CreateCredentialProviderData
import com.android.credentialmanager.createflow.CreateOptionInfo
import com.android.credentialmanager.getflow.CredentialOptionInfo
import com.android.credentialmanager.getflow.ActionEntryInfo
import com.android.credentialmanager.getflow.AuthenticationEntryInfo
import com.android.credentialmanager.getflow.CredentialEntryInfo
import com.android.credentialmanager.getflow.ProviderInfo
import com.android.credentialmanager.jetpack.provider.CredentialEntryUi
import com.android.credentialmanager.jetpack.provider.SaveEntryUi

/** Utility functions for converting CredentialManager data structures to or from UI formats. */
@@ -35,37 +38,65 @@ class GetFlowUtils {
    ): List<ProviderInfo> {
      return providerDataList.map {
        ProviderInfo(
          id = it.providerFlattenedComponentName,
          // TODO: replace to extract from the service data structure when available
          icon = context.getDrawable(R.drawable.ic_passkey)!!,
          name = it.providerFlattenedComponentName,
          // TODO: get the service display name and icon from the component name.
          displayName = it.providerFlattenedComponentName,
          credentialTypeIcon = context.getDrawable(R.drawable.ic_passkey)!!,
          credentialOptions = toCredentialOptionInfoList(it.credentialEntries, context),
          credentialEntryList = getCredentialOptionInfoList(
            it.providerFlattenedComponentName, it.credentialEntries, context),
          authenticationEntry = getAuthenticationEntry(
            it.providerFlattenedComponentName, it.authenticationEntry, context),
          actionEntryList = getActionEntryList(
            it.providerFlattenedComponentName, it.actionChips, context),
        )
      }
    }


    /* From service data structure to UI credential entry list representation. */
    private fun toCredentialOptionInfoList(
    private fun getCredentialOptionInfoList(
      providerId: String,
      credentialEntries: List<Entry>,
      context: Context,
    ): List<CredentialOptionInfo> {
    ): List<CredentialEntryInfo> {
      return credentialEntries.map {
        val credentialEntryUi = CredentialEntryUi.fromSlice(it.slice)

        // Consider directly move the UI object into the class.
        return@map CredentialOptionInfo(
          // TODO: remove fallbacks
          icon = credentialEntryUi.icon?.loadDrawable(context)
            ?: context.getDrawable(R.drawable.ic_passkey)!!,
        return@map CredentialEntryInfo(
          providerId = providerId,
          entryKey = it.key,
          entrySubkey = it.subkey,
          usageData = credentialEntryUi.usageData?.toString() ?: "Unknown usageData",
          credentialType = credentialEntryUi.credentialType.toString(),
          credentialTypeDisplayName = credentialEntryUi.credentialTypeDisplayName.toString(),
          userName = credentialEntryUi.userName.toString(),
          displayName = credentialEntryUi.userDisplayName?.toString(),
          // TODO: proper fallback
          icon = credentialEntryUi.entryIcon.loadDrawable(context)
            ?: context.getDrawable(R.drawable.ic_passkey)!!,
          lastUsedTimeMillis = credentialEntryUi.lastUsedTimeMillis,
        )
      }
    }

    private fun getAuthenticationEntry(
      providerId: String,
      authEntry: Entry?,
      context: Context,
    ): AuthenticationEntryInfo? {
      // TODO: implement
      return null
    }

    private fun getActionEntryList(
      providerId: String,
      actionEntries: List<Entry>,
      context: Context,
    ): List<ActionEntryInfo> {
      // TODO: implement
      return emptyList()
    }
  }
}

+28 −0
Original line number Diff line number Diff line
@@ -14,46 +14,15 @@
 * limitations under the License.
 */

package com.android.credentialmanager
package com.android.credentialmanager.common.ui

import android.app.slice.Slice
import android.credentials.ui.Entry
import android.graphics.drawable.Icon
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable

/**
 * UI representation for a credential entry used during the get credential flow.
 *
 * TODO: move to jetpack.
 */
class CredentialEntryUi(
  val userName: CharSequence,
  val displayName: CharSequence?,
  val icon: Icon?,
  val usageData: CharSequence?,
  // TODO: add last used.
) {
  companion object {
    fun fromSlice(slice: Slice): CredentialEntryUi {
      val items = slice.items

      var title: String? = null
      var subTitle: String? = null
      var icon: Icon? = null
      var usageData: String? = null

      items.forEach {
        if (it.hasHint(Entry.HINT_ICON)) {
          icon = it.icon
        } else if (it.hasHint(Entry.HINT_SUBTITLE) && it.subType == null) {
          subTitle = it.text.toString()
        } else if (it.hasHint(Entry.HINT_TITLE)) {
          title = it.text.toString()
        } else if (it.hasHint(Entry.HINT_SUBTITLE) && it.subType == Slice.SUBTYPE_MESSAGE) {
          usageData = it.text.toString()
        }
      }
      // TODO: fail NPE more elegantly.
      return CredentialEntryUi(title!!, subTitle, icon, usageData)
    }
@Composable
fun CancelButton(text: String, onClick: () -> Unit) {
    TextButton(onClick = onClick) {
        Text(text = text)
    }
}
 No newline at end of file
Loading