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

Commit e9324786 authored by Felipe Leme's avatar Felipe Leme
Browse files

New Autofill API: let service set optional header and footer for dataset picker.

Test: atest CtsAutoFillServiceTestCases:FillResponseTest#testBuilder_setAuthentication_illegalState,testBuilder_setHeaderOrFooterInvalid,testBuilder_setHeaderOrFooterAfterAuthentication,testBuilder_build_headerOrFooterWithoutDatasets
Test: atest CtsAutoFillServiceTestCases:LoginActivityTest#testAutoFillOneDataset_withHeader,testAutoFillOneDataset_withFooter,testAutoFillOneDataset_withHeaderAndFooter
Test: atest CtsAutoFillServiceTestCases

Fixes: 69458670

Change-Id: I34be762968ffcad97dbf86898d3f1745bc8d4d64
parent c758551f
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -37479,6 +37479,8 @@ package android.service.autofill {
    method public android.service.autofill.FillResponse.Builder setAuthentication(android.view.autofill.AutofillId[], android.content.IntentSender, android.widget.RemoteViews);
    method public android.service.autofill.FillResponse.Builder setClientState(android.os.Bundle);
    method public android.service.autofill.FillResponse.Builder setFlags(int);
    method public android.service.autofill.FillResponse.Builder setFooter(android.widget.RemoteViews);
    method public android.service.autofill.FillResponse.Builder setHeader(android.widget.RemoteViews);
    method public android.service.autofill.FillResponse.Builder setIgnoredIds(android.view.autofill.AutofillId...);
    method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo);
  }
