Add request to authentication entry pending intent

Test: Built & deployed locally
Bug: 264943831

Change-Id: I76f5fc493c5ad6960c19f3b80429dd45b4c4840f
This commit is contained in:
Reema Bajwa
2023-01-08 20:05:50 +00:00
parent a4597637e6
commit aa1b7e44df
10 changed files with 156 additions and 57 deletions

View File

@@ -39938,6 +39938,7 @@ package android.service.credentials {
method @NonNull public final android.os.IBinder onBind(@NonNull android.content.Intent);
method public abstract void onClearCredentialState(@NonNull android.service.credentials.ClearCredentialStateRequest, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<java.lang.Void,android.credentials.ClearCredentialStateException>);
field public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";
field public static final String EXTRA_BEGIN_GET_CREDENTIAL_REQUEST = "android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST";
field public static final String EXTRA_CREATE_CREDENTIAL_EXCEPTION = "android.service.credentials.extra.CREATE_CREDENTIAL_EXCEPTION";
field public static final String EXTRA_CREATE_CREDENTIAL_REQUEST = "android.service.credentials.extra.CREATE_CREDENTIAL_REQUEST";
field public static final String EXTRA_CREATE_CREDENTIAL_RESPONSE = "android.service.credentials.extra.CREATE_CREDENTIAL_RESPONSE";

View File

@@ -88,6 +88,7 @@ public final class Entry implements Parcelable {
/** Constructor to be used for an entry that requires a pending intent to be invoked
* when clicked.
*/
// TODO: Remove this constructor as it is no longer used
public Entry(@NonNull String key, @NonNull String subkey, @NonNull Slice slice,
@NonNull PendingIntent pendingIntent, @NonNull Intent intent) {
this(key, subkey, slice);

View File

@@ -125,6 +125,33 @@ public abstract class CredentialProviderService extends Service {
public static final String EXTRA_CREATE_CREDENTIAL_EXCEPTION =
"android.service.credentials.extra.CREATE_CREDENTIAL_EXCEPTION";
/**
* Intent extra: The {@link BeginGetCredentialRequest} attached with
* the {@code pendingIntent} that is invoked when the user selects an
* authentication entry (intending to unlock the provider app) on the UI.
*
* <p>When a provider app receives a {@link BeginGetCredentialRequest} through the
* {@link CredentialProviderService#onBeginGetCredential} call, it can construct the
* {@link BeginGetCredentialResponse} with either an authentication {@link Action} (if the app
* is locked), or a {@link CredentialsResponseContent} (if the app is unlocked). In the former
* case, i.e. the app is locked, user will be shown the authentication action. When selected,
* the underlying {@link PendingIntent} will be invoked which will lead the user to provider's
* unlock activity. This pending intent will also contain the original
* {@link BeginGetCredentialRequest} to be retrieved and processed after the unlock
* flow is complete.
*
* <p>After the app is unlocked, the {@link BeginGetCredentialResponse} must be constructed
* using a {@link CredentialsResponseContent}, which must be set on an {@link Intent} as an
* intent extra against CredentialProviderService#EXTRA_CREDENTIALS_RESPONSE_CONTENT}.
* This intent should then be set as a result through {@link android.app.Activity#setResult}
* before finishing the activity.
*
* <p>
* Type: {@link BeginGetCredentialRequest}
*/
public static final String EXTRA_BEGIN_GET_CREDENTIAL_REQUEST =
"android.service.credentials.extra.BEGIN_GET_CREDENTIAL_REQUEST";
private static final String TAG = "CredProviderService";
public static final String CAPABILITY_META_DATA_KEY = "android.credentials.capabilities";

View File

@@ -44,6 +44,7 @@ import com.android.credentialmanager.jetpack.developer.CreatePasswordRequest
import com.android.credentialmanager.jetpack.developer.CreatePublicKeyCredentialRequest
import com.android.credentialmanager.jetpack.developer.PublicKeyCredential.Companion.TYPE_PUBLIC_KEY_CREDENTIAL
import com.android.credentialmanager.jetpack.provider.Action
import com.android.credentialmanager.jetpack.provider.AuthenticationAction
import com.android.credentialmanager.jetpack.provider.CredentialCountInformation
import com.android.credentialmanager.jetpack.provider.CredentialEntry
import com.android.credentialmanager.jetpack.provider.CreateEntry
@@ -140,16 +141,20 @@ class GetFlowUtils {
providerIcon: Drawable,
authEntry: Entry?,
): AuthenticationEntryInfo? {
// TODO: should also call fromSlice after getting the official jetpack code.
if (authEntry == null) {
return null
}
val authStructuredEntry = AuthenticationAction.fromSlice(
authEntry!!.slice)
if (authStructuredEntry == null) {
return null
}
return AuthenticationEntryInfo(
providerId = providerId,
entryKey = authEntry.key,
entrySubkey = authEntry.subkey,
pendingIntent = authEntry.pendingIntent,
pendingIntent = authStructuredEntry.pendingIntent,
fillInIntent = authEntry.frameworkExtrasIntent,
title = providerDisplayName,
icon = providerIcon,

View File

@@ -0,0 +1,55 @@
/*
* 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.credentialmanager.jetpack.provider
import android.app.PendingIntent
import android.app.slice.Slice
import android.util.Log
import androidx.annotation.VisibleForTesting
/**
* UI representation for a credential entry used during the get credential flow.
*
* TODO: move to jetpack.
*/
class AuthenticationAction constructor(
val pendingIntent: PendingIntent
) {
companion object {
private const val TAG = "AuthenticationAction"
@VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
internal const val SLICE_HINT_PENDING_INTENT =
"androidx.credentials.provider.authenticationAction.SLICE_HINT_PENDING_INTENT"
@JvmStatic
fun fromSlice(slice: Slice): AuthenticationAction? {
slice.items.forEach {
if (it.hasHint(SLICE_HINT_PENDING_INTENT)) {
return try {
AuthenticationAction(it.action)
} catch (e: Exception) {
Log.i(TAG, "fromSlice failed with: " + e.message)
null
}
}
}
return null
}
}
}

View File

@@ -84,14 +84,12 @@ public final class ClearRequestSession extends RequestSession<ClearCredentialSta
respondToClientWithResponseAndFinish();
}
@Override
protected void onProviderResponseComplete(ComponentName componentName) {
if (!isAnyProviderPending()) {
onFinalResponseReceived(componentName, null);
}
}
@Override
protected void onProviderTerminated(ComponentName componentName) {
if (!isAnyProviderPending()) {
processResponses();

View File

@@ -86,12 +86,6 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR
}
}
@Override
public void onProviderStatusChanged(ProviderSession.Status status,
ComponentName componentName) {
super.onProviderStatusChanged(status, componentName);
}
@Override
public void onFinalResponseReceived(ComponentName componentName,
@Nullable CreateCredentialResponse response) {
@@ -136,4 +130,22 @@ public final class CreateRequestSession extends RequestSession<CreateCredentialR
}
finishSession();
}
@Override
public void onProviderStatusChanged(ProviderSession.Status status,
ComponentName componentName) {
Log.i(TAG, "in onProviderStatusChanged with status: " + status);
// 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 (!isAnyProviderPending()) {
if (isUiInvocationNeeded()) {
Log.i(TAG, "in onProviderStatusChanged - isUiInvocationNeeded");
getProviderDataAndInitiateUi();
} else {
respondToClientWithErrorAndFinish(CreateCredentialException.TYPE_NO_CREDENTIAL,
"No credentials available");
}
}
}
}

View File

@@ -81,12 +81,6 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest
}
}
@Override // from provider session
public void onProviderStatusChanged(ProviderSession.Status status,
ComponentName componentName) {
super.onProviderStatusChanged(status, componentName);
}
@Override
public void onFinalResponseReceived(ComponentName componentName,
@Nullable GetCredentialResponse response) {
@@ -132,4 +126,22 @@ public final class GetRequestSession extends RequestSession<GetCredentialRequest
respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL,
"User cancelled the selector");
}
@Override
public void onProviderStatusChanged(ProviderSession.Status status,
ComponentName componentName) {
Log.i(TAG, "in onStatusChanged with status: " + status);
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()) {
Log.i(TAG, "in onProviderStatusChanged - isUiInvocationNeeded");
getProviderDataAndInitiateUi();
} else {
respondToClientWithErrorAndFinish(GetCredentialException.TYPE_NO_CREDENTIAL,
"No credentials available");
}
}
}
}

