[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
This commit is contained in:
@@ -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<java.lang.Void,android.credentials.ClearCredentialStateException>);
|
||||
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<android.credentials.CreateCredentialResponse,android.credentials.CreateCredentialException>);
|
||||
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<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>);
|
||||
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<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>);
|
||||
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<android.credentials.PrepareGetCredentialResponse,android.credentials.GetCredentialException>);
|
||||
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<android.credentials.GetCredentialResponse> 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<android.credentials.CredentialDescription>);
|
||||
|
||||
@@ -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}.
|
||||
*
|
||||
* <p>The execution can potentially launch UI flows to collect user consent to using a
|
||||
* credential, display a picker when multiple credentials exist, etc.
|
||||
*
|
||||
* <p>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<GetCredentialResponse, GetCredentialException> 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.
|
||||
*
|
||||
* <p>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<GetPendingCredentialResponse, GetCredentialException> callback) {
|
||||
OutcomeReceiver<PrepareGetCredentialResponse, GetCredentialException> 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)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<GetCredentialResponse, GetCredentialException> 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<GetPendingCredentialResponse> CREATOR = new Creator<>() {
|
||||
@Override
|
||||
public GetPendingCredentialResponse[] newArray(int size) {
|
||||
return new GetPendingCredentialResponse[size];
|
||||
}
|
||||
|
||||
@Override
|
||||
public GetPendingCredentialResponse createFromParcel(@NonNull Parcel in) {
|
||||
return new GetPendingCredentialResponse(in);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
180
core/java/android/credentials/PrepareGetCredentialResponse.java
Normal file
180
core/java/android/credentials/PrepareGetCredentialResponse.java
Normal file
@@ -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<GetCredentialResponse, GetCredentialException> 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);
|
||||
}
|
||||
}
|
||||
@@ -16,4 +16,4 @@
|
||||
|
||||
package android.credentials;
|
||||
|
||||
parcelable GetPendingCredentialResponse;
|
||||
parcelable PrepareGetCredentialResponseInternal;
|
||||
@@ -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<String> 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<String> 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<String>) in.readArraySet(null);
|
||||
mHasAuthenticationResults = in.readBoolean();
|
||||
mHasRemoteResults = in.readBoolean();
|
||||
mPendingIntent = in.readTypedObject(PendingIntent.CREATOR);
|
||||
}
|
||||
|
||||
public static final @NonNull Creator<PrepareGetCredentialResponseInternal> CREATOR =
|
||||
new Creator<>() {
|
||||
@Override
|
||||
public PrepareGetCredentialResponseInternal[] newArray(int size) {
|
||||
return new PrepareGetCredentialResponseInternal[size];
|
||||
}
|
||||
|
||||
@Override
|
||||
public PrepareGetCredentialResponseInternal createFromParcel(@NonNull Parcel in) {
|
||||
return new PrepareGetCredentialResponseInternal(in);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -4493,6 +4493,12 @@
|
||||
<permission android:name="android.permission.CREDENTIAL_MANAGER_SET_ORIGIN"
|
||||
android:protectionLevel="normal" />
|
||||
|
||||
<!-- Allows a browser to invoke the set of query apis to get metadata about credential
|
||||
candidates prepared during the CredentialManager.prepareGetCredential API.
|
||||
<p>Protection level: normal -->
|
||||
<permission android:name="android.permission.CREDENTIAL_MANAGER_QUERY_CANDIDATE_CREDENTIALS"
|
||||
android:protectionLevel="normal" />
|
||||
|
||||
<!-- Allows permission to use Credential Manager UI for providing and saving credentials
|
||||
@hide -->
|
||||
<permission android:name="android.permission.LAUNCH_CREDENTIAL_SELECTOR"
|
||||
|
||||
@@ -141,8 +141,9 @@ public class CredentialManagerTest {
|
||||
|
||||
@Test
|
||||
public void testGetCredential_nullRequest() {
|
||||
GetCredentialRequest nullRequest = null;
|
||||
assertThrows(NullPointerException.class,
|
||||
() -> mCredentialManager.getCredential(null, mMockActivity, null, mExecutor,
|
||||
() -> mCredentialManager.getCredential(nullRequest, mMockActivity, null, mExecutor,
|
||||
result -> {
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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<ProviderSession> initiateProviderSessionsWithActiveContainers(
|
||||
PrepareGetRequestSession session,
|
||||
Set<Pair<CredentialOption, CredentialDescriptionRegistry.FilterResult>>
|
||||
activeCredentialContainers) {
|
||||
List<ProviderSession> providerSessions = new ArrayList<>();
|
||||
for (Pair<CredentialOption, CredentialDescriptionRegistry.FilterResult> 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<Pair<CredentialOption, CredentialDescriptionRegistry.FilterResult>>
|
||||
getFilteredResultFromRegistry(List<CredentialOption> 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<ProviderSession> providerSessions;
|
||||
|
||||
if (isCredentialDescriptionApiEnabled()) {
|
||||
List<CredentialOption> optionsThatRequireActiveCredentials =
|
||||
request.getCredentialOptions().stream()
|
||||
.filter(
|
||||
getCredentialOption ->
|
||||
!TextUtils.isEmpty(
|
||||
getCredentialOption
|
||||
.getCredentialRetrievalData()
|
||||
.getString(
|
||||
CredentialOption
|
||||
.FLATTENED_REQUEST,
|
||||
null)))
|
||||
.toList();
|
||||
|
||||
List<CredentialOption> optionsThatDoNotRequireActiveCredentials =
|
||||
request.getCredentialOptions().stream()
|
||||
.filter(
|
||||
getCredentialOption ->
|
||||
TextUtils.isEmpty(
|
||||
getCredentialOption
|
||||
.getCredentialRetrievalData()
|
||||
.getString(
|
||||
CredentialOption
|
||||
.FLATTENED_REQUEST,
|
||||
null)))
|
||||
.toList();
|
||||
|
||||
List<ProviderSession> sessionsWithoutRemoteService =
|
||||
initiateProviderSessionsWithActiveContainers(
|
||||
session,
|
||||
getFilteredResultFromRegistry(optionsThatRequireActiveCredentials));
|
||||
|
||||
List<ProviderSession> sessionsWithRemoteService =
|
||||
initiateProviderSessions(
|
||||
session,
|
||||
optionsThatDoNotRequireActiveCredentials.stream()
|
||||
.map(CredentialOption::getType)
|
||||
.collect(Collectors.toList()));
|
||||
|
||||
Set<ProviderSession> 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,
|
||||
|
||||
@@ -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<GetCredentialRequest,
|
||||
IGetCredentialCallback>
|
||||
implements ProviderSession.ProviderInternalCallback<GetCredentialResponse> {
|
||||
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<ProviderData> 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<ProviderData> 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;
|
||||
}
|
||||
}
|
||||
@@ -119,6 +119,40 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
|
||||
return null;
|
||||
}
|
||||
|
||||
/** Creates a new provider session to be used by the request session. */
|
||||
@Nullable public static ProviderGetSession createNewSession(
|
||||
Context context,
|
||||
@UserIdInt int userId,
|
||||
CredentialProviderInfo providerInfo,
|
||||
PrepareGetRequestSession getRequestSession,
|
||||
RemoteCredentialService remoteCredentialService) {
|
||||
android.credentials.GetCredentialRequest filteredRequest =
|
||||
filterOptions(providerInfo.getCapabilities(),
|
||||
getRequestSession.mClientRequest,
|
||||
providerInfo.getComponentName());
|
||||
if (filteredRequest != null) {
|
||||
Map<String, CredentialOption> 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,
|
||||
|
||||
@@ -76,6 +76,24 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption
|
||||
requestOption);
|
||||
}
|
||||
|
||||
/** Creates a new provider session to be used by the request session. */
|
||||
@Nullable
|
||||
public static ProviderRegistryGetSession createNewSession(
|
||||
@NonNull Context context,
|
||||
@UserIdInt int userId,
|
||||
@NonNull PrepareGetRequestSession getRequestSession,
|
||||
@NonNull CallingAppInfo callingAppInfo,
|
||||
@NonNull String credentialProviderPackageName,
|
||||
@NonNull CredentialOption requestOption) {
|
||||
return new ProviderRegistryGetSession(
|
||||
context,
|
||||
userId,
|
||||
getRequestSession,
|
||||
callingAppInfo,
|
||||
credentialProviderPackageName,
|
||||
requestOption);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
private final Map<String, CredentialEntry> mUiCredentialEntries = new HashMap<>();
|
||||
@NonNull
|
||||
@@ -106,6 +124,23 @@ public class ProviderRegistryGetSession extends ProviderSession<CredentialOption
|
||||
.getString(CredentialOption.FLATTENED_REQUEST);
|
||||
}
|
||||
|
||||
protected ProviderRegistryGetSession(@NonNull Context context,
|
||||
@NonNull int userId,
|
||||
@NonNull PrepareGetRequestSession session,
|
||||
@NonNull CallingAppInfo callingAppInfo,
|
||||
@NonNull String servicePackageName,
|
||||
@NonNull CredentialOption requestOption) {
|
||||
super(context, requestOption, session,
|
||||
new ComponentName(servicePackageName, servicePackageName) ,
|
||||
userId, null);
|
||||
mCredentialDescriptionRegistry = CredentialDescriptionRegistry.forUser(userId);
|
||||
mCallingAppInfo = callingAppInfo;
|
||||
mCredentialProviderPackageName = servicePackageName;
|
||||
mFlattenedRequestOptionString = requestOption
|
||||
.getCredentialRetrievalData()
|
||||
.getString(CredentialOption.FLATTENED_REQUEST);
|
||||
}
|
||||
|
||||
private List<Entry> prepareUiCredentialEntries(
|
||||
@NonNull List<CredentialEntry> credentialEntries) {
|
||||
Log.i(TAG, "in prepareUiProviderDataWithCredentials");
|
||||
|
||||
Reference in New Issue
Block a user