Merge "Support authentication entry with locked & unlocked state."

This commit is contained in:
Helen Qin
2023-02-09 18:19:24 +00:00
committed by Android (Google) Code Review
12 changed files with 244 additions and 62 deletions

View File

@@ -1033,6 +1033,22 @@ package android.content.rollback {
package android.credentials.ui {
public final class AuthenticationEntry implements android.os.Parcelable {
ctor public AuthenticationEntry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, int);
ctor public AuthenticationEntry(@NonNull String, @NonNull String, @NonNull android.app.slice.Slice, int, @NonNull android.content.Intent);
method public int describeContents();
method @Nullable public android.content.Intent getFrameworkExtrasIntent();
method @NonNull public String getKey();
method @NonNull public android.app.slice.Slice getSlice();
method @NonNull public int getStatus();
method @NonNull public String getSubkey();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.credentials.ui.AuthenticationEntry> CREATOR;
field public static final int STATUS_LOCKED = 0; // 0x0
field public static final int STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT = 1; // 0x1
field public static final int STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT = 2; // 0x2
}
public final class CreateCredentialProviderData extends android.credentials.ui.ProviderData implements android.os.Parcelable {
ctor public CreateCredentialProviderData(@NonNull String, @NonNull java.util.List<android.credentials.ui.Entry>, @Nullable android.credentials.ui.Entry);
method @Nullable public android.credentials.ui.Entry getRemoteEntry();
@@ -1064,15 +1080,12 @@ package android.credentials.ui {
method @NonNull public String getSubkey();
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.credentials.ui.Entry> CREATOR;
field @NonNull public static final String EXTRA_ENTRY_AUTHENTICATION_ACTION = "android.credentials.ui.extra.ENTRY_AUTHENTICATION_ACTION";
field @NonNull public static final String EXTRA_ENTRY_LIST_ACTION_CHIP = "android.credentials.ui.extra.ENTRY_LIST_ACTION_CHIP";
field @NonNull public static final String EXTRA_ENTRY_LIST_CREDENTIAL = "android.credentials.ui.extra.ENTRY_LIST_CREDENTIAL";
}
public final class GetCredentialProviderData extends android.credentials.ui.ProviderData implements android.os.Parcelable {
ctor public GetCredentialProviderData(@NonNull String, @NonNull java.util.List<android.credentials.ui.Entry>, @NonNull java.util.List<android.credentials.ui.Entry>, @NonNull java.util.List<android.credentials.ui.Entry>, @Nullable android.credentials.ui.Entry);
ctor public GetCredentialProviderData(@NonNull String, @NonNull java.util.List<android.credentials.ui.Entry>, @NonNull java.util.List<android.credentials.ui.Entry>, @NonNull java.util.List<android.credentials.ui.AuthenticationEntry>, @Nullable android.credentials.ui.Entry);
method @NonNull public java.util.List<android.credentials.ui.Entry> getActionChips();
method @NonNull public java.util.List<android.credentials.ui.Entry> getAuthenticationEntries();
method @NonNull public java.util.List<android.credentials.ui.AuthenticationEntry> getAuthenticationEntries();
method @NonNull public java.util.List<android.credentials.ui.Entry> getCredentialEntries();
method @Nullable public android.credentials.ui.Entry getRemoteEntry();
field @NonNull public static final android.os.Parcelable.Creator<android.credentials.ui.GetCredentialProviderData> CREATOR;
@@ -1082,7 +1095,7 @@ package android.credentials.ui {
ctor public GetCredentialProviderData.Builder(@NonNull String);
method @NonNull public android.credentials.ui.GetCredentialProviderData build();
method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setActionChips(@NonNull java.util.List<android.credentials.ui.Entry>);
method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setAuthenticationEntries(@NonNull java.util.List<android.credentials.ui.Entry>);
method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setAuthenticationEntries(@NonNull java.util.List<android.credentials.ui.AuthenticationEntry>);
method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setCredentialEntries(@NonNull java.util.List<android.credentials.ui.Entry>);
method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setRemoteEntry(@Nullable android.credentials.ui.Entry);
}

View File

@@ -0,0 +1,162 @@
/*
* Copyright 2022 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 android.credentials.ui;
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
import android.annotation.TestApi;
import android.app.slice.Slice;
import android.content.Intent;
import android.os.Parcel;
import android.os.Parcelable;
import com.android.internal.util.AnnotationValidations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
/**
* An authentication entry.
*
* @hide
*/
@TestApi
public final class AuthenticationEntry implements Parcelable {
@NonNull private final String mKey;
@NonNull private final String mSubkey;
@NonNull private final @Status int mStatus;
@Nullable private Intent mFrameworkExtrasIntent;
@NonNull private final Slice mSlice;
/** @hide **/
@IntDef(prefix = {"STATUS_"}, value = {
STATUS_LOCKED,
STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT,
STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT,
})
@Retention(RetentionPolicy.SOURCE)
public @interface Status {}
/** This entry is still locked, as initially supplied by the provider. */
public static final int STATUS_LOCKED = 0;
/** This entry was unlocked but didn't contain any credential. Meanwhile, "less recent" means
* there is another such entry that was unlocked more recently. */
public static final int STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT = 1;
/** This is the most recent entry that was unlocked but didn't contain any credential.
* There should be at most one authentication entry with this status. */
public static final int STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT = 2;
private AuthenticationEntry(@NonNull Parcel in) {
mKey = in.readString8();
mSubkey = in.readString8();
mStatus = in.readInt();
mSlice = in.readTypedObject(Slice.CREATOR);
mFrameworkExtrasIntent = in.readTypedObject(Intent.CREATOR);
AnnotationValidations.validate(NonNull.class, null, mKey);
AnnotationValidations.validate(NonNull.class, null, mSubkey);
AnnotationValidations.validate(NonNull.class, null, mSlice);
}
/** Constructor to be used for an entry that does not require further activities
* to be invoked when selected.
*/
public AuthenticationEntry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
@Status int status) {
mKey = key;
mSubkey = subkey;
mSlice = slice;
mStatus = status;
}
/** Constructor to be used for an entry that requires a pending intent to be invoked
* when clicked.
*/
public AuthenticationEntry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
@Status int status, @NonNull Intent intent) {
this(key, subkey, slice, status);
mFrameworkExtrasIntent = intent;
}
/**
* Returns the identifier of this entry that's unique within the context of the CredentialManager
* request.
*/
@NonNull
public String getKey() {
return mKey;
}
/**
* Returns the sub-identifier of this entry that's unique within the context of the {@code key}.
*/
@NonNull
public String getSubkey() {
return mSubkey;
}
/**
* Returns the Slice to be rendered.
*/
@NonNull
public Slice getSlice() {
return mSlice;
}
/**
* Returns the entry status.
*/
@NonNull
@Status
public int getStatus() {
return mStatus;
}
@Nullable
@SuppressLint("IntentBuilderName") // Not building a new intent.
public Intent getFrameworkExtrasIntent() {
return mFrameworkExtrasIntent;
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString8(mKey);
dest.writeString8(mSubkey);
dest.writeInt(mStatus);
dest.writeTypedObject(mSlice, flags);
dest.writeTypedObject(mFrameworkExtrasIntent, flags);
}
@Override
public int describeContents() {
return 0;
}
public static final @NonNull Creator<AuthenticationEntry> CREATOR = new Creator<>() {
@Override
public AuthenticationEntry createFromParcel(@NonNull Parcel in) {
return new AuthenticationEntry(in);
}
@Override
public AuthenticationEntry[] newArray(int size) {
return new AuthenticationEntry[size];
}
};
}

View File

@@ -29,30 +29,12 @@ import android.os.Parcelable;
import com.android.internal.util.AnnotationValidations;
/**
* A credential, save, or action entry to be rendered.
* A credential, create, or action entry to be rendered.
*
* @hide
*/
@TestApi
public final class Entry implements Parcelable {
/**
* The intent extra key for the action chip {@code Entry} list when launching the UX activities.
*/
@NonNull public static final String EXTRA_ENTRY_LIST_ACTION_CHIP =
"android.credentials.ui.extra.ENTRY_LIST_ACTION_CHIP";
/**
* The intent extra key for the credential / save {@code Entry} list when launching the UX
* activities.
*/
@NonNull public static final String EXTRA_ENTRY_LIST_CREDENTIAL =
"android.credentials.ui.extra.ENTRY_LIST_CREDENTIAL";
/**
* The intent extra key for the authentication action {@code Entry} when launching the UX
* activities.
*/
@NonNull public static final String EXTRA_ENTRY_AUTHENTICATION_ACTION =
"android.credentials.ui.extra.ENTRY_AUTHENTICATION_ACTION";
@NonNull private final String mKey;
@NonNull private final String mSubkey;
@Nullable private PendingIntent mPendingIntent;

View File

@@ -39,13 +39,14 @@ public final class GetCredentialProviderData extends ProviderData implements Par
@NonNull
private final List<Entry> mActionChips;
@NonNull
private final List<Entry> mAuthenticationEntries;
private final List<AuthenticationEntry> mAuthenticationEntries;
@Nullable
private final Entry mRemoteEntry;
public GetCredentialProviderData(
@NonNull String providerFlattenedComponentName, @NonNull List<Entry> credentialEntries,
@NonNull List<Entry> actionChips, @NonNull List<Entry> authenticationEntries,
@NonNull List<Entry> actionChips,
@NonNull List<AuthenticationEntry> authenticationEntries,
@Nullable Entry remoteEntry) {
super(providerFlattenedComponentName);
mCredentialEntries = credentialEntries;
@@ -65,7 +66,7 @@ public final class GetCredentialProviderData extends ProviderData implements Par
}
@NonNull
public List<Entry> getAuthenticationEntries() {
public List<AuthenticationEntry> getAuthenticationEntries() {
return mAuthenticationEntries;
}
@@ -87,8 +88,8 @@ public final class GetCredentialProviderData extends ProviderData implements Par
mActionChips = actionChips;
AnnotationValidations.validate(NonNull.class, null, mActionChips);
List<Entry> authenticationEntries = new ArrayList<>();
in.readTypedList(authenticationEntries, Entry.CREATOR);
List<AuthenticationEntry> authenticationEntries = new ArrayList<>();
in.readTypedList(authenticationEntries, AuthenticationEntry.CREATOR);
mAuthenticationEntries = authenticationEntries;
AnnotationValidations.validate(NonNull.class, null, mAuthenticationEntries);
@@ -133,7 +134,7 @@ public final class GetCredentialProviderData extends ProviderData implements Par
@NonNull private String mProviderFlattenedComponentName;
@NonNull private List<Entry> mCredentialEntries = new ArrayList<>();
@NonNull private List<Entry> mActionChips = new ArrayList<>();
@NonNull private List<Entry> mAuthenticationEntries = new ArrayList<>();
@NonNull private List<AuthenticationEntry> mAuthenticationEntries = new ArrayList<>();
@Nullable private Entry mRemoteEntry = null;
/** Constructor with required properties. */
@@ -157,7 +158,8 @@ public final class GetCredentialProviderData extends ProviderData implements Par
/** Sets the authentication entry to be displayed to the user. */
@NonNull
public Builder setAuthenticationEntries(@NonNull List<Entry> authenticationEntry) {
public Builder setAuthenticationEntries(
@NonNull List<AuthenticationEntry> authenticationEntry) {
mAuthenticationEntries = authenticationEntry;
return this;
}

View File

@@ -116,8 +116,10 @@
<string name="get_dialog_heading_for_username">For <xliff:g id="username" example="becket@gmail.com">%1$s</xliff:g></string>
<!-- Column heading for displaying locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-ins. [CHAR LIMIT=80] -->
<string name="get_dialog_heading_locked_password_managers">Locked password managers</string>
<!-- Explanatory sub/body text for an option entry to use a locked (that is, the user needs to first authenticate via pin, fingerprint, faceId, etc.) sign-in. [CHAR LIMIT=120] -->
<string name="locked_credential_entry_label_subtext">Tap to unlock</string>
<!-- Explanatory label for a button that takes the user to unlock a credential provider by authenticating via pin, fingerprint, faceId, etc. [CHAR LIMIT=120] -->
<string name="locked_credential_entry_label_subtext_tap_to_unlock">Tap to unlock</string>
<!-- Explanatory label for a disabled button explaining that this option isn't viable because it does not contain any available credential (e.g. password, passkey, etc.) for the user. [CHAR LIMIT=120] -->
<string name="locked_credential_entry_label_subtext_no_sign_in">No sign-in info</string>
<!-- Column heading for displaying action chips for managing sign-ins from each credential provider. [CHAR LIMIT=80] -->
<string name="get_dialog_heading_manage_sign_ins">Manage sign-ins</string>
<!-- Column heading for displaying option to use sign-ins saved on a different device. [CHAR LIMIT=80] -->

View File

@@ -24,6 +24,7 @@ import android.credentials.CreateCredentialRequest
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.credentials.CredentialOption
import android.credentials.GetCredentialRequest
import android.credentials.ui.AuthenticationEntry
import android.credentials.ui.Constants
import android.credentials.ui.Entry
import android.credentials.ui.CreateCredentialProviderData
@@ -291,9 +292,13 @@ class CredentialManagerRepo(
).setAuthenticationEntries(
listOf(
GetTestUtils.newAuthenticationEntry(
context, "key2", "subkey-1", "locked-user1@gmail.com"),
context, "key2", "subkey-1", "locked-user1@gmail.com",
AuthenticationEntry.STATUS_LOCKED
),
GetTestUtils.newAuthenticationEntry(
context, "key2", "subkey-2", "locked-user2@gmail.com"),
context, "key2", "subkey-2", "locked-user2@gmail.com",
AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT
),
)
).setActionChips(
listOf(
@@ -323,7 +328,9 @@ class CredentialManagerRepo(
)
).setAuthenticationEntries(
listOf(GetTestUtils.newAuthenticationEntry(
context, "key2", "subkey-1", "foo@email.com"))
context, "key2", "subkey-1", "foo@email.com",
AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_LESS_RECENT
))
).setActionChips(
listOf(
GetTestUtils.newActionEntry(

View File

@@ -22,6 +22,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.pm.PackageManager
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.credentials.ui.AuthenticationEntry
import android.credentials.ui.CreateCredentialProviderData
import android.credentials.ui.DisabledProviderData
import android.credentials.ui.Entry
@@ -265,7 +266,7 @@ class GetFlowUtils {
providerId: String,
providerDisplayName: String,
providerIcon: Drawable,
authEntryList: List<Entry>,
authEntryList: List<AuthenticationEntry>,
): List<AuthenticationEntryInfo> {
val result: MutableList<AuthenticationEntryInfo> = mutableListOf()
authEntryList.forEach { entry ->
@@ -287,6 +288,9 @@ class GetFlowUtils {
fillInIntent = entry.frameworkExtrasIntent,
title = title,
icon = providerIcon,
isUnlockedAndEmpty = entry.status != AuthenticationEntry.STATUS_LOCKED,
isLastUnlocked =
entry.status == AuthenticationEntry.STATUS_UNLOCKED_BUT_EMPTY_MOST_RECENT
))
}
return result

View File

@@ -22,6 +22,7 @@ import android.app.slice.SliceSpec
import android.content.Context
import android.content.Intent
import android.credentials.Credential.TYPE_PASSWORD_CREDENTIAL
import android.credentials.ui.AuthenticationEntry
import android.credentials.ui.Entry
import android.net.Uri
import android.provider.Settings
@@ -39,7 +40,8 @@ class GetTestUtils {
key: String,
subkey: String,
title: String,
): Entry {
status: Int
): AuthenticationEntry {
val slice = Slice.Builder(
Uri.EMPTY, SliceSpec("AuthenticationAction", 0)
)
@@ -59,10 +61,11 @@ class GetTestUtils {
null,
listOf("androidx.credentials.provider.authenticationAction.SLICE_HINT_TITLE")
)
return Entry(
return AuthenticationEntry(
key,
subkey,
slice.build()
slice.build(),
status
)
}

View File

@@ -840,7 +840,6 @@ fun PrimaryCreateOptionRow(
},
label = {
Column() {
// TODO: Add the function to hide/view password when the type is create password
when (requestDisplayInfo.type) {
CredentialType.PASSKEY -> {
TextOnSurfaceVariant(

View File

@@ -485,7 +485,6 @@ fun PerUserNameCredentials(
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CredentialEntryRow(
credentialEntryInfo: CredentialEntryInfo,
@@ -498,15 +497,13 @@ fun CredentialEntryRow(
Image(
modifier = Modifier.padding(start = 10.dp).size(32.dp),
bitmap = credentialEntryInfo.icon.toBitmap().asImageBitmap(),
// TODO: add description.
contentDescription = "",
contentDescription = null,
)
} else {
Icon(
modifier = Modifier.padding(start = 10.dp).size(32.dp),
painter = painterResource(R.drawable.ic_other_sign_in),
// TODO: add description.
contentDescription = "",
contentDescription = null,
tint = LocalAndroidColorScheme.current.colorAccentPrimaryVariant
)
}
@@ -553,8 +550,7 @@ fun AuthenticationEntryRow(
Image(
modifier = Modifier.padding(start = 10.dp).size(32.dp),
bitmap = authenticationEntryInfo.icon.toBitmap().asImageBitmap(),
// TODO: add description.
contentDescription = ""
contentDescription = null
)
},
label = {
@@ -563,23 +559,28 @@ fun AuthenticationEntryRow(
modifier = Modifier.fillMaxWidth().padding(horizontal = 5.dp),
) {
Column() {
// TODO: fix the text values.
TextOnSurfaceVariant(
text = authenticationEntryInfo.title,
style = MaterialTheme.typography.titleLarge,
modifier = Modifier.padding(top = 16.dp)
)
TextSecondary(
text = stringResource(R.string.locked_credential_entry_label_subtext),
text = stringResource(
if (authenticationEntryInfo.isUnlockedAndEmpty)
R.string.locked_credential_entry_label_subtext_no_sign_in
else R.string.locked_credential_entry_label_subtext_tap_to_unlock
),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
}
Icon(
Icons.Outlined.Lock,
null,
Modifier.align(alignment = Alignment.CenterVertically).padding(end = 10.dp),
)
if (!authenticationEntryInfo.isUnlockedAndEmpty) {
Icon(
Icons.Outlined.Lock,
null,
Modifier.align(alignment = Alignment.CenterVertically).padding(end = 10.dp),
)
}
}
}
)
@@ -596,8 +597,7 @@ fun ActionEntryRow(
Image(
modifier = Modifier.padding(start = 10.dp).size(24.dp),
bitmap = actionEntryInfo.icon.toBitmap().asImageBitmap(),
// TODO: add description.
contentDescription = ""
contentDescription = null,
)
},
label = {

View File

@@ -91,6 +91,11 @@ class AuthenticationEntryInfo(
fillInIntent: Intent?,
val title: String,
val icon: Drawable,
// The entry had been unlocked and turned out to be empty. Used to determine whether to
// show "Tap to unlock" or "No sign-in info" for this entry.
val isUnlockedAndEmpty: Boolean,
// True if the entry was the last one unlocked. Used to show the no sign-in info snackbar.
val isLastUnlocked: Boolean,
) : BaseEntry(
providerId,
entryKey, entrySubkey,

View File

@@ -24,6 +24,7 @@ import android.content.Intent;
import android.credentials.CredentialOption;
import android.credentials.GetCredentialException;
import android.credentials.GetCredentialResponse;
import android.credentials.ui.AuthenticationEntry;
import android.credentials.ui.Entry;
import android.credentials.ui.GetCredentialProviderData;
import android.credentials.ui.ProviderPendingIntentResponse;
@@ -278,16 +279,18 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
return remoteEntry;
}
private List<Entry> prepareUiAuthenticationEntries(
private List<AuthenticationEntry> prepareUiAuthenticationEntries(
@NonNull List<Action> authenticationEntries) {
List<Entry> authenticationUiEntries = new ArrayList<>();
List<AuthenticationEntry> authenticationUiEntries = new ArrayList<>();
// TODO: properly construct entries when they should have the unlocked status.
for (Action authenticationAction : authenticationEntries) {
String entryId = generateUniqueId();
mUiAuthenticationEntries.put(entryId, authenticationAction);
authenticationUiEntries.add(new Entry(
authenticationUiEntries.add(new AuthenticationEntry(
AUTHENTICATION_ACTION_ENTRY_KEY, entryId,
authenticationAction.getSlice(),
AuthenticationEntry.STATUS_LOCKED,
setUpFillInIntentForAuthentication()));
}
return authenticationUiEntries;
@@ -346,7 +349,7 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
}
private GetCredentialProviderData prepareUiProviderData(List<Entry> actionEntries,
List<Entry> credentialEntries, List<Entry> authenticationActionEntries,
List<Entry> credentialEntries, List<AuthenticationEntry> authenticationActionEntries,
Entry remoteEntry) {
return new GetCredentialProviderData.Builder(
mComponentName.flattenToString()).setActionChips(actionEntries)