diff options
8 files changed, 367 insertions, 68 deletions
diff --git a/api/test-current.txt b/api/test-current.txt index 9f02bc5b3668..584c265d0d67 100644 --- a/api/test-current.txt +++ b/api/test-current.txt @@ -35544,6 +35544,7 @@ package android.provider { field public static final java.lang.String ALLOWED_GEOLOCATION_ORIGINS = "allowed_geolocation_origins"; field public static final deprecated java.lang.String ALLOW_MOCK_LOCATION = "mock_location"; field public static final java.lang.String ANDROID_ID = "android_id"; + field public static final java.lang.String AUTOFILL_FEATURE_FIELD_DETECTION = "autofill_field_detection"; field public static final java.lang.String AUTOFILL_SERVICE = "autofill_service"; field public static final deprecated java.lang.String BACKGROUND_DATA = "background_data"; field public static final deprecated java.lang.String BLUETOOTH_ON = "bluetooth_on"; @@ -37599,6 +37600,13 @@ package android.service.autofill { method public android.service.autofill.Dataset.Builder setValue(android.view.autofill.AutofillId, android.view.autofill.AutofillValue, java.util.regex.Pattern, android.widget.RemoteViews); } + public final class FieldsDetection implements android.os.Parcelable { + ctor public FieldsDetection(android.view.autofill.AutofillId, java.lang.String, java.lang.String); + method public int describeContents(); + method public void writeToParcel(android.os.Parcel, int); + field public static final android.os.Parcelable.Creator<android.service.autofill.FieldsDetection> CREATOR; + } + public final class FillCallback { method public void onFailure(java.lang.CharSequence); method public void onSuccess(android.service.autofill.FillResponse); @@ -37624,6 +37632,7 @@ package android.service.autofill { method public java.util.Map<android.view.autofill.AutofillId, java.lang.String> getChangedFields(); method public android.os.Bundle getClientState(); method public java.lang.String getDatasetId(); + method public java.util.Map<java.lang.String, java.lang.Integer> getDetectedFields(); method public java.util.Set<java.lang.String> getIgnoredDatasetIds(); method public java.util.Map<android.view.autofill.AutofillId, java.util.Set<java.lang.String>> getManuallyEnteredField(); method public java.util.Set<java.lang.String> getSelectedDatasetIds(); @@ -37661,6 +37670,7 @@ package android.service.autofill { method public android.service.autofill.FillResponse.Builder disableAutofill(long); method public android.service.autofill.FillResponse.Builder setAuthentication(android.view.autofill.AutofillId[], android.content.IntentSender, android.widget.RemoteViews); method public android.service.autofill.FillResponse.Builder setClientState(android.os.Bundle); + method public android.service.autofill.FillResponse.Builder setFieldsDetection(android.service.autofill.FieldsDetection); method public android.service.autofill.FillResponse.Builder setFlags(int); method public android.service.autofill.FillResponse.Builder setIgnoredIds(android.view.autofill.AutofillId...); method public android.service.autofill.FillResponse.Builder setSaveInfo(android.service.autofill.SaveInfo); diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java index 433878e9511a..398e08ff47ce 100755 --- a/core/java/android/provider/Settings.java +++ b/core/java/android/provider/Settings.java @@ -5309,6 +5309,15 @@ public final class Settings { public static final String AUTOFILL_SERVICE = "autofill_service"; /** + * Experimental autofill feature. + * + * <p>TODO(b/67867469): remove once feature is finished + * @hide + */ + @TestApi + public static final String AUTOFILL_FEATURE_FIELD_DETECTION = "autofill_field_detection"; + + /** * @deprecated Use {@link android.provider.Settings.Global#DEVICE_PROVISIONED} instead */ @Deprecated diff --git a/core/java/android/service/autofill/FieldsDetection.java b/core/java/android/service/autofill/FieldsDetection.java new file mode 100644 index 000000000000..550ecf687349 --- /dev/null +++ b/core/java/android/service/autofill/FieldsDetection.java @@ -0,0 +1,127 @@ +/* + * Copyright 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.TestApi; +import android.os.Parcel; +import android.os.Parcelable; +import android.view.autofill.AutofillId; + +/** + * Class by service to improve autofillable fields detection by tracking the meaning of fields + * manually edited by the user (when they match values provided by the service). + * + * TODO(b/67867469): + * - proper javadoc + * - unhide / remove testApi + * - add FieldsDetection management so service can set it just once and reference it in further + * calls to improve performance (and also API to refresh it) + * - rename to FieldsDetectionInfo or FieldClassification? (same for CTS tests) + * - add FieldsDetectionUnitTest once API is well-defined + * @hide + */ +@TestApi +public final class FieldsDetection implements Parcelable { + + private final AutofillId mFieldId; + private final String mRemoteId; + private final String mValue; + + /** + * Creates a field detection for just one field / value pair. + * + * @param fieldId autofill id of the field in the screen. + * @param remoteId id used by the service to identify the field later. + * @param value field value known to the service. + * + * TODO(b/67867469): + * - proper javadoc + * - change signature to allow more fields / values / match methods + * - might also need to use a builder, where the constructor is the id for the fieldsdetector + * - might need id for values as well + * - add @NonNull / check it / add unit tests + * - make 'value' input more generic so it can accept distance-based match and other matches + * - throw exception if field value is less than X characters (somewhere between 7-10) + * - make sure to limit total number of fields to around 10 or so + * - use AutofillValue instead of String (so it can compare dates, for example) + */ + public FieldsDetection(AutofillId fieldId, String remoteId, String value) { + mFieldId = fieldId; + mRemoteId = remoteId; + mValue = value; + } + + /** @hide */ + public AutofillId getFieldId() { + return mFieldId; + } + + /** @hide */ + public String getRemoteId() { + return mRemoteId; + } + + /** @hide */ + public String getValue() { + return mValue; + } + + ///////////////////////////////////// + // Object "contract" methods. // + ///////////////////////////////////// + @Override + public String toString() { + // Cannot disclose remoteId or value because they could contain PII + return new StringBuilder("FieldsDetection: [field=").append(mFieldId) + .append(", remoteId_length=").append(mRemoteId.length()) + .append(", value_length=").append(mValue.length()) + .append("]").toString(); + } + + ///////////////////////////////////// + // Parcelable "contract" methods. // + ///////////////////////////////////// + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeParcelable(mFieldId, flags); + parcel.writeString(mRemoteId); + parcel.writeString(mValue); + } + + public static final Parcelable.Creator<FieldsDetection> CREATOR = + new Parcelable.Creator<FieldsDetection>() { + @Override + public FieldsDetection createFromParcel(Parcel parcel) { + // TODO(b/67867469): remove comment below if it does not use a builder at the end + // Always go through the builder to ensure the data ingested by + // the system obeys the contract of the builder to avoid attacks + // using specially crafted parcels. + return new FieldsDetection(parcel.readParcelable(null), parcel.readString(), + parcel.readString()); + } + + @Override + public FieldsDetection[] newArray(int size) { + return new FieldsDetection[size]; + } + }; +} diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java index b1857b303b36..736d9ef48d04 100644 --- a/core/java/android/service/autofill/FillEventHistory.java +++ b/core/java/android/service/autofill/FillEventHistory.java @@ -19,6 +19,7 @@ package android.service.autofill; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.content.IntentSender; import android.os.Bundle; import android.os.Parcel; @@ -164,6 +165,10 @@ public final class FillEventHistory implements Parcelable { dest.writeStringList(event.mManuallyFilledDatasetIds.get(j)); } } + dest.writeString(event.mDetectedRemoteId); + if (event.mDetectedRemoteId != null) { + dest.writeInt(event.mDetectedFieldScore); + } } } } @@ -226,6 +231,7 @@ public final class FillEventHistory implements Parcelable { * <p>See {@link android.view.autofill.AutofillManager} for more information about autofill * contexts. */ + // TODO(b/67867469): update with field detection behavior public static final int TYPE_CONTEXT_COMMITTED = 4; /** @hide */ @@ -253,6 +259,9 @@ 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; + /** * Returns the type of the event. * @@ -355,6 +364,39 @@ public final class FillEventHistory implements Parcelable { } /** + * Gets the results of the last {@link FieldsDetection} request. + * + * @return map of edit-distance match ({@code 0} means full match, + * {@code 1} means 1 character different, etc...) by remote id (as set in the + * {@link FieldsDetection} constructor), or {@code null} if none of the user-input values + * matched the requested detection. + * + * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the + * service requested {@link FillResponse.Builder#setFieldsDetection(FieldsDetection) fields + * detection}. + * + * 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 + * + * @hide + */ + @TestApi + @NonNull public Map<String, Integer> getDetectedFields() { + if (mDetectedRemoteId == null || mDetectedFieldScore == -1) { + return Collections.emptyMap(); + } + + final ArrayMap<String, Integer> map = new ArrayMap<>(1); + map.put(mDetectedRemoteId, mDetectedFieldScore); + return map; + } + + /** * Returns which fields were available on datasets provided by the service but manually * entered by the user. * @@ -430,7 +472,6 @@ public final class FillEventHistory implements Parcelable { * and belonged to datasets. * @param manuallyFilledDatasetIds The ids of datasets that had values matching the * respective entry on {@code manuallyFilledFieldIds}. - * * @throws IllegalArgumentException If the length of {@code changedFieldIds} and * {@code changedDatasetIds} doesn't match. * @throws IllegalArgumentException If the length of {@code manuallyFilledFieldIds} and @@ -438,13 +479,15 @@ public final class FillEventHistory implements Parcelable { * * @hide */ + // TODO(b/67867469): document detection field parameters once stable public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState, @Nullable List<String> selectedDatasetIds, @Nullable ArraySet<String> ignoredDatasetIds, @Nullable ArrayList<AutofillId> changedFieldIds, @Nullable ArrayList<String> changedDatasetIds, @Nullable ArrayList<AutofillId> manuallyFilledFieldIds, - @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds) { + @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds, + @Nullable String detectedRemoteId, int detectedFieldScore) { mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED, "eventType"); mDatasetId = datasetId; @@ -467,6 +510,8 @@ public final class FillEventHistory implements Parcelable { } mManuallyFilledFieldIds = manuallyFilledFieldIds; mManuallyFilledDatasetIds = manuallyFilledDatasetIds; + mDetectedRemoteId = detectedRemoteId; + mDetectedFieldScore = detectedFieldScore; } @Override @@ -479,6 +524,8 @@ public final class FillEventHistory implements Parcelable { + ", changedDatasetsIds=" + mChangedDatasetIds + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds + + ", detectedRemoteId=" + mDetectedRemoteId + + ", detectedFieldScore=" + mDetectedFieldScore + "]"; } } @@ -514,11 +561,15 @@ public final class FillEventHistory implements Parcelable { } else { manuallyFilledDatasetIds = null; } + final String detectedRemoteId = parcel.readString(); + final int detectedFieldScore = detectedRemoteId == null ? -1 + : parcel.readInt(); selection.addEvent(new Event(eventType, datasetId, clientState, selectedDatasetIds, ignoredDatasets, changedFieldIds, changedDatasetIds, - manuallyFilledFieldIds, manuallyFilledDatasetIds)); + manuallyFilledFieldIds, manuallyFilledDatasetIds, + detectedRemoteId, detectedFieldScore)); } return selection; } diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java index 2f6342af2b3b..4e6a88457484 100644 --- a/core/java/android/service/autofill/FillResponse.java +++ b/core/java/android/service/autofill/FillResponse.java @@ -22,6 +22,7 @@ import static android.view.autofill.Helper.sDebug; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.TestApi; import android.app.Activity; import android.content.IntentSender; import android.content.pm.ParceledListSlice; @@ -75,6 +76,7 @@ public final class FillResponse implements Parcelable { private final @Nullable AutofillId[] mAuthenticationIds; private final @Nullable AutofillId[] mIgnoredIds; private final long mDisableDuration; + private final @Nullable FieldsDetection mFieldsDetection; private final int mFlags; private int mRequestId; @@ -87,6 +89,7 @@ public final class FillResponse implements Parcelable { mAuthenticationIds = builder.mAuthenticationIds; mIgnoredIds = builder.mIgnoredIds; mDisableDuration = builder.mDisableDuration; + mFieldsDetection = builder.mFieldsDetection; mFlags = builder.mFlags; mRequestId = INVALID_REQUEST_ID; } @@ -132,6 +135,11 @@ public final class FillResponse implements Parcelable { } /** @hide */ + public @Nullable FieldsDetection getFieldsDetection() { + return mFieldsDetection; + } + + /** @hide */ public int getFlags() { return mFlags; } @@ -167,6 +175,7 @@ public final class FillResponse implements Parcelable { private AutofillId[] mAuthenticationIds; private AutofillId[] mIgnoredIds; private long mDisableDuration; + private FieldsDetection mFieldsDetection; private int mFlags; private boolean mDestroyed; @@ -315,6 +324,25 @@ public final class FillResponse implements Parcelable { } /** + * TODO(b/67867469): + * - javadoc it + * - javadoc how to check results + * - unhide + * - unhide / remove testApi + * - throw exception (and document) if response has datasets or saveinfo + * - throw exception (and document) if id on fieldsDetection is ignored + * + * @hide + */ + @TestApi + public Builder setFieldsDetection(@NonNull FieldsDetection fieldsDetection) { + throwIfDestroyed(); + throwIfDisableAutofillCalled(); + mFieldsDetection = Preconditions.checkNotNull(fieldsDetection); + return this; + } + + /** * Sets flags changing the response behavior. * * @param flags a combination of {@link #FLAG_TRACK_CONTEXT_COMMITED} and @@ -365,7 +393,8 @@ public final class FillResponse implements Parcelable { if (duration <= 0) { throw new IllegalArgumentException("duration must be greater than 0"); } - if (mAuthentication != null || mDatasets != null || mSaveInfo != null) { + if (mAuthentication != null || mDatasets != null || mSaveInfo != null + || mFieldsDetection != null) { throw new IllegalStateException("disableAutofill() must be the only method called"); } @@ -388,11 +417,11 @@ public final class FillResponse implements Parcelable { */ public FillResponse build() { throwIfDestroyed(); - if (mAuthentication == null && mDatasets == null && mSaveInfo == null - && mDisableDuration == 0) { - throw new IllegalStateException("need to provide at least one DataSet or a " - + "SaveInfo or an authentication with a presentation or disable autofill"); + && mDisableDuration == 0 && mFieldsDetection == null) { + throw new IllegalStateException("need to provide: at least one DataSet, or a " + + "SaveInfo, or an authentication with a presentation, " + + "or a FieldsDetection, or disable autofill"); } mDestroyed = true; return new FillResponse(this); @@ -430,6 +459,7 @@ public final class FillResponse implements Parcelable { .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds)) .append(", disableDuration=").append(mDisableDuration) .append(", flags=").append(mFlags) + .append(", fieldDetection=").append(mFieldsDetection) .append("]") .toString(); } @@ -453,6 +483,7 @@ public final class FillResponse implements Parcelable { parcel.writeParcelable(mPresentation, flags); parcel.writeParcelableArray(mIgnoredIds, flags); parcel.writeLong(mDisableDuration); + parcel.writeParcelable(mFieldsDetection, flags); parcel.writeInt(mFlags); parcel.writeInt(mRequestId); } @@ -488,6 +519,10 @@ public final class FillResponse implements Parcelable { if (disableDuration > 0) { builder.disableAutofill(disableDuration); } + final FieldsDetection fieldsDetection = parcel.readParcelable(null); + if (fieldsDetection != null) { + builder.setFieldsDetection(fieldsDetection); + } builder.setFlags(parcel.readInt()); final FillResponse response = builder.build(); diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java index d8eaccc13532..a3def1435df9 100644 --- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java +++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java @@ -602,7 +602,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, null, null, null, null, -1)); } } } @@ -616,7 +616,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)); + clientState, null, null, null, null, null, null, null, -1)); } } } @@ -628,7 +628,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, null, null, null, null, null, -1)); } } } @@ -642,7 +642,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, null, null, null, null, null, -1)); } } } @@ -656,13 +656,15 @@ final class AutofillManagerServiceImpl { @Nullable ArrayList<AutofillId> changedFieldIds, @Nullable ArrayList<String> changedDatasetIds, @Nullable ArrayList<AutofillId> manuallyFilledFieldIds, - @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds) { + @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds, + @Nullable String detectedRemoteId, int detectedFieldScore) { synchronized (mLock) { if (isValidEventLocked("logDatasetNotSelected()", sessionId)) { mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null, clientState, selectedDatasets, ignoredDatasets, changedFieldIds, changedDatasetIds, - manuallyFilledFieldIds, manuallyFilledDatasetIds)); + manuallyFilledFieldIds, manuallyFilledDatasetIds, + detectedRemoteId, detectedFieldScore)); } } } @@ -695,6 +697,7 @@ final class AutofillManagerServiceImpl { pw.print(prefix); pw.print("Default component: "); pw.println(mContext.getString(R.string.config_defaultAutofillService)); pw.print(prefix); pw.print("Disabled: "); pw.println(mDisabled); + pw.print(prefix); pw.print("Field detection: "); pw.println(isFieldDetectionEnabled()); pw.print(prefix); pw.print("Setup complete: "); pw.println(mSetupComplete); pw.print(prefix); pw.print("Last prune: "); pw.println(mLastPrune); @@ -935,6 +938,13 @@ final class AutofillManagerServiceImpl { return false; } + // TODO(b/67867469): remove once feature is finished + boolean isFieldDetectionEnabled() { + return Settings.Secure.getIntForUser( + mContext.getContentResolver(), Settings.Secure.AUTOFILL_FEATURE_FIELD_DETECTION, 0, + mUserId) == 1; + } + @Override public String toString() { return "AutofillManagerServiceImpl: [userId=" + mUserId diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java index 3564432e6b93..5823ab107adc 100644 --- a/services/autofill/java/com/android/server/autofill/Session.java +++ b/services/autofill/java/com/android/server/autofill/Session.java @@ -54,6 +54,7 @@ import android.os.RemoteException; import android.os.SystemClock; import android.service.autofill.AutofillService; import android.service.autofill.Dataset; +import android.service.autofill.FieldsDetection; import android.service.autofill.FillContext; import android.service.autofill.FillRequest; import android.service.autofill.FillResponse; @@ -492,6 +493,13 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } + // TODO(b/67867469): remove once feature is finished + if (response.getFieldsDetection() != null && !mService.isFieldDetectionEnabled()) { + Slog.w(TAG, "Ignoring " + response + " because field detection is disabled"); + processNullResponseLocked(requestFlags); + return; + } + mService.setLastResponse(serviceUid, id, response); int sessionFinishedState = 0; @@ -913,11 +921,29 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } } } - if (!hasAtLeastOneDataset) { - if (sVerbose) Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets)"); + final FieldsDetection fieldsDetection = lastResponse.getFieldsDetection(); + + if (!hasAtLeastOneDataset && fieldsDetection == null) { + if (sVerbose) { + Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets nor fields " + + "detection)"); + } return; } + final AutofillId detectableFieldId; + final String detectableRemoteId; + String detectedRemoteId = null; + if (fieldsDetection == null) { + detectableFieldId = null; + detectableRemoteId = null; + } else { + detectableFieldId = fieldsDetection.getFieldId(); + detectableRemoteId = fieldsDetection.getRemoteId(); + } + + int detectedFieldScore = -1; + for (int i = 0; i < mViewStates.size(); i++) { final ViewState viewState = mViewStates.valueAt(i); final int state = viewState.getState(); @@ -926,7 +952,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState // - autofilled -> changedDatasetIds // - not autofilled but matches a dataset value -> manuallyFilledIds if ((state & ViewState.STATE_CHANGED) != 0) { - // Check if autofilled value was changed if ((state & ViewState.STATE_AUTOFILLED) != 0) { final String datasetId = viewState.getDatasetId(); @@ -958,7 +983,6 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState changedFieldIds.add(viewState.id); changedDatasetIds.add(datasetId); } else { - // Check if value match a dataset. final AutofillValue currentValue = viewState.getCurrentValue(); if (currentValue == null) { if (sDebug) { @@ -967,58 +991,78 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState } continue; } - for (int j = 0; j < responseCount; j++) { - final FillResponse response = mResponses.valueAt(j); - final List<Dataset> datasets = response.getDatasets(); - if (datasets == null || datasets.isEmpty()) { - if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + j); - } else { - for (int k = 0; k < datasets.size(); k++) { - final Dataset dataset = datasets.get(k); - final String datasetId = dataset.getId(); - if (datasetId == null) { - if (sVerbose) { - Slog.v(TAG, "logContextCommitted() skipping idless dataset " - + dataset); - } - } else { - final ArrayList<AutofillValue> values = dataset.getFieldValues(); - for (int l = 0; l < values.size(); l++) { - final AutofillValue candidate = values.get(l); - if (currentValue.equals(candidate)) { - if (sDebug) { - Slog.d(TAG, "field " + viewState.id - + " was manually filled with value set by " - + "dataset " + datasetId); + // Check if value match a dataset. + if (hasAtLeastOneDataset) { + for (int j = 0; j < responseCount; j++) { + final FillResponse response = mResponses.valueAt(j); + final List<Dataset> datasets = response.getDatasets(); + if (datasets == null || datasets.isEmpty()) { + if (sVerbose) { + Slog.v(TAG, "logContextCommitted() no datasets at " + j); + } + } else { + for (int k = 0; k < datasets.size(); k++) { + final Dataset dataset = datasets.get(k); + final String datasetId = dataset.getId(); + if (datasetId == null) { + if (sVerbose) { + Slog.v(TAG, "logContextCommitted() skipping idless " + + "dataset " + dataset); + } + } else { + final ArrayList<AutofillValue> values = + dataset.getFieldValues(); + for (int l = 0; l < values.size(); l++) { + final AutofillValue candidate = values.get(l); + if (currentValue.equals(candidate)) { + if (sDebug) { + Slog.d(TAG, "field " + viewState.id + " was " + + "manually filled with value set by " + + "dataset " + datasetId); + } + if (manuallyFilledIds == null) { + manuallyFilledIds = new ArrayMap<>(); + } + ArraySet<String> datasetIds = + manuallyFilledIds.get(viewState.id); + if (datasetIds == null) { + datasetIds = new ArraySet<>(1); + manuallyFilledIds.put(viewState.id, datasetIds); + } + datasetIds.add(datasetId); } - if (manuallyFilledIds == null) { - manuallyFilledIds = new ArrayMap<>(); + } // for l + if (mSelectedDatasetIds == null + || !mSelectedDatasetIds.contains(datasetId)) { + if (sVerbose) { + Slog.v(TAG, "adding ignored dataset " + datasetId); } - ArraySet<String> datasetIds = - manuallyFilledIds.get(viewState.id); - if (datasetIds == null) { - datasetIds = new ArraySet<>(1); - manuallyFilledIds.put(viewState.id, datasetIds); + if (ignoredDatasets == null) { + ignoredDatasets = new ArraySet<>(); } - datasetIds.add(datasetId); - } - } - if (mSelectedDatasetIds == null - || !mSelectedDatasetIds.contains(datasetId)) { - if (sVerbose) { - Slog.v(TAG, "adding ignored dataset " + datasetId); - } - if (ignoredDatasets == null) { - ignoredDatasets = new ArraySet<>(); - } - ignoredDatasets.add(datasetId); - } - } - } + ignoredDatasets.add(datasetId); + } // if + } // if + } // for k + } // else + } // for j + } + + // Check if detectable field changed. + if (detectableFieldId != null && detectableFieldId.equals(viewState.id) + && currentValue.isText() && currentValue.getTextValue() != null) { + final String actualValue = currentValue.getTextValue().toString(); + final String expectedValue = fieldsDetection.getValue(); + if (actualValue.equalsIgnoreCase(expectedValue)) { + detectedRemoteId = detectableRemoteId; + detectedFieldScore = 0; + } else if (sVerbose) { + Slog.v(TAG, "Detection mismatch for field " + detectableFieldId); } + // TODO(b/67867469): set score on partial hits } - } - } + } // else + } // else } if (sVerbose) { @@ -1027,7 +1071,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState + ", ignoredDatasetIds=" + ignoredDatasets + ", changedAutofillIds=" + changedFieldIds + ", changedDatasetIds=" + changedDatasetIds - + ", manuallyFilledIds=" + manuallyFilledIds); + + ", manuallyFilledIds=" + manuallyFilledIds + + ", detectableFieldId=" + detectableFieldId + + ", detectedFieldScore=" + detectedFieldScore + ); } ArrayList<AutofillId> manuallyFilledFieldIds = null; @@ -1045,9 +1092,11 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState manuallyFilledDatasetIds.add(new ArrayList<>(datasetIds)); } } + mService.logContextCommitted(id, mClientState, mSelectedDatasetIds, ignoredDatasets, changedFieldIds, changedDatasetIds, - manuallyFilledFieldIds, manuallyFilledDatasetIds); + manuallyFilledFieldIds, manuallyFilledDatasetIds, + detectedRemoteId, detectedFieldScore); } /** @@ -1535,6 +1584,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState viewState = new ViewState(this, id, this, isIgnored ? ViewState.STATE_IGNORED : ViewState.STATE_INITIAL); mViewStates.put(id, viewState); + + // TODO(b/67867469): for optimization purposes, should also ignore if change is + // detectable, and batch-send them when the session is finished (but that will + // require tracking detectable fields on AutofillManager) if (isIgnored) { if (sDebug) Slog.d(TAG, "updateLocked(): ignoring view " + id); return; diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java index 1d8110f08364..832a66b26b12 100644 --- a/services/autofill/java/com/android/server/autofill/ViewState.java +++ b/services/autofill/java/com/android/server/autofill/ViewState.java @@ -134,7 +134,11 @@ final class ViewState { } String getStateAsString() { - return DebugUtils.flagsToString(ViewState.class, "STATE_", mState); + return getStateAsString(mState); + } + + static String getStateAsString(int state) { + return DebugUtils.flagsToString(ViewState.class, "STATE_", state); } void setState(int state) { |