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

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

Merge changes from topics "moar_fields", "fc_refactor_score"

* changes:
  Implemented autofill field classification on multiple fields and user data.
  Refactored the FieldsClassification score mechanism.
parents 081be42c 5672defa
Loading
Loading
Loading
Loading
+20 −1
Original line number Diff line number Diff line
@@ -457,8 +457,27 @@ package android.service.autofill {
    method public void apply(android.service.autofill.ValueFinder, android.widget.RemoteViews, int) throws java.lang.Exception;
  }

  public final class FieldClassification implements android.os.Parcelable {
    method public int describeContents();
    method public android.service.autofill.FieldClassification.Match getTopMatch();
    method public void writeToParcel(android.os.Parcel, int);
    field public static final android.os.Parcelable.Creator<android.service.autofill.FieldClassification> CREATOR;
  }

  public static final class FieldClassification.Match implements android.os.Parcelable {
    method public int describeContents();
    method public java.lang.String getRemoteId();
    method public int getScore();
    method public void writeToParcel(android.os.Parcel, int);
    field public static final android.os.Parcelable.Creator<android.service.autofill.FieldClassification.Match> CREATOR;
  }

  public final class FieldsClassificationScorer {
    method public static int getScore(android.view.autofill.AutofillValue, java.lang.String);
  }

  public static final class FillEventHistory.Event {
    method public java.util.Map<java.lang.String, java.lang.Integer> getFieldsClassification();
    method public java.util.Map<android.view.autofill.AutofillId, android.service.autofill.FieldClassification> getFieldsClassification();
  }

  public static final class FillResponse.Builder {
+184 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.service.autofill;

import static android.view.autofill.Helper.sDebug;

import android.annotation.NonNull;
import android.annotation.TestApi;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.autofill.Helper;

import com.android.internal.util.Preconditions;

/**
 * Gets the <a href="#FieldsClassification">fields classification</a> results for a given field.
 *
 * TODO(b/67867469):
 * - improve javadoc
 * - unhide / remove testApi
 *
 * @hide
 */
@TestApi
public final class FieldClassification implements Parcelable {

    private final Match mMatch;

    /** @hide */
    public FieldClassification(@NonNull Match match) {
        mMatch = Preconditions.checkNotNull(match);
    }

    /**
     * Gets the {@link Match} with the highest {@link Match#getScore() score} for the field.
     */
    @NonNull
    public Match getTopMatch() {
        return mMatch;
    }

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

        return "FieldClassification: " + mMatch;
    }

    /////////////////////////////////////
    // Parcelable "contract" methods. //
    /////////////////////////////////////

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

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

    public static final Parcelable.Creator<FieldClassification> CREATOR =
            new Parcelable.Creator<FieldClassification>() {

        @Override
        public FieldClassification createFromParcel(Parcel parcel) {
            return new FieldClassification(parcel.readParcelable(null));
        }

        @Override
        public FieldClassification[] newArray(int size) {
            return new FieldClassification[size];
        }
    };

    /**
     * Gets the score of a {@link UserData} entry for the field.
     *
     * TODO(b/67867469):
     * - improve javadoc
     * - unhide / remove testApi
     *
     * @hide
     */
    @TestApi
    public static final class Match implements Parcelable {

        private final String mRemoteId;
        private final int mScore;

        /** @hide */
        public Match(String remoteId, int score) {
            mRemoteId = Preconditions.checkNotNull(remoteId);
            mScore = score;
        }

        /**
         * Gets the remote id of the {@link UserData} entry.
         */
        @NonNull
        public String getRemoteId() {
            return mRemoteId;
        }

        /**
         * Gets a score between the value of this field and the value of the {@link UserData} entry.
         *
         * <p>The score is based in a case-insensitive comparisson of all characters from both the
         * field value and the user data entry, and it ranges from {@code 0} to {@code 1000000}:
         * <ul>
         *   <li>{@code 1000000} represents a full match ({@code 100.0000%}).
         *   <li>{@code 0} represents a full mismatch ({@code 0.0000%}).
         *   <li>Any other value is a partial match.
         * </ul>
         *
         * <p>How the score is calculated depends on the algorithm used by the Android System.
         * For example, if the user  data is {@code "abc"} and the field value us {@code " abc"},
         * the result could be:
         * <ul>
         *   <li>{@code 1000000} if the algorithm trims the values.
         *   <li>{@code 0} if the algorithm compares the values sequentially.
         *   <li>{@code 750000} if the algorithm consideres that 3/4 (75%) of the characters match.
         * </ul>
         *
         * <p>Currently, the autofill service cannot configure the algorithm.
         */
        public int getScore() {
            return mScore;
        }

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

            final StringBuilder string = new StringBuilder("Match: remoteId=");
            Helper.appendRedacted(string, mRemoteId);
            return string.append(", score=").append(mScore).toString();
        }

        /////////////////////////////////////
        // Parcelable "contract" methods. //
        /////////////////////////////////////

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

        @Override
        public void writeToParcel(Parcel parcel, int flags) {
            parcel.writeString(mRemoteId);
            parcel.writeInt(mScore);
        }

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

            @Override
            public Match createFromParcel(Parcel parcel) {
                return new Match(parcel.readString(), parcel.readInt());
            }

            @Override
            public Match[] newArray(int size) {
                return new Match[size];
            }
        };
    }
}
+64 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2017 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package android.service.autofill;

