Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 00aa0ae7 authored by Ahaan Ugale's avatar Ahaan Ugale
Browse files

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
parent cd50731b
Loading
Loading
Loading
Loading
+19 −12
Original line number Diff line number Diff line
@@ -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<Dataset> 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);
+2 −0
Original line number Diff line number Diff line
@@ -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;

+3 −0
Original line number Diff line number Diff line
@@ -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
+47 −6
Original line number Diff line number Diff line
@@ -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<InlinePresentation> 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<InlinePresentation> inlineActions,
            @NonNull AutofillId autofillId, @NonNull Context context,
            @NonNull Runnable onErrorCallback,
            @NonNull Dataset[] datasets, @Nullable String filterText,
            @Nullable List<InlinePresentation> inlineActions, @NonNull AutofillId autofillId,
            @NonNull Context context, @NonNull Runnable onErrorCallback,
            @NonNull BiFunction<Dataset, Integer, View.OnClickListener> onClickListenerFactory) {
        final ArrayList<InlineSuggestion> 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,