From 00aa0ae7bf902e1207c7f75dde73a6c8e108e236 Mon Sep 17 00:00:00 2001 From: Ahaan Ugale Date: Fri, 7 Feb 2020 17:47:09 +0100 Subject: [PATCH] Implement filtering for Inline Autofill. Does not include filtering for Augmented Autofill. The InlineSuggestionsResponse callback is called on every input change as a mitigation for an IME attempting to glean potentially sensitive content of the suggestions. CLs will follow for disabling filtering in certain situations (to prevent the Keyboard from gleaning the suggestions) and for caching the inflates). Test: manual Test: atest CtsAutoFillServiceTestCases CtsInputMethodTestCases Bug: 146452916 Change-Id: I396c2cb279f1eb552bbae8c3509cb29c95e86609 --- .../com/android/server/autofill/Session.java | 31 ++++++----- .../android/server/autofill/ViewState.java | 2 + .../android/server/autofill/ui/FillUi.java | 3 ++ .../autofill/ui/InlineSuggestionFactory.java | 53 ++++++++++++++++--- 4 files changed, 71 insertions(+), 18 deletions(-) diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index ae018ea527ff8..72029d178534d 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -2540,6 +2540,16 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState ViewState viewState, int flags) { viewState.setCurrentValue(value); + final String filterText; + if (value == null || !value.isText()) { + filterText = null; + } else { + final CharSequence text = value.getTextValue(); + // Text should never be null, but it doesn't hurt to check to avoid a + // system crash... + filterText = (text == null) ? null : text.toString(); + } + final AutofillValue filledValue = viewState.getAutofilledValue(); if (filledValue != null) { if (filledValue.equals(value)) { @@ -2560,18 +2570,12 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState final ViewState currentView = mViewStates.get(mCurrentViewId); currentView.maybeCallOnFillReady(flags); } + } else if (viewState.id.equals(this.mCurrentViewId) + && (viewState.getState() & ViewState.STATE_INLINE_SHOWN) != 0) { + requestShowInlineSuggestionsLocked(viewState.getResponse(), filterText); } viewState.setState(ViewState.STATE_CHANGED); - final String filterText; - if (value == null || !value.isText()) { - filterText = null; - } else { - final CharSequence text = value.getTextValue(); - // Text should never be null, but it doesn't hurt to check to avoid a - // system crash... - filterText = (text == null) ? null : text.toString(); - } getUiForShowing().filterFillUi(filterText, this); } @@ -2604,7 +2608,9 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState if (response.supportsInlineSuggestions()) { synchronized (mLock) { - if (requestShowInlineSuggestionsLocked(response)) { + if (requestShowInlineSuggestionsLocked(response, filterText)) { + final ViewState currentView = mViewStates.get(mCurrentViewId); + currentView.setState(ViewState.STATE_INLINE_SHOWN); //TODO(b/137800469): Fix it to log showed only when IME asks for inflation, // rather than here where framework sends back the response. mService.logDatasetShown(id, mClientState); @@ -2647,7 +2653,8 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState /** * Returns whether we made a request to show inline suggestions. */ - private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response) { + private boolean requestShowInlineSuggestionsLocked(@NonNull FillResponse response, + @Nullable String filterText) { final List datasets = response.getDatasets(); if (datasets == null) { Log.w(TAG, "response returned null datasets"); @@ -2665,7 +2672,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState InlineSuggestionsResponse inlineSuggestionsResponse = InlineSuggestionFactory.createInlineSuggestionsResponse(request, response.getRequestId(), - datasets.toArray(new Dataset[]{}), response.getInlineActions(), + datasets.toArray(new Dataset[]{}), filterText, response.getInlineActions(), mCurrentViewId, mContext, this, () -> { synchronized (mLock) { requestHideFillUi(mCurrentViewId); diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java index 84886f83027d2..f7c24f080fa45 100644 --- a/services/autofill/java/com/android/server/autofill/ViewState.java +++ b/services/autofill/java/com/android/server/autofill/ViewState.java @@ -74,6 +74,8 @@ final class ViewState { public static final int STATE_AUTOFILLED_ONCE = 0x800; /** View triggered the latest augmented autofill request. */ public static final int STATE_TRIGGERED_AUGMENTED_AUTOFILL = 0x1000; + /** Inline suggestions were shown for this View. */ + public static final int STATE_INLINE_SHOWN = 0x2000; public final AutofillId id; diff --git a/services/autofill/java/com/android/server/autofill/ui/FillUi.java b/services/autofill/java/com/android/server/autofill/ui/FillUi.java index 57961423061f1..5dc43ef8ad56a 100644 --- a/services/autofill/java/com/android/server/autofill/ui/FillUi.java +++ b/services/autofill/java/com/android/server/autofill/ui/FillUi.java @@ -313,6 +313,8 @@ final class FillUi { Slog.e(TAG, "Error inflating remote views", e); continue; } + // TODO: Extract the shared filtering logic here and in FillUi to a common + // method. final DatasetFieldFilter filter = dataset.getFilter(index); Pattern filterPattern = null; String valueText = null; @@ -602,6 +604,7 @@ final class FillUi { * Returns whether this item matches the value input by the user so it can be included * in the filtered datasets. */ + // TODO: Extract the shared filtering logic here and in FillUi to a common method. public boolean matches(CharSequence filterText) { if (TextUtils.isEmpty(filterText)) { // Always show item when the user input is empty 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 95a4a191a52e3..5f6e47b041137 100644 --- a/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java +++ b/services/autofill/java/com/android/server/autofill/ui/InlineSuggestionFactory.java @@ -17,6 +17,7 @@ package com.android.server.autofill.ui; import static com.android.server.autofill.Helper.sDebug; +import static com.android.server.autofill.Helper.sVerbose; import android.annotation.NonNull; import android.annotation.Nullable; @@ -24,10 +25,12 @@ import android.content.Context; import android.os.RemoteException; import android.service.autofill.Dataset; import android.service.autofill.InlinePresentation; +import android.text.TextUtils; import android.util.Slog; import android.view.SurfaceControl; import android.view.View; import android.view.autofill.AutofillId; +import android.view.autofill.AutofillValue; import android.view.inline.InlinePresentationSpec; import android.view.inputmethod.InlineSuggestion; import android.view.inputmethod.InlineSuggestionInfo; @@ -42,6 +45,7 @@ import com.android.server.UiThread; import java.util.ArrayList; import java.util.List; import java.util.function.BiFunction; +import java.util.regex.Pattern; public final class InlineSuggestionFactory { private static final String TAG = "InlineSuggestionFactory"; @@ -69,17 +73,19 @@ public final class InlineSuggestionFactory { @NonNull Runnable onErrorCallback) { if (sDebug) Slog.d(TAG, "createAugmentedInlineSuggestionsResponse called"); return createInlineSuggestionsResponseInternal(/* isAugmented= */ true, request, - datasets, /* inlineActions= */ null, autofillId, context, onErrorCallback, + datasets, /* filterText= */ null, /* inlineActions= */ null, autofillId, context, + onErrorCallback, (dataset, filedIndex) -> (v -> inlineSuggestionUiCallback.autofill(dataset))); } /** * Creates an {@link InlineSuggestionsResponse} with the {@code datasets} provided by the - * autofill service. + * autofill service, potentially filtering the datasets. */ public static InlineSuggestionsResponse createInlineSuggestionsResponse( @NonNull InlineSuggestionsRequest request, int requestId, @NonNull Dataset[] datasets, + @Nullable String filterText, @Nullable List inlineActions, @NonNull AutofillId autofillId, @NonNull Context context, @@ -87,15 +93,15 @@ public final class InlineSuggestionFactory { @NonNull Runnable onErrorCallback) { if (sDebug) Slog.d(TAG, "createInlineSuggestionsResponse called"); return createInlineSuggestionsResponseInternal(/* isAugmented= */ false, request, datasets, - inlineActions, autofillId, context, onErrorCallback, + filterText, inlineActions, autofillId, context, onErrorCallback, (dataset, filedIndex) -> (v -> client.fill(requestId, filedIndex, dataset))); } private static InlineSuggestionsResponse createInlineSuggestionsResponseInternal( boolean isAugmented, @NonNull InlineSuggestionsRequest request, - @NonNull Dataset[] datasets, @Nullable List inlineActions, - @NonNull AutofillId autofillId, @NonNull Context context, - @NonNull Runnable onErrorCallback, + @NonNull Dataset[] datasets, @Nullable String filterText, + @Nullable List inlineActions, @NonNull AutofillId autofillId, + @NonNull Context context, @NonNull Runnable onErrorCallback, @NonNull BiFunction onClickListenerFactory) { final ArrayList inlineSuggestions = new ArrayList<>(); final InlineSuggestionUi inlineSuggestionUi = new InlineSuggestionUi(context, @@ -113,6 +119,9 @@ public final class InlineSuggestionFactory { Slog.w(TAG, "InlinePresentation not found in dataset"); return null; } + if (!includeDataset(dataset, fieldIndex, filterText)) { + continue; + } InlineSuggestion inlineSuggestion = createInlineSuggestion(isAugmented, dataset, fieldIndex, mergedInlinePresentation(request, i, inlinePresentation), inlineSuggestionUi, onClickListenerFactory); @@ -129,6 +138,38 @@ public final class InlineSuggestionFactory { return new InlineSuggestionsResponse(inlineSuggestions); } + // TODO: Extract the shared filtering logic here and in FillUi to a common method. + private static boolean includeDataset(Dataset dataset, int fieldIndex, + @Nullable String filterText) { + // Show everything when the user input is empty. + if (TextUtils.isEmpty(filterText)) { + return true; + } + + final String constraintLowerCase = filterText.toString().toLowerCase(); + + // Use the filter provided by the service, if available. + final Dataset.DatasetFieldFilter filter = dataset.getFilter(fieldIndex); + if (filter != null) { + Pattern filterPattern = filter.pattern; + if (filterPattern == null) { + if (sVerbose) { + Slog.v(TAG, "Explicitly disabling filter for dataset id" + dataset.getId()); + } + return true; + } + return filterPattern.matcher(constraintLowerCase).matches(); + } + + final AutofillValue value = dataset.getFieldValues().get(fieldIndex); + if (value == null || !value.isText()) { + return dataset.getAuthentication() == null; + } + final String valueText = value.getTextValue().toString().toLowerCase(); + return valueText.toLowerCase().startsWith(constraintLowerCase); + } + + private static InlineSuggestion createInlineAction(boolean isAugmented, @NonNull Context context, @NonNull InlinePresentation inlinePresentation,