diff --git a/core/api/current.txt b/core/api/current.txt index 46989a44a8998..4f6ccff454a14 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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); 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"; diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java index 9f2edae6d8e40..37a5724258231 100644 --- a/core/java/android/credentials/ui/Entry.java +++ b/core/java/android/credentials/ui/Entry.java @@ -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); diff --git a/core/java/android/service/credentials/CredentialProviderService.java b/core/java/android/service/credentials/CredentialProviderService.java index 0a3d95de722a3..ee386c31a9847 100644 --- a/core/java/android/service/credentials/CredentialProviderService.java +++ b/core/java/android/service/credentials/CredentialProviderService.java @@ -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. + * + *

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. + * + *

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. + * + *

+ * 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"; diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt index 9803fc64cd2fd..09f9b5eaaf6a0 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt @@ -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, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt new file mode 100644 index 0000000000000..283c7ba16fa5e --- /dev/null +++ b/packages/CredentialManager/src/com/android/credentialmanager/jetpack/provider/AuthenticationAction.kt @@ -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 + } + } +} diff --git a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java index 20bda71645e13..595d03d9845dc 100644 --- a/services/credentials/java/com/android/server/credentials/ClearRequestSession.java +++ b/services/credentials/java/com/android/server/credentials/ClearRequestSession.java @@ -84,14 +84,12 @@ public final class ClearRequestSession extends RequestSession(entryId, authenticationAction); return authEntry; } @@ -311,6 +312,15 @@ public final class ProviderGetSession extends ProviderSession prepareUiActionEntries(@Nullable List actions) { List actionEntries = new ArrayList<>(); for (Action action : actions) { @@ -365,7 +375,6 @@ public final class ProviderGetSession extends ProviderSession 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 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 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());