diff options
| author | 2017-12-04 11:22:25 -0800 | |
|---|---|---|
| committer | 2017-12-06 11:20:10 -0800 | |
| commit | bb6bfea6801cff5b50c990bdcfbd2df93ddf9023 (patch) | |
| tree | 7ac959bd6d2a4d336a6348f56a17694586fa4788 | |
| parent | a044c1d27e9380d649b6b9dadfb582136be5fa79 (diff) | |
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
6 files changed, 336 insertions, 56 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index 5cbefb29a071..52c1731d1d3c 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -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 { diff --git a/core/java/android/service/autofill/FieldClassification.java b/core/java/android/service/autofill/FieldClassification.java new file mode 100644 index 000000000000..5d0c81cd59a0 --- /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 000000000000..fea8ebf4bbd9 --- /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 eedb972e4ea6..cf6f2969bc9d 100644 --- a/core/java/android/service/autofill/FillEventHistory.java +++ b/core/java/android/service/autofill/FillEventHistory.java @@ -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; } diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index 8b6dc2028b91..77e8907435f1 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.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)); } } } diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 3615bca48e35..106ac8ff2a00 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.RemoteException; 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.SaveInfo; 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 class Session implements RemoteFillService.FillServiceCallbacks, ViewState } 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // 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 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + ", manuallyFilledIds=" + manuallyFilledIds + ", detectableFieldId=" + detectableFieldId + ", detectedFieldScore=" + detectedFieldScore + + ", fieldsClassification=" + fieldsClassification ); } @@ -1108,8 +1116,7 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState mService.logContextCommitted(id, mClientState, mSelectedDatasetIds, ignoredDatasets, changedFieldIds, changedDatasetIds, - manuallyFilledFieldIds, manuallyFilledDatasetIds, - detectedRemoteId, detectedFieldScore); + manuallyFilledFieldIds, manuallyFilledDatasetIds, fieldsClassification); } /** |