Very initial field detection prototype.

A.K.A "OMG, It's full of TODOs!"

Test: cts-tradefed run commandAndExit cts-dev -m CtsAutoFillServiceTestCases -t android.autofillservice.cts.FieldsDetectionTest

Bug: 67867469

Change-Id: I7c8f7c3e35ccbae0134e2a446b7b44e1e57261fc
diff --git a/api/test-current.txt b/api/test-current.txt
index 9f02bc5..584c265 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -35544,6 +35544,7 @@
     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 @@
     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 @@
     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 @@
     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 433878e..398e08f 100755
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -5309,6 +5309,15 @@
         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 0000000..550ecf6
--- /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 b1857b3..736d9ef 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -19,6 +19,7 @@
 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 @@
                         dest.writeStringList(event.mManuallyFilledDatasetIds.get(j));
                     }
                 }
+                dest.writeString(event.mDetectedRemoteId);
+                if (event.mDetectedRemoteId != null) {
+                    dest.writeInt(event.mDetectedFieldScore);
+                }
             }
         }
     }
@@ -226,6 +231,7 @@
          * <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 @@
         @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 @@
         }
 
         /**
+         * 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 @@
          * 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 @@
          *
          * @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 @@
             }
             mManuallyFilledFieldIds = manuallyFilledFieldIds;
             mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
+            mDetectedRemoteId = detectedRemoteId;
+            mDetectedFieldScore = detectedFieldScore;
         }
 
         @Override
@@ -479,6 +524,8 @@
                     + ", changedDatasetsIds=" + mChangedDatasetIds
                     + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
                     + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
+                    + ", detectedRemoteId=" + mDetectedRemoteId
+                    + ", detectedFieldScore=" + mDetectedFieldScore
                     + "]";
         }
     }
@@ -514,11 +561,15 @@
                         } 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 2f6342a..4e6a884 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -22,6 +22,7 @@
 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 @@
     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 @@
         mAuthenticationIds = builder.mAuthenticationIds;
         mIgnoredIds = builder.mIgnoredIds;
         mDisableDuration = builder.mDisableDuration;
+        mFieldsDetection = builder.mFieldsDetection;
         mFlags = builder.mFlags;
         mRequestId = INVALID_REQUEST_ID;
     }
@@ -132,6 +135,11 @@
     }
 
     /** @hide */
+    public @Nullable FieldsDetection getFieldsDetection() {
+        return mFieldsDetection;
+    }
+
+    /** @hide */
     public int getFlags() {
         return mFlags;
     }
@@ -167,6 +175,7 @@
         private AutofillId[] mAuthenticationIds;
         private AutofillId[] mIgnoredIds;
         private long mDisableDuration;
+        private FieldsDetection mFieldsDetection;
         private int mFlags;
         private boolean mDestroyed;
 
@@ -315,6 +324,25 @@
         }
 
         /**
+         * 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 @@
             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 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 @@
                 .append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
                 .append(", disableDuration=").append(mDisableDuration)
                 .append(", flags=").append(mFlags)
+                .append(", fieldDetection=").append(mFieldsDetection)
                 .append("]")
                 .toString();
     }
@@ -453,6 +483,7 @@
         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 @@
             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 d8eaccc..a3def14 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 @@
             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 @@
             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 @@
         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 @@
             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 @@
             @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 @@
         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 @@
         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 3564432..5823ab1 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.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 @@
             }
         }
 
+        // 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 @@
                 }
             }
         }
-        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 @@
             // - 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 @@
                     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 @@
                         }
                         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);
-                                            }
-                                            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 (mSelectedDatasetIds == null
-                                            || !mSelectedDatasetIds.contains(datasetId)) {
-                                        if (sVerbose) {
-                                            Slog.v(TAG, "adding ignored dataset " + datasetId);
-                                        }
-                                        if (ignoredDatasets == null) {
-                                            ignoredDatasets = new ArraySet<>();
-                                        }
-                                        ignoredDatasets.add(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);
+                                            }
+                                        } // for l
+                                        if (mSelectedDatasetIds == null
+                                                || !mSelectedDatasetIds.contains(datasetId)) {
+                                            if (sVerbose) {
+                                                Slog.v(TAG, "adding ignored dataset " + datasetId);
+                                            }
+                                            if (ignoredDatasets == null) {
+                                                ignoredDatasets = new ArraySet<>();
+                                            }
+                                            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 @@
                     + ", ignoredDatasetIds=" + ignoredDatasets
                     + ", changedAutofillIds=" + changedFieldIds
                     + ", changedDatasetIds=" + changedDatasetIds
-                    + ", manuallyFilledIds=" + manuallyFilledIds);
+                    + ", manuallyFilledIds=" + manuallyFilledIds
+                    + ", detectableFieldId=" + detectableFieldId
+                    + ", detectedFieldScore=" + detectedFieldScore
+                    );
         }
 
         ArrayList<AutofillId> manuallyFilledFieldIds = null;
@@ -1045,9 +1092,11 @@
                 manuallyFilledDatasetIds.add(new ArrayList<>(datasetIds));
             }
         }
+
         mService.logContextCommitted(id, mClientState, mSelectedDatasetIds, ignoredDatasets,
                 changedFieldIds, changedDatasetIds,
-                manuallyFilledFieldIds, manuallyFilledDatasetIds);
+                manuallyFilledFieldIds, manuallyFilledDatasetIds,
+                detectedRemoteId, detectedFieldScore);
     }
 
     /**
@@ -1535,6 +1584,10 @@
                 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 1d8110f..832a66b 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 @@
     }
 
     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) {