From 48c0a642e7d228cd765b849cfd4d7681cac0d642 Mon Sep 17 00:00:00 2001 From: Reema Bajwa Date: Wed, 22 Feb 2023 02:17:26 +0000 Subject: [PATCH] Make RemoteEntry flow independent Based on API feedback from DP1 testers, we need to make RemoteEntry a standalone feature irrespective of the type of the credential, or a particular option from the GetCredentialRequest. With this change, remote entry can be created with just some UI metadata (defined on jetpack side). When user selects this entry, we send all CredentialOptions to this provider and the entire list can be serviced on a remote device. Consequently, this change includes modifying android.service.GetCredentialRequest to take in a list of CredentialOption's as opposed to a single CredentialOption. When a CredentialEntry is selected, that list will always contain a single option. When a RemoteEntry is selected, that list may conttain >1 options. A RemoteEntry is not tied to a type and hence we need a dedicated type for it that only contains Slice (holding the UI metadata). Earlier we were using a CredentialEntry to represent a remote entry in the get flow. However a CredentialEntry requires a type and hence is not applicable. In the create flow, we were using a CreateEntry which does not require a type but in order to keep the flows symmetric, we decide to use RemoteEntry as a separate entry type in both flows. Test: built locally & CTS tests Bug: 270186121 Change-Id: Ibfe76d88bde727cb8eed3227ae0dc26ae7fe6834 --- core/api/current.txt | 20 ++-- .../credentials/GetCredentialRequest.java | 1 - .../BeginCreateCredentialResponse.java | 12 +-- .../BeginGetCredentialResponse.java | 12 +-- .../credentials/GetCredentialRequest.java | 49 +++++++--- .../service/credentials/RemoteEntry.java | 97 +++++++++++++++++++ .../credentials/ProviderCreateSession.java | 9 +- .../credentials/ProviderGetSession.java | 20 ++-- .../ProviderRegistryGetSession.java | 2 +- 9 files changed, 177 insertions(+), 45 deletions(-) create mode 100644 core/java/android/service/credentials/RemoteEntry.java diff --git a/core/api/current.txt b/core/api/current.txt index 21611860047b8..9baded3cf169d 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -40508,7 +40508,7 @@ package android.service.credentials { ctor public BeginCreateCredentialResponse(); method public int describeContents(); method @NonNull public java.util.List getCreateEntries(); - method @Nullable public android.service.credentials.CreateEntry getRemoteCreateEntry(); + method @Nullable public android.service.credentials.RemoteEntry getRemoteCreateEntry(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; } @@ -40518,7 +40518,7 @@ package android.service.credentials { method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder addCreateEntry(@NonNull android.service.credentials.CreateEntry); method @NonNull public android.service.credentials.BeginCreateCredentialResponse build(); method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder setCreateEntries(@NonNull java.util.List); - method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder setRemoteCreateEntry(@Nullable android.service.credentials.CreateEntry); + method @NonNull public android.service.credentials.BeginCreateCredentialResponse.Builder setRemoteCreateEntry(@Nullable android.service.credentials.RemoteEntry); } public class BeginGetCredentialOption implements android.os.Parcelable { @@ -40553,7 +40553,7 @@ package android.service.credentials { method @NonNull public java.util.List getActions(); method @NonNull public java.util.List getAuthenticationActions(); method @NonNull public java.util.List getCredentialEntries(); - method @Nullable public android.service.credentials.CredentialEntry getRemoteCredentialEntry(); + method @Nullable public android.service.credentials.RemoteEntry getRemoteCredentialEntry(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; } @@ -40567,7 +40567,7 @@ package android.service.credentials { method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setActions(@NonNull java.util.List); method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setAuthenticationActions(@NonNull java.util.List); method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setCredentialEntries(@NonNull java.util.List); - method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setRemoteCredentialEntry(@Nullable android.service.credentials.CredentialEntry); + method @NonNull public android.service.credentials.BeginGetCredentialResponse.Builder setRemoteCredentialEntry(@Nullable android.service.credentials.RemoteEntry); } public final class CallingAppInfo implements android.os.Parcelable { @@ -40637,14 +40637,22 @@ package android.service.credentials { } public final class GetCredentialRequest implements android.os.Parcelable { - ctor public GetCredentialRequest(@NonNull android.service.credentials.CallingAppInfo, @NonNull android.credentials.CredentialOption); + ctor public GetCredentialRequest(@NonNull android.service.credentials.CallingAppInfo, @NonNull java.util.List); method public int describeContents(); method @NonNull public android.service.credentials.CallingAppInfo getCallingAppInfo(); - method @NonNull public android.credentials.CredentialOption getCredentialOption(); + method @NonNull public java.util.List getCredentialOptions(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator CREATOR; } + public class RemoteEntry implements android.os.Parcelable { + ctor public RemoteEntry(@NonNull android.app.slice.Slice); + method public int describeContents(); + method @NonNull public android.app.slice.Slice getSlice(); + method public void writeToParcel(@NonNull android.os.Parcel, int); + field @NonNull public static final android.os.Parcelable.Creator CREATOR; + } + } package android.service.dreams { diff --git a/core/java/android/credentials/GetCredentialRequest.java b/core/java/android/credentials/GetCredentialRequest.java index 951cbe4e58c08..c58d2dcd2dbf1 100644 --- a/core/java/android/credentials/GetCredentialRequest.java +++ b/core/java/android/credentials/GetCredentialRequest.java @@ -149,7 +149,6 @@ public final class GetCredentialRequest implements Parcelable { mCredentialOptions = credentialOptions; AnnotationValidations.validate(NonNull.class, null, mCredentialOptions); - Bundle data = in.readBundle(); mData = data; AnnotationValidations.validate(NonNull.class, null, mData); diff --git a/core/java/android/service/credentials/BeginCreateCredentialResponse.java b/core/java/android/service/credentials/BeginCreateCredentialResponse.java index f0f954dda1827..eebd31c6fa968 100644 --- a/core/java/android/service/credentials/BeginCreateCredentialResponse.java +++ b/core/java/android/service/credentials/BeginCreateCredentialResponse.java @@ -32,7 +32,7 @@ import java.util.Objects; */ public final class BeginCreateCredentialResponse implements Parcelable { private final @NonNull List mCreateEntries; - private final @Nullable CreateEntry mRemoteCreateEntry; + private final @Nullable RemoteEntry mRemoteCreateEntry; /** * Creates an empty response instance, to be used when there are no {@link CreateEntry} @@ -46,7 +46,7 @@ public final class BeginCreateCredentialResponse implements Parcelable { List createEntries = new ArrayList<>(); in.readTypedList(createEntries, CreateEntry.CREATOR); mCreateEntries = createEntries; - mRemoteCreateEntry = in.readTypedObject(CreateEntry.CREATOR); + mRemoteCreateEntry = in.readTypedObject(RemoteEntry.CREATOR); } @Override @@ -75,7 +75,7 @@ public final class BeginCreateCredentialResponse implements Parcelable { /* package-private */ BeginCreateCredentialResponse( @NonNull List createEntries, - @Nullable CreateEntry remoteCreateEntry) { + @Nullable RemoteEntry remoteCreateEntry) { this.mCreateEntries = createEntries; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mCreateEntries); @@ -88,7 +88,7 @@ public final class BeginCreateCredentialResponse implements Parcelable { } /** Returns the remote create entry to be displayed on the UI. */ - public @Nullable CreateEntry getRemoteCreateEntry() { + public @Nullable RemoteEntry getRemoteCreateEntry() { return mRemoteCreateEntry; } @@ -98,7 +98,7 @@ public final class BeginCreateCredentialResponse implements Parcelable { @SuppressWarnings("WeakerAccess") /* synthetic access */ public static final class Builder { private @NonNull List mCreateEntries = new ArrayList<>(); - private @Nullable CreateEntry mRemoteCreateEntry; + private @Nullable RemoteEntry mRemoteCreateEntry; /** * Sets the list of create entries to be shown on the UI. @@ -138,7 +138,7 @@ public final class BeginCreateCredentialResponse implements Parcelable { * {@link CredentialProviderService#EXTRA_CREATE_CREDENTIAL_RESPONSE} key should be populated * with a {@link android.credentials.CreateCredentialResponse} object. */ - public @NonNull Builder setRemoteCreateEntry(@Nullable CreateEntry remoteCreateEntry) { + public @NonNull Builder setRemoteCreateEntry(@Nullable RemoteEntry remoteCreateEntry) { mRemoteCreateEntry = remoteCreateEntry; return this; } diff --git a/core/java/android/service/credentials/BeginGetCredentialResponse.java b/core/java/android/service/credentials/BeginGetCredentialResponse.java index 3652742d4ff4d..97f5079393a0d 100644 --- a/core/java/android/service/credentials/BeginGetCredentialResponse.java +++ b/core/java/android/service/credentials/BeginGetCredentialResponse.java @@ -42,7 +42,7 @@ public final class BeginGetCredentialResponse implements Parcelable { private final @NonNull List mActions; /** Remote credential entry to get the response from a different device. */ - private final @Nullable CredentialEntry mRemoteCredentialEntry; + private final @Nullable RemoteEntry mRemoteCredentialEntry; /** * Creates an empty response instance, to be used when there are no {@link CredentialEntry}, @@ -57,7 +57,7 @@ public final class BeginGetCredentialResponse implements Parcelable { private BeginGetCredentialResponse(@NonNull List credentialEntries, @NonNull List authenticationEntries, @NonNull List actions, - @Nullable CredentialEntry remoteCredentialEntry) { + @Nullable RemoteEntry remoteCredentialEntry) { mCredentialEntries = new ArrayList<>(credentialEntries); mAuthenticationEntries = new ArrayList<>(authenticationEntries); mActions = new ArrayList<>(actions); @@ -74,7 +74,7 @@ public final class BeginGetCredentialResponse implements Parcelable { List actions = new ArrayList<>(); in.readTypedList(actions, Action.CREATOR); mActions = actions; - mRemoteCredentialEntry = in.readTypedObject(CredentialEntry.CREATOR); + mRemoteCredentialEntry = in.readTypedObject(RemoteEntry.CREATOR); } public static final @NonNull Creator CREATOR = @@ -127,7 +127,7 @@ public final class BeginGetCredentialResponse implements Parcelable { /** * Returns the remote credential entry to be displayed on the UI. */ - public @Nullable CredentialEntry getRemoteCredentialEntry() { + public @Nullable RemoteEntry getRemoteCredentialEntry() { return mRemoteCredentialEntry; } @@ -139,7 +139,7 @@ public final class BeginGetCredentialResponse implements Parcelable { private List mAuthenticationEntries = new ArrayList<>(); private List mActions = new ArrayList<>(); - private CredentialEntry mRemoteCredentialEntry; + private RemoteEntry mRemoteCredentialEntry; /** * Sets a remote credential entry to be shown on the UI. Provider must set this if they @@ -155,7 +155,7 @@ public final class BeginGetCredentialResponse implements Parcelable { * {@link CredentialProviderService#EXTRA_GET_CREDENTIAL_RESPONSE} key should be populated * with a {@link android.credentials.Credential} object. */ - public @NonNull Builder setRemoteCredentialEntry(@Nullable CredentialEntry + public @NonNull Builder setRemoteCredentialEntry(@Nullable RemoteEntry remoteCredentialEntry) { mRemoteCredentialEntry = remoteCredentialEntry; return this; diff --git a/core/java/android/service/credentials/GetCredentialRequest.java b/core/java/android/service/credentials/GetCredentialRequest.java index 4f13922caf15b..5bad9abb2eedc 100644 --- a/core/java/android/service/credentials/GetCredentialRequest.java +++ b/core/java/android/service/credentials/GetCredentialRequest.java @@ -23,14 +23,16 @@ import android.os.Parcelable; import com.android.internal.util.AnnotationValidations; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; /** * Request for getting user's credential from a given credential provider. * - *

