summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/api/current.txt8
-rw-r--r--core/java/android/app/assist/AssistStructure.java65
-rw-r--r--core/java/android/credentials/GetCredentialResponse.java15
-rw-r--r--core/java/android/view/View.java121
-rw-r--r--core/java/android/view/ViewCredentialHandler.java46
-rw-r--r--core/java/android/view/ViewStructure.java54
-rw-r--r--core/tests/coretests/src/android/app/assist/AssistStructureTest.java99
-rw-r--r--packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt70
-rw-r--r--services/autofill/java/com/android/server/autofill/Session.java78
9 files changed, 537 insertions, 19 deletions
diff --git a/core/api/current.txt b/core/api/current.txt
index bca15bd20657..7eb2310c5b9b 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -52078,6 +52078,7 @@ package android.view {
method public final void cancelPendingInputEvents();
method public boolean checkInputConnectionProxy(android.view.View);
method public void clearAnimation();
+ method @FlaggedApi("autofill_credman_dev_integration") public void clearCredentialManagerRequest();
method public void clearFocus();
method public void clearViewTranslationCallback();
method public static int combineMeasuredStates(int, int);
@@ -52186,6 +52187,8 @@ package android.view {
method public CharSequence getContentDescription();
method @UiContext public final android.content.Context getContext();
method protected android.view.ContextMenu.ContextMenuInfo getContextMenuInfo();
+ method @FlaggedApi("autofill_credman_dev_integration") @Nullable public final android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException> getCredentialManagerCallback();
+ method @FlaggedApi("autofill_credman_dev_integration") @Nullable public final android.credentials.GetCredentialRequest getCredentialManagerRequest();
method public final boolean getDefaultFocusHighlightEnabled();
method public static int getDefaultSize(int, int);
method public android.view.Display getDisplay();
@@ -52568,6 +52571,7 @@ package android.view {
method public void setContentCaptureSession(@Nullable android.view.contentcapture.ContentCaptureSession);
method public void setContentDescription(CharSequence);
method public void setContextClickable(boolean);
+ method @FlaggedApi("autofill_credman_dev_integration") public void setCredentialManagerRequest(@NonNull android.credentials.GetCredentialRequest, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>);
method public void setDefaultFocusHighlightEnabled(boolean);
method @Deprecated public void setDrawingCacheBackgroundColor(@ColorInt int);
method @Deprecated public void setDrawingCacheEnabled(boolean);
@@ -53441,8 +53445,11 @@ package android.view {
method public abstract int addChildCount(int);
method public abstract void asyncCommit();
method public abstract android.view.ViewStructure asyncNewChild(int);
+ method @FlaggedApi("autofill_credman_dev_integration") public void clearCredentialManagerRequest();
method @Nullable public abstract android.view.autofill.AutofillId getAutofillId();
method public abstract int getChildCount();
+ method @FlaggedApi("autofill_credman_dev_integration") @Nullable public android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException> getCredentialManagerCallback();
+ method @FlaggedApi("autofill_credman_dev_integration") @Nullable public android.credentials.GetCredentialRequest getCredentialManagerRequest();
method public abstract android.os.Bundle getExtras();
method public abstract CharSequence getHint();
method public abstract CharSequence getText();
@@ -53467,6 +53474,7 @@ package android.view {
method public abstract void setClickable(boolean);
method public abstract void setContentDescription(CharSequence);
method public abstract void setContextClickable(boolean);
+ method @FlaggedApi("autofill_credman_dev_integration") public void setCredentialManagerRequest(@NonNull android.credentials.GetCredentialRequest, @NonNull android.os.OutcomeReceiver<android.credentials.GetCredentialResponse,android.credentials.GetCredentialException>);
method public abstract void setDataIsSensitive(boolean);
method public abstract void setDimens(int, int, int, int, int, int);
method public abstract void setElevation(float);
diff --git a/core/java/android/app/assist/AssistStructure.java b/core/java/android/app/assist/AssistStructure.java
index e2689687f388..7a4a3f9c8f27 100644
--- a/core/java/android/app/assist/AssistStructure.java
+++ b/core/java/android/app/assist/AssistStructure.java
@@ -1,5 +1,6 @@
package android.app.assist;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
@@ -7,6 +8,9 @@ import android.annotation.SystemApi;
import android.app.Activity;
import android.content.ComponentName;
import android.content.Context;
+import android.credentials.GetCredentialException;
+import android.credentials.GetCredentialRequest;
+import android.credentials.GetCredentialResponse;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.net.Uri;
@@ -15,6 +19,7 @@ import android.os.Binder;
import android.os.Bundle;
import android.os.IBinder;
import android.os.LocaleList;
+import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.PooledStringReader;
@@ -637,6 +642,12 @@ public class AssistStructure implements Parcelable {
AutofillId mAutofillId;
@View.AutofillType int mAutofillType = View.AUTOFILL_TYPE_NONE;
@Nullable String[] mAutofillHints;
+
+ @Nullable GetCredentialRequest mGetCredentialRequest;
+
+ @Nullable OutcomeReceiver<GetCredentialResponse, GetCredentialException>
+ mGetCredentialCallback;
+
AutofillValue mAutofillValue;
CharSequence[] mAutofillOptions;
boolean mSanitized;
@@ -1262,6 +1273,32 @@ public class AssistStructure implements Parcelable {
}
/**
+ * Returns the request associated with this node
+ * @return
+ *
+ * @hide
+ */
+ @FlaggedApi("autofill_credman_dev_integration")
+ @Nullable
+ public GetCredentialRequest getCredentialManagerRequest() {
+ return mGetCredentialRequest;
+ }
+
+ /**
+ *
+ * @return
+ *
+ * @hide
+ *
+ */
+ @FlaggedApi("autofill_credman_dev_integration")
+ @Nullable
+ public OutcomeReceiver<GetCredentialResponse,
+ GetCredentialException> getCredentialManagerCallback() {
+ return mGetCredentialCallback;
+ }
+
+ /**
* Gets the {@link android.text.InputType} bits of this structure.
*
* @return bits as defined by {@link android.text.InputType}.
@@ -2139,6 +2176,19 @@ public class AssistStructure implements Parcelable {
}
}
+ @Nullable
+ @Override
+ public GetCredentialRequest getCredentialManagerRequest() {
+ return mNode.mGetCredentialRequest;
+ }
+
+ @Nullable
+ @Override
+ public OutcomeReceiver<
+ GetCredentialResponse, GetCredentialException> getCredentialManagerCallback() {
+ return mNode.mGetCredentialCallback;
+ }
+
@Override
public void asyncCommit() {
synchronized (mAssist) {
@@ -2204,6 +2254,13 @@ public class AssistStructure implements Parcelable {
}
@Override
+ public void setCredentialManagerRequest(@NonNull GetCredentialRequest request,
+ @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
+ mNode.mGetCredentialRequest = request;
+ mNode.mGetCredentialCallback = callback;
+ }
+
+ @Override
public void setReceiveContentMimeTypes(@Nullable String[] mimeTypes) {
mNode.mReceiveContentMimeTypes = mimeTypes;
}
@@ -2523,6 +2580,14 @@ public class AssistStructure implements Parcelable {
+ ", isCredential=" + node.isCredential()
);
}
+ GetCredentialRequest getCredentialRequest = node.getCredentialManagerRequest();
+ if (getCredentialRequest == null) {
+ Log.i(TAG, prefix + " NO Credential Manager Request");
+ } else {
+ Log.i(TAG, prefix + " GetCredentialRequest: no. of options= "
+ + getCredentialRequest.getCredentialOptions().size()
+ );
+ }
final int NCHILDREN = node.getChildCount();
if (NCHILDREN > 0) {
diff --git a/core/java/android/credentials/GetCredentialResponse.java b/core/java/android/credentials/GetCredentialResponse.java
index 4f8b026ccb83..ea699b9a74e5 100644
--- a/core/java/android/credentials/GetCredentialResponse.java
+++ b/core/java/android/credentials/GetCredentialResponse.java
@@ -21,6 +21,8 @@ import static java.util.Objects.requireNonNull;
import android.annotation.NonNull;
import android.os.Parcel;
import android.os.Parcelable;
+import android.service.credentials.CredentialProviderService;
+import android.view.autofill.AutofillId;
import com.android.internal.util.AnnotationValidations;
@@ -35,6 +37,7 @@ public final class GetCredentialResponse implements Parcelable {
@NonNull
private final Credential mCredential;
+
/**
* Returns the credential that can be used to authenticate the user, or {@code null} if no
* credential is available.
@@ -60,6 +63,18 @@ public final class GetCredentialResponse implements Parcelable {
}
/**
+ *
+ * @return
+ *
+ * @hide
+ */
+ public AutofillId getAutofillId() {
+ return mCredential.getData().getParcelable(
+ CredentialProviderService.EXTRA_AUTOFILL_ID,
+ AutofillId.class);
+ }
+
+ /**
* Constructs a {@link GetCredentialResponse}.
*
* @param credential the credential successfully retrieved from the user.
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 5c5817feb23b..5dc72e0aef68 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -79,6 +79,10 @@ import android.content.res.CompatibilityInfo;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.content.res.TypedArray;
+import android.credentials.CredentialManager;
+import android.credentials.GetCredentialException;
+import android.credentials.GetCredentialRequest;
+import android.credentials.GetCredentialResponse;
import android.graphics.Bitmap;
import android.graphics.BlendMode;
import android.graphics.Canvas;
@@ -111,6 +115,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.Message;
+import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.RemoteCallback;
@@ -1034,6 +1039,10 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
*/
private static String sTraceRequestLayoutClass;
+ @Nullable
+ private ViewCredentialHandler mViewCredentialHandler;
+
+
/** Used to avoid computing the full strings each time when layout tracing is enabled. */
@Nullable
private ViewTraversalTracingStrings mTracingStrings;
@@ -6900,6 +6909,64 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * Clears the request and callback previously set
+ * through {@link View#setCredentialManagerRequest}.
+ * Once this API is invoked, there will be no request fired to {@link CredentialManager}
+ * on future view focus events.
+ *
+ * @see #setCredentialManagerRequest
+ */
+ @FlaggedApi("autofill_credman_dev_integration")
+ public void clearCredentialManagerRequest() {
+ if (Log.isLoggable(AUTOFILL_LOG_TAG, Log.VERBOSE)) {
+ Log.v(AUTOFILL_LOG_TAG, "clearCredentialManagerRequest called");
+ }
+ mViewCredentialHandler = null;
+ }
+
+ /**
+ * Sets a {@link CredentialManager} request to retrieve credentials, when the user focuses
+ * on this given view.
+ *
+ * When this view is focused, the given {@code request} will be fired to
+ * {@link CredentialManager}, which will fetch content from all
+ * {@link android.service.credentials.CredentialProviderService} services on the
+ * device, and then display credential options to the user on a relevant UI
+ * (dropdown, keyboard suggestions etc.).
+ *
+ * When the user selects a credential, the final {@link GetCredentialResponse} will be
+ * propagated to the given {@code callback}. Developers are expected to handle the response
+ * programmatically and perform a relevant action, e.g. signing in the user.
+ *
+ * <p> For details on how to build a Credential Manager request, please see
+ * {@link GetCredentialRequest}.
+ *
+ * <p> This API should be called at any point before the user focuses on the view, e.g. during
+ * {@code onCreate} of an Activity.
+ *
+ * @param request the request to be fired when this view is entered
+ * @param callback to be invoked when either a response or an exception needs to be
+ * propagated for the given view
+ */
+ @FlaggedApi("autofill_credman_dev_integration")
+ public void setCredentialManagerRequest(@NonNull GetCredentialRequest request,
+ @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
+ Preconditions.checkNotNull(request, "request must not be null");
+ Preconditions.checkNotNull(callback, "request must not be null");
+
+ mViewCredentialHandler = new ViewCredentialHandler(request, callback);
+ }
+
+ /**
+ *
+ * @hide
+ */
+ @Nullable
+ public ViewCredentialHandler getViewCredentialHandler() {
+ return mViewCredentialHandler;
+ }
+
+ /**
* Returns the size of the horizontal faded edges used to indicate that more
* content in this view is visible.
*
@@ -9364,6 +9431,11 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
structure.setAutofillValue(getAutofillValue());
structure.setIsCredential(isCredential());
}
+ if (getViewCredentialHandler() != null) {
+ structure.setCredentialManagerRequest(
+ getViewCredentialHandler().getRequest(),
+ getViewCredentialHandler().getCallback());
+ }
structure.setImportantForAutofill(getImportantForAutofill());
structure.setReceiveContentMimeTypes(getReceiveContentMimeTypes());
}
@@ -9781,6 +9853,53 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
/**
+ * Returns the {@link GetCredentialRequest} associated with the view.
+ * If the return value is null, that means no request has been set
+ * on the view and no {@link CredentialManager} flow will be invoked
+ * when this view is focused. Traditioanl autofill flows will still
+ * work, autofilling content if applicable, from
+ * the active {@link android.service.autofill.AutofillService} on
+ * the device.
+ *
+ * <p>See {@link #setCredentialManagerRequest} for more info.
+ *
+ * @return The credential request associated with this View.
+ */
+ @FlaggedApi("autofill_credman_dev_integration")
+ @Nullable
+ public final GetCredentialRequest getCredentialManagerRequest() {
+ if (mViewCredentialHandler == null) {
+ return null;
+ }
+ return mViewCredentialHandler.getRequest();
+ }
+
+
+ /**
+ * Returns the callback that has previously been set up on this view through
+ * the {@link #setCredentialManagerRequest} API.
+ * If the return value is null, that means no callback, or request, has been set
+ * on the view and no {@link CredentialManager} flow will be invoked
+ * when this view is focused. Traditioanl autofill flows will still
+ * work, and autofillable content will still be returned through the
+ * {@link #autofill(AutofillValue)} )} API.
+ *
+ * <p>See {@link #setCredentialManagerRequest} for more info.
+ *
+ * @return The callback associated with this view that will be invoked on a response from
+ * {@link CredentialManager} .
+ */
+ @FlaggedApi("autofill_credman_dev_integration")
+ @Nullable
+ public final OutcomeReceiver<GetCredentialResponse,
+ GetCredentialException> getCredentialManagerCallback() {
+ if (mViewCredentialHandler == null) {
+ return null;
+ }
+ return mViewCredentialHandler.getCallback();
+ }
+
+ /**
* Sets the unique, logical identifier of this view in the activity, for autofill purposes.
*
* <p>The autofill id is created on demand, and this method should only be called when a view is
@@ -10618,6 +10737,8 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
structure.setAutofillId(new AutofillId(getAutofillId(),
AccessibilityNodeInfo.getVirtualDescendantId(info.getSourceNodeId())));
}
+ structure.setCredentialManagerRequest(getCredentialManagerRequest(),
+ getCredentialManagerCallback());
CharSequence cname = info.getClassName();
structure.setClassName(cname != null ? cname.toString() : null);
structure.setContentDescription(info.getContentDescription());
diff --git a/core/java/android/view/ViewCredentialHandler.java b/core/java/android/view/ViewCredentialHandler.java
new file mode 100644
index 000000000000..11488abfde04
--- /dev/null
+++ b/core/java/android/view/ViewCredentialHandler.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2024 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.view;
+
+import android.credentials.GetCredentialException;
+import android.credentials.GetCredentialRequest;
+import android.credentials.GetCredentialResponse;
+import android.os.OutcomeReceiver;
+
+/**
+ * @hide
+ */
+public class ViewCredentialHandler {
+ private GetCredentialRequest mRequest;
+
+ private OutcomeReceiver<GetCredentialResponse, GetCredentialException> mCallback;
+
+ ViewCredentialHandler(GetCredentialRequest request,
+ OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {
+ mRequest = request;
+ mCallback = callback;
+ }
+
+ public GetCredentialRequest getRequest() {
+ return mRequest;
+ }
+
+ public OutcomeReceiver<GetCredentialResponse,
+ GetCredentialException> getCallback() {
+ return mCallback;
+ }
+}
diff --git a/core/java/android/view/ViewStructure.java b/core/java/android/view/ViewStructure.java
index bb2c7c8b198b..d86cc4ac781d 100644
--- a/core/java/android/view/ViewStructure.java
+++ b/core/java/android/view/ViewStructure.java
@@ -16,13 +16,18 @@
package android.view;
+import android.annotation.FlaggedApi;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SuppressLint;
+import android.credentials.GetCredentialException;
+import android.credentials.GetCredentialRequest;
+import android.credentials.GetCredentialResponse;
import android.graphics.Matrix;
import android.graphics.Rect;
import android.os.Bundle;
import android.os.LocaleList;
+import android.os.OutcomeReceiver;
import android.util.Pair;
import android.view.View.AutofillImportance;
import android.view.autofill.AutofillId;
@@ -347,6 +352,37 @@ public abstract class ViewStructure {
public abstract ViewStructure asyncNewChild(int index);
/**
+ * Gets the {@link GetCredentialRequest} associated with this node.
+ *
+ * <p> If null, no request is associated with this node, and hence no
+ * {@link android.credentials.CredentialManager} request will be fired when this
+ * node is focused.
+ * <p> For details on how a request and callback can be set, see
+ * {@link ViewStructure#setCredentialManagerRequest(GetCredentialRequest, OutcomeReceiver)}
+ */
+ @Nullable
+ @FlaggedApi("autofill_credman_dev_integration")
+ public GetCredentialRequest getCredentialManagerRequest() {
+ return null;
+ }
+
+ /**
+ * Gets the {@code callback} associated with this node.
+ *
+ * <p> If null, no callback or request is associated with this node, and hence no
+ * {@link android.credentials.CredentialManager} request will be fired when this
+ * node is focused.
+ * <p> For details on how a request and callback can be set, see
+ * {@link ViewStructure#setCredentialManagerRequest(GetCredentialRequest, OutcomeReceiver)}
+ */
+ @Nullable
+ @FlaggedApi("autofill_credman_dev_integration")
+ public OutcomeReceiver<
+ GetCredentialResponse, GetCredentialException> getCredentialManagerCallback() {
+ return null;
+ }
+
+ /**
* Gets the {@link AutofillId} associated with this node.
*/
@Nullable
@@ -509,6 +545,24 @@ public abstract class ViewStructure {
public abstract void setHtmlInfo(@NonNull HtmlInfo htmlInfo);
/**
+ * Sets a credential request to be fired to {@link android.credentials.CredentialManager}
+ * when this node is focused
+ *
+ * @param request the request to be fired
+ * @param callback the callback where the response or exception, is returned
+ */
+ @FlaggedApi("autofill_credman_dev_integration")
+ public void setCredentialManagerRequest(@NonNull GetCredentialRequest request,
+ @NonNull OutcomeReceiver<GetCredentialResponse, GetCredentialException> callback) {}
+
+ /**
+ * Clears the credential request previously set through
+ * {@link ViewStructure#setCredentialManagerRequest(GetCredentialRequest, OutcomeReceiver)}
+ */
+ @FlaggedApi("autofill_credman_dev_integration")
+ public void clearCredentialManagerRequest() {}
+
+ /**
* Simplified representation of the HTML properties of a node that represents an HTML element.
*/
public abstract static class HtmlInfo {
diff --git a/core/tests/coretests/src/android/app/assist/AssistStructureTest.java b/core/tests/coretests/src/android/app/assist/AssistStructureTest.java
index 0e5f2e1ae37b..abeb08caf23d 100644
--- a/core/tests/coretests/src/android/app/assist/AssistStructureTest.java
+++ b/core/tests/coretests/src/android/app/assist/AssistStructureTest.java
@@ -22,11 +22,20 @@ import static android.view.View.IMPORTANT_FOR_AUTOFILL_YES;
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
import android.app.assist.AssistStructure.ViewNode;
import android.app.assist.AssistStructure.ViewNodeBuilder;
import android.app.assist.AssistStructure.ViewNodeParcelable;
import android.content.Context;
+import android.credentials.CredentialOption;
+import android.credentials.GetCredentialException;
+import android.credentials.GetCredentialRequest;
+import android.credentials.GetCredentialResponse;
+import android.os.Bundle;
import android.os.LocaleList;
+import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.SystemClock;
import android.text.InputFilter;
@@ -38,6 +47,7 @@ import android.widget.FrameLayout;
import android.widget.LinearLayout;
import android.widget.TextView;
+import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
import androidx.test.rule.ActivityTestRule;
import androidx.test.runner.AndroidJUnit4;
@@ -74,6 +84,28 @@ public class AssistStructureTest {
private static final int BIG_VIEW_SIZE = 10_000_000;
private static final char BIG_VIEW_CHAR = '6';
private static final String BIG_STRING = repeat(BIG_VIEW_CHAR, BIG_VIEW_SIZE);
+
+ private static final GetCredentialRequest GET_CREDENTIAL_REQUEST = new
+ GetCredentialRequest.Builder(Bundle.EMPTY)
+ .addCredentialOption(new CredentialOption(
+ "TYPE_OPTION",
+ new Bundle(),
+ new Bundle(),
+ false))
+ .build();
+
+ private static final OutcomeReceiver<GetCredentialResponse,
+ GetCredentialException> GET_CREDENTIAL_REQUEST_CALLBACK = new OutcomeReceiver<>() {
+ @Override
+ public void onResult(@NonNull GetCredentialResponse response) {
+ // Do nothing
+ }
+
+ @Override
+ public void onError(@NonNull GetCredentialException e) {
+ // Do nothing
+ }
+ };
// Cannot be much big because it could hang test due to blocking GC
private static final int NUMBER_SMALL_VIEWS = 10_000;
@@ -224,6 +256,53 @@ public class AssistStructureTest {
}
@Test
+ public void testViewNodeParcelableForCredentialManager() {
+ Log.d(TAG, "Adding view with " + BIG_VIEW_SIZE + " chars");
+
+ View view = newCredentialView();
+ mActivity.addView(view);
+ waitUntilViewsAreLaidOff();
+
+ assertThat(view.getViewRootImpl()).isNotNull();
+ ViewNodeBuilder viewStructure = new ViewNodeBuilder();
+ viewStructure.setAutofillId(view.getAutofillId());
+ viewStructure.setCredentialManagerRequest(view.getCredentialManagerRequest(),
+ view.getCredentialManagerCallback());
+ view.onProvideAutofillStructure(viewStructure, /* flags= */ 0);
+ ViewNodeParcelable viewNodeParcelable = new ViewNodeParcelable(viewStructure.getViewNode());
+
+ // Check properties on "original" view node.
+ assertCredentialView(viewNodeParcelable.getViewNode());
+
+ // Check properties on "cloned" view node.
+ ViewNodeParcelable clone = cloneThroughParcel(viewNodeParcelable);
+ assertCredentialView(clone.getViewNode());
+ }
+
+ @Test
+ public void testViewNodeClearCredentialManagerRequest() {
+ Log.d(TAG, "Adding view with " + BIG_VIEW_SIZE + " chars");
+
+ View view = newCredentialView();
+ mActivity.addView(view);
+ waitUntilViewsAreLaidOff();
+
+ assertThat(view.getViewRootImpl()).isNotNull();
+ ViewNodeBuilder viewStructure = new ViewNodeBuilder();
+ viewStructure.setCredentialManagerRequest(view.getCredentialManagerRequest(),
+ view.getCredentialManagerCallback());
+
+ assertEquals(viewStructure.getCredentialManagerRequest(), GET_CREDENTIAL_REQUEST);
+ assertEquals(viewStructure.getCredentialManagerCallback(),
+ GET_CREDENTIAL_REQUEST_CALLBACK);
+
+ viewStructure.clearCredentialManagerRequest();
+
+ assertNull(viewStructure.getCredentialManagerRequest());
+ assertNull(viewStructure.getCredentialManagerCallback());
+ }
+
+ @Test
public void testViewNodeParcelableForAutofill() {
Log.d(TAG, "Adding view with " + BIG_VIEW_SIZE + " chars");
@@ -307,6 +386,14 @@ public class AssistStructureTest {
EditText view = new EditText(mContext);
view.setText("Big Hint in Little View");
view.setAutofillHints(BIG_STRING);
+ view.setCredentialManagerRequest(GET_CREDENTIAL_REQUEST, GET_CREDENTIAL_REQUEST_CALLBACK);
+ return view;
+ }
+
+ private EditText newCredentialView() {
+ EditText view = new EditText(mContext);
+ view.setText("Credential Request");
+ view.setCredentialManagerRequest(GET_CREDENTIAL_REQUEST, GET_CREDENTIAL_REQUEST_CALLBACK);
return view;
}
@@ -316,6 +403,7 @@ public class AssistStructureTest {
assertThat(view.getIdEntry()).isNull();
assertThat(view.getAutofillId()).isNotNull();
assertThat(view.getText().toString()).isEqualTo("Big Hint in Little View");
+ assertThat(view.getText().toString()).isEqualTo("Big Hint in Little View");
String[] hints = view.getAutofillHints();
assertThat(hints.length).isEqualTo(1);
@@ -326,6 +414,17 @@ public class AssistStructureTest {
assertThat(hint.charAt(BIG_VIEW_SIZE - 1)).isEqualTo(BIG_VIEW_CHAR);
}
+ private void assertCredentialView(ViewNode view) {
+ assertThat(view.getClassName()).isEqualTo(EditText.class.getName());
+ assertThat(view.getChildCount()).isEqualTo(0);
+ assertThat(view.getIdEntry()).isNull();
+ assertThat(view.getAutofillId()).isNotNull();
+ assertThat(view.getText().toString()).isEqualTo("Big Hint in Little View");
+
+ assertThat(view.getCredentialManagerRequest()).isEqualTo(GET_CREDENTIAL_REQUEST);
+ assertThat(view.getCredentialManagerCallback()).isEqualTo(GET_CREDENTIAL_REQUEST_CALLBACK);
+ }
+
/**
* Assert the lowest and highest bit control flags.
*
diff --git a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
index 8fde5d78c498..121f207122a0 100644
--- a/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
+++ b/packages/CredentialManager/src/com/android/credentialmanager/autofill/CredentialAutofillService.kt
@@ -19,9 +19,10 @@ package com.android.credentialmanager.autofill
import android.app.PendingIntent
import android.app.assist.AssistStructure
import android.content.Context
-import android.content.Intent
import android.credentials.CredentialManager
import android.credentials.GetCredentialRequest
+import android.credentials.GetCredentialResponse
+import android.credentials.GetCredentialException
import android.credentials.GetCandidateCredentialsResponse
import android.credentials.GetCandidateCredentialsException
import android.credentials.CredentialOption
@@ -45,6 +46,7 @@ import android.service.autofill.SaveCallback
import android.service.autofill.SaveRequest
import android.service.credentials.CredentialProviderService
import android.util.Log
+import android.content.Intent
import android.view.autofill.AutofillId
import android.view.autofill.IAutoFillManagerClient
import android.widget.RemoteViews
@@ -64,7 +66,6 @@ import java.util.concurrent.Executors
import org.json.JSONException
import org.json.JSONObject
-
class CredentialAutofillService : AutofillService() {
companion object {
@@ -118,10 +119,16 @@ class CredentialAutofillService : AutofillService() {
responseClientState.putBoolean(WEBVIEW_REQUESTED_CREDENTIAL_KEY, false)
val getCredRequest: GetCredentialRequest? = getCredManRequest(structure, sessionId,
requestId, responseClientState)
+ // TODO(b/324635774): Use callback for validating. If the request is coming
+ // directly from the view, there should be a corresponding callback, otherwise
+ // we should fail fast,
+ val getCredCallback = getCredManCallback(structure)
if (getCredRequest == null) {
Log.i(TAG, "No credential manager request found")
callback.onFailure("No credential manager request found")
return
+ } else if (getCredCallback == null) {
+ Log.i(TAG, "No credential manager callback found")
}
val credentialManager: CredentialManager =
getSystemService(Context.CREDENTIAL_SERVICE) as CredentialManager
@@ -505,6 +512,42 @@ class CredentialAutofillService : AutofillService() {
TODO("Not yet implemented")
}
+ private fun getCredManCallback(structure: AssistStructure): OutcomeReceiver<
+ GetCredentialResponse, GetCredentialException>? {
+ return traverseStructureForCallback(structure)
+ }
+
+ private fun traverseStructureForCallback(
+ structure: AssistStructure
+ ): OutcomeReceiver<GetCredentialResponse, GetCredentialException>? {
+ val windowNodes: List<AssistStructure.WindowNode> =
+ structure.run {
+ (0 until windowNodeCount).map { getWindowNodeAt(it) }
+ }
+
+ windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
+ return traverseNodeForCallback(windowNode.rootViewNode)
+ }
+ return null
+ }
+
+ private fun traverseNodeForCallback(
+ viewNode: AssistStructure.ViewNode
+ ): OutcomeReceiver<GetCredentialResponse, GetCredentialException>? {
+ val children: List<AssistStructure.ViewNode> =
+ viewNode.run {
+ (0 until childCount).map { getChildAt(it) }
+ }
+
+ children.forEach { childNode: AssistStructure.ViewNode ->
+ if (childNode.isFocused() && childNode.credentialManagerCallback != null) {
+ return childNode.credentialManagerCallback
+ }
+ return traverseNodeForCallback(childNode)
+ }
+ return null
+ }
+
private fun getCredManRequest(
structure: AssistStructure,
sessionId: Int,
@@ -512,7 +555,7 @@ class CredentialAutofillService : AutofillService() {
responseClientState: Bundle
): GetCredentialRequest? {
val credentialOptions: MutableList<CredentialOption> = mutableListOf()
- traverseStructure(structure, credentialOptions, responseClientState)
+ traverseStructureForRequest(structure, credentialOptions, responseClientState)
if (credentialOptions.isNotEmpty()) {
val dataBundle = Bundle()
@@ -525,7 +568,7 @@ class CredentialAutofillService : AutofillService() {
return null
}
- private fun traverseStructure(
+ private fun traverseStructureForRequest(
structure: AssistStructure,
cmRequests: MutableList<CredentialOption>,
responseClientState: Bundle
@@ -536,18 +579,17 @@ class CredentialAutofillService : AutofillService() {
}
windowNodes.forEach { windowNode: AssistStructure.WindowNode ->
- traverseNode(windowNode.rootViewNode, cmRequests, responseClientState)
+ traverseNodeForRequest(windowNode.rootViewNode, cmRequests, responseClientState)
}
}
- private fun traverseNode(
+ private fun traverseNodeForRequest(
viewNode: AssistStructure.ViewNode,
cmRequests: MutableList<CredentialOption>,
responseClientState: Bundle
) {
viewNode.autofillId?.let {
- val options = getCredentialOptionsFromViewNode(viewNode, it, responseClientState)
- cmRequests.addAll(options)
+ cmRequests.addAll(getCredentialOptionsFromViewNode(viewNode, it, responseClientState))
}
val children: List<AssistStructure.ViewNode> =
@@ -556,7 +598,7 @@ class CredentialAutofillService : AutofillService() {
}
children.forEach { childNode: AssistStructure.ViewNode ->
- traverseNode(childNode, cmRequests, responseClientState)
+ traverseNodeForRequest(childNode, cmRequests, responseClientState)
}
}
@@ -564,8 +606,16 @@ class CredentialAutofillService : AutofillService() {
viewNode: AssistStructure.ViewNode,
autofillId: AutofillId,
responseClientState: Bundle
- ): List<CredentialOption> {
+ ): MutableList<CredentialOption> {
+ if (viewNode.credentialManagerRequest != null &&
+ viewNode.credentialManagerCallback != null) {
+ val options = viewNode.credentialManagerRequest?.getCredentialOptions()
+ if (options != null) {
+ return options
+ }
+ }
val credentialHints: MutableList<String> = mutableListOf()
+
if (viewNode.autofillHints != null) {
for (hint in viewNode.autofillHints!!) {
if (hint.startsWith(CRED_HINT_PREFIX)) {
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index b89e0d8c72df..96c65565c96b 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -127,6 +127,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.IBinder.DeathRecipient;
+import android.os.OutcomeReceiver;
import android.os.Parcel;
import android.os.Parcelable;
import android.os.Process;
@@ -2828,9 +2829,18 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
replaceResponseLocked(authenticatedResponse, (FillResponse) result, newClientState);
} else if (result instanceof GetCredentialResponse) {
Slog.d(TAG, "Received GetCredentialResponse from authentication flow");
- Dataset dataset = getDatasetFromCredentialResponse((GetCredentialResponse) result);
- if (dataset != null) {
- autoFill(requestId, datasetIdx, dataset, false, UI_TYPE_UNKNOWN);
+ boolean isCredmanCallbackInvoked = false;
+ if (Flags.autofillCredmanIntegration()) {
+ GetCredentialResponse response = (GetCredentialResponse) result;
+ isCredmanCallbackInvoked = invokeCredentialManagerCallback(response);
+ }
+
+ if (!isCredmanCallbackInvoked) {
+ Dataset dataset = getDatasetFromCredentialResponse(
+ (GetCredentialResponse) result);
+ if (dataset != null) {
+ autoFill(requestId, datasetIdx, dataset, false, UI_TYPE_UNKNOWN);
+ }
}
} else if (result instanceof Dataset) {
if (datasetIdx != AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED) {
@@ -2868,6 +2878,49 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
}
+ private boolean invokeCredentialManagerCallback(GetCredentialResponse response) {
+ synchronized (mLock) {
+ return invokeCredentialManagerCallbackLocked(response);
+ }
+ }
+
+ @GuardedBy("mLock")
+ private boolean invokeCredentialManagerCallbackLocked(GetCredentialResponse response) {
+ AutofillId autofillId = response.getAutofillId();
+ if (autofillId != null) {
+ OutcomeReceiver<GetCredentialResponse,
+ GetCredentialException> callback =
+ getCredmanCallbackFromContextsLocked(autofillId);
+ if (callback != null) {
+ Slog.w(TAG, "Propagating response to Credential Manager callback");
+ callback.onResult(response);
+ return true;
+ } else {
+ Slog.w(TAG, "Received Credential Manager response but no callback found");
+ }
+ } else {
+ Slog.w(TAG, "Received Credential Manager response but no autofillId found");
+ }
+ return false;
+ }
+
+ @GuardedBy("mLock")
+ @Nullable
+ private OutcomeReceiver<GetCredentialResponse,
+ GetCredentialException> getCredmanCallbackFromContextsLocked(
+ @NonNull AutofillId autofillId) {
+ final int numContexts = mContexts.size();
+ for (int i = numContexts - 1; i >= 0; i--) {
+ final FillContext context = mContexts.get(i);
+ final ViewNode node = Helper.findViewNodeByAutofillId(context.getStructure(),
+ autofillId);
+ if (node != null) {
+ return node.getCredentialManagerCallback();
+ }
+ }
+ return null;
+ }
+
private Dataset getDatasetFromCredentialResponse(GetCredentialResponse result) {
if (result == null) {
return null;
@@ -5036,16 +5089,23 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == SUCCESS_CREDMAN_SELECTOR) {
Slog.d(TAG, "onReceiveResult from Credential Manager bottom sheet");
+ boolean isCredmanCallbackInvoked = false;
GetCredentialResponse getCredentialResponse =
resultData.getParcelable(
CredentialProviderService.EXTRA_GET_CREDENTIAL_RESPONSE,
GetCredentialResponse.class);
- Dataset datasetFromCredential = getDatasetFromCredentialResponse(
- getCredentialResponse);
- if (datasetFromCredential != null) {
- autoFill(requestId, /*datasetIndex=*/-1,
- datasetFromCredential, false,
- UI_TYPE_CREDMAN_BOTTOM_SHEET);
+
+ isCredmanCallbackInvoked =
+ invokeCredentialManagerCallback(getCredentialResponse);
+
+ if (!isCredmanCallbackInvoked) {
+ Dataset datasetFromCredential = getDatasetFromCredentialResponse(
+ getCredentialResponse);
+ if (datasetFromCredential != null) {
+ autoFill(requestId, /*datasetIndex=*/-1,
+ datasetFromCredential, false,
+ UI_TYPE_CREDMAN_BOTTOM_SHEET);
+ }
}
} else if (resultCode == FAILURE_CREDMAN_SELECTOR) {
GetCredentialException exception = resultData.getParcelable(