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

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

Refactored the FieldsClassification score mechanism.

Before, FillEvent.getFieldsClassification() returned a map of remote ids and
scores. Now, it returns a Map of FieldClassication by AutofillId, which allows
multiple fields and scores for multiple user datas (although the initial
implementation supports only the top match for a field).

This is mostly a refactoring CL, as the implementation is still saving just one
user data entry and one field. But full support is coming next...

Test: atest CtsAutoFillServiceTestCases:FieldsClassificationTest
Test: atest CtsAutoFillServiceTestCases:UserDataTest
Test: atest CtsAutoFillServiceTestCases:FieldsClassificationScorerTest

Bug: 68045531

Change-Id: I08b29f24efbd527216f9bce2343e1bcd4b4554c0
parent a044c1d2
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];
            }
        };
    }
}
+49 −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) {
        // TODO(b/67867469): implement edit distance - currently it's returning either 0 or 100%
        if (actualValue == null || !actualValue.isText() || userData == null) return 0;
        return actualValue.getTextValue().toString().equalsIgnoreCase(userData) ? MAX_VALUE : 0;
    }

    private FieldsClassificationScorer() {
        throw new UnsupportedOperationException("contains only static methods");
    }
}
+59 −41
Original line number Diff line number Diff line
@@ -38,6 +38,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