View File

@@ -275,7 +275,8 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
String entryId = generateEntryId();
Entry authEntry = new Entry(
AUTHENTICATION_ACTION_ENTRY_KEY, entryId,
authenticationAction.getSlice());
authenticationAction.getSlice(),
setUpFillInIntentForAuthentication());
mUiAuthenticationAction = new Pair<>(entryId, authenticationAction);
return authEntry;
}
@@ -311,6 +312,15 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
return intent;
}
private Intent setUpFillInIntentForAuthentication() {
Intent intent = new Intent();
intent.putExtra(
CredentialProviderService
.EXTRA_BEGIN_GET_CREDENTIAL_REQUEST,
mProviderRequest);
return intent;
}
private List<Entry> prepareUiActionEntries(@Nullable List<Action> actions) {
List<Entry> actionEntries = new ArrayList<>();
for (Action action : actions) {
@@ -365,7 +375,6 @@ public final class ProviderGetSession extends ProviderSession<BeginGetCredential
private void onAuthenticationEntrySelected(
@Nullable ProviderPendingIntentResponse providerPendingIntentResponse) {
//TODO: Other provider intent statuses
// Check if pending intent has an error
GetCredentialException exception = maybeGetPendingIntentException(
providerPendingIntentResponse);
if (exception != null) {

View File

@@ -18,7 +18,6 @@ package com.android.server.credentials;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.content.ComponentName;
import android.content.Context;
import android.credentials.ui.ProviderData;
import android.credentials.ui.UserSelectionDialogResult;
@@ -98,41 +97,6 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan
finishSession();
}
protected void onProviderStatusChanged(ProviderSession.Status status,
ComponentName componentName) {
Log.i(TAG, "in onStatusChanged with status: " + status);
if (ProviderSession.isTerminatingStatus(status)) {
Log.i(TAG, "in onStatusChanged terminating status");
onProviderTerminated(componentName);
//TODO: Check if this was the provider we were waiting for and can invoke the UI now
} else if (ProviderSession.isCompletionStatus(status)) {
Log.i(TAG, "in onStatusChanged isCompletionStatus status");
onProviderResponseComplete(componentName);
} else if (ProviderSession.isUiInvokingStatus(status)) {
Log.i(TAG, "in onStatusChanged isUiInvokingStatus status");
onProviderResponseRequiresUi();
}
}
protected void onProviderTerminated(ComponentName componentName) {
//TODO: Implement
}
protected void onProviderResponseComplete(ComponentName componentName) {
//TODO: Implement
}
protected void onProviderResponseRequiresUi() {
Log.i(TAG, "in onProviderResponseComplete");
// TODO: Determine whether UI has already been invoked, and deal accordingly
if (!isAnyProviderPending()) {
Log.i(TAG, "in onProviderResponseComplete - isResponseCompleteAcrossProviders");
getProviderDataAndInitiateUi();
} else {
Log.i(TAG, "Can't invoke UI - waiting on some providers");
}
}
protected void finishSession() {
Log.i(TAG, "finishing session");
clearProviderSessions();
@@ -144,7 +108,7 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan
mProviders.clear();
}
boolean isAnyProviderPending() {
protected boolean isAnyProviderPending() {
for (ProviderSession session : mProviders.values()) {
if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
return true;
@@ -153,7 +117,22 @@ abstract class RequestSession<T, U> implements CredentialManagerUi.CredentialMan
return false;
}
private void getProviderDataAndInitiateUi() {
/**
* Returns true if at least one provider is ready for UI invocation, and no
* provider is pending a response.
*/
boolean isUiInvocationNeeded() {
for (ProviderSession session : mProviders.values()) {
if (ProviderSession.isUiInvokingStatus(session.getStatus())) {
return true;
} else if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
return false;
}
}
return false;
}
void getProviderDataAndInitiateUi() {
Log.i(TAG, "In getProviderDataAndInitiateUi");
Log.i(TAG, "In getProviderDataAndInitiateUi providers size: " + mProviders.size());