import android.annotation.NonNull;
import android.annotation.TestApi;
import android.view.autofill.AutofillValue;

/**
 * Helper used to calculate the classification score between an actual {@link AutofillValue} filled
 * by the user and the expected value predicted by an autofill service.
 *
 * @hide
 */
@TestApi
public final class FieldsClassificationScorer {

    private static final int MAX_VALUE = 100_0000; // 100.0000%

    /**
     * Returns the classification score between an actual {@link AutofillValue} filled
     * by the user and the expected value predicted by an autofill service.
     *
     * <p>A full-match is {@code 1000000} (representing 100.0000%), a full mismatch is {@code 0} and
     * partial mathces are something in between, typically using edit-distance algorithms.
     */
    public static int getScore(@NonNull AutofillValue actualValue, @NonNull String userData) {
        if (actualValue == null || !actualValue.isText() || userData == null) return 0;
        // TODO(b/67867469): implement edit distance - currently it's returning either 0, 100%, or
        // partial match when number of chars match
        final String textValue = actualValue.getTextValue().toString();
        final int total = textValue.length();
        if (total != userData.length()) return 0;

        int matches = 0;
        for (int i = 0; i < total; i++) {
            if (Character.toLowerCase(textValue.charAt(i)) == Character
                    .toLowerCase(userData.charAt(i))) {
                matches++;
            }
        }

        final float percentage = ((float) matches) / total;
        final int rounded = (int) (percentage * MAX_VALUE);
        return rounded;
    }

    private FieldsClassificationScorer() {
        throw new UnsupportedOperationException("contains only static methods");
    }
}
+54 −47
Original line number Diff line number Diff line
@@ -16,6 +16,8 @@

package android.service.autofill;

import static android.view.autofill.Helper.sVerbose;

import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
@@ -24,8 +26,10 @@ import android.content.IntentSender;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
import android.service.autofill.FieldClassification.Match;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.Log;
import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;

@@ -35,6 +39,7 @@ import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
@@ -57,6 +62,8 @@ import java.util.Set;
 * the history will clear out after some pre-defined time).
 */
public final class FillEventHistory implements Parcelable {
    private static final String TAG = "FillEventHistory";