Provider will receive this request once the user selects a given {@link CredentialEntry} - * on the selector, that was sourced from provider's result to - * {@link CredentialProviderService#onBeginGetCredential}. + *

A credential provider will receive this request once the user selects a + * given {@link CredentialEntry}, or {@link RemoteEntry} on the selector, that was sourced + * from provider's initial response to {@link CredentialProviderService#onBeginGetCredential}. */ public final class GetCredentialRequest implements Parcelable { /** Calling package of the app requesting for credentials. */ @@ -38,24 +40,27 @@ public final class GetCredentialRequest implements Parcelable { private final CallingAppInfo mCallingAppInfo; /** - * Holds parameters to be used for retrieving a specific type of credential. + * Holds a list of options (parameters) to be used for retrieving a specific type of credential. */ @NonNull - private final CredentialOption mCredentialOption; + private final List mCredentialOptions; public GetCredentialRequest(@NonNull CallingAppInfo callingAppInfo, - @NonNull CredentialOption credentialOption) { + @NonNull List credentialOptions) { this.mCallingAppInfo = Objects.requireNonNull(callingAppInfo, "callingAppInfo must not be null"); - this.mCredentialOption = Objects.requireNonNull(credentialOption, - "credentialOption must not be null"); + this.mCredentialOptions = Objects.requireNonNull(credentialOptions, + "credentialOptions must not be null"); } private GetCredentialRequest(@NonNull Parcel in) { mCallingAppInfo = in.readTypedObject(CallingAppInfo.CREATOR); AnnotationValidations.validate(NonNull.class, null, mCallingAppInfo); - mCredentialOption = in.readTypedObject(CredentialOption.CREATOR); - AnnotationValidations.validate(NonNull.class, null, mCredentialOption); + + List credentialOptions = new ArrayList<>(); + in.readTypedList(credentialOptions, CredentialOption.CREATOR); + mCredentialOptions = credentialOptions; + AnnotationValidations.validate(NonNull.class, null, mCredentialOptions); } @NonNull public static final Creator CREATOR = @@ -79,7 +84,7 @@ public final class GetCredentialRequest implements Parcelable { @Override public void writeToParcel(@NonNull Parcel dest, int flags) { dest.writeTypedObject(mCallingAppInfo, flags); - dest.writeTypedObject(mCredentialOption, flags); + dest.writeTypedList(mCredentialOptions, flags); } /** @@ -91,10 +96,26 @@ public final class GetCredentialRequest implements Parcelable { } /** - * Returns the parameters needed to return a given type of credential. + * Returns a list of options containing parameters needed to return a given type of credential. + * This is part of the request that the credential provider receives after the user has + * selected an entry on a selector UI. + * + * When the user selects a {@link CredentialEntry} and the credential provider receives a + * {@link GetCredentialRequest}, this list is expected to contain a single + * {@link CredentialOption} only. A {@link CredentialEntry} is always created for a given + * {@link BeginGetCredentialOption}, and hence when the user selects it, the provider + * receives a corresponding {@link CredentialOption} that contains all the required parameters + * to actually retrieve the credential. + * + * When the user selects a {@link RemoteEntry} and the credential provider receives a + * {@link GetCredentialRequest}, this list may contain greater than a single + * {@link CredentialOption}, representing the number of options specified by the developer + * in the original {@link android.credentials.GetCredentialRequest}. This is because a + * {@link RemoteEntry} indicates that the entire request will be processed on a different + * device and is not tied to a particular option. */ @NonNull - public CredentialOption getCredentialOption() { - return mCredentialOption; + public List getCredentialOptions() { + return mCredentialOptions; } } diff --git a/core/java/android/service/credentials/RemoteEntry.java b/core/java/android/service/credentials/RemoteEntry.java new file mode 100644 index 0000000000000..716c00d726f3f --- /dev/null +++ b/core/java/android/service/credentials/RemoteEntry.java @@ -0,0 +1,97 @@ +/* + * Copyright (C) 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.service.credentials; + +import android.annotation.NonNull; +import android.annotation.SuppressLint; +import android.app.PendingIntent; +import android.app.slice.Slice; +import android.os.Parcel; +import android.os.Parcelable; + +/** + * An entry to be shown on the UI. This entry represents remote execution of a get/create flow + * whereby credentials are retrieved from, or stored to a remote device. + * + *

