Add request to authentication entry pending intent

Test: Built & deployed locally
Bug: 264943831

Change-Id: I76f5fc493c5ad6960c19f3b80429dd45b4c4840f
diff --git a/core/api/current.txt b/core/api/current.txt
index 46989a4..4f6ccff4 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -39938,6 +39938,7 @@
     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";
diff --git a/core/java/android/credentials/ui/Entry.java b/core/java/android/credentials/ui/Entry.java
index 9f2edae..37a5724 100644
--- a/core/java/android/credentials/ui/Entry.java
+++ b/core/java/android/credentials/ui/Entry.java
@@ -88,6 +88,7 @@
     /** 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 0a3d95d..ee386c3 100644
--- a/core/java/android/service/credentials/CredentialProviderService.java
+++ b/core/java/android/service/credentials/CredentialProviderService.java
@@ -125,6 +125,33 @@
     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";
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt b/packages/CredentialManager/src/com/android/credentialmanager/DataConverter.kt
index 9803fc6..09f9b5e 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.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 @@
             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 0000000..283c7ba
--- /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 20bda71..595d03d 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 @@
         respondToClientWithResponseAndFinish();
     }
 
-    @Override
     protected void onProviderResponseComplete(ComponentName componentName) {
         if (!isAnyProviderPending()) {
             onFinalResponseReceived(componentName, null);
         }
     }
 
-    @Override
     protected void onProviderTerminated(ComponentName componentName) {
         if (!isAnyProviderPending()) {
             processResponses();
diff --git a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
index 5db8dd5..82c2358 100644
--- a/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/CreateRequestSession.java
@@ -87,12 +87,6 @@
     }
 
     @Override
-    public void onProviderStatusChanged(ProviderSession.Status status,
-            ComponentName componentName) {
-        super.onProviderStatusChanged(status, componentName);
-    }
-
-    @Override
     public void onFinalResponseReceived(ComponentName componentName,
             @Nullable CreateCredentialResponse response) {
         Log.i(TAG, "onFinalCredentialReceived from: " + componentName.flattenToString());
@@ -136,4 +130,22 @@
         }
         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");
+            }
+        }
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/GetRequestSession.java b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
index 8099ff1..c7fa72c 100644
--- a/services/credentials/java/com/android/server/credentials/GetRequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/GetRequestSession.java
@@ -81,12 +81,6 @@
         }
     }
 
-    @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 @@
         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");
+            }
+        }
+    }
 }
diff --git a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
index 86dd217..de93af4 100644
--- a/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
+++ b/services/credentials/java/com/android/server/credentials/ProviderGetSession.java
@@ -275,7 +275,8 @@
         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 @@
         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 @@
     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) {
diff --git a/services/credentials/java/com/android/server/credentials/RequestSession.java b/services/credentials/java/com/android/server/credentials/RequestSession.java
index f59a0ef..0c3c34e 100644
--- a/services/credentials/java/com/android/server/credentials/RequestSession.java
+++ b/services/credentials/java/com/android/server/credentials/RequestSession.java
@@ -18,7 +18,6 @@
 
 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 @@
         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 @@
         mProviders.clear();
     }
 
-    boolean isAnyProviderPending() {
+    protected boolean isAnyProviderPending() {
         for (ProviderSession session : mProviders.values()) {
             if (ProviderSession.isStatusWaitingForRemoteResponse(session.getStatus())) {
                 return true;
@@ -153,7 +117,22 @@
         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());