summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--api/current.txt8
-rw-r--r--api/system-current.txt8
-rw-r--r--api/test-current.txt8
-rw-r--r--core/java/android/service/autofill/FillEventHistory.java283
-rw-r--r--core/java/android/service/autofill/FillResponse.java42
-rw-r--r--services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java36
-rw-r--r--services/autofill/java/com/android/server/autofill/Session.java199
-rw-r--r--services/autofill/java/com/android/server/autofill/ViewState.java14
8 files changed, 579 insertions, 19 deletions
diff --git a/api/current.txt b/api/current.txt
index 0d22721be7c6..fda971694a44 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -37182,10 +37182,15 @@ package android.service.autofill {
}
public static final class FillEventHistory.Event {
+ 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.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();
method public int getType();
field public static final int TYPE_AUTHENTICATION_SELECTED = 2; // 0x2
+ field public static final int TYPE_CONTEXT_COMMITTED = 4; // 0x4
field public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1; // 0x1
field public static final int TYPE_DATASET_SELECTED = 0; // 0x0
field public static final int TYPE_SAVE_SHOWN = 3; // 0x3
@@ -37206,6 +37211,7 @@ package android.service.autofill {
method public int describeContents();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.service.autofill.FillResponse> CREATOR;
+ field public static final int FLAG_TRACK_CONTEXT_COMMITED = 1; // 0x1
}
public static final class FillResponse.Builder {
@@ -37214,6 +37220,7 @@ package android.service.autofill {
method public android.service.autofill.FillResponse build();
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 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);
}
@@ -51961,6 +51968,7 @@ package android.widget {
method public android.graphics.Typeface getTypeface();
method public android.text.style.URLSpan[] getUrls();
method public boolean hasSelection();
+ method public void invalidate(int, int, int, int);
method public boolean isAllCaps();
method public boolean isCursorVisible();
method public boolean isElegantTextHeight();
diff --git a/api/system-current.txt b/api/system-current.txt
index 6fbb0ab04467..bc0aee03218e 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -40277,10 +40277,15 @@ package android.service.autofill {
}
public static final class FillEventHistory.Event {
+ 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.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();
method public int getType();
field public static final int TYPE_AUTHENTICATION_SELECTED = 2; // 0x2
+ field public static final int TYPE_CONTEXT_COMMITTED = 4; // 0x4
field public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1; // 0x1
field public static final int TYPE_DATASET_SELECTED = 0; // 0x0
field public static final int TYPE_SAVE_SHOWN = 3; // 0x3
@@ -40301,6 +40306,7 @@ package android.service.autofill {
method public int describeContents();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.service.autofill.FillResponse> CREATOR;
+ field public static final int FLAG_TRACK_CONTEXT_COMMITED = 1; // 0x1
}
public static final class FillResponse.Builder {
@@ -40309,6 +40315,7 @@ package android.service.autofill {
method public android.service.autofill.FillResponse build();
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 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);
}
@@ -56057,6 +56064,7 @@ package android.widget {
method public android.graphics.Typeface getTypeface();
method public android.text.style.URLSpan[] getUrls();
method public boolean hasSelection();
+ method public void invalidate(int, int, int, int);
method public boolean isAllCaps();
method public boolean isCursorVisible();
method public boolean isElegantTextHeight();
diff --git a/api/test-current.txt b/api/test-current.txt
index cba7abae0e73..0eda58ac5bc2 100644
--- a/api/test-current.txt
+++ b/api/test-current.txt
@@ -37474,10 +37474,15 @@ package android.service.autofill {
}
public static final class FillEventHistory.Event {
+ 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.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();
method public int getType();
field public static final int TYPE_AUTHENTICATION_SELECTED = 2; // 0x2
+ field public static final int TYPE_CONTEXT_COMMITTED = 4; // 0x4
field public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1; // 0x1
field public static final int TYPE_DATASET_SELECTED = 0; // 0x0
field public static final int TYPE_SAVE_SHOWN = 3; // 0x3
@@ -37498,6 +37503,7 @@ package android.service.autofill {
method public int describeContents();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.service.autofill.FillResponse> CREATOR;
+ field public static final int FLAG_TRACK_CONTEXT_COMMITED = 1; // 0x1
}
public static final class FillResponse.Builder {
@@ -37506,6 +37512,7 @@ package android.service.autofill {
method public android.service.autofill.FillResponse build();
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 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);
}
@@ -52560,6 +52567,7 @@ package android.widget {
method public android.graphics.Typeface getTypeface();
method public android.text.style.URLSpan[] getUrls();
method public boolean hasSelection();
+ method public void invalidate(int, int, int, int);
method public boolean isAllCaps();
method public boolean isCursorVisible();
method public boolean isElegantTextHeight();
diff --git a/core/java/android/service/autofill/FillEventHistory.java b/core/java/android/service/autofill/FillEventHistory.java
index 3b719ac7027e..d6c0dbfee23c 100644
--- a/core/java/android/service/autofill/FillEventHistory.java
+++ b/core/java/android/service/autofill/FillEventHistory.java
@@ -17,19 +17,27 @@
package android.service.autofill;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.content.IntentSender;
import android.os.Bundle;
import android.os.Parcel;
import android.os.Parcelable;
+import android.util.ArrayMap;
+import android.util.ArraySet;
+import android.view.autofill.AutofillId;
import android.view.autofill.AutofillManager;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
+import java.util.Collections;
import java.util.List;
+import java.util.Map;
+import java.util.Set;
/**
* Describes what happened after the last
@@ -136,9 +144,21 @@ public final class FillEventHistory implements Parcelable {
int numEvents = mEvents.size();
for (int i = 0; i < numEvents; i++) {
Event event = mEvents.get(i);
- dest.writeInt(event.getType());
- dest.writeString(event.getDatasetId());
- dest.writeBundle(event.getClientState());
+ 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);
+ if (event.mManuallyFilledFieldIds != null) {
+ final int size = event.mManuallyFilledFieldIds.size();
+ for (int j = 0; j < size; j++) {
+ dest.writeStringList(event.mManuallyFilledDatasetIds.get(j));
+ }
+ }
}
}
}
@@ -176,12 +196,40 @@ public final class FillEventHistory implements Parcelable {
/** A save UI was shown. */
public static final int TYPE_SAVE_SHOWN = 3;
+ /**
+ * A committed autofill context for which the autofill service provided datasets.
+ *
+ * <p>This event is useful to track:
+ * <ul>
+ * <li>Which datasets (if any) were selected by the user
+ * ({@link #getSelectedDatasetIds()}).
+ * <li>Which datasets (if any) were NOT selected by the user
+ * ({@link #getIgnoredDatasetIds()}).
+ * <li>Which fields in the selected datasets were changed by the user after the dataset
+ * was selected ({@link #getChangedFields()}.
+ * </ul>
+ *
+ * <p><b>Note: </b>This event is only generated when:
+ * <ul>
+ * <li>The autofill context is committed.
+ * <li>The service provides at least one dataset in the
+ * {@link FillResponse fill responses} associated with the context.
+ * <li>The last {@link FillResponse fill responses} associated with the context has the
+ * {@link FillResponse#FLAG_TRACK_CONTEXT_COMMITED} flag.
+ * </ul>
+ *
+ * <p>See {@link android.view.autofill.AutofillManager} for more information about autofill
+ * contexts.
+ */
+ public static final int TYPE_CONTEXT_COMMITTED = 4;
+
/** @hide */
@IntDef(
value = {TYPE_DATASET_SELECTED,
TYPE_DATASET_AUTHENTICATION_SELECTED,
TYPE_AUTHENTICATION_SELECTED,
- TYPE_SAVE_SHOWN})
+ TYPE_SAVE_SHOWN,
+ TYPE_CONTEXT_COMMITTED})
@Retention(RetentionPolicy.SOURCE)
@interface EventIds{}
@@ -189,6 +237,17 @@ public final class FillEventHistory implements Parcelable {
@Nullable private final String mDatasetId;
@Nullable private final Bundle mClientState;
+ // Note: mSelectedDatasetIds is stored as List<> instead of Set because Session already
+ // stores it as List
+ @Nullable private final List<String> mSelectedDatasetIds;
+ @Nullable private final ArraySet<String> mIgnoredDatasetIds;
+
+ @Nullable private final ArrayList<AutofillId> mChangedFieldIds;
+ @Nullable private final ArrayList<String> mChangedDatasetIds;
+
+ @Nullable private final ArrayList<AutofillId> mManuallyFilledFieldIds;
+ @Nullable private final ArrayList<ArrayList<String>> mManuallyFilledDatasetIds;
+
/**
* Returns the type of the event.
*
@@ -220,25 +279,202 @@ public final class FillEventHistory implements Parcelable {
}
/**
+ * Returns which datasets were selected by the user.
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ */
+ @NonNull public Set<String> getSelectedDatasetIds() {
+ return mSelectedDatasetIds == null ? Collections.emptySet()
+ : new ArraySet<>(mSelectedDatasetIds);
+ }
+
+ /**
+ * Returns which datasets were NOT selected by the user.
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ */
+ @NonNull public Set<String> getIgnoredDatasetIds() {
+ return mIgnoredDatasetIds == null ? Collections.emptySet() : mIgnoredDatasetIds;
+ }
+
+ /**
+ * Returns which fields in the selected datasets were changed by the user after the dataset
+ * was selected.
+ *
+ * <p>For example, server provides:
+ *
+ * <pre class="prettyprint">
+ * FillResponse response = new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder(presentation1)
+ * .setId("4815")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation2)
+ * .setId("162342")
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>User select both datasets (for username and password) but after the fields are
+ * autofilled, user changes them to:
+ *
+ * <pre class="prettyprint">
+ * username = "ElBarto";
+ * password = "AyCaramba";
+ * </pre>
+ *
+ * <p>Then the result is the following map:
+ *
+ * <pre class="prettyprint">
+ * usernameId => "4815"
+ * passwordId => "162342"
+ * </pre>
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ *
+ * @return map map whose key is the id of the change fields, and value is the id of
+ * dataset that has that field and was selected by the user.
+ */
+ @NonNull public Map<AutofillId, String> getChangedFields() {
+ if (mChangedFieldIds == null || mChangedDatasetIds == null) {
+ return Collections.emptyMap();
+ }
+
+ final int size = mChangedFieldIds.size();
+ final ArrayMap<AutofillId, String> changedFields = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ changedFields.put(mChangedFieldIds.get(i), mChangedDatasetIds.get(i));
+ }
+ return changedFields;
+ }
+
+ /**
+ * Returns which fields were available on datasets provided by the service but manually
+ * entered by the user.
+ *
+ * <p>For example, server provides:
+ *
+ * <pre class="prettyprint">
+ * FillResponse response = new FillResponse.Builder()
+ * .addDataset(new Dataset.Builder(presentation1)
+ * .setId("4815")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .setValue(passwordId, AutofillValue.forText("AyCaramba"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation2)
+ * .setId("162342")
+ * .setValue(usernameId, AutofillValue.forText("ElBarto"))
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .addDataset(new Dataset.Builder(presentation3)
+ * .setId("108")
+ * .setValue(usernameId, AutofillValue.forText("MrPlow"))
+ * .setValue(passwordId, AutofillValue.forText("D'OH"))
+ * .build())
+ * .build();
+ * </pre>
+ *
+ * <p>User doesn't select a dataset but manually enters:
+ *
+ * <pre class="prettyprint">
+ * username = "MrPlow";
+ * password = "D'OH";
+ * </pre>
+ *
+ * <p>Then the result is the following map:
+ *
+ * <pre class="prettyprint">
+ * usernameId => { "4815", "108"}
+ * passwordId => { "162342", "108" }
+ * </pre>
+ *
+ * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}.
+ *
+ * @return map map whose key is the id of the manually-entered field, and value is the
+ * ids of the datasets that have that value but were not selected by the user.
+ */
+ @Nullable public Map<AutofillId, Set<String>> getManuallyEnteredField() {
+ if (mManuallyFilledFieldIds == null || mManuallyFilledDatasetIds == null) {
+ return Collections.emptyMap();
+ }
+
+ final int size = mManuallyFilledFieldIds.size();
+ final Map<AutofillId, Set<String>> manuallyFilledFields = new ArrayMap<>(size);
+ for (int i = 0; i < size; i++) {
+ final AutofillId fieldId = mManuallyFilledFieldIds.get(i);
+ final ArrayList<String> datasetIds = mManuallyFilledDatasetIds.get(i);
+ manuallyFilledFields.put(fieldId, new ArraySet<>(datasetIds));
+ }
+ return manuallyFilledFields;
+ }
+
+ /**
* Creates a new event.
*
* @param eventType The type of the event
* @param datasetId The dataset the event was on, or {@code null} if the event was on the
* whole response.
* @param clientState The client state associated with the event.
+ * @param selectedDatasetIds The ids of datasets selected by the user.
+ * @param ignoredDatasetIds The ids of datasets NOT select by the user.
+ * @param changedFieldIds The ids of fields changed by the user.
+ * @param changedDatasetIds The ids of the datasets that havd values matching the
+ * respective entry on {@code changedFieldIds}.
+ * @param manuallyFilledFieldIds The ids of fields that were manually entered by the user
+ * 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
+ * {@code manuallyFilledDatasetIds} doesn't match.
*
* @hide
*/
- public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState) {
- mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_SAVE_SHOWN,
+ 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) {
+ mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_CONTEXT_COMMITTED,
"eventType");
mDatasetId = datasetId;
mClientState = clientState;
+ mSelectedDatasetIds = selectedDatasetIds;
+ mIgnoredDatasetIds = ignoredDatasetIds;
+ if (changedFieldIds != null) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(changedFieldIds)
+ && changedDatasetIds != null
+ && changedFieldIds.size() == changedDatasetIds.size(),
+ "changed ids must have same length and not be empty");
+ }
+ mChangedFieldIds = changedFieldIds;
+ mChangedDatasetIds = changedDatasetIds;
+ if (manuallyFilledFieldIds != null) {
+ Preconditions.checkArgument(!ArrayUtils.isEmpty(manuallyFilledFieldIds)
+ && manuallyFilledDatasetIds != null
+ && manuallyFilledFieldIds.size() == manuallyFilledDatasetIds.size(),
+ "manually filled ids must have same length and not be empty");
+ }
+ mManuallyFilledFieldIds = manuallyFilledFieldIds;
+ mManuallyFilledDatasetIds = manuallyFilledDatasetIds;
}
@Override
public String toString() {
- return "FillEvent [datasetId=" + mDatasetId + ", type=" + mEventType + "]";
+ return "FillEvent [datasetId=" + mDatasetId
+ + ", type=" + mEventType
+ + ", selectedDatasets=" + mSelectedDatasetIds
+ + ", ignoredDatasetIds=" + mIgnoredDatasetIds
+ + ", changedFieldIds=" + mChangedFieldIds
+ + ", changedDatasetsIds=" + mChangedDatasetIds
+ + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
+ + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
+ + "]";
}
}
@@ -248,12 +484,37 @@ public final class FillEventHistory implements Parcelable {
public FillEventHistory createFromParcel(Parcel parcel) {
FillEventHistory selection = new FillEventHistory(0, 0, parcel.readBundle());
- int numEvents = parcel.readInt();
+ final int numEvents = parcel.readInt();
for (int i = 0; i < numEvents; i++) {
- selection.addEvent(new Event(parcel.readInt(), parcel.readString(),
- parcel.readBundle()));
- }
+ final int eventType = parcel.readInt();
+ final String datasetId = parcel.readString();
+ final Bundle clientState = parcel.readBundle();
+ final ArrayList<String> selectedDatasetIds = parcel.createStringArrayList();
+ @SuppressWarnings("unchecked")
+ final ArraySet<String> ignoredDatasets =
+ (ArraySet<String>) parcel.readArraySet(null);
+ final ArrayList<AutofillId> changedFieldIds =
+ parcel.createTypedArrayList(AutofillId.CREATOR);
+ final ArrayList<String> changedDatasetIds = parcel.createStringArrayList();
+
+ final ArrayList<AutofillId> manuallyFilledFieldIds =
+ parcel.createTypedArrayList(AutofillId.CREATOR);
+ final ArrayList<ArrayList<String>> manuallyFilledDatasetIds;
+ if (manuallyFilledFieldIds != null) {
+ final int size = manuallyFilledFieldIds.size();
+ manuallyFilledDatasetIds = new ArrayList<>(size);
+ for (int j = 0; j < size; j++) {
+ manuallyFilledDatasetIds.add(parcel.createStringArrayList());
+ }
+ } else {
+ manuallyFilledDatasetIds = null;
+ }
+ selection.addEvent(new Event(eventType, datasetId, clientState,
+ selectedDatasetIds, ignoredDatasets,
+ changedFieldIds, changedDatasetIds,
+ manuallyFilledFieldIds, manuallyFilledDatasetIds));
+ }
return selection;
}
diff --git a/core/java/android/service/autofill/FillResponse.java b/core/java/android/service/autofill/FillResponse.java
index 6d8a95991f05..d91cebb1face 100644
--- a/core/java/android/service/autofill/FillResponse.java
+++ b/core/java/android/service/autofill/FillResponse.java
@@ -19,6 +19,7 @@ package android.service.autofill;
import static android.service.autofill.FillRequest.INVALID_REQUEST_ID;
import static android.view.autofill.Helper.sDebug;
+import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.Activity;
@@ -30,6 +31,8 @@ import android.os.Parcelable;
import android.view.autofill.AutofillId;
import android.widget.RemoteViews;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
@@ -42,6 +45,19 @@ import java.util.List;
*/
public final class FillResponse implements Parcelable {
+ /**
+ * Must be set in the last response to generate
+ * {@link FillEventHistory.Event#TYPE_CONTEXT_COMMITTED} events.
+ */
+ public static final int FLAG_TRACK_CONTEXT_COMMITED = 0x1;
+
+ /** @hide */
+ @IntDef(flag = true, value = {
+ FLAG_TRACK_CONTEXT_COMMITED
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface FillResponseFlags {}
+
private final @Nullable ParceledListSlice<Dataset> mDatasets;
private final @Nullable SaveInfo mSaveInfo;
private final @Nullable Bundle mClientState;
@@ -49,6 +65,7 @@ public final class FillResponse implements Parcelable {
private final @Nullable IntentSender mAuthentication;
private final @Nullable AutofillId[] mAuthenticationIds;
private final @Nullable AutofillId[] mIgnoredIds;
+ private final int mFlags;
private int mRequestId;
private FillResponse(@NonNull Builder builder) {
@@ -59,6 +76,7 @@ public final class FillResponse implements Parcelable {
mAuthentication = builder.mAuthentication;
mAuthenticationIds = builder.mAuthenticationIds;
mIgnoredIds = builder.mIgnoredIds;
+ mFlags = builder.mFlags;
mRequestId = INVALID_REQUEST_ID;
}
@@ -97,6 +115,11 @@ public final class FillResponse implements Parcelable {
return mIgnoredIds;
}
+ /** @hide */
+ public int getFlags() {
+ return mFlags;
+ }
+
/**
* Associates a {@link FillResponse} to a request.
*
@@ -127,6 +150,7 @@ public final class FillResponse implements Parcelable {
private IntentSender mAuthentication;
private AutofillId[] mAuthenticationIds;
private AutofillId[] mIgnoredIds;
+ private int mFlags;
private boolean mDestroyed;
/**
@@ -269,6 +293,19 @@ public final class FillResponse implements Parcelable {
}
/**
+ * Sets flags changing the response behavior.
+ *
+ * @param flags {@link #FLAG_TRACK_CONTEXT_COMMITED}, or {@code 0}.
+ *
+ * @return This builder.
+ */
+ public Builder setFlags(@FillResponseFlags int flags) {
+ throwIfDestroyed();
+ mFlags = flags;
+ return this;
+ }
+
+ /**
* Builds a new {@link FillResponse} instance.
*
* <p>You must provide at least one dataset or some savable ids or an authentication with a
@@ -311,6 +348,7 @@ public final class FillResponse implements Parcelable {
.append(", hasAuthentication=").append(mAuthentication != null)
.append(", authenticationIds=").append(Arrays.toString(mAuthenticationIds))
.append(", ignoredIds=").append(Arrays.toString(mIgnoredIds))
+ .append(", flags=").append(mFlags)
.append("]")
.toString();
}
@@ -333,6 +371,7 @@ public final class FillResponse implements Parcelable {
parcel.writeParcelable(mAuthentication, flags);
parcel.writeParcelable(mPresentation, flags);
parcel.writeParcelableArray(mIgnoredIds, flags);
+ parcel.writeInt(mFlags);
parcel.writeInt(mRequestId);
}
@@ -363,8 +402,9 @@ public final class FillResponse implements Parcelable {
}
builder.setIgnoredIds(parcel.readParcelableArray(null, AutofillId.class));
- final FillResponse response = builder.build();
+ builder.setFlags(parcel.readInt());
+ final FillResponse response = builder.build();
response.setRequestId(parcel.readInt());
return response;
diff --git a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
index 880f236cbe76..075c741e7b87 100644
--- a/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
+++ b/services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java
@@ -52,6 +52,7 @@ import android.service.autofill.FillEventHistory.Event;
import android.service.autofill.FillResponse;
import android.service.autofill.IAutoFillService;
import android.text.TextUtils;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.util.DebugUtils;
import android.util.LocalLog;
@@ -338,6 +339,8 @@ final class AutofillManagerServiceImpl {
return;
}
+ session.logContextCommittedLocked();
+
final boolean finished = session.showSaveLocked();
if (sVerbose) Slog.v(TAG, "finishSessionLocked(): session finished on save? " + finished);
@@ -563,8 +566,9 @@ final class AutofillManagerServiceImpl {
void setAuthenticationSelected(int sessionId, @Nullable Bundle clientState) {
synchronized (mLock) {
if (isValidEventLocked("setAuthenticationSelected()", sessionId)) {
- mEventHistory
- .addEvent(new Event(Event.TYPE_AUTHENTICATION_SELECTED, null, clientState));
+ mEventHistory.addEvent(
+ new Event(Event.TYPE_AUTHENTICATION_SELECTED, null, clientState, null, null,
+ null, null, null, null));
}
}
}
@@ -578,7 +582,7 @@ final class AutofillManagerServiceImpl {
if (isValidEventLocked("logDatasetAuthenticationSelected()", sessionId)) {
mEventHistory.addEvent(
new Event(Event.TYPE_DATASET_AUTHENTICATION_SELECTED, selectedDataset,
- clientState));
+ clientState, null, null, null, null, null, null));
}
}
}
@@ -589,7 +593,8 @@ final class AutofillManagerServiceImpl {
void logSaveShown(int sessionId, @Nullable Bundle clientState) {
synchronized (mLock) {
if (isValidEventLocked("logSaveShown()", sessionId)) {
- mEventHistory.addEvent(new Event(Event.TYPE_SAVE_SHOWN, null, clientState));
+ mEventHistory.addEvent(new Event(Event.TYPE_SAVE_SHOWN, null, clientState, null,
+ null, null, null, null, null));
}
}
}
@@ -602,7 +607,28 @@ final class AutofillManagerServiceImpl {
synchronized (mLock) {
if (isValidEventLocked("logDatasetSelected()", sessionId)) {
mEventHistory.addEvent(
- new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState));
+ new Event(Event.TYPE_DATASET_SELECTED, selectedDataset, clientState, null,
+ null, null, null, null, null));
+ }
+ }
+ }
+
+ /**
+ * Updates the last fill response when an autofill context is committed.
+ */
+ void logContextCommitted(int sessionId, @Nullable Bundle clientState,
+ @Nullable ArrayList<String> selectedDatasets,
+ @Nullable ArraySet<String> ignoredDatasets,
+ @Nullable ArrayList<AutofillId> changedFieldIds,
+ @Nullable ArrayList<String> changedDatasetIds,
+ @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
+ @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds) {
+ synchronized (mLock) {
+ if (isValidEventLocked("logDatasetNotSelected()", sessionId)) {
+ mEventHistory.addEvent(new Event(Event.TYPE_CONTEXT_COMMITTED, null,
+ clientState, selectedDatasets, ignoredDatasets,
+ changedFieldIds, changedDatasetIds,
+ manuallyFilledFieldIds, manuallyFilledDatasetIds));
}
}
}
diff --git a/services/autofill/java/com/android/server/autofill/Session.java b/services/autofill/java/com/android/server/autofill/Session.java
index 3c12d670c296..596f02289e4e 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.FillContext;
+import android.service.autofill.FillEventHistory;
import android.service.autofill.FillRequest;
import android.service.autofill.FillResponse;
import android.service.autofill.InternalSanitizer;
@@ -90,6 +91,7 @@ import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Map;
+import java.util.Set;
import java.util.concurrent.atomic.AtomicInteger;
/**
@@ -840,6 +842,194 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
/**
+ * Generates a {@link android.service.autofill.FillEventHistory.Event#TYPE_CONTEXT_COMMITTED}
+ * when necessary.
+ */
+ public void logContextCommittedLocked() {
+ if (mResponses == null) {
+ if (sVerbose) Slog.v(TAG, "logContextCommittedLocked(): skipped (no responses)");
+ return;
+ }
+
+ final FillResponse lastResponse = mResponses.valueAt(mResponses.size() -1);
+ final int flags = lastResponse.getFlags();
+ if ((flags & FillResponse.FLAG_TRACK_CONTEXT_COMMITED) == 0) {
+ if (sDebug) Slog.d(TAG, "logContextCommittedLocked(): ignored by flags " + flags);
+ return;
+ }
+
+ ArraySet<String> ignoredDatasets = null;
+ ArrayList<AutofillId> changedFieldIds = null;
+ ArrayList<String> changedDatasetIds = null;
+ ArrayMap<AutofillId, ArraySet<String>> manuallyFilledIds = null;
+
+ boolean hasAtLeastOneDataset = false;
+ final int responseCount = mResponses.size();
+ for (int i = 0; i < responseCount; i++) {
+ final FillResponse response = mResponses.valueAt(i);
+ final List<Dataset> datasets = response.getDatasets();
+ if (datasets == null || datasets.isEmpty()) {
+ if (sVerbose) Slog.v(TAG, "logContextCommitted() no datasets at " + i);
+ } else {
+ for (int j = 0; j < datasets.size(); j++) {
+ final Dataset dataset = datasets.get(j);
+ final String datasetId = dataset.getId();
+ if (datasetId == null) {
+ if (sVerbose) {
+ Slog.v(TAG, "logContextCommitted() skipping idless dataset " + dataset);
+ }
+ } else {
+ hasAtLeastOneDataset = true;
+ 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 (!hasAtLeastOneDataset) {
+ if (sVerbose) Slog.v(TAG, "logContextCommittedLocked(): skipped (no datasets)");
+ return;
+ }
+
+ for (int i = 0; i < mViewStates.size(); i++) {
+ final ViewState viewState = mViewStates.valueAt(i);
+ final int state = viewState.getState();
+
+ // When value changed, we need to log if it was:
+ // - 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();
+ if (datasetId == null) {
+ // Sanity check - should never happen.
+ Slog.w(TAG, "logContextCommitted(): no dataset id on " + viewState);
+ continue;
+ }
+
+ // Must first check if final changed value is not the same as value sent by
+ // service.
+ final AutofillValue autofilledValue = viewState.getAutofilledValue();
+ final AutofillValue currentValue = viewState.getCurrentValue();
+ if (autofilledValue != null && autofilledValue.equals(currentValue)) {
+ if (sDebug) {
+ Slog.d(TAG, "logContextCommitted(): ignoring changed " + viewState
+ + " because it has same value that was autofilled");
+ }
+ continue;
+ }
+
+ if (sDebug) {
+ Slog.d(TAG, "logContextCommitted() found changed state: " + viewState);
+ }
+ if (changedFieldIds == null) {
+ changedFieldIds = new ArrayList<>();
+ changedDatasetIds = new ArrayList<>();
+ }
+ changedFieldIds.add(viewState.id);
+ changedDatasetIds.add(datasetId);
+ } else {
+ // Check if value match a dataset.
+ final AutofillValue currentValue = viewState.getCurrentValue();
+ if (currentValue == null) {
+ if (sDebug) {
+ Slog.d(TAG, "logContextCommitted(): skipping view witout current value "
+ + "( " + 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);
+ }
+ 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);
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ if (sVerbose) {
+ Slog.v(TAG, "logContextCommitted(): id=" + id
+ + ", selectedDatasetids=" + mSelectedDatasetIds
+ + ", ignoredDatasetIds=" + ignoredDatasets
+ + ", changedAutofillIds=" + changedFieldIds
+ + ", changedDatasetIds=" + changedDatasetIds
+ + ", manuallyFilledIds=" + manuallyFilledIds);
+ }
+
+ ArrayList<AutofillId> manuallyFilledFieldIds = null;
+ ArrayList<ArrayList<String>> manuallyFilledDatasetIds = null;
+
+ // Must "flatten" the map to the parcellable collection primitives
+ if (manuallyFilledIds != null) {
+ final int size = manuallyFilledIds.size();
+ manuallyFilledFieldIds = new ArrayList<>(size);
+ manuallyFilledDatasetIds = new ArrayList<>(size);
+ for (int i = 0; i < size; i++) {
+ final AutofillId fieldId = manuallyFilledIds.keyAt(i);
+ final ArraySet<String> datasetIds = manuallyFilledIds.valueAt(i);
+ manuallyFilledFieldIds.add(fieldId);
+ manuallyFilledDatasetIds.add(new ArrayList<>(datasetIds));
+ }
+ }
+ mService.logContextCommitted(id, mClientState, mSelectedDatasetIds, ignoredDatasets,
+ changedFieldIds, changedDatasetIds,
+ manuallyFilledFieldIds, manuallyFilledDatasetIds);
+ }
+
+ /**
* Shows the save UI, when session can be saved.
*
* @return {@code true} if session is done, or {@code false} if it's pending user action.
@@ -1600,7 +1790,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
}
if (mResponses == null) {
- mResponses = new SparseArray<>(4);
+ // Set initial capacity as 2 to handle cases where service always requires auth.
+ // TODO: add a metric for number of responses set by server, so we can use its average
+ // as the initial array capacitiy.
+ mResponses = new SparseArray<>(2);
}
mResponses.put(requestId, newResponse);
mClientState = newClientState != null ? newClientState : newResponse.getClientState();
@@ -1676,6 +1869,10 @@ final class Session implements RemoteFillService.FillServiceCallbacks, ViewState
final AutofillId id = ids.get(j);
final AutofillValue value = values.get(j);
final ViewState viewState = createOrUpdateViewStateLocked(id, state, value);
+ final String datasetId = dataset.getId();
+ if (datasetId != null) {
+ viewState.setDatasetId(datasetId);
+ }
if (response != null) {
viewState.setResponse(response);
} else if (clearResponse) {
diff --git a/services/autofill/java/com/android/server/autofill/ViewState.java b/services/autofill/java/com/android/server/autofill/ViewState.java
index 51659bb8c8b3..1d8110f08364 100644
--- a/services/autofill/java/com/android/server/autofill/ViewState.java
+++ b/services/autofill/java/com/android/server/autofill/ViewState.java
@@ -78,6 +78,7 @@ final class ViewState {
private AutofillValue mAutofilledValue;
private Rect mVirtualBounds;
private int mState;
+ private String mDatasetId;
ViewState(Session session, AutofillId id, Listener listener, int state) {
mSession = session;
@@ -148,6 +149,15 @@ final class ViewState {
mState &= ~state;
}
+ @Nullable
+ String getDatasetId() {
+ return mDatasetId;
+ }
+
+ void setDatasetId(String datasetId) {
+ mDatasetId = datasetId;
+ }
+
// TODO: refactor / rename / document this method (and maybeCallOnFillReady) to make it clear
// that it can change the value and update the UI; similarly, should replace code that
// directly sets mAutofillValue to use encapsulation.
@@ -182,13 +192,15 @@ final class ViewState {
@Override
public String toString() {
- return "ViewState: [id=" + id + ", currentValue=" + mCurrentValue
+ return "ViewState: [id=" + id + ", datasetId=" + mDatasetId
+ + ", currentValue=" + mCurrentValue
+ ", autofilledValue=" + mAutofilledValue
+ ", bounds=" + mVirtualBounds + ", state=" + getStateAsString() + "]";
}
void dump(String prefix, PrintWriter pw) {
pw.print(prefix); pw.print("id:" ); pw.println(this.id);
+ pw.print(prefix); pw.print("datasetId:" ); pw.println(this.mDatasetId);
pw.print(prefix); pw.print("state:" ); pw.println(getStateAsString());
pw.print(prefix); pw.print("response:");
if (mResponse == null) {