From 5be0f15801033f463c965d7b71eafcf13c6b337e Mon Sep 17 00:00:00 2001 From: Adam He Date: Mon, 3 Feb 2020 15:53:32 -0800 Subject: [PATCH] Response and Dataset authentication via inline autofill. Bug: 148815880 Test: manual verification Change-Id: Ic72f6d828521737db5ce890c0e79cd6f8d16a6c6 --- api/current.txt | 1 + core/java/android/app/Activity.java | 3 +- .../service/autofill/FillResponse.java | 79 +++++++++++++++++- .../view/autofill/AutofillManager.java | 15 ++-- .../view/autofill/IAutoFillManagerClient.aidl | 2 +- .../com/android/server/autofill/Session.java | 24 +++--- .../server/autofill/ui/AutoFillUI.java | 5 +- .../autofill/ui/InlineSuggestionFactory.java | 82 +++++++++++++++---- 8 files changed, 169 insertions(+), 42 deletions(-) diff --git a/api/current.txt b/api/current.txt index 6a918d91ba388..83fa3b8aa7fb4 100644 --- a/api/current.txt +++ b/api/current.txt @@ -43048,6 +43048,7 @@ package android.service.autofill { method @NonNull public android.service.autofill.FillResponse build(); method @NonNull public android.service.autofill.FillResponse.Builder disableAutofill(long); method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews); + method @NonNull public android.service.autofill.FillResponse.Builder setAuthentication(@NonNull android.view.autofill.AutofillId[], @Nullable android.content.IntentSender, @Nullable android.widget.RemoteViews, @Nullable android.service.autofill.InlinePresentation); method @NonNull public android.service.autofill.FillResponse.Builder setClientState(@Nullable android.os.Bundle); method @NonNull public android.service.autofill.FillResponse.Builder setFieldClassificationIds(@NonNull android.view.autofill.AutofillId...); method @NonNull public android.service.autofill.FillResponse.Builder setFlags(int); diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java index 395a196420fad..5df67bc624fd8 100644 --- a/core/java/android/app/Activity.java +++ b/core/java/android/app/Activity.java @@ -1907,6 +1907,7 @@ public class Activity extends ContextThemeWrapper if (!mAutoFillIgnoreFirstResumePause) { View focus = getCurrentFocus(); if (focus != null && focus.canNotifyAutofillEnterExitEvent()) { + // TODO(b/148815880): Bring up keyboard if resumed from inline authentication. // TODO: in Activity killed/recreated case, i.e. SessionLifecycleTest# // testDatasetVisibleWhileAutofilledAppIsLifecycled: the View's initial // window visibility after recreation is INVISIBLE in onResume() and next frame @@ -8467,7 +8468,7 @@ public class Activity extends ContextThemeWrapper /** @hide */ @Override public final void autofillClientAuthenticate(int authenticationId, IntentSender intent, - Intent fillInIntent) { + Intent fillInIntent, boolean authenticateInline) { try { startIntentSenderForResultInner(intent, AUTO_FILL_AUTH_WHO_PREFIX, authenticationId, fillInIntent, 0, 0, null); diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index 7032825c3c620..e8e1223a29d06 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -75,6 +75,7 @@ public final class FillResponse implements Parcelable { private final @Nullable SaveInfo mSaveInfo; private final @Nullable Bundle mClientState; private final @Nullable RemoteViews mPresentation; + private final @Nullable InlinePresentation mInlinePresentation; private final @Nullable RemoteViews mHeader; private final @Nullable RemoteViews mFooter; private final @Nullable IntentSender mAuthentication; @@ -95,6 +96,7 @@ public final class FillResponse implements Parcelable { mSaveInfo = builder.mSaveInfo; mClientState = builder.mClientState; mPresentation = builder.mPresentation; + mInlinePresentation = builder.mInlinePresentation; mHeader = builder.mHeader; mFooter = builder.mFooter; mAuthentication = builder.mAuthentication; @@ -130,6 +132,11 @@ public final class FillResponse implements Parcelable { return mPresentation; } + /** @hide */ + public @Nullable InlinePresentation getInlinePresentation() { + return mInlinePresentation; + } + /** @hide */ public @Nullable RemoteViews getHeader() { return mHeader; @@ -219,6 +226,7 @@ public final class FillResponse implements Parcelable { private SaveInfo mSaveInfo; private Bundle mClientState; private RemoteViews mPresentation; + private InlinePresentation mInlinePresentation; private RemoteViews mHeader; private RemoteViews mFooter; private IntentSender mAuthentication; @@ -317,6 +325,67 @@ public final class FillResponse implements Parcelable { return this; } + /** + * Triggers a custom UI before before autofilling the screen with any data set in this + * response. + * + *

Note: Although the name of this method suggests that it should be used just for + * authentication flow, it can be used for other advanced flows; see {@link AutofillService} + * for examples. + * + *

This method is similar to + * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)}, but also accepts + * an {@link InlinePresentation} presentation which is required for authenticating through + * the inline autofill flow. + * + *

