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
diff --git a/api/test-current.txt b/api/test-current.txt
index 5cbefb2..52c1731 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -457,8 +457,27 @@
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 {
diff --git a/core/java/android/service/autofill/FieldClassification.java b/core/java/android/service/autofill/FieldClassification.java
new file mode 100644
index 0000000..5d0c81c
--- /dev/null
+++ b/core/java/android/service/autofill/FieldClassification.java
@@ -0,0 +1,184 @@
+/*
+ * 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];
+ }
+ };
+ }
+}
diff --git a/core/java/android/service/autofill/FieldsClassificationScorer.java b/core/java/android/service/autofill/FieldsClassificationScorer.java
new file mode 100644
index 0000000..fea8ebf
--- /dev/null
+++ b/core/java/android/service/autofill/FieldsClassificationScorer.java
@@ -0,0 +1,49 @@
+/*
+ * 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");
+ }
+}
diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java
index eedb972..cf6f296 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -38,6 +38,7 @@
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 @@
}
@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);
+ 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);
- dest.writeTypedList(event.mManuallyFilledFieldIds);
+ 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 @@
@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 @@
}
/**
- * 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 @@
*
* @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 @@
@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 @@
}
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 @@
+ ", changedDatasetsIds=" + mChangedDatasetIds
+ ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
+ ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
+ + ", detectedFieldId=" + mDetectedFieldId
+ ", detectedRemoteId=" + mDetectedRemoteId
+ ", detectedFieldScore=" + mDetectedFieldScore
+ "]";
@@ -546,15 +556,23 @@
} 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;
}
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 8b6dc20..77e8907 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -47,6 +47,7 @@
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 java.io.PrintWriter;
import java.util.ArrayList;
+import java.util.Map;
import java.util.Random;
/**
@@ -626,7 +628,7 @@
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 @@
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 @@
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 @@
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 @@
@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));
}
}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3615bca..106ac8f 100644
--- a/services/autofill/java/com/android/server/autofill/Session.java
+++ b/services/autofill/java/com/android/server/autofill/Session.java
@@ -55,6 +55,7 @@
import android.os.SystemClock;
import android.service.autofill.AutofillService;
import android.service.autofill.Dataset;
+import android.service.autofill.FieldClassification;
import android.service.autofill.FillContext;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
@@ -64,6 +65,7 @@
import android.service.autofill.SaveRequest;
import android.service.autofill.UserData;
import android.service.autofill.ValueFinder;
+import android.service.autofill.FieldsClassificationScorer;
import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.LocalLog;
@@ -942,9 +944,12 @@
}
final UserData userData = mService.getUserData();
+ // TODO(b/67867469): support multiple fields / merge redundant variables below
final AutofillId detectableFieldId;
final String detectableRemoteId;
String detectedRemoteId = null;
+ Map<AutofillId, FieldClassification> fieldsClassification = null;
+
if (userData == null) {
detectableFieldId = null;
detectableRemoteId = null;
@@ -1063,17 +1068,19 @@
// Check if detectable field changed.
if (detectableFieldId != null && detectableFieldId.equals(viewState.id)
&& currentValue.isText() && currentValue.getTextValue() != null) {
- final String actualValue = currentValue.getTextValue().toString();
// TODO(b/67867469): hardcoded to just first entry on initial refactoring.
- final String expectedValue = userData.getValues()[0];
- if (actualValue.equalsIgnoreCase(expectedValue)) {
+ detectedFieldScore = FieldsClassificationScorer.getScore(currentValue,
+ userData.getValues()[0]);
+ if (detectedFieldScore > 0) {
detectedRemoteId = detectableRemoteId;
- detectedFieldScore = 0;
+ fieldsClassification = new ArrayMap<AutofillId, FieldClassification>(1);
+ fieldsClassification.put(detectableFieldId,
+ new FieldClassification(new FieldClassification.Match(
+ detectedRemoteId, detectedFieldScore)));
} else if (sVerbose) {
Slog.v(TAG, "Detection mismatch for field " + detectableFieldId);
}
- // TODO(b/67867469): set score on partial hits
- }
+ } // if
} // else
} // else
}
@@ -1087,6 +1094,7 @@
+ ", manuallyFilledIds=" + manuallyFilledIds
+ ", detectableFieldId=" + detectableFieldId
+ ", detectedFieldScore=" + detectedFieldScore
+ + ", fieldsClassification=" + fieldsClassification
);
}
@@ -1108,8 +1116,7 @@
mService.logContextCommitted(id, mClientState, mSelectedDatasetIds, ignoredDatasets,
changedFieldIds, changedDatasetIds,
- manuallyFilledFieldIds, manuallyFilledDatasetIds,
- detectedRemoteId, detectedFieldScore);
+ manuallyFilledFieldIds, manuallyFilledDatasetIds, fieldsClassification);
}
/**