If user selects this entry, the corresponding {@link PendingIntent} set on the + * {@code slice} as a {@link androidx.slice.core.SliceAction} will get invoked. + * Once the resulting activity fulfills the required user engagement, + * the {@link android.app.Activity} result should be set to {@link android.app.Activity#RESULT_OK}, + * and the result of the operation must be set as the activity result. + * + * For a get flow, invoked through {@link CredentialProviderService#onBeginGetCredential}, + * providers must set a {@link android.credentials.GetCredentialResponse} on the activity result, + * against the key {@link CredentialProviderService#EXTRA_GET_CREDENTIAL_RESPONSE}. + * + * For a creates flow, invoked through {@link CredentialProviderService#onBeginCreateCredential}, + * providers must set a {@link android.credentials.CreateCredentialResponse} on the activity + * result against the ket {@link CredentialProviderService#EXTRA_CREATE_CREDENTIAL_RESPONSE}. + * + *

Any class that extends this class must only add extra field values to the {@code slice} + * object passed into the constructor. Any other field will not be parceled through. If the + * derived class has custom parceling implementation, this class will not be able to unpack + * the parcel without having access to that implementation. + */ +@SuppressLint("ParcelNotFinal") +public class RemoteEntry implements Parcelable { + private final @NonNull Slice mSlice; + + private RemoteEntry(@NonNull Parcel in) { + mSlice = in.readTypedObject(Slice.CREATOR); + } + + @NonNull + public static final Creator CREATOR = new Creator() { + @Override + public RemoteEntry createFromParcel(@NonNull Parcel in) { + return new RemoteEntry(in); + } + + @Override + public RemoteEntry[] newArray(int size) { + return new RemoteEntry[size]; + } + }; + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeTypedObject(mSlice, flags); + } + + /** + * Constructs a RemoteEntry to be displayed on the UI. + * + * @param slice the display content to be displayed on the UI, along with this entry + */ + public RemoteEntry( + @NonNull Slice slice) { + this.mSlice = slice; + com.android.internal.util.AnnotationValidations.validate( + NonNull.class, null, mSlice); + } + + /** Returns the content to be displayed with this remote entry on the UI. */ + @NonNull + public Slice getSlice() { + return mSlice; + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java index 3245c914b2b83..97b78116bbfac 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderCreateSession.java @@ -35,6 +35,7 @@ import android.service.credentials.CreateCredentialRequest; import android.service.credentials.CreateEntry; import android.service.credentials.CredentialProviderInfo; import android.service.credentials.CredentialProviderService; +import android.service.credentials.RemoteEntry; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -301,14 +302,14 @@ public final class ProviderCreateSession extends ProviderSession< @NonNull private final Map> mUiCreateEntries = new HashMap<>(); - @Nullable private Pair> mUiRemoteEntry = null; + @Nullable private Pair> mUiRemoteEntry = null; ProviderResponseDataHandler(String hybridService) { mExpectedRemoteEntryProviderService = ComponentName.unflattenFromString(hybridService); } public void addResponseContent(List createEntries, - CreateEntry remoteEntry) { + RemoteEntry remoteEntry) { createEntries.forEach(this::addCreateEntry); setRemoteEntry(remoteEntry); } @@ -319,7 +320,7 @@ public final class ProviderCreateSession extends ProviderSession< mUiCreateEntries.put(id, new Pair<>(createEntry, entry)); } - public void setRemoteEntry(@Nullable CreateEntry remoteEntry) { + public void setRemoteEntry(@Nullable RemoteEntry remoteEntry) { if (remoteEntry == null) { mUiRemoteEntry = null; return; @@ -363,7 +364,7 @@ public final class ProviderCreateSession extends ProviderSession< return mUiCreateEntries.isEmpty() && mUiRemoteEntry == null; } @Nullable - public CreateEntry getRemoteEntry(String entryKey) { + public RemoteEntry getRemoteEntry(String entryKey) { return mUiRemoteEntry == null || mUiRemoteEntry .first == null || !mUiRemoteEntry.first.equals(entryKey) || mUiRemoteEntry.second == null diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 12074c7494bf9..b6f3553e08d7c 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -38,6 +38,7 @@ import android.service.credentials.CredentialEntry; import android.service.credentials.CredentialProviderInfo; import android.service.credentials.CredentialProviderService; import android.service.credentials.GetCredentialRequest; +import android.service.credentials.RemoteEntry; import android.util.Log; import android.util.Pair; import android.util.Slog; @@ -298,7 +299,7 @@ public final class ProviderGetSession extends ProviderSession> mUiAuthenticationEntries = new HashMap<>(); - @Nullable private Pair> mUiRemoteEntry = null; + @Nullable private Pair> mUiRemoteEntry = null; ProviderResponseDataHandler(ComponentName expectedRemoteEntryProviderService) { mExpectedRemoteEntryProviderService = expectedRemoteEntryProviderService; @@ -475,7 +476,7 @@ public final class ProviderGetSession extends ProviderSession credentialEntries, List actions, List authenticationActions, - CredentialEntry remoteEntry) { + RemoteEntry remoteEntry) { credentialEntries.forEach(this::addCredentialEntry); actions.forEach(this::addAction); authenticationActions.forEach( @@ -517,7 +518,7 @@ public final class ProviderGetSession extends ProviderSession(generateUniqueId(), new Pair<>(remoteEntry, entry)); } @@ -599,7 +599,7 @@ public final class ProviderGetSession extends ProviderSession