    /**
     * Not in parcel. The ID of the autofill session that created the {@link FillResponse}.
     */
@@ -123,34 +130,35 @@ public final class FillEventHistory implements Parcelable {
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeBundle(mClientState);
    public void writeToParcel(Parcel parcel, int flags) {
        parcel.writeBundle(mClientState);
        if (mEvents == null) {
            dest.writeInt(0);
            parcel.writeInt(0);
        } else {
            dest.writeInt(mEvents.size());
            parcel.writeInt(mEvents.size());

            int numEvents = mEvents.size();
            for (int i = 0; i < numEvents; i++) {
                Event event = mEvents.get(i);
                dest.writeInt(event.mEventType);
                dest.writeString(event.mDatasetId);
                dest.writeBundle(event.mClientState);
                dest.writeStringList(event.mSelectedDatasetIds);
                dest.writeArraySet(event.mIgnoredDatasetIds);
                dest.writeTypedList(event.mChangedFieldIds);
                dest.writeStringList(event.mChangedDatasetIds);

                dest.writeTypedList(event.mManuallyFilledFieldIds);
                parcel.writeInt(event.mEventType);
                parcel.writeString(event.mDatasetId);
                parcel.writeBundle(event.mClientState);
                parcel.writeStringList(event.mSelectedDatasetIds);
                parcel.writeArraySet(event.mIgnoredDatasetIds);
                parcel.writeTypedList(event.mChangedFieldIds);
                parcel.writeStringList(event.mChangedDatasetIds);

                parcel.writeTypedList(event.mManuallyFilledFieldIds);
                if (event.mManuallyFilledFieldIds != null) {
                    final int size = event.mManuallyFilledFieldIds.size();
                    for (int j = 0; j < size; j++) {
                        dest.writeStringList(event.mManuallyFilledDatasetIds.get(j));
                        parcel.writeStringList(event.mManuallyFilledDatasetIds.get(j));
                    }
                }
                dest.writeString(event.mDetectedRemoteId);
                if (event.mDetectedRemoteId != null) {
                    dest.writeInt(event.mDetectedFieldScore);
                final AutofillId[] detectedFields = event.mDetectedFieldIds;
                parcel.writeParcelableArray(detectedFields, flags);
                if (detectedFields != null) {
                    parcel.writeParcelableArray(event.mDetectedMatches, flags);
                }
            }
        }
@@ -242,8 +250,8 @@ public final class FillEventHistory implements Parcelable {
        @Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds;
        @Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds;

        @Nullable private final String mDetectedRemoteId;
        private final int mDetectedFieldScore;
        @Nullable private final AutofillId[] mDetectedFieldIds;
        @Nullable private final Match[] mDetectedMatches;

        /**
         * Returns the type of the event.
@@ -347,37 +355,33 @@ public final class FillEventHistory implements Parcelable {
        }

        /**
         * Gets the results of the last fields classification request.
         *
         * @return map of edit-distance match ({@code 0} means full match,
         * {@code 1} means 1 character different, etc...) by remote id (as set on
         * {@link UserData.Builder#add(String, android.view.autofill.AutofillValue)}),
         * or {@code null} if none of the user-input values
         * matched the requested detection.
         * Gets the <a href="#FieldsClassification">fields classification</a> results.
         *
         * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the
         * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)
         * fields detection}.
         * fields classification}.
         *
         * TODO(b/67867469):
         *  - improve javadoc
         *  - refine score meaning (for example, should 1 be different of -1?)
         *  - mention when it's set
         *  - unhide
         *  - unhide / remove testApi
         *  - add @NonNull / check it / add unit tests
         *  - add link to AutofillService #FieldsClassification anchor
         *
         * @hide
         */
        @TestApi
        @NonNull public Map<String, Integer> getFieldsClassification() {
            if (mDetectedRemoteId == null || mDetectedFieldScore == -1) {
        @NonNull public Map<AutofillId, FieldClassification> getFieldsClassification() {
            if (mDetectedFieldIds == null) {
                return Collections.emptyMap();
            }

            final ArrayMap<String, Integer> map = new ArrayMap<>(1);
            map.put(mDetectedRemoteId, mDetectedFieldScore);
            final int size = mDetectedFieldIds.length;
            final ArrayMap<AutofillId, FieldClassification> map = new ArrayMap<>(size);
            for (int i = 0; i < size; i++) {
                final AutofillId id = mDetectedFieldIds[i];
                final Match match = mDetectedMatches[i];
                if (sVerbose) {
                    Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", match=" + match);
                }
                map.put(id, new FieldClassification(match));
            }
            return map;
        }

@@ -464,7 +468,7 @@ public final class FillEventHistory implements Parcelable {
         *
         * @hide
         */
        // TODO(b/67867469): document detection field parameters once stable
        // TODO(b/67867469): document field classification parameters once stable
        public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState,
                @Nullable List<String> selectedDatasetIds,
                @Nullable ArraySet<String> ignoredDatasetIds,
@@ -472,7 +476,7 @@ public final class FillEventHistory implements Parcelable {
                @Nullable ArrayList<String> changedDatasetIds,
                @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
                @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
                @Nullable String detectedRemoteId, int detectedFieldScore) {
                @Nullable AutofillId[] detectedFieldIds, @Nullable Match[] detectedMaches) {
            mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED,
                    "eventType");
            mDatasetId = datasetId;
@@ -495,8 +499,9 @@ public final class FillEventHistory implements Parcelable {
            }
            mManuallyFilledFieldIds = manuallyFilledFieldIds;
            mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
            mDetectedRemoteId = detectedRemoteId;
            mDetectedFieldScore = detectedFieldScore;

            mDetectedFieldIds = detectedFieldIds;
            mDetectedMatches = detectedMaches;
        }

        @Override
@@ -509,8 +514,8 @@ public final class FillEventHistory implements Parcelable {
                    + ", changedDatasetsIds=" + mChangedDatasetIds
                    + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
                    + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
                    + ", detectedRemoteId=" + mDetectedRemoteId
                    + ", detectedFieldScore=" + mDetectedFieldScore
                    + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds)
                    + ", detectedMaches =" + Arrays.toString(mDetectedMatches)
                    + "]";
        }
    }
@@ -546,15 +551,17 @@ public final class FillEventHistory implements Parcelable {
                        } else {
                            manuallyFilledDatasetIds = null;
                        }
                        final String detectedRemoteId = parcel.readString();
                        final int detectedFieldScore = detectedRemoteId == null ? -1
                                : parcel.readInt();
                        final AutofillId[] detectedFieldIds = parcel.readParcelableArray(null,
                                AutofillId.class);
                        final Match[] detectedMatches = (detectedFieldIds != null)
                                ? parcel.readParcelableArray(null, Match.class)
                                : null;

                        selection.addEvent(new Event(eventType, datasetId, clientState,
                                selectedDatasetIds, ignoredDatasets,
                                changedFieldIds, changedDatasetIds,
                                manuallyFilledFieldIds, manuallyFilledDatasetIds,
                                detectedRemoteId, detectedFieldScore));
                                detectedFieldIds, detectedMatches));
                    }
                    return selection;
                }