/**
@@ -123,34 +124,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);
                parcel.writeParcelable(event.mDetectedFieldId, flags);
                if (event.mDetectedRemoteId != null) {
                    dest.writeInt(event.mDetectedFieldScore);
                    parcel.writeString(event.mDetectedRemoteId);
                    parcel.writeInt(event.mDetectedFieldScore);
                }
            }
        }
@@ -242,6 +244,8 @@ public final class FillEventHistory implements Parcelable {
        @Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds;
        @Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds;

        // TODO(b/67867469): store list of fields instead of hardcoding just one
        @Nullable private final AutofillId mDetectedFieldId;
        @Nullable private final String mDetectedRemoteId;
        private final int mDetectedFieldScore;

@@ -347,37 +351,29 @@ 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 (mDetectedFieldId == null || mDetectedRemoteId == null
                    || mDetectedFieldScore == -1) {
                return Collections.emptyMap();
            }

            final ArrayMap<String, Integer> map = new ArrayMap<>(1);
            map.put(mDetectedRemoteId, mDetectedFieldScore);
            final ArrayMap<AutofillId, FieldClassification> map = new ArrayMap<>(1);
            map.put(mDetectedFieldId,
                    new FieldClassification(new FieldClassification.Match(
                            mDetectedRemoteId, mDetectedFieldScore)));
            return map;
        }

@@ -464,7 +460,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 +468,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 Map<AutofillId, FieldClassification> fieldsClassification) {
            mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED,
                    "eventType");
            mDatasetId = datasetId;
@@ -495,8 +491,21 @@ public final class FillEventHistory implements Parcelable {
            }
            mManuallyFilledFieldIds = manuallyFilledFieldIds;
            mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
            mDetectedRemoteId = detectedRemoteId;
            mDetectedFieldScore = detectedFieldScore;

            // TODO(b/67867469): store list of fields instead of hardcoding just one
            if (fieldsClassification == null) {
                mDetectedFieldId = null;
                mDetectedRemoteId = null;
                mDetectedFieldScore = 0;

            } else {
                final Entry<AutofillId, FieldClassification> tmpEntry = fieldsClassification
                        .entrySet().iterator().next();
                final FieldClassification.Match tmpMatch = tmpEntry.getValue().getTopMatch();
                mDetectedFieldId = tmpEntry.getKey();
                mDetectedRemoteId = tmpMatch.getRemoteId();
                mDetectedFieldScore = tmpMatch.getScore();
            }
        }

        @Override
@@ -509,6 +518,7 @@ public final class FillEventHistory implements Parcelable {
                    + ", changedDatasetsIds=" + mChangedDatasetIds
                    + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
                    + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
                    + ", detectedFieldId=" + mDetectedFieldId
                    + ", detectedRemoteId=" + mDetectedRemoteId
                    + ", detectedFieldScore=" + mDetectedFieldScore
                    + "]";
@@ -546,15 +556,23 @@ public final class FillEventHistory implements Parcelable {
                        } else {
                            manuallyFilledDatasetIds = null;
                        }
                        final String detectedRemoteId = parcel.readString();
                        final int detectedFieldScore = detectedRemoteId == null ? -1
                                : parcel.readInt();
                        // TODO(b/67867469): store list of fields instead of hardcoding just one
                        ArrayMap<AutofillId, FieldClassification> fieldsClassification = null;
                        final AutofillId detectedFieldId = parcel.readParcelable(null);
                        if (detectedFieldId == null) {
                            fieldsClassification = null;
                        } else {
                            fieldsClassification = new ArrayMap<AutofillId, FieldClassification>(1);
                            fieldsClassification.put(detectedFieldId,
                                    new FieldClassification(new FieldClassification.Match(
                                            parcel.readString(), parcel.readInt())));
                        }

                        selection.addEvent(new Event(eventType, datasetId, clientState,
                                selectedDatasetIds, ignoredDatasets,
                                changedFieldIds, changedDatasetIds,
                                manuallyFilledFieldIds, manuallyFilledDatasetIds,
                                detectedRemoteId, detectedFieldScore));
                                fieldsClassification));
                    }
                    return selection;
                }
+9 −6
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ import android.os.UserManager;
import android.provider.Settings;
import android.service.autofill.AutofillService;
import android.service.autofill.AutofillServiceInfo;
import android.service.autofill.FieldClassification;
import android.service.autofill.FillEventHistory;
import android.service.autofill.FillEventHistory.Event;
import android.service.autofill.FillResponse;
@@ -74,6 +75,7 @@ import com.android.server.autofill.ui.AutoFillUI;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Map;
import java.util.Random;

/**
@@ -626,7 +628,7 @@ final class AutofillManagerServiceImpl {
            if (isValidEventLocked("setAuthenticationSelected()", sessionId)) {
                mEventHistory.addEvent(
                        new Event(Event.TYPE_AUTHENTICATION_SELECTED, null, clientState, null, null,
                                null, null, null, null, null, -1));
                                null, null, null, null, null));
            }
        }
    }
@@ -640,7 +642,7 @@ final class AutofillManagerServiceImpl {
            if (isValidEventLocked("logDatasetAuthenticationSelected()", sessionId)) {
                mEventHistory.addEvent(
                        new Event(Event.TYPE_DATASET_AUTHENTICATION_SELECTED, selectedDataset,
                                clientState, null, null, null, null, null, null, null, -1));
                                clientState, null, null, null, null, null, null, null));
            }
        }
    }
@@ -652,7 +654,7 @@ final class AutofillManagerServiceImpl {
        synchronized (mLock) {
            if (isValidEventLocked("logSaveShown()", sessionId)) {
                mEventHistory.addEvent(new Event(Event.TYPE_SAVE_SHOWN, null, clientState, null,
                        null, null, null, null, null, null, -1));
                        null, null, null, null, null, null));
            }
        }
    }
@@ -666,7 +668,7 @@ final class AutofillManagerServiceImpl {
            if (isValidEventLocked("logDatasetSelected()", sessionId)) {
                mEventHistory.addEvent(
                        new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState, null,
                                null, null, null, null, null, null, -1));
                                null, null, null, null, null, null));
            }
        }
    }
@@ -681,14 +683,15 @@ final class AutofillManagerServiceImpl {
            @Nullable ArrayList<String> changedDatasetIds,
            @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
            @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
            @Nullable String detectedRemoteId, int detectedFieldScore) {
            @Nullable Map<AutofillId, FieldClassification> fieldsClassification) {

        synchronized (mLock) {
            if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
                mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null,
                        clientState, selectedDatasets, ignoredDatasets,
                        changedFieldIds, changedDatasetIds,
                        manuallyFilledFieldIds, manuallyFilledDatasetIds,
                        detectedRemoteId, detectedFieldScore));
                        fieldsClassification));
            }
        }
    }
Loading