diff options
| -rw-r--r-- | api/current.txt | 8 | ||||
| -rw-r--r-- | api/system-current.txt | 8 | ||||
| -rw-r--r-- | api/test-current.txt | 8 | ||||
| -rw-r--r-- | core/java/android/service/autofill/FillEventHistory.java | 283 | ||||
| -rw-r--r-- | core/java/android/service/autofill/FillResponse.java | 42 | ||||
| -rw-r--r-- | services/autofill/java/com/android/server/autofill/AutofillManagerServiceImpl.java | 36 | ||||
| -rw-r--r-- | services/autofill/java/com/android/server/autofill/Session.java | 199 | ||||
| -rw-r--r-- | services/autofill/java/com/android/server/autofill/ViewState.java | 14 |
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) { |