+14 −5
Original line number Diff line number Diff line
@@ -29,7 +29,6 @@ import android.content.ContentResolver;
import android.os.Parcel;
import android.os.Parcelable;
import android.provider.Settings;
import android.util.ArraySet;
import android.util.Log;
import android.view.autofill.Helper;

@@ -79,10 +78,16 @@ public final class UserData implements Parcelable {

    /** @hide */
    public void dump(String prefix, PrintWriter pw) {
        // Cannot disclose remote ids because they could contain PII
        // Cannot disclose remote ids or values because they could contain PII
        pw.print(prefix); pw.print("Remote ids size: "); pw.println(mRemoteIds.length);
        for (int i = 0; i < mRemoteIds.length; i++) {
            pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
            pw.println(Helper.getRedacted(mRemoteIds[i]));
        }
        pw.print(prefix); pw.print("Values size: "); pw.println(mValues.length);
        for (int i = 0; i < mValues.length; i++) {
            pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": "); pw.println(mValues[i]);
            pw.print(prefix); pw.print(prefix); pw.print(i); pw.print(": ");
            pw.println(Helper.getRedacted(mValues[i]));
        }
    }

@@ -104,7 +109,7 @@ public final class UserData implements Parcelable {
     */
    @TestApi
    public static final class Builder {
        private final ArraySet<String> mRemoteIds;
        private final ArrayList<String> mRemoteIds;
        private final ArrayList<String> mValues;
        private boolean mDestroyed;

@@ -120,7 +125,7 @@ public final class UserData implements Parcelable {
            checkValidRemoteId(remoteId);
            checkValidValue(value);
            final int capacity = getMaxUserDataSize();
            mRemoteIds = new ArraySet<>(capacity);
            mRemoteIds = new ArrayList<>(capacity);
            mValues = new ArrayList<>(capacity);
            mRemoteIds.add(remoteId);
            mValues.add(value);
@@ -149,10 +154,14 @@ public final class UserData implements Parcelable {
            Preconditions.checkState(!mRemoteIds.contains(remoteId),
                    // Don't include remoteId on message because it could contain PII
                    "already has entry with same remoteId");
            Preconditions.checkState(!mValues.contains(value),
                    // Don't include remoteId on message because it could contain PII
                    "already has entry with same value");
            Preconditions.checkState(mRemoteIds.size() < getMaxUserDataSize(),
                    "already added " + mRemoteIds.size() + " elements");
            mRemoteIds.add(remoteId);
            mValues.add(value);

            return this;
        }

Loading