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

Commit a9379d0b authored by Svetoslav Ganov's avatar Svetoslav Ganov
Browse files

Allow null fill values to support authentication case

If a dataset needs to be authenticated the fill service
may not have the values but needs to tell the system for
which fields to show the fill UI. We now allow passing
a null value to mean the view is a part of the dataset
semantically but its value should remain unchanged.

If a dataset has no values, i.e. the related autofill ids
are mapped to null, we cannot properly filter. In this case
we always match such items regardless what the user typed.

While at this improved accessibility support for filtering
to announce when the number of items being filtered changes.

Also while at this allowed a dataset authentication to return
a response which replaces the current response and refreshes
the UI. Matching datasets with null values to any text plus
allowing a response to be returned from a dataset auth enables
the use case where there is always "Import" item at the
end of the list which when clicked can show arbitrarily more
data entries associated to other apps.

Another change is that we now provide the client state
bundle on both request and dataset auth.

Finally, this change gets rid of dataset waiting auth and
response waiting auth concepts since the reference to the
response and the dataset is piped with the auth request.

Fixed a bug where the width of the autofill UI was not
properly measured by going over all items in the adapter.
Now we measure enough height to fit the first three and the
width id the width of the widest item in the adapter.

Test: Added LoginActivityTest#testDatasetAuthTwoFieldsReplaceResponse
      Added LoginActivityTest#testDatasetAuthTwoFieldsNoValues
      Added LiginActivityTest#filterTextNullValuesAlwaysMatched
      All autofill CTS tests pass

bug:37724701
bug:37424539

