From b1d1033b49e560c485e29e45a95abf0da38008d8 Mon Sep 17 00:00:00 2001 From: Helen Qin Date: Wed, 15 Mar 2023 05:16:49 +0000 Subject: [PATCH] [CredMan] Unhide the getPendingCredential api. Per partner feedback, expose a version of getCredential api that splits prefetching candidates and selection UI into two separate calls. This allows performing prefetch work and an early time and delaying the UI to a later time which will be significantly faster to surface upon request. Bug: 273308895 Test: CTS & local tested Change-Id: I2f08a4333ac90bb9fdc77cb8f522b415cd694336 --- core/api/current.txt | 13 + .../credentials/CredentialManager.java | 133 ++++++-- .../GetPendingCredentialResponse.java | 127 -------- .../credentials/ICredentialManager.aidl | 4 +- ...idl => IPrepareGetCredentialCallback.aidl} | 8 +- .../PrepareGetCredentialResponse.java | 180 +++++++++++ ...PrepareGetCredentialResponseInternal.aidl} | 2 +- .../PrepareGetCredentialResponseInternal.java | 167 ++++++++++ core/res/AndroidManifest.xml | 6 + .../credentials/CredentialManagerTest.java | 3 +- .../credentials/CredentialManagerService.java | 135 +++++++- .../credentials/PrepareGetRequestSession.java | 306 ++++++++++++++++++ .../credentials/ProviderGetSession.java | 34 ++ .../ProviderRegistryGetSession.java | 35 ++ 14 files changed, 994 insertions(+), 159 deletions(-) delete mode 100644 core/java/android/credentials/GetPendingCredentialResponse.java rename core/java/android/credentials/{IGetPendingCredentialCallback.aidl => IPrepareGetCredentialCallback.aidl} (75%) create mode 100644 core/java/android/credentials/PrepareGetCredentialResponse.java rename core/java/android/credentials/{GetPendingCredentialResponse.aidl => PrepareGetCredentialResponseInternal.aidl} (93%) create mode 100644 core/java/android/credentials/PrepareGetCredentialResponseInternal.java create mode 100644 services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java diff --git a/core/api/current.txt b/core/api/current.txt index 70dcd542d715b..b91144b3a0457 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -83,6 +83,7 @@ package android { field public static final String CLEAR_APP_CACHE = "android.permission.CLEAR_APP_CACHE"; field public static final String CONFIGURE_WIFI_DISPLAY = "android.permission.CONFIGURE_WIFI_DISPLAY"; field public static final String CONTROL_LOCATION_UPDATES = "android.permission.CONTROL_LOCATION_UPDATES"; + field public static final String CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS = "android.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS"; field public static final String CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS = "android.permission.CREDENTIAL_MANAGER_SET_ALLOWED_PROVIDERS"; field public static final String CREDENTIAL_MANAGER_SET_ORIGIN = "android.permission.CREDENTIAL_MANAGER_SET_ORIGIN"; field public static final String DELETE_CACHE_FILES = "android.permission.DELETE_CACHE_FILES"; @@ -13669,7 +13670,9 @@ package android.credentials { method public void clearCredentialState(@NonNull android.credentials.ClearCredentialStateRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); method public void createCredential(@NonNull android.credentials.CreateCredentialRequest, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); method public void getCredential(@NonNull android.credentials.GetCredentialRequest, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); + method public void getCredential(@NonNull android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle, @NonNull android.app.Activity, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); method public boolean isEnabledCredentialProviderService(@NonNull android.content.ComponentName); + method public void prepareGetCredential(@NonNull android.credentials.GetCredentialRequest, @Nullable android.os.CancellationSignal, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver); method public void registerCredentialDescription(@NonNull android.credentials.RegisterCredentialDescriptionRequest); method public void unregisterCredentialDescription(@NonNull android.credentials.UnregisterCredentialDescriptionRequest); } @@ -13734,6 +13737,16 @@ package android.credentials { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } + public final class PrepareGetCredentialResponse { + method @NonNull public android.credentials.PrepareGetCredentialResponse.PendingGetCredentialHandle getPendingGetCredentialHandle(); + method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasAuthenticationResults(); + method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasCredentialResults(@NonNull String); + method @RequiresPermission(android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) public boolean hasRemoteResults(); + } + + public static final class PrepareGetCredentialResponse.PendingGetCredentialHandle { + } + public final class RegisterCredentialDescriptionRequest implements android.os.Parcelable { ctor public RegisterCredentialDescriptionRequest(@NonNull android.credentials.CredentialDescription); ctor public RegisterCredentialDescriptionRequest(@NonNull java.util.Set); diff --git a/core/java/android/credentials/CredentialManager.java b/core/java/android/credentials/CredentialManager.java index 493a4ff3c4cf8..5579d2263d066 100644 --- a/core/java/android/credentials/CredentialManager.java +++ b/core/java/android/credentials/CredentialManager.java @@ -16,8 +16,6 @@ package android.credentials; -import static android.Manifest.permission.CREDENTIAL_MANAGER_SET_ORIGIN; - import static java.util.Objects.requireNonNull; import android.annotation.CallbackExecutor; @@ -167,37 +165,83 @@ public final class CredentialManager { } /** - * Gets a {@link GetPendingCredentialResponse} that can launch the credential retrieval UI flow - * to request a user credential for your app. + * Launches the remaining flows to retrieve an app credential from the user, after the + * completed prefetch work corresponding to the given {@code pendingGetCredentialHandle}. + * + *

The execution can potentially launch UI flows to collect user consent to using a + * credential, display a picker when multiple credentials exist, etc. + * + *

Use this API to complete the full credential retrieval operation after you initiated a + * request through the {@link #prepareGetCredential( + * GetCredentialRequest, CancellationSignal, Executor, OutcomeReceiver)} API. + * + * @param pendingGetCredentialHandle the handle representing the pending operation to resume + * @param activity the activity used to launch any UI needed + * @param cancellationSignal an optional signal that allows for cancelling this call + * @param executor the callback will take place on this {@link Executor} + * @param callback the callback invoked when the request succeeds or fails + */ + public void getCredential( + @NonNull PrepareGetCredentialResponse.PendingGetCredentialHandle + pendingGetCredentialHandle, + @NonNull Activity activity, + @Nullable CancellationSignal cancellationSignal, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + requireNonNull(pendingGetCredentialHandle, "pendingGetCredentialHandle must not be null"); + requireNonNull(activity, "activity must not be null"); + requireNonNull(executor, "executor must not be null"); + requireNonNull(callback, "callback must not be null"); + + if (cancellationSignal != null && cancellationSignal.isCanceled()) { + Log.w(TAG, "getCredential already canceled"); + return; + } + + pendingGetCredentialHandle.show(activity, cancellationSignal, executor, callback); + } + + /** + * Prepare for a get-credential operation. Returns a {@link PrepareGetCredentialResponse} that + * can launch the credential retrieval UI flow to request a user credential for your app. + * + *

This API doesn't invoke any UI. It only performs the preparation work so that you can + * later launch the remaining get-credential operation (involves UIs) through the {@link + * #getCredential(PrepareGetCredentialResponse.PendingGetCredentialHandle, Activity, + * CancellationSignal, Executor, OutcomeReceiver)} API which incurs less latency compared to + * the {@link #getCredential(GetCredentialRequest, Activity, CancellationSignal, Executor, + * OutcomeReceiver)} API that executes the whole operation in one call. * * @param request the request specifying type(s) of credentials to get from the user * @param cancellationSignal an optional signal that allows for cancelling this call * @param executor the callback will take place on this {@link Executor} * @param callback the callback invoked when the request succeeds or fails - * - * @hide */ - public void getPendingCredential( + public void prepareGetCredential( @NonNull GetCredentialRequest request, @Nullable CancellationSignal cancellationSignal, @CallbackExecutor @NonNull Executor executor, @NonNull OutcomeReceiver< - GetPendingCredentialResponse, GetCredentialException> callback) { + PrepareGetCredentialResponse, GetCredentialException> callback) { requireNonNull(request, "request must not be null"); requireNonNull(executor, "executor must not be null"); requireNonNull(callback, "callback must not be null"); if (cancellationSignal != null && cancellationSignal.isCanceled()) { - Log.w(TAG, "getPendingCredential already canceled"); + Log.w(TAG, "prepareGetCredential already canceled"); return; } ICancellationSignal cancelRemote = null; + GetCredentialTransportPendingUseCase getCredentialTransport = + new GetCredentialTransportPendingUseCase(); try { cancelRemote = - mService.executeGetPendingCredential( + mService.executePrepareGetCredential( request, - new GetPendingCredentialTransport(executor, callback), + new PrepareGetCredentialTransport( + executor, callback, getCredentialTransport), + getCredentialTransport, mContext.getOpPackageName()); } catch (RemoteException e) { e.rethrowFromSystemServer(); @@ -484,23 +528,27 @@ public final class CredentialManager { } } - private static class GetPendingCredentialTransport extends IGetPendingCredentialCallback.Stub { + private static class PrepareGetCredentialTransport extends IPrepareGetCredentialCallback.Stub { // TODO: listen for cancellation to release callback. private final Executor mExecutor; private final OutcomeReceiver< - GetPendingCredentialResponse, GetCredentialException> mCallback; + PrepareGetCredentialResponse, GetCredentialException> mCallback; + private final GetCredentialTransportPendingUseCase mGetCredentialTransport; - private GetPendingCredentialTransport( + private PrepareGetCredentialTransport( Executor executor, - OutcomeReceiver callback) { + OutcomeReceiver callback, + GetCredentialTransportPendingUseCase getCredentialTransport) { mExecutor = executor; mCallback = callback; + mGetCredentialTransport = getCredentialTransport; } @Override - public void onResponse(GetPendingCredentialResponse response) { - mExecutor.execute(() -> mCallback.onResult(response)); + public void onResponse(PrepareGetCredentialResponseInternal response) { + mExecutor.execute(() -> mCallback.onResult( + new PrepareGetCredentialResponse(response, mGetCredentialTransport))); } @Override @@ -510,6 +558,51 @@ public final class CredentialManager { } } + /** @hide */ + protected static class GetCredentialTransportPendingUseCase + extends IGetCredentialCallback.Stub { + @Nullable private PrepareGetCredentialResponse.GetPendingCredentialInternalCallback + mCallback = null; + + private GetCredentialTransportPendingUseCase() {} + + public void setCallback( + PrepareGetCredentialResponse.GetPendingCredentialInternalCallback callback) { + if (mCallback == null) { + mCallback = callback; + } else { + throw new IllegalStateException("callback has already been set once"); + } + } + + @Override + public void onPendingIntent(PendingIntent pendingIntent) { + if (mCallback != null) { + mCallback.onPendingIntent(pendingIntent); + } else { + Log.d(TAG, "Unexpected onPendingIntent call before the show invocation"); + } + } + + @Override + public void onResponse(GetCredentialResponse response) { + if (mCallback != null) { + mCallback.onResponse(response); + } else { + Log.d(TAG, "Unexpected onResponse call before the show invocation"); + } + } + + @Override + public void onError(String errorType, String message) { + if (mCallback != null) { + mCallback.onError(errorType, message); + } else { + Log.d(TAG, "Unexpected onError call before the show invocation"); + } + } + } + private static class GetCredentialTransport extends IGetCredentialCallback.Stub { // TODO: listen for cancellation to release callback. @@ -535,7 +628,8 @@ public final class CredentialManager { TAG, "startIntentSender() failed for intent:" + pendingIntent.getIntentSender(), e); - // TODO: propagate the error. + mExecutor.execute(() -> mCallback.onError( + new GetCredentialException(GetCredentialException.TYPE_UNKNOWN))); } } @@ -577,7 +671,8 @@ public final class CredentialManager { TAG, "startIntentSender() failed for intent:" + pendingIntent.getIntentSender(), e); - // TODO: propagate the error. + mExecutor.execute(() -> mCallback.onError( + new CreateCredentialException(CreateCredentialException.TYPE_UNKNOWN))); } } diff --git a/core/java/android/credentials/GetPendingCredentialResponse.java b/core/java/android/credentials/GetPendingCredentialResponse.java deleted file mode 100644 index 9005d9dfa0310..0000000000000 --- a/core/java/android/credentials/GetPendingCredentialResponse.java +++ /dev/null @@ -1,127 +0,0 @@ -/* - * 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; - -import android.annotation.CallbackExecutor; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.Activity; -import android.os.CancellationSignal; -import android.os.OutcomeReceiver; -import android.os.Parcel; -import android.os.Parcelable; - -import java.util.concurrent.Executor; - - -/** - * A response object that prefetches user app credentials and provides metadata about them. It can - * then be used to issue the full credential retrieval flow via the - * {@link #show(Activity, CancellationSignal, Executor, OutcomeReceiver)} method to perform the - * necessary flows such as consent collection and officially retrieve a credential. - * - * @hide - */ -public final class GetPendingCredentialResponse implements Parcelable { - private final boolean mHasCredentialResults; - private final boolean mHasAuthenticationResults; - private final boolean mHasRemoteResults; - - /** Returns true if the user has any candidate credentials, and false otherwise. */ - public boolean hasCredentialResults() { - return mHasCredentialResults; - } - - /** - * Returns true if the user has any candidate authentication actions (locked credential - * supplier), and false otherwise. - */ - public boolean hasAuthenticationResults() { - return mHasAuthenticationResults; - } - - /** - * Returns true if the user has any candidate remote credential results, and false otherwise. - */ - public boolean hasRemoteResults() { - return mHasRemoteResults; - } - - /** - * Launches the necessary flows such as consent collection and credential selection to - * officially retrieve a credential among the pending credential candidates. - * - * @param activity the activity used to launch any UI needed - * @param cancellationSignal an optional signal that allows for cancelling this call - * @param executor the callback will take place on this {@link Executor} - * @param callback the callback invoked when the request succeeds or fails - */ - public void show(@NonNull Activity activity, @Nullable CancellationSignal cancellationSignal, - @CallbackExecutor @NonNull Executor executor, - @NonNull OutcomeReceiver callback) { - // TODO(b/273308895): implement - } - - @Override - public void writeToParcel(@NonNull Parcel dest, int flags) { - dest.writeBoolean(mHasCredentialResults); - dest.writeBoolean(mHasAuthenticationResults); - dest.writeBoolean(mHasRemoteResults); - } - - @Override - public int describeContents() { - return 0; - } - - @Override - public String toString() { - return "GetCredentialResponse {" + "credential=" + mHasCredentialResults + "}"; - } - - /** - * Constructs a {@link GetPendingCredentialResponse}. - * - * @param hasCredentialResults whether the user has any candidate credentials - * @param hasAuthenticationResults whether the user has any candidate authentication actions - * @param hasRemoteResults whether the user has any candidate remote options - */ - public GetPendingCredentialResponse(boolean hasCredentialResults, - boolean hasAuthenticationResults, boolean hasRemoteResults) { - mHasCredentialResults = hasCredentialResults; - mHasAuthenticationResults = hasAuthenticationResults; - mHasRemoteResults = hasRemoteResults; - } - - private GetPendingCredentialResponse(@NonNull Parcel in) { - mHasCredentialResults = in.readBoolean(); - mHasAuthenticationResults = in.readBoolean(); - mHasRemoteResults = in.readBoolean(); - } - - public static final @NonNull Creator CREATOR = new Creator<>() { - @Override - public GetPendingCredentialResponse[] newArray(int size) { - return new GetPendingCredentialResponse[size]; - } - - @Override - public GetPendingCredentialResponse createFromParcel(@NonNull Parcel in) { - return new GetPendingCredentialResponse(in); - } - }; -} diff --git a/core/java/android/credentials/ICredentialManager.aidl b/core/java/android/credentials/ICredentialManager.aidl index af8e7b413ea9d..5fde96b0b9ff0 100644 --- a/core/java/android/credentials/ICredentialManager.aidl +++ b/core/java/android/credentials/ICredentialManager.aidl @@ -27,7 +27,7 @@ import android.credentials.UnregisterCredentialDescriptionRequest; import android.credentials.IClearCredentialStateCallback; import android.credentials.ICreateCredentialCallback; import android.credentials.IGetCredentialCallback; -import android.credentials.IGetPendingCredentialCallback; +import android.credentials.IPrepareGetCredentialCallback; import android.credentials.ISetEnabledProvidersCallback; import android.content.ComponentName; import android.os.ICancellationSignal; @@ -41,7 +41,7 @@ interface ICredentialManager { @nullable ICancellationSignal executeGetCredential(in GetCredentialRequest request, in IGetCredentialCallback callback, String callingPackage); - @nullable ICancellationSignal executeGetPendingCredential(in GetCredentialRequest request, in IGetPendingCredentialCallback callback, String callingPackage); + @nullable ICancellationSignal executePrepareGetCredential(in GetCredentialRequest request, in IPrepareGetCredentialCallback prepareGetCredentialCallback, in IGetCredentialCallback getCredentialCallback, String callingPackage); @nullable ICancellationSignal executeCreateCredential(in CreateCredentialRequest request, in ICreateCredentialCallback callback, String callingPackage); diff --git a/core/java/android/credentials/IGetPendingCredentialCallback.aidl b/core/java/android/credentials/IPrepareGetCredentialCallback.aidl similarity index 75% rename from core/java/android/credentials/IGetPendingCredentialCallback.aidl rename to core/java/android/credentials/IPrepareGetCredentialCallback.aidl index 4ab0f998adaee..c918aec623497 100644 --- a/core/java/android/credentials/IGetPendingCredentialCallback.aidl +++ b/core/java/android/credentials/IPrepareGetCredentialCallback.aidl @@ -17,14 +17,14 @@ package android.credentials; import android.app.PendingIntent; -import android.credentials.GetPendingCredentialResponse; +import android.credentials.PrepareGetCredentialResponseInternal; /** - * Listener for a executeGetPendingCredential request. + * Listener for a executePrepareGetCredential request. * * @hide */ -interface IGetPendingCredentialCallback { - oneway void onResponse(in GetPendingCredentialResponse response); +interface IPrepareGetCredentialCallback { + oneway void onResponse(in PrepareGetCredentialResponseInternal response); oneway void onError(String errorType, String message); } \ No newline at end of file diff --git a/core/java/android/credentials/PrepareGetCredentialResponse.java b/core/java/android/credentials/PrepareGetCredentialResponse.java new file mode 100644 index 0000000000000..81e906859cb84 --- /dev/null +++ b/core/java/android/credentials/PrepareGetCredentialResponse.java @@ -0,0 +1,180 @@ +/* + * 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; + +import static android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS; + +import android.annotation.CallbackExecutor; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.app.Activity; +import android.app.PendingIntent; +import android.content.IntentSender; +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; +import android.util.Log; + +import java.util.concurrent.Executor; + + +/** + * A response object that prefetches user app credentials and provides metadata about them. It can + * then be used to issue the full credential retrieval flow via the + * {@link CredentialManager#getCredential(PendingGetCredentialHandle, Activity, CancellationSignal, + * Executor, OutcomeReceiver)} method to perform the remaining flows such as consent collection + * and credential selection, to officially retrieve a credential. + */ +public final class PrepareGetCredentialResponse { + + /** + * A handle that represents a pending get-credential operation. Pass this handle to {@link + * CredentialManager#getCredential(PendingGetCredentialHandle, Activity, CancellationSignal, + * Executor, OutcomeReceiver)} to perform the remaining flows to officially retrieve a + * credential. + */ + public static final class PendingGetCredentialHandle { + @NonNull + private final CredentialManager.GetCredentialTransportPendingUseCase + mGetCredentialTransport; + /** + * The pending intent to be launched to finalize the user credential. If null, the callback + * will fail with {@link GetCredentialException#TYPE_NO_CREDENTIAL}. + */ + @Nullable + private final PendingIntent mPendingIntent; + + /** @hide */ + PendingGetCredentialHandle( + @NonNull CredentialManager.GetCredentialTransportPendingUseCase transport, + @Nullable PendingIntent pendingIntent) { + mGetCredentialTransport = transport; + mPendingIntent = pendingIntent; + } + + /** @hide */ + void show(@NonNull Activity activity, @Nullable CancellationSignal cancellationSignal, + @CallbackExecutor @NonNull Executor executor, + @NonNull OutcomeReceiver callback) { + if (mPendingIntent == null) { + executor.execute(() -> callback.onError( + new GetCredentialException(GetCredentialException.TYPE_NO_CREDENTIAL))); + return; + } + + mGetCredentialTransport.setCallback(new GetPendingCredentialInternalCallback() { + @Override + public void onPendingIntent(PendingIntent pendingIntent) { + try { + activity.startIntentSender(pendingIntent.getIntentSender(), null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, "startIntentSender() failed for intent for show()", e); + executor.execute(() -> callback.onError( + new GetCredentialException(GetCredentialException.TYPE_UNKNOWN))); + } + } + + @Override + public void onResponse(GetCredentialResponse response) { + executor.execute(() -> callback.onResult(response)); + } + + @Override + public void onError(String errorType, String message) { + executor.execute( + () -> callback.onError(new GetCredentialException(errorType, message))); + } + }); + + try { + activity.startIntentSender(mPendingIntent.getIntentSender(), null, 0, 0, 0); + } catch (IntentSender.SendIntentException e) { + Log.e(TAG, "startIntentSender() failed for intent for show()", e); + executor.execute(() -> callback.onError( + new GetCredentialException(GetCredentialException.TYPE_UNKNOWN))); + } + } + } + private static final String TAG = "CredentialManager"; + + @NonNull private final PrepareGetCredentialResponseInternal mResponseInternal; + + @NonNull private final PendingGetCredentialHandle mPendingGetCredentialHandle; + + /** + * Returns true if the user has any candidate credentials for the given {@code credentialType}, + * and false otherwise. + */ + @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) + public boolean hasCredentialResults(@NonNull String credentialType) { + return mResponseInternal.hasCredentialResults(credentialType); + } + + /** + * Returns true if the user has any candidate authentication actions (locked credential + * supplier), and false otherwise. + */ + @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) + public boolean hasAuthenticationResults() { + return mResponseInternal.hasAuthenticationResults(); + } + + /** + * Returns true if the user has any candidate remote credential results, and false otherwise. + */ + @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) + public boolean hasRemoteResults() { + return mResponseInternal.hasRemoteResults(); + } + + /** + * Returns a handle that represents this pending get-credential operation. Pass this handle to + * {@link CredentialManager#getCredential(PendingGetCredentialHandle, Activity, + * CancellationSignal, Executor, OutcomeReceiver)} to perform the remaining flows to officially + * retrieve a credential. + */ + @NonNull + public PendingGetCredentialHandle getPendingGetCredentialHandle() { + return mPendingGetCredentialHandle; + } + + /** + * Constructs a {@link PrepareGetCredentialResponse}. + * + * @param responseInternal whether caller has the permission to query the credential + * result metadata + * @param getCredentialTransport the transport for the operation to finalaze a credential + * @hide + */ + protected PrepareGetCredentialResponse( + @NonNull PrepareGetCredentialResponseInternal responseInternal, + @NonNull CredentialManager.GetCredentialTransportPendingUseCase + getCredentialTransport) { + mResponseInternal = responseInternal; + mPendingGetCredentialHandle = new PendingGetCredentialHandle( + getCredentialTransport, responseInternal.getPendingIntent()); + } + + /** @hide */ + protected interface GetPendingCredentialInternalCallback { + void onPendingIntent(@NonNull PendingIntent pendingIntent); + + void onResponse(@NonNull GetCredentialResponse response); + + void onError(@NonNull String errorType, @Nullable String message); + } +} diff --git a/core/java/android/credentials/GetPendingCredentialResponse.aidl b/core/java/android/credentials/PrepareGetCredentialResponseInternal.aidl similarity index 93% rename from core/java/android/credentials/GetPendingCredentialResponse.aidl rename to core/java/android/credentials/PrepareGetCredentialResponseInternal.aidl index 1cdd637c75112..217dac8cd136a 100644 --- a/core/java/android/credentials/GetPendingCredentialResponse.aidl +++ b/core/java/android/credentials/PrepareGetCredentialResponseInternal.aidl @@ -16,4 +16,4 @@ package android.credentials; -parcelable GetPendingCredentialResponse; \ No newline at end of file +parcelable PrepareGetCredentialResponseInternal; \ No newline at end of file diff --git a/core/java/android/credentials/PrepareGetCredentialResponseInternal.java b/core/java/android/credentials/PrepareGetCredentialResponseInternal.java new file mode 100644 index 0000000000000..9afd44f09a8c5 --- /dev/null +++ b/core/java/android/credentials/PrepareGetCredentialResponseInternal.java @@ -0,0 +1,167 @@ +/* + * 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; + +import static android.Manifest.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.annotation.RequiresPermission; +import android.app.Activity; +import android.app.PendingIntent; +import android.os.CancellationSignal; +import android.os.OutcomeReceiver; +import android.os.Parcel; +import android.os.Parcelable; +import android.util.ArraySet; + +import java.util.Set; +import java.util.concurrent.Executor; + + +/** + * An internal response object that prefetches user app credentials and provides metadata about + * them. + * + * @hide + */ +public final class PrepareGetCredentialResponseInternal implements Parcelable { + private static final String TAG = "CredentialManager"; + + private final boolean mHasQueryApiPermission; + @Nullable + private final ArraySet mCredentialResultTypes; + private final boolean mHasAuthenticationResults; + private final boolean mHasRemoteResults; + /** + * The pending intent to be launched to finalize the user credential. If null, the callback + * will fail with {@link GetCredentialException#TYPE_NO_CREDENTIAL}. + */ + @Nullable + private final PendingIntent mPendingIntent; + + @Nullable + public PendingIntent getPendingIntent() { + return mPendingIntent; + } + + /** + * Returns true if the user has any candidate credentials for the given {@code credentialType}, + * and false otherwise. + */ + @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) + public boolean hasCredentialResults(@NonNull String credentialType) { + if (!mHasQueryApiPermission) { + throw new SecurityException( + "caller doesn't have the permission to query credential results"); + } + if (mCredentialResultTypes == null) { + return false; + } + return mCredentialResultTypes.contains(credentialType); + } + + /** + * Returns true if the user has any candidate authentication actions (locked credential + * supplier), and false otherwise. + */ + @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) + public boolean hasAuthenticationResults() { + if (!mHasQueryApiPermission) { + throw new SecurityException( + "caller doesn't have the permission to query authentication results"); + } + return mHasAuthenticationResults; + } + + /** + * Returns true if the user has any candidate remote credential results, and false otherwise. + */ + @RequiresPermission(CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS) + public boolean hasRemoteResults() { + if (!mHasQueryApiPermission) { + throw new SecurityException( + "caller doesn't have the permission to query remote results"); + } + return mHasRemoteResults; + } + + @Override + public void writeToParcel(@NonNull Parcel dest, int flags) { + dest.writeBoolean(mHasQueryApiPermission); + dest.writeArraySet(mCredentialResultTypes); + dest.writeBoolean(mHasAuthenticationResults); + dest.writeBoolean(mHasRemoteResults); + dest.writeTypedObject(mPendingIntent, flags); + } + + @Override + public int describeContents() { + return 0; + } + + /** + * Constructs a {@link PrepareGetCredentialResponseInternal}. + * + * @param hasQueryApiPermission whether caller has the permission to query the credential + * result metadata + * @param credentialResultTypes a set of credential types that each has candidate credentials + * found, or null if the caller doesn't have the permission to + * this information + * @param hasAuthenticationResults whether the user has any candidate authentication actions, or + * false if the caller doesn't have the permission to this + * information + * @param hasRemoteResults whether the user has any candidate remote options, or false + * if the caller doesn't have the permission to this information + * @param pendingIntent the pending intent to be launched during + * {@link #show(Activity, CancellationSignal, Executor, + * OutcomeReceiver)}} to + * finalize the user credential + * @hide + */ + public PrepareGetCredentialResponseInternal(boolean hasQueryApiPermission, + @Nullable Set credentialResultTypes, + boolean hasAuthenticationResults, boolean hasRemoteResults, + @Nullable PendingIntent pendingIntent) { + mHasQueryApiPermission = hasQueryApiPermission; + mCredentialResultTypes = new ArraySet<>(credentialResultTypes); + mHasAuthenticationResults = hasAuthenticationResults; + mHasRemoteResults = hasRemoteResults; + mPendingIntent = pendingIntent; + } + + private PrepareGetCredentialResponseInternal(@NonNull Parcel in) { + mHasQueryApiPermission = in.readBoolean(); + mCredentialResultTypes = (ArraySet) in.readArraySet(null); + mHasAuthenticationResults = in.readBoolean(); + mHasRemoteResults = in.readBoolean(); + mPendingIntent = in.readTypedObject(PendingIntent.CREATOR); + } + + public static final @NonNull Creator CREATOR = + new Creator<>() { + @Override + public PrepareGetCredentialResponseInternal[] newArray(int size) { + return new PrepareGetCredentialResponseInternal[size]; + } + + @Override + public PrepareGetCredentialResponseInternal createFromParcel(@NonNull Parcel in) { + return new PrepareGetCredentialResponseInternal(in); + } + }; +} diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml index 0f1e558c98e16..4c6272365fcd4 100644 --- a/core/res/AndroidManifest.xml +++ b/core/res/AndroidManifest.xml @@ -4493,6 +4493,12 @@ + + + mCredentialManager.getCredential(null, mMockActivity, null, mExecutor, + () -> mCredentialManager.getCredential(nullRequest, mMockActivity, null, mExecutor, result -> { })); } diff --git a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java index a15d66300eca1..90b92f43d80f9 100644 --- a/services/credentials/java/com/android/server/credentials/CredentialManagerService.java +++ b/services/credentials/java/com/android/server/credentials/CredentialManagerService.java @@ -42,8 +42,9 @@ import android.credentials.IClearCredentialStateCallback; import android.credentials.ICreateCredentialCallback; import android.credentials.ICredentialManager; import android.credentials.IGetCredentialCallback; -import android.credentials.IGetPendingCredentialCallback; +import android.credentials.IPrepareGetCredentialCallback; import android.credentials.ISetEnabledProvidersCallback; +import android.credentials.PrepareGetCredentialResponseInternal; import android.credentials.RegisterCredentialDescriptionRequest; import android.credentials.UnregisterCredentialDescriptionRequest; import android.credentials.ui.IntentFactory; @@ -307,6 +308,29 @@ public final class CredentialManagerService return providerSessions; } + @SuppressWarnings("GuardedBy") // ErrorProne requires initiateProviderSessionForRequestLocked + // to be guarded by 'service.mLock', which is the same as mLock. + private List initiateProviderSessionsWithActiveContainers( + PrepareGetRequestSession session, + Set> + activeCredentialContainers) { + List providerSessions = new ArrayList<>(); + for (Pair result : + activeCredentialContainers) { + ProviderSession providerSession = ProviderRegistryGetSession.createNewSession( + mContext, + UserHandle.getCallingUserId(), + session, + session.mClientAppInfo, + result.second.mPackageName, + result.first); + providerSessions.add(providerSession); + session.addProviderSession(providerSession.getComponentName(), providerSession); + } + return providerSessions; + } + + @NonNull private Set> getFilteredResultFromRegistry(List options) { @@ -443,19 +467,120 @@ public final class CredentialManagerService } @Override - public ICancellationSignal executeGetPendingCredential( + public ICancellationSignal executePrepareGetCredential( GetCredentialRequest request, - IGetPendingCredentialCallback callback, + IPrepareGetCredentialCallback prepareGetCredentialCallback, + IGetCredentialCallback getCredentialCallback, final String callingPackage) { - // TODO(b/273308895): implement - + final long timestampBegan = System.nanoTime(); ICancellationSignal cancelTransport = CancellationSignal.createTransport(); + if (request.getOrigin() != null) { + // Check privileged permissions + mContext.enforceCallingPermission(CREDENTIAL_MANAGER_SET_ORIGIN, null); + } enforcePermissionForAllowedProviders(request); + final int userId = UserHandle.getCallingUserId(); + final int callingUid = Binder.getCallingUid(); + enforceCallingPackage(callingPackage, callingUid); + + final PrepareGetRequestSession session = + new PrepareGetRequestSession( + getContext(), + userId, + callingUid, + prepareGetCredentialCallback, + getCredentialCallback, + request, + constructCallingAppInfo(callingPackage, userId, request.getOrigin()), + CancellationSignal.fromTransport(cancelTransport), + timestampBegan); + + processGetCredential(request, prepareGetCredentialCallback, session); + return cancelTransport; } + private void processGetCredential( + GetCredentialRequest request, + IPrepareGetCredentialCallback callback, + PrepareGetRequestSession session) { + List providerSessions; + + if (isCredentialDescriptionApiEnabled()) { + List optionsThatRequireActiveCredentials = + request.getCredentialOptions().stream() + .filter( + getCredentialOption -> + !TextUtils.isEmpty( + getCredentialOption + .getCredentialRetrievalData() + .getString( + CredentialOption + .FLATTENED_REQUEST, + null))) + .toList(); + + List optionsThatDoNotRequireActiveCredentials = + request.getCredentialOptions().stream() + .filter( + getCredentialOption -> + TextUtils.isEmpty( + getCredentialOption + .getCredentialRetrievalData() + .getString( + CredentialOption + .FLATTENED_REQUEST, + null))) + .toList(); + + List sessionsWithoutRemoteService = + initiateProviderSessionsWithActiveContainers( + session, + getFilteredResultFromRegistry(optionsThatRequireActiveCredentials)); + + List sessionsWithRemoteService = + initiateProviderSessions( + session, + optionsThatDoNotRequireActiveCredentials.stream() + .map(CredentialOption::getType) + .collect(Collectors.toList())); + + Set all = new LinkedHashSet<>(); + all.addAll(sessionsWithRemoteService); + all.addAll(sessionsWithoutRemoteService); + + providerSessions = new ArrayList<>(all); + } else { + // Initiate all provider sessions + providerSessions = + initiateProviderSessions( + session, + request.getCredentialOptions().stream() + .map(CredentialOption::getType) + .collect(Collectors.toList())); + } + + if (providerSessions.isEmpty()) { + try { + // TODO: fix + callback.onResponse(new PrepareGetCredentialResponseInternal( + false, null, false, false, null)); + } catch (RemoteException e) { + Log.i( + TAG, + "Issue invoking onError on IGetCredentialCallback " + + "callback: " + + e.getMessage()); + } + } + + finalizeAndEmitInitialPhaseMetric(session); + // TODO(b/271135048) - May still be worth emitting in the empty cases above. + providerSessions.forEach(ProviderSession::invokeSession); + } + private void processGetCredential( GetCredentialRequest request, IGetCredentialCallback callback, diff --git a/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java new file mode 100644 index 0000000000000..9165901ad81e9 --- /dev/null +++ b/services/credentials/java/com/android/server/credentials/PrepareGetRequestSession.java @@ -0,0 +1,306 @@ +/* + * 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 com.android.server.credentials; + +import android.annotation.Nullable; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.credentials.CredentialOption; +import android.credentials.CredentialProviderInfo; +import android.credentials.GetCredentialException; +import android.credentials.GetCredentialRequest; +import android.credentials.GetCredentialResponse; +import android.credentials.IGetCredentialCallback; +import android.credentials.IPrepareGetCredentialCallback; +import android.credentials.PrepareGetCredentialResponseInternal; +import android.credentials.ui.ProviderData; +import android.credentials.ui.RequestInfo; +import android.os.CancellationSignal; +import android.os.RemoteException; +import android.service.credentials.CallingAppInfo; +import android.util.Log; + +import com.android.server.credentials.metrics.ApiName; +import com.android.server.credentials.metrics.ProviderStatusForMetrics; + +import java.util.ArrayList; +import java.util.stream.Collectors; + +/** + * Central session for a single prepareGetCredentials request. This class listens to the + * responses from providers, and the UX app, and updates the provider(S) state. + */ +public class PrepareGetRequestSession extends RequestSession + implements ProviderSession.ProviderInternalCallback { + private static final String TAG = "GetRequestSession"; + + private final IPrepareGetCredentialCallback mPrepareGetCredentialCallback; + private boolean mIsInitialQuery = true; + + public PrepareGetRequestSession(Context context, int userId, int callingUid, + IPrepareGetCredentialCallback prepareGetCredentialCallback, + IGetCredentialCallback getCredCallback, GetCredentialRequest request, + CallingAppInfo callingAppInfo, CancellationSignal cancellationSignal, + long startedTimestamp) { + super(context, userId, callingUid, request, getCredCallback, RequestInfo.TYPE_GET, + callingAppInfo, cancellationSignal, startedTimestamp); + int numTypes = (request.getCredentialOptions().stream() + .map(CredentialOption::getType).collect( + Collectors.toSet())).size(); // Dedupe type strings + setupInitialPhaseMetric(ApiName.GET_CREDENTIAL.getMetricCode(), numTypes); + mPrepareGetCredentialCallback = prepareGetCredentialCallback; + } + + /** + * Creates a new provider session, and adds it list of providers that are contributing to + * this session. + * + * @return the provider session created within this request session, for the given provider + * info. + */ + @Override + @Nullable + public ProviderSession initiateProviderSession(CredentialProviderInfo providerInfo, + RemoteCredentialService remoteCredentialService) { + ProviderGetSession providerGetSession = ProviderGetSession + .createNewSession(mContext, mUserId, providerInfo, + this, remoteCredentialService); + if (providerGetSession != null) { + Log.i(TAG, "In startProviderSession - provider session created and being added"); + mProviders.put(providerGetSession.getComponentName().flattenToString(), + providerGetSession); + } + return providerGetSession; + } + + @Override + protected void launchUiWithProviderData(ArrayList providerDataList) { + mChosenProviderFinalPhaseMetric.setUiCallStartTimeNanoseconds(System.nanoTime()); + try { + mClientCallback.onPendingIntent(mCredentialManagerUi.createPendingIntent( + RequestInfo.newGetRequestInfo( + mRequestId, mClientRequest, mClientAppInfo.getPackageName()), + providerDataList)); + } catch (RemoteException e) { + mChosenProviderFinalPhaseMetric.setUiReturned(false); + respondToClientWithErrorAndFinish( + GetCredentialException.TYPE_UNKNOWN, "Unable to instantiate selector"); + } + } + + @Override + public void onFinalResponseReceived(ComponentName componentName, + @Nullable GetCredentialResponse response) { + mChosenProviderFinalPhaseMetric.setUiReturned(true); + mChosenProviderFinalPhaseMetric.setUiCallEndTimeNanoseconds(System.nanoTime()); + Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString()); + setChosenMetric(componentName); + if (response != null) { + mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + ProviderStatusForMetrics.FINAL_SUCCESS.getMetricCode()); + respondToClientWithResponseAndFinish(response); + } else { + mChosenProviderFinalPhaseMetric.setChosenProviderStatus( + ProviderStatusForMetrics.FINAL_FAILURE.getMetricCode()); + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, + "Invalid response from provider"); + } + } + + //TODO: Try moving the three error & response methods below to RequestSession to be shared + // between get & create. + @Override + public void onFinalErrorReceived(ComponentName componentName, String errorType, + String message) { + respondToClientWithErrorAndFinish(errorType, message); + } + + private void respondToClientWithResponseAndFinish(GetCredentialResponse response) { + if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { + Log.i(TAG, "Request has already been completed. This is strange."); + return; + } + if (isSessionCancelled()) { +// TODO: properly log the new api +// logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ +// ApiStatus.CLIENT_CANCELED); + finishSession(/*propagateCancellation=*/true); + return; + } + try { + mClientCallback.onResponse(response); +// TODO: properly log the new api +// logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ +// ApiStatus.SUCCESS); + } catch (RemoteException e) { + Log.i(TAG, "Issue while responding to client with a response : " + e.getMessage()); +// TODO: properly log the new api +// logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ +// ApiStatus.FAILURE); + } + finishSession(/*propagateCancellation=*/false); + } + + private void respondToClientWithErrorAndFinish(String errorType, String errorMsg) { + if (mRequestSessionStatus == RequestSessionStatus.COMPLETE) { + Log.i(TAG, "Request has already been completed. This is strange."); + return; + } + if (isSessionCancelled()) { +// TODO: properly log the new api +// logApiCall(ApiName.GET_CREDENTIAL, /* apiStatus */ +// ApiStatus.CLIENT_CANCELED); + finishSession(/*propagateCancellation=*/true); + return; + } + + try { + mClientCallback.onError(errorType, errorMsg); + } catch (RemoteException e) { + Log.i(TAG, "Issue while responding to client with error : " + e.getMessage()); + } + logFailureOrUserCancel(errorType); + finishSession(/*propagateCancellation=*/false); + } + + private void logFailureOrUserCancel(String errorType) { + if (GetCredentialException.TYPE_USER_CANCELED.equals(errorType)) { +// TODO: properly log the new api +// logApiCall(ApiName.GET_CREDENTIAL, +// /* apiStatus */ ApiStatus.USER_CANCELED); + } else { +// TODO: properly log the new api +// logApiCall(ApiName.GET_CREDENTIAL, +// /* apiStatus */ ApiStatus.FAILURE); + } + } + + @Override + public void onUiCancellation(boolean isUserCancellation) { + if (isUserCancellation) { + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_USER_CANCELED, + "User cancelled the selector"); + } else { + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_INTERRUPTED, + "The UI was interrupted - please try again."); + } + } + + @Override + public void onUiSelectorInvocationFailure() { + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, + "No credentials available."); + } + + @Override + public void onProviderStatusChanged(ProviderSession.Status status, + ComponentName componentName) { + Log.i(TAG, "in onStatusChanged with status: " + status); + // Auth entry was selected, and it did not have any underlying credentials + if (status == ProviderSession.Status.NO_CREDENTIALS_FROM_AUTH_ENTRY) { + handleEmptyAuthenticationSelection(componentName); + return; + } + // For any other status, we check if all providers are done and then invoke UI if needed + if (!isAnyProviderPending()) { + // If all provider responses have been received, we can either need the UI, + // or we need to respond with error. The only other case is the entry being + // selected after the UI has been invoked which has a separate code path. + if (isUiInvocationNeeded()) { + if (mIsInitialQuery) { + try { + mPrepareGetCredentialCallback.onResponse( + new PrepareGetCredentialResponseInternal( + false, null, false, false, getUiIntent())); + } catch (Exception e) { + Log.e(TAG, "EXCEPTION while mPendingCallback.onResponse", e); + } + mIsInitialQuery = false; + } else { + getProviderDataAndInitiateUi(); + } + } else { + if (mIsInitialQuery) { + try { + mPrepareGetCredentialCallback.onResponse( + new PrepareGetCredentialResponseInternal( + false, null, false, false, null)); + } catch (Exception e) { + Log.e(TAG, "EXCEPTION while mPendingCallback.onResponse", e); + } + mIsInitialQuery = false; + // TODO(273308895): should also clear session here + } else { + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, + "No credentials available"); + } + } + } + } + + private PendingIntent getUiIntent() { + ArrayList providerDataList = new ArrayList<>(); + for (ProviderSession session : mProviders.values()) { + Log.i(TAG, "preparing data for : " + session.getComponentName()); + ProviderData providerData = session.prepareUiData(); + if (providerData != null) { + Log.i(TAG, "Provider data is not null"); + providerDataList.add(providerData); + } + } + if (!providerDataList.isEmpty()) { + return mCredentialManagerUi.createPendingIntent( + RequestInfo.newGetRequestInfo( + mRequestId, mClientRequest, mClientAppInfo.getPackageName()), + providerDataList); + } else { + return null; + } + } + + private void handleEmptyAuthenticationSelection(ComponentName componentName) { + // Update auth entry statuses across different provider sessions + mProviders.keySet().forEach(key -> { + ProviderGetSession session = (ProviderGetSession) mProviders.get(key); + if (!session.mComponentName.equals(componentName)) { + session.updateAuthEntriesStatusFromAnotherSession(); + } + }); + + // Invoke UI since it needs to show a snackbar if last auth entry, or a status on each + // auth entries along with other valid entries + getProviderDataAndInitiateUi(); + + // Respond to client if all auth entries are empty and nothing else to show on the UI + if (providerDataContainsEmptyAuthEntriesOnly()) { + respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL, + "No credentials available"); + } + } + + private boolean providerDataContainsEmptyAuthEntriesOnly() { + for (String key : mProviders.keySet()) { + ProviderGetSession session = (ProviderGetSession) mProviders.get(key); + if (!session.containsEmptyAuthEntriesOnly()) { + return false; + } + } + return true; + } +} diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java index 95b0ff024bfc6..d17e98439978c 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java @@ -119,6 +119,40 @@ public final class ProviderGetSession extends ProviderSession beginGetOptionToCredentialOptionMap = + new HashMap<>(); + return new ProviderGetSession( + context, + providerInfo, + getRequestSession, + userId, + remoteCredentialService, + constructQueryPhaseRequest( + filteredRequest, getRequestSession.mClientAppInfo, + getRequestSession.mClientRequest.alwaysSendAppInfoToProvider(), + beginGetOptionToCredentialOptionMap), + filteredRequest, + getRequestSession.mClientAppInfo, + beginGetOptionToCredentialOptionMap, + getRequestSession.mHybridService + ); + } + Log.i(TAG, "Unable to create provider session"); + return null; + } + private static BeginGetCredentialRequest constructQueryPhaseRequest( android.credentials.GetCredentialRequest filteredRequest, CallingAppInfo callingAppInfo, diff --git a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java index a5196c5197401..85c78445e66b1 100644 --- a/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java +++ b/services/credentials/java/com/android/server/credentials/ProviderRegistryGetSession.java @@ -76,6 +76,24 @@ public class ProviderRegistryGetSession extends ProviderSession mUiCredentialEntries = new HashMap<>(); @NonNull @@ -106,6 +124,23 @@ public class ProviderRegistryGetSession extends ProviderSession prepareUiCredentialEntries( @NonNull List credentialEntries) { Log.i(TAG, "in prepareUiProviderDataWithCredentials");