+104 −0
Original line number Diff line number Diff line
@@ -72,6 +72,8 @@ public final class FillResponse implements Parcelable {
    private final @Nullable SaveInfo mSaveInfo;
    private final @Nullable Bundle mClientState;
    private final @Nullable RemoteViews mPresentation;
    private final @Nullable RemoteViews mHeader;
    private final @Nullable RemoteViews mFooter;
    private final @Nullable IntentSender mAuthentication;
    private final @Nullable AutofillId[] mAuthenticationIds;
    private final @Nullable AutofillId[] mIgnoredIds;
@@ -85,6 +87,8 @@ public final class FillResponse implements Parcelable {
        mSaveInfo = builder.mSaveInfo;
        mClientState = builder.mClientState;
        mPresentation = builder.mPresentation;
        mHeader = builder.mHeader;
        mFooter = builder.mFooter;
        mAuthentication = builder.mAuthentication;
        mAuthenticationIds = builder.mAuthenticationIds;
        mIgnoredIds = builder.mIgnoredIds;
@@ -114,6 +118,16 @@ public final class FillResponse implements Parcelable {
        return mPresentation;
    }

    /** @hide */
    public @Nullable RemoteViews getHeader() {
        return mHeader;
    }

    /** @hide */
    public @Nullable RemoteViews getFooter() {
        return mFooter;
    }

    /** @hide */
    public @Nullable IntentSender getAuthentication() {
        return mAuthentication;
@@ -171,6 +185,8 @@ public final class FillResponse implements Parcelable {
        private SaveInfo mSaveInfo;
        private Bundle mClientState;
        private RemoteViews mPresentation;
        private RemoteViews mHeader;
        private RemoteViews mFooter;
        private IntentSender mAuthentication;
        private AutofillId[] mAuthenticationIds;
        private AutofillId[] mIgnoredIds;
@@ -226,16 +242,24 @@ public final class FillResponse implements Parcelable {
         * @param ids id of Views that when focused will display the authentication UI.
         *
         * @return This builder.

         * @throws IllegalArgumentException if {@code ids} is {@code null} or empty, or if
         * both {@code authentication} and {@code presentation} are {@code null}, or if
         * both {@code authentication} and {@code presentation} are non-{@code null}
         *
         * @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()
         */
        public @NonNull Builder setAuthentication(@NonNull AutofillId[] ids,
                @Nullable IntentSender authentication, @Nullable RemoteViews presentation) {
            throwIfDestroyed();
            throwIfDisableAutofillCalled();
            if (mHeader != null || mFooter != null) {
                throw new IllegalStateException("Already called #setHeader() or #setFooter()");
            }

            if (ids == null || ids.length == 0) {
                throw new IllegalArgumentException("ids cannot be null or empry");
            }
@@ -417,6 +441,62 @@ public final class FillResponse implements Parcelable {
            return this;
        }

        /**
         * Sets a header to be shown as the first element in the list of datasets.
         *
         * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset},
         * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this
         * method should only be used on {@link FillResponse FillResponses} that do not require
         * authentication (as the header could have been set directly in the main presentation in
         * these cases).
         *
         * @param header a presentation to represent the header. This presentation is not clickable
         * &mdash;calling
         * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would
         * have no effect.
         *
         * @return this builder
         *
         * @throws IllegalStateException if an
         * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews) authentication} was
         * already set for this builder.
         */
        // TODO(b/69796626): make it sticky / update javadoc
        public Builder setHeader(@NonNull RemoteViews header) {
            throwIfDestroyed();
            throwIfAuthenticationCalled();
            mHeader = Preconditions.checkNotNull(header);
            return this;
        }

        /**
         * Sets a footer to be shown as the last element in the list of datasets.
         *
         * <p>When this method is called, you must also {@link #addDataset(Dataset) add a dataset},
         * otherwise {@link #build()} throws an {@link IllegalStateException}. Similarly, this
         * method should only be used on {@link FillResponse FillResponses} that do not require
         * authentication (as the footer could have been set directly in the main presentation in
         * these cases).
         *
         * @param footer a presentation to represent the footer. This presentation is not clickable
         * &mdash;calling
         * {@link RemoteViews#setOnClickPendingIntent(int, android.app.PendingIntent)} on it would
         * have no effect.
         *
         * @return this builder
         *
         * @throws IllegalStateException if the FillResponse
         * {@link #setAuthentication(AutofillId[], IntentSender, RemoteViews)
         * requires authentication}.
         */
        // TODO(b/69796626): make it sticky / update javadoc
        public Builder setFooter(@NonNull RemoteViews footer) {
            throwIfDestroyed();
            throwIfAuthenticationCalled();
            mFooter = Preconditions.checkNotNull(footer);
            return this;
        }

        /**
         * Builds a new {@link FillResponse} instance.
         *
@@ -428,6 +508,8 @@ public final class FillResponse implements Parcelable {
         *       {@link #setSaveInfo(SaveInfo)}, {@link #disableAutofill(long)},
         *       {@link #setClientState(Bundle)},
         *       or {link #setFieldClassificationIds(AutofillId...)}.
         *   <li>{@link #setHeader(RemoteViews)} or {@link #setFooter(RemoteViews)} is called
         *       without any previous calls to {@link #addDataset(Dataset)}.
         * </ol>
         *
         * @return A built response.
@@ -442,6 +524,10 @@ public final class FillResponse implements Parcelable {
                        + "SaveInfo, or an authentication with a presentation, "
                        + "or a FieldsDetection, or a client state, or disable autofill");
            }
            if (mDatasets == null && (mHeader != null || mFooter != null)) {
                throw new IllegalStateException(
                        "must add at least 1 dataset when using header or footer");
            }
            mDestroyed = true;
            return new FillResponse(this);
        }
@@ -457,6 +543,12 @@ public final class FillResponse implements Parcelable {
                throw new IllegalStateException("Already called #disableAutofill()");
            }
        }

        private void throwIfAuthenticationCalled() {
            if (mAuthentication != null) {
                throw new IllegalStateException("Already called #setAuthentication()");
            }
        }
    }

    /////////////////////////////////////
@@ -473,6 +565,8 @@ public final class FillResponse implements Parcelable {
                .append(", saveInfo=").append(mSaveInfo)
                .append(", clientState=").append(mClientState != null)
                .append(", hasPresentation=").append(mPresentation != null)
                .append(", hasHeader=").append(mHeader != null)
                .append(", hasFooter=").append(mFooter != null)
                .append(", hasAuthentication=").append(mAuthentication != null)
                .append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds))
                .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
@@ -501,6 +595,8 @@ public final class FillResponse implements Parcelable {
        parcel.writeParcelableArray(mAuthenticationIds, flags);
        parcel.writeParcelable(mAuthentication, flags);
        parcel.writeParcelable(mPresentation, flags);
        parcel.writeParcelable(mHeader, flags);
        parcel.writeParcelable(mFooter, flags);
        parcel.writeParcelableArray(mIgnoredIds, flags);
        parcel.writeLong(mDisableDuration);
        parcel.writeParcelableArray(mFieldClassificationIds, flags);
@@ -533,6 +629,14 @@ public final class FillResponse implements Parcelable {
            if (authenticationIds != null) {
                builder.setAuthentication(authenticationIds, authentication, presentation);
            }
            final RemoteViews header = parcel.readParcelable(null);
            if (header != null) {
                builder.setHeader(header);
            }
            final RemoteViews footer = parcel.readParcelable(null);
            if (footer != null) {
                builder.setFooter(footer);
            }

            builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
            final long disableDuration = parcel.readLong();
+1 −1
Original line number Diff line number Diff line
@@ -2932,8 +2932,8 @@
  <!-- com.android.server.autofill -->
  <java-symbol type="layout" name="autofill_save"/>
  <java-symbol type="layout" name="autofill_dataset_picker"/>
  <java-symbol type="id" name="autofill_dataset_picker"/>
  <java-symbol type="id" name="autofill_dataset_list"/>
  <java-symbol type="id" name="autofill_dataset_picker"/>
  <java-symbol type="id" name="autofill" />
  <java-symbol type="id" name="autofill_save_custom_subtitle" />
  <java-symbol type="id" name="autofill_save_icon" />
+79 −37
Original line number Diff line number Diff line
@@ -108,9 +108,11 @@ final class FillUi {
        mCallback = callback;

        final LayoutInflater inflater = LayoutInflater.from(context);

        final ViewGroup decor = (ViewGroup) inflater.inflate(
                R.layout.autofill_dataset_picker, null);


        final RemoteViews.OnClickHandler interceptionHandler = new RemoteViews.OnClickHandler() {
            @Override
            public boolean onClickHandler(View view, PendingIntent pendingIntent,
@@ -153,7 +155,38 @@ final class FillUi {
            mCallback.requestShowFillUi(mContentWidth, mContentHeight, mWindowPresenter);
        } else {
            final int datasetCount = response.getDatasets().size();
            final ArrayList<ViewItem> items = new ArrayList<>(datasetCount);

            // Total items include the (optional) header and footer - we cannot use listview's
            // addHeader() and addFooter() because it would complicate the scrolling logic.
            int totalItems = datasetCount;

            RemoteViews.OnClickHandler clickBlocker = null;
            final RemoteViews headerPresentation = response.getHeader();
            View header = null;
            if (headerPresentation != null) {
                clickBlocker = newClickBlocker();
                header = headerPresentation.apply(context, null, clickBlocker);
                totalItems++;
            }

            final RemoteViews footerPresentation = response.getFooter();
            View footer = null;
            if (footerPresentation != null) {
                if (clickBlocker == null) { // already set for header
                    clickBlocker = newClickBlocker();
                }
                footer = footerPresentation.apply(context, null, clickBlocker);
                totalItems++;
            }
            if (sVerbose) {
                Slog.v(TAG, "Number datasets: " + datasetCount + " Total items: " + totalItems);
            }

            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));
            }
            for (int i = 0; i < datasetCount; i++) {
                final Dataset dataset = response.getDatasets().get(i);
                final int index = dataset.getFieldIds().indexOf(focusedViewId);
@@ -184,6 +217,10 @@ final class FillUi {
                    items.add(new ViewItem(dataset, filter, valueText, view));
                }
            }
            if (footer != null) {
                if (sVerbose) Slog.v(TAG, "adding footer");
                items.add(new ViewItem(null, null, null, footer));
            }

            mAdapter = new ItemsAdapter(items);

@@ -192,7 +229,12 @@ final class FillUi {
            mListView.setVisibility(View.VISIBLE);
            mListView.setOnItemClickListener((adapter, view, position, id) -> {
                final ViewItem vi = mAdapter.getItem(position);
                mCallback.onDatasetPicked(vi.getDataset());
                if (vi.dataset == null) {
                    // Clicked on header or footer; ignore.
                    if (sDebug) Slog.d(TAG, "Ignoring click on item " + position + ": " + view);
                    return;
                }
                mCallback.onDatasetPicked(vi.dataset);
            });

            if (filterText == null) {
@@ -206,6 +248,20 @@ final class FillUi {
        }
    }

    /**
     * Creates a remoteview interceptor used to block clicks.
     */
    private RemoteViews.OnClickHandler newClickBlocker() {
        return new RemoteViews.OnClickHandler() {
            @Override
            public boolean onClickHandler(View view, PendingIntent pendingIntent,
                    Intent fillInIntent) {
                if (sVerbose) Slog.v(TAG, "Ignoring click on " + view);
                return true;
            }
        };
    }

    private void applyNewFilterText() {
        final int oldCount = mAdapter.getCount();
        mAdapter.getFilter().filter(mFilterText, (count) -> {
@@ -298,7 +354,7 @@ final class FillUi {
                MeasureSpec.AT_MOST);
        final int itemCount = mAdapter.getCount();
        for (int i = 0; i < itemCount; i++) {
            View view = mAdapter.getItem(i).getView();
            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);
@@ -336,33 +392,21 @@ final class FillUi {
        outPoint.y = (int) typedValue.getFraction(outPoint.y, outPoint.y);
    }

    /**
     * An item for the list view - either a (clickable) dataset or a (read-only) header / footer.
     */
    private static class ViewItem {
        private final String mValue;
        private final Dataset mDataset;
        private final View mView;
        private final Pattern mFilter;

        ViewItem(Dataset dataset, Pattern filter, String value, View view) {
            mDataset = dataset;
            mValue = value;
            mView = view;
            mFilter = filter;
        }

        public Pattern getFilter() {
            return mFilter;
        }

        public View getView() {
            return mView;
        }

        public Dataset getDataset() {
            return mDataset;
        }
        public final @Nullable String value;
        public final @Nullable Dataset dataset;
        public final @NonNull View view;
        public final @Nullable Pattern filter;

        public String getValue() {
            return mValue;
        ViewItem(@Nullable Dataset dataset, @Nullable Pattern filter, @Nullable String value,
                @NonNull View view) {
            this.dataset = dataset;
            this.value = value;
            this.view = view;
            this.filter = filter;
        }
    }

@@ -525,15 +569,13 @@ final class FillUi {
                    final int itemCount = mAllItems.size();
                    for (int i = 0; i < itemCount; i++) {
                        final ViewItem item = mAllItems.get(i);
                        final String value = item.getValue();
                        final Pattern filter = item.getFilter();
                        final boolean matches;
                        if (filter != null) {
                            matches = filter.matcher(constraintLowerCase).matches();
                        if (item.filter != null) {
                            matches = item.filter.matcher(constraintLowerCase).matches();
                        } else {
                            matches = (value == null)
                                    ? (item.mDataset.getAuthentication() == null)
                                    : value.toLowerCase().startsWith(constraintLowerCase);
                            matches = (item.value == null)
                                    ? (item.dataset.getAuthentication() == null)
                                    : item.value.toLowerCase().startsWith(constraintLowerCase);
                        }
                        if (matches) {
                            filteredItems.add(item);
@@ -580,7 +622,7 @@ final class FillUi {

        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            return getItem(position).getView();
            return getItem(position).view;
        }
    }