Change-Id: Ic19e5d7cbdbb7d110c9e7da0ad60b540cbf1aecf
parent 62643ddd
Loading
Loading
Loading
Loading
+4 −3
Original line number Diff line number Diff line
@@ -7261,7 +7261,7 @@ public class Activity extends ContextThemeWrapper
            }
        } else if (who.startsWith(AUTO_FILL_AUTH_WHO_PREFIX)) {
            Intent resultData = (resultCode == Activity.RESULT_OK) ? data : null;
            getAutofillManager().onAuthenticationResult(resultData);
            getAutofillManager().onAuthenticationResult(requestCode, resultData);
        } else {
            Fragment frag = mFragments.findFragmentByWho(who);
            if (frag != null) {
@@ -7406,10 +7406,11 @@ public class Activity extends ContextThemeWrapper

    /** @hide */
    @Override
    final public void autofillCallbackAuthenticate(IntentSender intent, Intent fillInIntent) {
    final public void autofillCallbackAuthenticate(int authenticationId, IntentSender intent,
            Intent fillInIntent) {
        try {
            startIntentSenderForResultInner(intent, AUTO_FILL_AUTH_WHO_PREFIX,
                    0, fillInIntent, 0, 0, null);
                    authenticationId, fillInIntent, 0, 0, null);
        } catch (IntentSender.SendIntentException e) {
            Log.e(TAG, "authenticate() failed for intent:" + intent, e);
        }
+22 −13
Original line number Diff line number Diff line
@@ -162,14 +162,18 @@ public final class Dataset implements Parcelable {
         *
         * <p>When a user triggers autofill, the system launches the provided intent
         * whose extras will have the {@link
         * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content}. Once
         * you complete your authentication flow you should set the activity result to {@link
         * android.app.Activity#RESULT_OK} and provide the fully populated {@link Dataset
         * dataset} by setting it to the {@link
         * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. For example,
         * if you provided credit card information without the CVV for the data set in the
         * {@link FillResponse response} then the returned data set should contain the
         * CVV entry.</p>
         * android.view.autofill.AutofillManager#EXTRA_ASSIST_STRUCTURE screen content},
         * and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE client
         * state}. Once you complete your authentication flow you should set the activity
         * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
         * {@link Dataset dataset} or a fully-populated {@link FillResponse response} by
         * setting it to the {@link
         * android.view.autofill.AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra. If you
         * provide a dataset in the result, it will replace the authenticated dataset and
         * will be immediately filled in. If you provide a response, it will replace the
         * current response and the UI will be refreshed. For example, if you provided
         * credit card information without the CVV for the data set in the {@link FillResponse
         * response} then the returned data set should contain the CVV entry.
         *
         * <p></><strong>Note:</strong> Do not make the provided pending intent
         * immutable by using {@link android.app.PendingIntent#FLAG_IMMUTABLE} as the
@@ -210,12 +214,15 @@ public final class Dataset implements Parcelable {
         *
         * @param id id returned by {@link
         *         android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
         * @param value value to be auto filled.
         * @param value value to be auto filled. 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 an authentication and you have no access to the value.
         *        Filtering matches any user typed string to {@code null} values.
         * @return This builder.
         * @throws IllegalStateException if the builder was constructed without a presentation
         * ({@link RemoteViews}).
         */
        public @NonNull Builder setValue(@NonNull AutofillId id, @NonNull AutofillValue value) {
        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value) {
            throwIfDestroyed();
            if (mPresentation == null) {
                throw new IllegalStateException("Dataset presentation not set on constructor");
@@ -229,11 +236,14 @@ public final class Dataset implements Parcelable {
         *
         * @param id id returned by {@link
         *         android.app.assist.AssistStructure.ViewNode#getAutofillId()}.
         * @param value value to be auto filled.
         * @param value value to be auto filled. 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 an authentication and you have no access to the value.
         *        Filtering matches any user typed string to {@code null} values.
         * @param presentation The presentation used to visualize this field.
         * @return This builder.
         */
        public @NonNull Builder setValue(@NonNull AutofillId id, @NonNull AutofillValue value,
        public @NonNull Builder setValue(@NonNull AutofillId id, @Nullable AutofillValue value,
                @NonNull RemoteViews presentation) {
            throwIfDestroyed();
            Preconditions.checkNotNull(presentation, "presentation cannot be null");
@@ -244,7 +254,6 @@ public final class Dataset implements Parcelable {
        private void setValueAndPresentation(AutofillId id, AutofillValue value,
                RemoteViews presentation) {
            Preconditions.checkNotNull(id, "id cannot be null");
            Preconditions.checkNotNull(value, "value cannot be null");
            if (mFieldIds != null) {
                final int existingIdx = mFieldIds.indexOf(id);
                if (existingIdx >= 0) {
+2 −1
Original line number Diff line number Diff line
@@ -236,7 +236,8 @@ public final class FillResponse implements Parcelable {
         *
         * <p>When a user triggers autofill, the system launches the provided intent
         * whose extras will have the {@link AutofillManager#EXTRA_ASSIST_STRUCTURE screen
         * content}. Once you complete your authentication flow you should set the activity
         * content} and your {@link android.view.autofill.AutofillManager#EXTRA_CLIENT_STATE
         * client state}. Once you complete your authentication flow you should set the activity
         * result to {@link android.app.Activity#RESULT_OK} and provide the fully populated
         * {@link FillResponse response} by setting it to the {@link
         * AutofillManager#EXTRA_AUTHENTICATION_RESULT} extra.
+54 −7
Original line number Diff line number Diff line
@@ -117,6 +117,48 @@ public final class AutofillManager {
    /** @hide */ public static final int FLAG_ADD_CLIENT_DEBUG = 0x2;
    /** @hide */ public static final int FLAG_ADD_CLIENT_VERBOSE = 0x4;

    /** Which bits in an authentication id are used for the dataset id */
    private static final int AUTHENTICATION_ID_DATASET_ID_MASK = 0xFFFF;
    /** How many bits in an authentication id are used for the dataset id */
    private static final int AUTHENTICATION_ID_DATASET_ID_SHIFT = 16;
    /** @hide The index for an undefined data set */
    public static final int AUTHENTICATION_ID_DATASET_ID_UNDEFINED = 0xFFFF;

    /**
     * Makes an authentication id from a request id and a dataset id.
     *
     * @param requestId The request id.
     * @param datasetId The dataset id.
     * @return The authentication id.
     * @hide
     */
    public static int makeAuthenticationId(int requestId, int datasetId) {
        return (requestId << AUTHENTICATION_ID_DATASET_ID_SHIFT)
                | (datasetId & AUTHENTICATION_ID_DATASET_ID_MASK);
    }

    /**
     * Gets the request id from an authentication id.
     *
     * @param authRequestId The authentication id.
     * @return The request id.
     * @hide
     */
    public static int getRequestIdFromAuthenticationId(int authRequestId) {
        return (authRequestId >> AUTHENTICATION_ID_DATASET_ID_SHIFT);
    }

    /**
     * Gets the dataset id from an authentication id.
     *
     * @param authRequestId The authentication id.
     * @return The dataset id.
     * @hide
     */
    public static int getDatasetIdFromAuthenticationId(int authRequestId) {
        return (authRequestId & AUTHENTICATION_ID_DATASET_ID_MASK);
    }

    private final MetricsLogger mMetricsLogger = new MetricsLogger();

    /**
@@ -156,10 +198,12 @@ public final class AutofillManager {
        /**
         * Asks the client to start an authentication flow.
         *
         * @param authenticationId A unique id of the authentication operation.
         * @param intent The authentication intent.
         * @param fillInIntent The authentication fill-in intent.
         */
        void autofillCallbackAuthenticate(IntentSender intent, Intent fillInIntent);
        void autofillCallbackAuthenticate(int authenticationId, IntentSender intent,
                Intent fillInIntent);

        /**
         * Tells the client this manager has state to be reset.
@@ -675,7 +719,7 @@ public final class AutofillManager {
    }

    /** @hide */
    public void onAuthenticationResult(Intent data) {
    public void onAuthenticationResult(int authenticationId, Intent data) {
        if (!hasAutofillFeature()) {
            return;
        }
@@ -694,7 +738,8 @@ public final class AutofillManager {
            final Bundle responseData = new Bundle();
            responseData.putParcelable(EXTRA_AUTHENTICATION_RESULT, result);
            try {
                mService.setAuthenticationResult(responseData, mSessionId, mContext.getUserId());
                mService.setAuthenticationResult(responseData, mSessionId, authenticationId,
                        mContext.getUserId());
            } catch (RemoteException e) {
                Log.e(TAG, "Error delivering authentication result", e);
            }
@@ -870,12 +915,13 @@ public final class AutofillManager {
        }
    }

    private void authenticate(int sessionId, IntentSender intent, Intent fillInIntent) {
    private void authenticate(int sessionId, int authenticationId, IntentSender intent,
            Intent fillInIntent) {
        synchronized (mLock) {
            if (sessionId == mSessionId) {
                AutofillClient client = getClientLocked();
                if (client != null) {
                    client.autofillCallbackAuthenticate(intent, fillInIntent);
                    client.autofillCallbackAuthenticate(authenticationId, intent, fillInIntent);
                }
            }
        }
@@ -1393,11 +1439,12 @@ public final class AutofillManager {
        }

        @Override
        public void authenticate(int sessionId, IntentSender intent, Intent fillInIntent) {
        public void authenticate(int sessionId, int authenticationId, IntentSender intent,
                Intent fillInIntent) {
            final AutofillManager afm = mAfm.get();
            if (afm != null) {
                afm.mContext.getMainThreadHandler().post(
                        () -> afm.authenticate(sessionId, intent, fillInIntent));
                        () -> afm.authenticate(sessionId, authenticationId, intent, fillInIntent));
            }
        }

+1 −1
Original line number Diff line number Diff line
@@ -41,7 +41,7 @@ interface IAutoFillManager {
            in AutofillValue value, int action, int flags, int userId);
    void finishSession(int sessionId, int userId);
    void cancelSession(int sessionId, int userId);
    void setAuthenticationResult(in Bundle data, int sessionId, int userId);
    void setAuthenticationResult(in Bundle data, int sessionId, int authenticationId, int userId);
    void setHasCallback(int sessionId, int userId, boolean hasIt);
    void disableOwnedAutofillServices(int userId);
    boolean isServiceSupported(int userId);
Loading