diff options
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( |