Note: {@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} does + * not work with {@link InlinePresentation}.

+ * + * @param authentication Intent to an activity with your authentication flow. + * @param presentation The presentation to visualize the response. + * @param inlinePresentation The inlinePresentation to visualize the response inline. + * @param ids id of Views that when focused will display the authentication UI. + * + * @return This builder. + * + * @throws IllegalArgumentException if any of the following occurs: + * + * + * @throws IllegalStateException if a {@link #setHeader(RemoteViews) header} or a + * {@link #setFooter(RemoteViews) footer} are already set for this builder. + * + * @see android.app.PendingIntent#getIntentSender() + */ + @NonNull + public Builder setAuthentication(@NonNull AutofillId[] ids, + @Nullable IntentSender authentication, @Nullable RemoteViews presentation, + @Nullable InlinePresentation inlinePresentation) { + throwIfDestroyed(); + throwIfDisableAutofillCalled(); + if (mHeader != null || mFooter != null) { + throw new IllegalStateException("Already called #setHeader() or #setFooter()"); + } + + if (authentication == null ^ (presentation == null && inlinePresentation == null)) { + throw new IllegalArgumentException("authentication and presentation " + + "(dropdown or inline), must be both non-null or null"); + } + mAuthentication = authentication; + mPresentation = presentation; + mInlinePresentation = inlinePresentation; + mAuthenticationIds = assertValid(ids); + return this; + } + /** * Specifies views that should not trigger new * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, @@ -644,6 +713,8 @@ public final class FillResponse implements Parcelable { break; } } + } else if (mInlinePresentation != null) { + mSupportsInlineSuggestions = true; } mDestroyed = true; @@ -691,6 +762,9 @@ public final class FillResponse implements Parcelable { if (mPresentation != null) { builder.append(", hasPresentation"); } + if (mInlinePresentation != null) { + builder.append(", hasInlinePresentation"); + } if (mHeader != null) { builder.append(", hasHeader"); } @@ -740,6 +814,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelableArray(mAuthenticationIds, flags); parcel.writeParcelable(mAuthentication, flags); parcel.writeParcelable(mPresentation, flags); + parcel.writeParcelable(mInlinePresentation, flags); parcel.writeParcelable(mHeader, flags); parcel.writeParcelable(mFooter, flags); parcel.writeParcelable(mUserData, flags); @@ -774,8 +849,10 @@ public final class FillResponse implements Parcelable { AutofillId.class); final IntentSender authentication = parcel.readParcelable(null); final RemoteViews presentation = parcel.readParcelable(null); + final InlinePresentation inlinePresentation = parcel.readParcelable(null); if (authenticationIds != null) { - builder.setAuthentication(authenticationIds, authentication, presentation); + builder.setAuthentication(authenticationIds, authentication, presentation, + inlinePresentation); } final RemoteViews header = parcel.readParcelable(null); if (header != null) { diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index c159f89cf2ac5..e9a8a7300777d 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -86,9 +86,10 @@ import java.util.List; import java.util.Objects; import java.util.Set; -//TODO: use java.lang.ref.Cleaner once Android supports Java 9 import sun.misc.Cleaner; +//TODO: use java.lang.ref.Cleaner once Android supports Java 9 + /** *

The {@link AutofillManager} class provides ways for apps and custom views to * integrate with the Autofill Framework lifecycle. @@ -547,7 +548,7 @@ public final class AutofillManager { * @param fillInIntent The authentication fill-in intent. */ void autofillClientAuthenticate(int authenticationId, IntentSender intent, - Intent fillInIntent); + Intent fillInIntent, boolean authenticateInline); /** * Tells the client this manager has state to be reset. @@ -2070,7 +2071,7 @@ public final class AutofillManager { } private void authenticate(int sessionId, int authenticationId, IntentSender intent, - Intent fillInIntent) { + Intent fillInIntent, boolean authenticateInline) { synchronized (mLock) { if (sessionId == mSessionId) { final AutofillClient client = getClient(); @@ -2078,7 +2079,8 @@ public final class AutofillManager { // clear mOnInvisibleCalled and we will see if receive onInvisibleForAutofill() // before onAuthenticationResult() mOnInvisibleCalled = false; - client.autofillClientAuthenticate(authenticationId, intent, fillInIntent); + client.autofillClientAuthenticate(authenticationId, intent, fillInIntent, + authenticateInline); } } } @@ -3250,10 +3252,11 @@ public final class AutofillManager { @Override public void authenticate(int sessionId, int authenticationId, IntentSender intent, - Intent fillInIntent) { + Intent fillInIntent, boolean authenticateInline) { final AutofillManager afm = mAfm.get(); if (afm != null) { - afm.post(() -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent)); + afm.post(() -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent, + authenticateInline)); } } diff --git a/core/java/android/view/autofill/IAutoFillManagerClient.aidl b/core/java/android/view/autofill/IAutoFillManagerClient.aidl index 84949c87377b9..3903665f2cded 100644 --- a/core/java/android/view/autofill/IAutoFillManagerClient.aidl +++ b/core/java/android/view/autofill/IAutoFillManagerClient.aidl @@ -50,7 +50,7 @@ oneway interface IAutoFillManagerClient { * Authenticates a fill response or a data set. */ void authenticate(int sessionId, int authenticationId, in IntentSender intent, - in Intent fillInIntent); + in Intent fillInIntent, boolean authenticateInline); /** * Sets the views to track. If saveOnAllViewsInvisible is set and all these view are invisible diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index b3ef1346c80b2..697e5d74446ef 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -1018,7 +1018,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // FillServiceCallbacks @Override - public void authenticate(int requestId, int datasetIndex, IntentSender intent, Bundle extras) { + public void authenticate(int requestId, int datasetIndex, IntentSender intent, Bundle extras, + boolean authenticateInline) { if (sDebug) { Slog.d(TAG, "authenticate(): requestId=" + requestId + "; datasetIdx=" + datasetIndex + "; intentSender=" + intent); @@ -1042,7 +1043,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex); mHandler.sendMessage(obtainMessage( Session::startAuthentication, - this, authenticationId, intent, fillInIntent)); + this, authenticationId, intent, fillInIntent, authenticateInline)); } // VultureCallback @@ -2623,8 +2624,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mService.getServicePackageName(), mComponentName, serviceLabel, serviceIcon, this, id, mCompatMode); - mService.logDatasetShown(id, mClientState); - synchronized (mLock) { if (mUiShownTime == 0) { // Log first time UI is shown. @@ -2656,10 +2655,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response, @Nullable String filterText) { final List datasets = response.getDatasets(); - if (datasets == null) { - Log.w(TAG, "response returned null datasets"); - return false; - } final InlineSuggestionSession.ImeResponse imeResponse = mInlineSuggestionSession.waitAndGetImeResponse(); @@ -2671,9 +2666,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final InlineSuggestionsRequest request = imeResponse.getRequest(); InlineSuggestionsResponse inlineSuggestionsResponse = InlineSuggestionFactory.createInlineSuggestionsResponse(request, - response.getRequestId(), - datasets.toArray(new Dataset[]{}), filterText, response.getInlineActions(), - mCurrentViewId, mContext, this, () -> { + response, filterText, response.getInlineActions(), mCurrentViewId, mContext, + this, () -> { synchronized (mLock) { requestHideFillUi(mCurrentViewId); } @@ -3160,7 +3154,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } final int authenticationId = AutofillManager.makeAuthenticationId(requestId, datasetIndex); - startAuthentication(authenticationId, dataset.getAuthentication(), fillInIntent); + startAuthentication(authenticationId, dataset.getAuthentication(), fillInIntent, + /* authenticateInline= */false); } } @@ -3184,10 +3179,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } private void startAuthentication(int authenticationId, IntentSender intent, - Intent fillInIntent) { + Intent fillInIntent, boolean authenticateInline) { try { synchronized (mLock) { - mClient.authenticate(id, authenticationId, intent, fillInIntent); + mClient.authenticate(id, authenticationId, intent, fillInIntent, + authenticateInline); } } catch (RemoteException e) { Slog.e(TAG, "Error launching auth intent", e); diff --git a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java index 26bb7c35b9dc4..71c3c16a2c067 100644 --- a/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java +++ b/services/autofill/java/com/android/server/autofill/ui/AutoFillUI.java @@ -79,7 +79,7 @@ public final class AutoFillUI { public interface AutoFillUiCallback { void authenticate(int requestId, int datasetIndex, @NonNull IntentSender intent, - @Nullable Bundle extras); + @Nullable Bundle extras, boolean authenticateInline); void fill(int requestId, int datasetIndex, @NonNull Dataset dataset); void save(); void cancelSave(); @@ -217,7 +217,8 @@ public final class AutoFillUI { if (mCallback != null) { mCallback.authenticate(response.getRequestId(), AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED, - response.getAuthentication(), response.getClientState()); + response.getAuthentication(), response.getClientState(), + /* authenticateInline= */ false); } } diff --git a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java index 54d04335cbbdb..a8886fc06c5af 100644 --- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java +++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java @@ -25,12 +25,14 @@ import android.content.Context; import android.os.IBinder; import android.os.RemoteException; import android.service.autofill.Dataset; +import android.service.autofill.FillResponse; import android.service.autofill.IInlineSuggestionUiCallback; import android.service.autofill.InlinePresentation; import android.text.TextUtils; import android.util.Slog; import android.view.SurfaceControl; import android.view.autofill.AutofillId; +import android.view.autofill.AutofillManager; import android.view.autofill.AutofillValue; import android.view.inline.InlinePresentationSpec; import android.view.inputmethod.InlineSuggestion; @@ -69,8 +71,7 @@ public final class InlineSuggestionFactory { * autofill service, potentially filtering the datasets. */ public static InlineSuggestionsResponse createInlineSuggestionsResponse( - @NonNull InlineSuggestionsRequest request, int requestId, - @NonNull Dataset[] datasets, + @NonNull InlineSuggestionsRequest request, @NonNull FillResponse response, @Nullable String filterText, @Nullable List inlineActions, @NonNull AutofillId autofillId, @@ -79,10 +80,24 @@ public final class InlineSuggestionFactory { @NonNull Runnable onErrorCallback, @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { if (sDebug) Slog.d(TAG, "createInlineSuggestionsResponse called"); - return createInlineSuggestionsResponseInternal(/* isAugmented= */ false, request, datasets, - filterText, inlineActions, autofillId, context, onErrorCallback, - (dataset, datasetIndex) -> client.fill(requestId, datasetIndex, dataset), - remoteRenderService); + final BiConsumer onClickFactory; + if (response.getAuthentication() != null) { + onClickFactory = (dataset, datasetIndex) -> client.authenticate(response.getRequestId(), + datasetIndex, response.getAuthentication(), response.getClientState(), + /* authenticateInline= */ true); + } else { + onClickFactory = (dataset, datasetIndex) -> + client.fill(response.getRequestId(), datasetIndex, dataset); + } + + final List datasetList = response.getDatasets(); + final Dataset[] datasets = datasetList == null + ? null + : datasetList.toArray(new Dataset[]{}); + + return createInlineSuggestionsResponseInternal(/* isAugmented= */ false, request, response, + datasets, filterText, inlineActions, autofillId, context, onErrorCallback, + onClickFactory, remoteRenderService); } /** @@ -99,22 +114,38 @@ public final class InlineSuggestionFactory { @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { if (sDebug) Slog.d(TAG, "createAugmentedInlineSuggestionsResponse called"); return createInlineSuggestionsResponseInternal(/* isAugmented= */ true, request, - datasets, /* filterText= */ null, /* inlineActions= */ null, autofillId, context, - onErrorCallback, - (dataset, fieldIndex) -> inlineSuggestionUiCallback.autofill(dataset), - remoteRenderService); + /* fillResponse= */ null, datasets, /* filterText= */ null, + /* inlineActions= */ null, autofillId, context, onErrorCallback, + (dataset, fieldIndex) -> + inlineSuggestionUiCallback.autofill(dataset), remoteRenderService); } private static InlineSuggestionsResponse createInlineSuggestionsResponseInternal( boolean isAugmented, @NonNull InlineSuggestionsRequest request, - @NonNull Dataset[] datasets, @Nullable String filterText, + @Nullable FillResponse response, @Nullable Dataset[] datasets, + @Nullable String filterText, @Nullable List inlineActions, @NonNull AutofillId autofillId, @NonNull Context context, @NonNull Runnable onErrorCallback, @NonNull BiConsumer onClickFactory, @Nullable RemoteInlineSuggestionRenderService remoteRenderService) { + final ArrayList inlineSuggestions = new ArrayList<>(); - for (int i = 0; i < datasets.length; i++) { - final Dataset dataset = datasets[i]; + if (response.getAuthentication() != null) { + InlineSuggestion inlineAuthSuggestion = createInlineAuthSuggestion(response, + remoteRenderService, onClickFactory, onErrorCallback, + request.getHostInputToken()); + inlineSuggestions.add(inlineAuthSuggestion); + + return new InlineSuggestionsResponse(inlineSuggestions); + } + + if (datasets == null) { + Slog.w(TAG, "Datasets should not be null here"); + return null; + } + + for (int datasetIndex = 0; datasetIndex < datasets.length; datasetIndex++) { + final Dataset dataset = datasets[datasetIndex]; final int fieldIndex = dataset.getFieldIds().indexOf(autofillId); if (fieldIndex < 0) { Slog.w(TAG, "AutofillId=" + autofillId + " not found in dataset"); @@ -130,9 +161,10 @@ public final class InlineSuggestionFactory { continue; } InlineSuggestion inlineSuggestion = createInlineSuggestion(isAugmented, dataset, - fieldIndex, mergedInlinePresentation(request, i, inlinePresentation), + fieldIndex, mergedInlinePresentation(request, datasetIndex, inlinePresentation), onClickFactory, remoteRenderService, onErrorCallback, request.getHostInputToken()); + inlineSuggestions.add(inlineSuggestion); } if (inlineActions != null) { @@ -198,7 +230,7 @@ public final class InlineSuggestionFactory { } private static InlineSuggestion createInlineSuggestion(boolean isAugmented, - @NonNull Dataset dataset, int fieldIndex, + @NonNull Dataset dataset, int datasetIndex, @NonNull InlinePresentation inlinePresentation, @NonNull BiConsumer onClickFactory, @NonNull RemoteInlineSuggestionRenderService remoteRenderService, @@ -212,12 +244,28 @@ public final class InlineSuggestionFactory { final InlineSuggestion inlineSuggestion = new InlineSuggestion(inlineSuggestionInfo, createInlineContentProvider(inlinePresentation, - () -> onClickFactory.accept(dataset, fieldIndex), onErrorCallback, + () -> onClickFactory.accept(dataset, datasetIndex), onErrorCallback, remoteRenderService, hostInputToken)); return inlineSuggestion; } + private static InlineSuggestion createInlineAuthSuggestion(@NonNull FillResponse response, + @NonNull RemoteInlineSuggestionRenderService remoteRenderService, + @NonNull BiConsumer onClickFactory, @NonNull Runnable onErrorCallback, + @Nullable IBinder hostInputToken) { + final InlinePresentation inlinePresentation = response.getInlinePresentation(); + final InlineSuggestionInfo inlineSuggestionInfo = new InlineSuggestionInfo( + inlinePresentation.getInlinePresentationSpec(), + InlineSuggestionInfo.SOURCE_AUTOFILL, null, InlineSuggestionInfo.TYPE_SUGGESTION); + + return new InlineSuggestion(inlineSuggestionInfo, + createInlineContentProvider(inlinePresentation, + () -> onClickFactory.accept(null, + AutofillManager.AUTHENTICATION_ID_DATASET_ID_UNDEFINED), + onErrorCallback, remoteRenderService, hostInputToken)); + } + /** * Returns an {@link InlinePresentation} with the style spec from the request/host, and * everything else from the provided {@code inlinePresentation}. @@ -299,4 +347,4 @@ public final class InlineSuggestionFactory { private InlineSuggestionFactory() { } -} +} \ No newline at end of file