diff --git a/core/api/test-current.txt b/core/api/test-current.txt index 51e90d28abdc9..776e3dc5c3569 100644 --- a/core/api/test-current.txt +++ b/core/api/test-current.txt @@ -993,6 +993,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 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, @Nullable android.credentials.ui.Entry); method @Nullable public android.credentials.ui.Entry getRemoteEntry(); @@ -1024,15 +1040,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 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, @NonNull java.util.List, @NonNull java.util.List, @Nullable android.credentials.ui.Entry); + ctor public GetCredentialProviderData(@NonNull String, @NonNull java.util.List, @NonNull java.util.List, @NonNull java.util.List, @Nullable android.credentials.ui.Entry); method @NonNull public java.util.List getActionChips(); - method @NonNull public java.util.List getAuthenticationEntries(); + method @NonNull public java.util.List getAuthenticationEntries(); method @NonNull public java.util.List getCredentialEntries(); method @Nullable public android.credentials.ui.Entry getRemoteEntry(); field @NonNull public static final android.os.Parcelable.Creator CREATOR; @@ -1042,7 +1055,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); - method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setAuthenticationEntries(@NonNull java.util.List); + method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setAuthenticationEntries(@NonNull java.util.List); method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setCredentialEntries(@NonNull java.util.List); method @NonNull public android.credentials.ui.GetCredentialProviderData.Builder setRemoteEntry(@Nullable android.credentials.ui.Entry); } diff --git a/core/java/android/credentials/ui/AuthenticationEntry.java b/core/java/android/credentials/ui/AuthenticationEntry.java new file mode 100644 index 0000000000000..b1a382c8ea966 --- /dev/null +++ b/core/java/android/credentials/ui/AuthenticationEntry.java @@ -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 CREATOR = new Creator<>() { + @Override + public AuthenticationEntry createFromParcel(@NonNull Parcel in) { + return new AuthenticationEntry(in); + } + + @Override + public AuthenticationEntry[] newArray(int size) { + return new AuthenticationEntry[size]; + } + }; +} diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 37a5724258231..12665bad53a95 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -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; diff --git a/core/java/android/credentials/ui/GetCredentialProviderData.java b/core/java/android/credentials/ui/GetCredentialProviderData.java index c44d1dd76fe4a..e4688a84a3fbe 100644 --- a/core/java/android/credentials/ui/GetCredentialProviderData.java +++ b/core/java/android/credentials/ui/GetCredentialProviderData.java @@ -39,13 +39,14 @@ public final class GetCredentialProviderData extends ProviderData implements Par @NonNull private final List mActionChips; @NonNull - private final List mAuthenticationEntries; + private final List mAuthenticationEntries; @Nullable private final Entry mRemoteEntry; public GetCredentialProviderData( @NonNull String providerFlattenedComponentName, @NonNull List credentialEntries, - @NonNull List actionChips, @NonNull List authenticationEntries, + @NonNull List actionChips, + @NonNull List authenticationEntries, @Nullable Entry remoteEntry) { super(providerFlattenedComponentName); mCredentialEntries = credentialEntries; @@ -65,7 +66,7 @@ public final class GetCredentialProviderData extends ProviderData implements Par } @NonNull - public List getAuthenticationEntries() { + public List getAuthenticationEntries() { return mAuthenticationEntries; } @@ -87,8 +88,8 @@ public final class GetCredentialProviderData extends ProviderData implements Par mActionChips = actionChips; AnnotationValidations.validate(NonNull.class, null, mActionChips); - List authenticationEntries = new ArrayList<>(); - in.readTypedList(authenticationEntries, Entry.CREATOR); + List 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 mCredentialEntries = new ArrayList<>(); @NonNull private List mActionChips = new ArrayList<>(); - @NonNull private List mAuthenticationEntries = new ArrayList<>(); + @NonNull private List 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 authenticationEntry) { + public Builder setAuthenticationEntries( + @NonNull List authenticationEntry) { mAuthenticationEntries = authenticationEntry; return this; } diff --git a/packages/CredentialManager/res/values/strings.xml b/packages/CredentialManager/res/values/strings.xml index 49ac482699cfb..9eda82a94623f 100644 --- a/packages/CredentialManager/res/values/strings.xml +++ b/packages/CredentialManager/res/values/strings.xml @@ -116,8 +116,10 @@ For %1$s Locked password managers - - Tap to unlock + + Tap to unlock + + No sign-in info Manage sign-ins diff --git a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt index 51dc233a31c12..0723c1ac6f6ac 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/CredentialManagerRepo.kt @@ -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( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index ae87d9576f087..50036e8a239b6 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -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, + authEntryList: List, ): List { val result: MutableList = 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 diff --git a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt index a580c6cbf2afd..eb3d188eab075 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/TestUtils.kt @@ -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 ) } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt index 823ee5b1f5e41..a7f17c855d050 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/createflow/CreateCredentialComponents.kt @@ -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( diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 48ee287f8672d..438978cb4143b 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -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 = { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt index bca06c729073a..1a30295e07cdf 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetModel.kt @@ -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, diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 764aac3719b79..f116c1bd002b4 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -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 prepareUiAuthenticationEntries( + private List prepareUiAuthenticationEntries( @NonNull List authenticationEntries) { - List authenticationUiEntries = new ArrayList<>(); + List 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 actionEntries, - List credentialEntries, List authenticationActionEntries, + List credentialEntries, List authenticationActionEntries, Entry remoteEntry) { return new GetCredentialProviderData.Builder( mComponentName.flattenToString()).setActionChips(actionEntries)