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

Commit b7158102 authored by TreeHugger Robot's avatar TreeHugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Changed dataset methods that take a Pattern filter to accept null."

parents 1810393c 09d58a4e
Loading
Loading
Loading
Loading
+80 −28
Original line number Diff line number Diff line
@@ -29,7 +29,6 @@ import android.widget.RemoteViews;

import com.android.internal.util.Preconditions;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.regex.Pattern;

@@ -99,7 +98,7 @@ public final class Dataset implements Parcelable {
    private final ArrayList<AutofillId> mFieldIds;
    private final ArrayList<AutofillValue> mFieldValues;
    private final ArrayList<RemoteViews> mFieldPresentations;
    private final ArrayList<Pattern> mFieldFilters;
    private final ArrayList<DatasetFieldFilter> mFieldFilters;
    private final RemoteViews mPresentation;
    private final IntentSender mAuthentication;
    @Nullable String mId;
@@ -132,7 +131,7 @@ public final class Dataset implements Parcelable {

    /** @hide */
    @Nullable
    public Pattern getFilter(int index) {
    public DatasetFieldFilter getFilter(int index) {
        return mFieldFilters.get(index);
    }

@@ -189,7 +188,7 @@ public final class Dataset implements Parcelable {
        private ArrayList<AutofillId> mFieldIds;
        private ArrayList<AutofillValue> mFieldValues;
        private ArrayList<RemoteViews> mFieldPresentations;
        private ArrayList<Pattern> mFieldFilters;
        private ArrayList<DatasetFieldFilter> mFieldFilters;
        private RemoteViews mPresentation;
        private IntentSender mAuthentication;
        private boolean mDestroyed;
@@ -363,19 +362,21 @@ public final class Dataset implements Parcelable {
         * @param value the value to be autofilled. Pass {@code null} if you do not have the value
         *        but the target view is a logical part of the dataset. For example, if
         *        the dataset needs authentication and you have no access to the value.
         * @param filter regex used to determine if the dataset should be shown in the autofill UI.
         * @param filter regex used to determine if the dataset should be shown in the autofill UI;
         *        when {@code null}, it disables filtering on that dataset (this is the recommended
         *        approach when {@code value} is not {@code null} and field contains sensitive data
         *        such as passwords).
         *
         * @return this builder.
         * @throws IllegalStateException if the builder was constructed without a
         *         {@link RemoteViews presentation}.
         */
        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
                @NonNull Pattern filter) {
                @Nullable Pattern filter) {
            throwIfDestroyed();
            Preconditions.checkNotNull(filter, "filter cannot be null");
            Preconditions.checkState(mPresentation != null,
                    "Dataset presentation not set on constructor");
            setLifeTheUniverseAndEverything(id, value, null, filter);
            setLifeTheUniverseAndEverything(id, value, null, new DatasetFieldFilter(filter));
            return this;
        }

@@ -398,23 +399,26 @@ public final class Dataset implements Parcelable {
         * @param value the value to be autofilled. Pass {@code null} if you do not have the value
         *        but the target view is a logical part of the dataset. For example, if
         *        the dataset needs authentication and you have no access to the value.
         * @param filter regex used to determine if the dataset should be shown in the autofill UI;
         *        when {@code null}, it disables filtering on that dataset (this is the recommended
         *        approach when {@code value} is not {@code null} and field contains sensitive data
         *        such as passwords).
         * @param presentation the presentation used to visualize this field.
         * @param filter regex used to determine if the dataset should be shown in the autofill UI.
         *
         * @return this builder.
         */
        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
                @NonNull Pattern filter, @NonNull RemoteViews presentation) {
                @Nullable Pattern filter, @NonNull RemoteViews presentation) {
            throwIfDestroyed();
            Preconditions.checkNotNull(filter, "filter cannot be null");
            Preconditions.checkNotNull(presentation, "presentation cannot be null");
            setLifeTheUniverseAndEverything(id, value, presentation, filter);
            setLifeTheUniverseAndEverything(id, value, presentation,
                    new DatasetFieldFilter(filter));
            return this;
        }

        private void setLifeTheUniverseAndEverything(@NonNull AutofillId id,
                @Nullable AutofillValue value, @Nullable RemoteViews presentation,
                @Nullable Pattern filter) {
                @Nullable DatasetFieldFilter filter) {
            Preconditions.checkNotNull(id, "id cannot be null");
            if (mFieldIds != null) {
                final int existingIdx = mFieldIds.indexOf(id);
@@ -477,8 +481,8 @@ public final class Dataset implements Parcelable {
        parcel.writeParcelable(mPresentation, flags);
        parcel.writeTypedList(mFieldIds, flags);
        parcel.writeTypedList(mFieldValues, flags);
        parcel.writeParcelableList(mFieldPresentations, flags);
        parcel.writeSerializable(mFieldFilters);
        parcel.writeTypedList(mFieldPresentations, flags);
        parcel.writeTypedList(mFieldFilters, flags);
        parcel.writeParcelable(mAuthentication, flags);
        parcel.writeString(mId);
    }
@@ -493,22 +497,19 @@ public final class Dataset implements Parcelable {
            final Builder builder = (presentation == null)
                    ? new Builder()
                    : new Builder(presentation);
            final ArrayList<AutofillId> ids = parcel.createTypedArrayList(AutofillId.CREATOR);
            final ArrayList<AutofillId> ids =
                    parcel.createTypedArrayList(AutofillId.CREATOR);
            final ArrayList<AutofillValue> values =
                    parcel.createTypedArrayList(AutofillValue.CREATOR);
            final ArrayList<RemoteViews> presentations = new ArrayList<>();
            parcel.readParcelableList(presentations, null);
            @SuppressWarnings("unchecked")
            final ArrayList<Serializable> filters =
                    (ArrayList<Serializable>) parcel.readSerializable();
            final int idCount = (ids != null) ? ids.size() : 0;
            final int valueCount = (values != null) ? values.size() : 0;
            for (int i = 0; i < idCount; i++) {
            final ArrayList<RemoteViews> presentations =
                    parcel.createTypedArrayList(RemoteViews.CREATOR);
            final ArrayList<DatasetFieldFilter> filters =
                    parcel.createTypedArrayList(DatasetFieldFilter.CREATOR);
            for (int i = 0; i < ids.size(); i++) {
                final AutofillId id = ids.get(i);
                final AutofillValue value = (valueCount > i) ? values.get(i) : null;
                final RemoteViews fieldPresentation = presentations.isEmpty() ? null
                        : presentations.get(i);
                final Pattern filter = (Pattern) filters.get(i);
                final AutofillValue value = values.get(i);
                final RemoteViews fieldPresentation = presentations.get(i);
                final DatasetFieldFilter filter = filters.get(i);
                builder.setLifeTheUniverseAndEverything(id, value, fieldPresentation, filter);
            }
            builder.setAuthentication(parcel.readParcelable(null));
@@ -521,4 +522,55 @@ public final class Dataset implements Parcelable {
            return new Dataset[size];
        }
    };

    /**
     * Helper class used to indicate when the service explicitly set a {@link Pattern} filter for a
     * dataset field&dash; we cannot use a {@link Pattern} directly because then we wouldn't be
     * able to differentiate whether the service explicitly passed a {@code null} filter to disable
     * filter, or when it called the methods that does not take a filter {@link Pattern}.
     *
     * @hide
     */
    public static final class DatasetFieldFilter implements Parcelable {

        @Nullable
        public final Pattern pattern;

        private DatasetFieldFilter(@Nullable Pattern pattern) {
            this.pattern = pattern;
        }

        @Override
        public String toString() {
            if (!sDebug) return super.toString();

            // Cannot log pattern because it could contain PII
            return pattern == null ? "null" : pattern.pattern().length() + "_chars";
        }

        @Override
        public int describeContents() {
            return 0;
        }

        @Override
        public void writeToParcel(Parcel parcel, int flags) {
            parcel.writeSerializable(pattern);
        }

        @SuppressWarnings("hiding")
        public static final Creator<DatasetFieldFilter> CREATOR =
                new Creator<DatasetFieldFilter>() {

            @Override
            public DatasetFieldFilter createFromParcel(Parcel parcel) {
                return new DatasetFieldFilter((Pattern) parcel.readSerializable());
            }

            @Override
            public DatasetFieldFilter[] newArray(int size) {
                return new DatasetFieldFilter[size];
            }
        };
    }
}
+81 −33
Original line number Diff line number Diff line
@@ -27,6 +27,7 @@ import android.content.IntentSender;
import android.graphics.Point;
import android.graphics.Rect;
import android.service.autofill.Dataset;
import android.service.autofill.Dataset.DatasetFieldFilter;
import android.service.autofill.FillResponse;
import android.text.TextUtils;
import android.util.Slog;
@@ -58,6 +59,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

final class FillUi {
    private static final String TAG = "FillUi";
@@ -185,7 +187,7 @@ final class FillUi {
            final ArrayList<ViewItem> items = new ArrayList<>(totalItems);
            if (header != null) {
                if (sVerbose) Slog.v(TAG, "adding header");
                items.add(new ViewItem(null, null, null, header));
                items.add(new ViewItem(null, null, false, null, header));
            }
            for (int i = 0; i < datasetCount; i++) {
                final Dataset dataset = response.getDatasets().get(i);
@@ -205,21 +207,32 @@ final class FillUi {
                        Slog.e(TAG, "Error inflating remote views", e);
                        continue;
                    }
                    final Pattern filter = dataset.getFilter(index);
                    final DatasetFieldFilter filter = dataset.getFilter(index);
                    Pattern filterPattern = null;
                    String valueText = null;
                    boolean filterable = true;
                    if (filter == null) {
                        final AutofillValue value = dataset.getFieldValues().get(index);
                        if (value != null && value.isText()) {
                            valueText = value.getTextValue().toString().toLowerCase();
                        }
                    } else {
                        filterPattern = filter.pattern;
                        if (filterPattern == null) {
                            if (sVerbose) {
                                Slog.v(TAG, "Explicitly disabling filter at id " + focusedViewId
                                        + " for dataset #" + index);
                            }
                            filterable = false;
                        }
                    }

                    items.add(new ViewItem(dataset, filter, valueText, view));
                    items.add(new ViewItem(dataset, filterPattern, filterable, valueText, view));
                }
            }
            if (footer != null) {
                if (sVerbose) Slog.v(TAG, "adding footer");
                items.add(new ViewItem(null, null, null, footer));
                items.add(new ViewItem(null, null, false, null, footer));
            }

            mAdapter = new ItemsAdapter(items);
@@ -354,7 +367,7 @@ final class FillUi {
                MeasureSpec.AT_MOST);
        final int itemCount = mAdapter.getCount();
        for (int i = 0; i < itemCount; i++) {
            View view = mAdapter.getItem(i).view;
            final View view = mAdapter.getItem(i).view;
            view.measure(widthMeasureSpec, heightMeasureSpec);
            final int clampedMeasuredWidth = Math.min(view.getMeasuredWidth(), maxSize.x);
            final int newContentWidth = Math.max(mContentWidth, clampedMeasuredWidth);
@@ -400,13 +413,62 @@ final class FillUi {
        public final @Nullable Dataset dataset;
        public final @NonNull View view;
        public final @Nullable Pattern filter;
        public final boolean filterable;

        ViewItem(@Nullable Dataset dataset, @Nullable Pattern filter, @Nullable String value,
                @NonNull View view) {
        /**
         * Default constructor.
         *
         * @param dataset dataset associated with the item or {@code null} if it's a header or
         * footer (TODO(b/69796626): make @NonNull if header/footer is refactored out of the list)
         * @param filter optional filter set by the service to determine how the item should be
         * filtered
         * @param filterable optional flag set by the service to indicate this item should not be
         * filtered (typically used when the dataset has value but it's sensitive, like a password)
         * @param value dataset value
         * @param view dataset presentation.
         */
        ViewItem(@Nullable Dataset dataset, @Nullable Pattern filter, boolean filterable,
                @Nullable String value, @NonNull View view) {
            this.dataset = dataset;
            this.value = value;
            this.view = view;
            this.filter = filter;
            this.filterable = filterable;
        }

        /**
         * Returns whether this item matches the value input by the user so it can be included
         * in the filtered datasets.
         */
        public boolean matches(CharSequence filterText) {
            if (TextUtils.isEmpty(filterText)) {
                // Always show item when the user input is empty
                return true;
            }
            if (!filterable) {
                // Service explicitly disabled filtering using a null Pattern.
                return false;
            }
            final String constraintLowerCase = filterText.toString().toLowerCase();
            if (filter != null) {
                // Uses pattern provided by service
                return filter.matcher(constraintLowerCase).matches();
            } else {
                // Compares it with dataset value with dataset
                return (value == null)
                        ? (dataset.getAuthentication() == null)
                        : value.toLowerCase().startsWith(constraintLowerCase);
            }
        }

        @Override
        public String toString() {
            return "ViewItem: [dataset=" + (dataset == null ? "null" : dataset.getId())
                    + ", value=" + (value == null ? "null" : value.length() + "_chars")
                    + ", filterable=" + filterable
                    + ", filter=" + (filter == null ? "null" : filter.pattern().length() + "_chars")
                    + ", view=" + view.getAutofillId()
                    + "]";
        }
    }

@@ -509,7 +571,7 @@ final class FillUi {
    public void dump(PrintWriter pw, String prefix) {
        pw.print(prefix); pw.print("mCallback: "); pw.println(mCallback != null);
        pw.print(prefix); pw.print("mListView: "); pw.println(mListView);
        pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter != null);
        pw.print(prefix); pw.print("mAdapter: "); pw.println(mAdapter);
        pw.print(prefix); pw.print("mFilterText: ");
        Helper.printlnRedactedText(pw, mFilterText);
        pw.print(prefix); pw.print("mContentWidth: "); pw.println(mContentWidth);
@@ -556,33 +618,14 @@ final class FillUi {
        public Filter getFilter() {
            return new Filter() {
                @Override
                protected FilterResults performFiltering(CharSequence constraint) {
                protected FilterResults performFiltering(CharSequence filterText) {
                    // No locking needed as mAllItems is final an immutable
                    final List<ViewItem> filtered = mAllItems.stream()
                            .filter((item) -> item.matches(filterText))
                            .collect(Collectors.toList());
                    final FilterResults results = new FilterResults();
                    if (TextUtils.isEmpty(constraint)) {
                        results.values = mAllItems;
                        results.count = mAllItems.size();
                        return results;
                    }
                    final List<ViewItem> filteredItems = new ArrayList<>();
                    final String constraintLowerCase = constraint.toString().toLowerCase();
                    final int itemCount = mAllItems.size();
                    for (int i = 0; i < itemCount; i++) {
                        final ViewItem item = mAllItems.get(i);
                        final boolean matches;
                        if (item.filter != null) {
                            matches = item.filter.matcher(constraintLowerCase).matches();
                        } else {
                            matches = (item.value == null)
                                    ? (item.dataset.getAuthentication() == null)
                                    : item.value.toLowerCase().startsWith(constraintLowerCase);
                        }
                        if (matches) {
                            filteredItems.add(item);
                        }
                    }
                    results.values = filteredItems;
                    results.count = filteredItems.size();
                    results.values = filtered;
                    results.count = filtered.size();
                    return results;
                }

@@ -624,6 +667,11 @@ final class FillUi {
        public View getView(int position, View convertView, ViewGroup parent) {
            return getItem(position).view;
        }

        @Override
        public String toString() {
            return "ItemsAdapter: [all=" + mAllItems + ", filtered=" + mFilteredItems + "]";
        }
    }

    private final class AnnounceFilterResult implements Runnable {