/* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.service.autofill; import static android.view.autofill.Helper.sVerbose; 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.util.Log; 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.Arrays; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Set; /** * Describes what happened after the last * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)} * call. * *

This history is typically used to keep track of previous user actions to optimize further * requests. For example, the service might return email addresses in alphabetical order by * default, but change that order based on the address the user picked on previous requests. * *

The history is not persisted over reboots, and it's cleared every time the service * replies to a * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)} * by calling {@link FillCallback#onSuccess(FillResponse)} or * {@link FillCallback#onFailure(CharSequence)} (if the service doesn't call any of these methods, * the history will clear out after some pre-defined time). */ public final class FillEventHistory implements Parcelable { private static final String TAG = "FillEventHistory"; /** * Not in parcel. The ID of the autofill session that created the {@link FillResponse}. */ private final int mSessionId; @Nullable private final Bundle mClientState; @Nullable List mEvents; /** @hide */ public int getSessionId() { return mSessionId; } /** * Returns the client state set in the previous {@link FillResponse}. * *

Note: the state is associated with the app that was autofilled in the previous * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)} * , which is not necessary the same app being autofilled now. * * @deprecated use {@link #getEvents()} then {@link Event#getClientState()} instead. */ @Deprecated @Nullable public Bundle getClientState() { return mClientState; } /** * Returns the events occurred after the latest call to * {@link FillCallback#onSuccess(FillResponse)}. * * @return The list of events or {@code null} if non occurred. */ @Nullable public List getEvents() { return mEvents; } /** * @hide */ public void addEvent(Event event) { if (mEvents == null) { mEvents = new ArrayList<>(1); } mEvents.add(event); } /** * @hide */ public FillEventHistory(int sessionId, @Nullable Bundle clientState) { mClientState = clientState; mSessionId = sessionId; } @Override public String toString() { return mEvents == null ? "no events" : mEvents.toString(); } @Override public int describeContents() { return 0; } @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeBundle(mClientState); if (mEvents == null) { parcel.writeInt(0); } else { parcel.writeInt(mEvents.size()); int numEvents = mEvents.size(); for (int i = 0; i < numEvents; i++) { Event event = mEvents.get(i); parcel.writeInt(event.mEventType); parcel.writeString(event.mDatasetId); parcel.writeBundle(event.mClientState); parcel.writeStringList(event.mSelectedDatasetIds); parcel.writeArraySet(event.mIgnoredDatasetIds); parcel.writeTypedList(event.mChangedFieldIds); parcel.writeStringList(event.mChangedDatasetIds); parcel.writeTypedList(event.mManuallyFilledFieldIds); if (event.mManuallyFilledFieldIds != null) { final int size = event.mManuallyFilledFieldIds.size(); for (int j = 0; j < size; j++) { parcel.writeStringList(event.mManuallyFilledDatasetIds.get(j)); } } final AutofillId[] detectedFields = event.mDetectedFieldIds; parcel.writeParcelableArray(detectedFields, flags); if (detectedFields != null) { FieldClassification.writeArrayToParcel(parcel, event.mDetectedFieldClassifications); } parcel.writeInt(event.mSaveDialogNotShowReason); parcel.writeInt(event.mUiType); } } } /** * Description of an event that occurred after the latest call to * {@link FillCallback#onSuccess(FillResponse)}. */ public static final class Event { /** * A dataset was selected. The dataset selected can be read from {@link #getDatasetId()}. * *

Note: on Android {@link android.os.Build.VERSION_CODES#O}, this event was also * incorrectly reported after a * {@link Dataset.Builder#setAuthentication(IntentSender) dataset authentication} was * selected and the service returned a dataset in the * {@link AutofillManager#EXTRA_AUTHENTICATION_RESULT} of the activity launched from that * {@link IntentSender}. This behavior was fixed on Android * {@link android.os.Build.VERSION_CODES#O_MR1}. */ public static final int TYPE_DATASET_SELECTED = 0; /** * A {@link Dataset.Builder#setAuthentication(IntentSender) dataset authentication} was * selected. The dataset authenticated can be read from {@link #getDatasetId()}. */ public static final int TYPE_DATASET_AUTHENTICATION_SELECTED = 1; /** * A {@link FillResponse.Builder#setAuthentication(android.view.autofill.AutofillId[], * IntentSender, android.widget.RemoteViews) fill response authentication} was selected. */ public static final int TYPE_AUTHENTICATION_SELECTED = 2; /** A save UI was shown. */ public static final int TYPE_SAVE_SHOWN = 3; /** * A committed autofill context for which the autofill service provided datasets. * *

This event is useful to track: *

* *

Note: This event is only generated when: *

* *

See {@link android.view.autofill.AutofillManager} for more information about autofill * contexts. */ public static final int TYPE_CONTEXT_COMMITTED = 4; /** * A dataset selector was shown. * *

This event is fired whenever the autofill UI was presented to the user.

*/ public static final int TYPE_DATASETS_SHOWN = 5; /** * The app/user requested for a field to be Autofilled. * * This event is fired when the view has been entered (by user or app) in order * to differentiate from FillRequests that have been pretriggered for FillDialogs. * * For example, the user might navigate away from a screen without tapping any * fields. In this case, a FillRequest/FillResponse has been generated, but was * not used for Autofilling. The user did not intend to see an Autofill result, * but a FillRequest was still generated. This is different from when the user * did tap on a field after the pretriggered FillRequest, this event will appear * in the FillEventHistory, signaling that the user did intend to Autofill * something. */ public static final int TYPE_VIEW_REQUESTED_AUTOFILL = 6; /** @hide */ @IntDef(prefix = { "TYPE_" }, value = { TYPE_DATASET_SELECTED, TYPE_DATASET_AUTHENTICATION_SELECTED, TYPE_AUTHENTICATION_SELECTED, TYPE_SAVE_SHOWN, TYPE_CONTEXT_COMMITTED, TYPE_DATASETS_SHOWN, TYPE_VIEW_REQUESTED_AUTOFILL }) @Retention(RetentionPolicy.SOURCE) @interface EventIds{} /** No reason for save dialog. */ public static final int NO_SAVE_UI_REASON_NONE = 0; /** The SaveInfo associated with the FillResponse is null. */ public static final int NO_SAVE_UI_REASON_NO_SAVE_INFO = 1; /** The service asked to delay save. */ public static final int NO_SAVE_UI_REASON_WITH_DELAY_SAVE_FLAG = 2; /** There was empty value for required ids. */ public static final int NO_SAVE_UI_REASON_HAS_EMPTY_REQUIRED = 3; /** No value has been changed. */ public static final int NO_SAVE_UI_REASON_NO_VALUE_CHANGED = 4; /** Fields failed validation. */ public static final int NO_SAVE_UI_REASON_FIELD_VALIDATION_FAILED = 5; /** All fields matched contents of datasets. */ public static final int NO_SAVE_UI_REASON_DATASET_MATCH = 6; /** @hide */ @IntDef(prefix = { "NO_SAVE_UI_REASON_" }, value = { NO_SAVE_UI_REASON_NONE, NO_SAVE_UI_REASON_NO_SAVE_INFO, NO_SAVE_UI_REASON_WITH_DELAY_SAVE_FLAG, NO_SAVE_UI_REASON_HAS_EMPTY_REQUIRED, NO_SAVE_UI_REASON_NO_VALUE_CHANGED, NO_SAVE_UI_REASON_FIELD_VALIDATION_FAILED, NO_SAVE_UI_REASON_DATASET_MATCH }) @Retention(RetentionPolicy.SOURCE) public @interface NoSaveReason{} /** The autofill suggestion presentation is unknown, this will be set for the event * that is unrelated to fill Ui presentation */ public static final int UI_TYPE_UNKNOWN = 0; /** The autofill suggestion is shown as a menu popup presentation. */ public static final int UI_TYPE_MENU = 1; /** The autofill suggestion is shown as a keyboard inline presentation. */ public static final int UI_TYPE_INLINE = 2; /** The autofill suggestion is shown as a dialog presentation. */ public static final int UI_TYPE_DIALOG = 3; /** * The autofill suggestion is shown os a credman bottom sheet * @hide */ public static final int UI_TYPE_CREDMAN_BOTTOM_SHEET = 4; /** @hide */ @IntDef(prefix = { "UI_TYPE_" }, value = { UI_TYPE_UNKNOWN, UI_TYPE_MENU, UI_TYPE_INLINE, UI_TYPE_DIALOG, UI_TYPE_CREDMAN_BOTTOM_SHEET }) @Retention(RetentionPolicy.SOURCE) public @interface UiType {} @EventIds private final int mEventType; @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 mSelectedDatasetIds; @Nullable private final ArraySet mIgnoredDatasetIds; @Nullable private final ArrayList mChangedFieldIds; @Nullable private final ArrayList mChangedDatasetIds; @Nullable private final ArrayList mManuallyFilledFieldIds; @Nullable private final ArrayList> mManuallyFilledDatasetIds; @Nullable private final AutofillId[] mDetectedFieldIds; @Nullable private final FieldClassification[] mDetectedFieldClassifications; @NoSaveReason private final int mSaveDialogNotShowReason; @UiType private final int mUiType; /** * Returns the type of the event. * * @return The type of the event */ public int getType() { return mEventType; } /** * Returns the id of dataset the id was on. * * @return The id of dataset, or {@code null} the event is not associated with a dataset. */ @Nullable public String getDatasetId() { return mDatasetId; } /** * Returns the client state from the {@link FillResponse} used to generate this event. * *

Note: the state is associated with the app that was autofilled in the previous * {@link * AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)}, * which is not necessary the same app being autofilled now. */ @Nullable public Bundle getClientState() { return mClientState; } /** * Returns which datasets were selected by the user. * *

Note: Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}. */ @NonNull public Set getSelectedDatasetIds() { return mSelectedDatasetIds == null ? Collections.emptySet() : new ArraySet<>(mSelectedDatasetIds); } /** * Returns which datasets were NOT selected by the user. * *

Note: Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}. */ @NonNull public Set getIgnoredDatasetIds() { return mIgnoredDatasetIds == null ? Collections.emptySet() : mIgnoredDatasetIds; } /** * Returns which fields in the selected datasets were changed by the user after the dataset * was selected. * *

For example, server provides: * *

         *  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();
         * 
* *

User select both datasets (for username and password) but after the fields are * autofilled, user changes them to: * *

         *   username = "ElBarto";
         *   password = "AyCaramba";
         * 
* *

Then the result is the following map: * *

         *   usernameId => "4815"
         *   passwordId => "162342"
         * 
* *

Note: 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 getChangedFields() { if (mChangedFieldIds == null || mChangedDatasetIds == null) { return Collections.emptyMap(); } final int size = mChangedFieldIds.size(); final ArrayMap changedFields = new ArrayMap<>(size); for (int i = 0; i < size; i++) { changedFields.put(mChangedFieldIds.get(i), mChangedDatasetIds.get(i)); } return changedFields; } /** * Gets the field classification * results. * *

Note: Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...) * field classification}. */ @NonNull public Map getFieldsClassification() { if (mDetectedFieldIds == null) { return Collections.emptyMap(); } final int size = mDetectedFieldIds.length; final ArrayMap map = new ArrayMap<>(size); for (int i = 0; i < size; i++) { final AutofillId id = mDetectedFieldIds[i]; final FieldClassification fc = mDetectedFieldClassifications[i]; if (sVerbose) { Log.v(TAG, "getFieldsClassification[" + i + "]: id=" + id + ", fc=" + fc); } map.put(id, fc); } return map; } /** * Returns which fields were available on datasets provided by the service but manually * entered by the user. * *

For example, server provides: * *

         *  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();
         * 
* *

User doesn't select a dataset but manually enters: * *

         *   username = "MrPlow";
         *   password = "D'OH";
         * 
* *

Then the result is the following map: * *

         *   usernameId => { "4815", "108"}
         *   passwordId => { "162342", "108" }
         * 
* *

Note: 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. */ @NonNull public Map> getManuallyEnteredField() { if (mManuallyFilledFieldIds == null || mManuallyFilledDatasetIds == null) { return Collections.emptyMap(); } final int size = mManuallyFilledFieldIds.size(); final Map> manuallyFilledFields = new ArrayMap<>(size); for (int i = 0; i < size; i++) { final AutofillId fieldId = mManuallyFilledFieldIds.get(i); final ArrayList datasetIds = mManuallyFilledDatasetIds.get(i); manuallyFilledFields.put(fieldId, new ArraySet<>(datasetIds)); } return manuallyFilledFields; } /** * Returns the reason why a save dialog was not shown. * *

Note: Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}. For the other * event types, the reason is set to NO_SAVE_UI_REASON_NONE. * * @return The reason why a save dialog was not shown. */ @NoSaveReason public int getNoSaveUiReason() { return mSaveDialogNotShowReason; } /** * Returns fill suggestion ui presentation type which corresponds to types * defined in {@link android.service.autofill.Presentations). * *

Note: Only set on events of type {@link #TYPE_DATASETS_SHOWN} and * {@link #TYPE_DATASET_SELECTED}. For the other event types, the type is set to * {@link #UI_TYPE_UNKNOWN }. * * @return The ui presentation type shown for user. */ @UiType public int getUiType() { return mUiType; } /** * 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}. * @param detectedFieldClassifications the field classification matches. * * @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, @Nullable List selectedDatasetIds, @Nullable ArraySet ignoredDatasetIds, @Nullable ArrayList changedFieldIds, @Nullable ArrayList changedDatasetIds, @Nullable ArrayList manuallyFilledFieldIds, @Nullable ArrayList> manuallyFilledDatasetIds, @Nullable AutofillId[] detectedFieldIds, @Nullable FieldClassification[] detectedFieldClassifications) { this(eventType, datasetId, clientState, selectedDatasetIds, ignoredDatasetIds, changedFieldIds, changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds, detectedFieldIds, detectedFieldClassifications, NO_SAVE_UI_REASON_NONE); } /** * 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}. * @param detectedFieldClassifications the field classification matches. * @param saveDialogNotShowReason The reason why a save dialog was not shown. * * @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, @Nullable List selectedDatasetIds, @Nullable ArraySet ignoredDatasetIds, @Nullable ArrayList changedFieldIds, @Nullable ArrayList changedDatasetIds, @Nullable ArrayList manuallyFilledFieldIds, @Nullable ArrayList> manuallyFilledDatasetIds, @Nullable AutofillId[] detectedFieldIds, @Nullable FieldClassification[] detectedFieldClassifications, int saveDialogNotShowReason) { this(eventType, datasetId, clientState, selectedDatasetIds, ignoredDatasetIds, changedFieldIds, changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds, detectedFieldIds, detectedFieldClassifications, saveDialogNotShowReason, UI_TYPE_UNKNOWN); } /** * 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}. * @param detectedFieldClassifications the field classification matches. * @param saveDialogNotShowReason The reason why a save dialog was not shown. * @param uiType The ui presentation type for fill suggestion. * * @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, @Nullable List selectedDatasetIds, @Nullable ArraySet ignoredDatasetIds, @Nullable ArrayList changedFieldIds, @Nullable ArrayList changedDatasetIds, @Nullable ArrayList manuallyFilledFieldIds, @Nullable ArrayList> manuallyFilledDatasetIds, @Nullable AutofillId[] detectedFieldIds, @Nullable FieldClassification[] detectedFieldClassifications, int saveDialogNotShowReason, int uiType) { mEventType = Preconditions.checkArgumentInRange(eventType, 0, TYPE_VIEW_REQUESTED_AUTOFILL, "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; mDetectedFieldIds = detectedFieldIds; mDetectedFieldClassifications = detectedFieldClassifications; mSaveDialogNotShowReason = Preconditions.checkArgumentInRange(saveDialogNotShowReason, NO_SAVE_UI_REASON_NONE, NO_SAVE_UI_REASON_DATASET_MATCH, "saveDialogNotShowReason"); mUiType = uiType; } @Override public String toString() { return "FillEvent [datasetId=" + mDatasetId + ", type=" + eventToString(mEventType) + ", uiType=" + uiTypeToString(mUiType) + ", selectedDatasets=" + mSelectedDatasetIds + ", ignoredDatasetIds=" + mIgnoredDatasetIds + ", changedFieldIds=" + mChangedFieldIds + ", changedDatasetsIds=" + mChangedDatasetIds + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds) + ", detectedFieldClassifications =" + Arrays.toString(mDetectedFieldClassifications) + ", saveDialogNotShowReason=" + mSaveDialogNotShowReason + "]"; } private static String eventToString(int eventType) { switch (eventType) { case TYPE_DATASET_SELECTED: return "TYPE_DATASET_SELECTED"; case TYPE_DATASET_AUTHENTICATION_SELECTED: return "TYPE_DATASET_AUTHENTICATION_SELECTED"; case TYPE_AUTHENTICATION_SELECTED: return "TYPE_AUTHENTICATION_SELECTED"; case TYPE_SAVE_SHOWN: return "TYPE_SAVE_SHOWN"; case TYPE_CONTEXT_COMMITTED: return "TYPE_CONTEXT_COMMITTED"; case TYPE_DATASETS_SHOWN: return "TYPE_DATASETS_SHOWN"; case TYPE_VIEW_REQUESTED_AUTOFILL: return "TYPE_VIEW_REQUESTED_AUTOFILL"; default: return "TYPE_UNKNOWN"; } } private static String uiTypeToString(int uiType) { switch (uiType) { case UI_TYPE_MENU: return "UI_TYPE_MENU"; case UI_TYPE_INLINE: return "UI_TYPE_INLINE"; case UI_TYPE_DIALOG: return "UI_TYPE_FILL_DIALOG"; case UI_TYPE_CREDMAN_BOTTOM_SHEET: return "UI_TYPE_CREDMAN_BOTTOM_SHEET"; default: return "UI_TYPE_UNKNOWN"; } } } public static final @android.annotation.NonNull Parcelable.Creator CREATOR = new Parcelable.Creator() { @Override public FillEventHistory createFromParcel(Parcel parcel) { FillEventHistory selection = new FillEventHistory(0, parcel.readBundle()); final int numEvents = parcel.readInt(); for (int i = 0; i < numEvents; i++) { final int eventType = parcel.readInt(); final String datasetId = parcel.readString(); final Bundle clientState = parcel.readBundle(); final ArrayList selectedDatasetIds = parcel.createStringArrayList(); @SuppressWarnings("unchecked") final ArraySet ignoredDatasets = (ArraySet) parcel.readArraySet(null); final ArrayList changedFieldIds = parcel.createTypedArrayList(AutofillId.CREATOR); final ArrayList changedDatasetIds = parcel.createStringArrayList(); final ArrayList manuallyFilledFieldIds = parcel.createTypedArrayList(AutofillId.CREATOR); final ArrayList> 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; } final AutofillId[] detectedFieldIds = parcel.readParcelableArray(null, AutofillId.class); final FieldClassification[] detectedFieldClassifications = (detectedFieldIds != null) ? FieldClassification.readArrayFromParcel(parcel) : null; final int saveDialogNotShowReason = parcel.readInt(); final int uiType = parcel.readInt(); selection.addEvent(new Event(eventType, datasetId, clientState, selectedDatasetIds, ignoredDatasets, changedFieldIds, changedDatasetIds, manuallyFilledFieldIds, manuallyFilledDatasetIds, detectedFieldIds, detectedFieldClassifications, saveDialogNotShowReason, uiType)); } return selection; } @Override public FillEventHistory[] newArray(int size) { return new FillEventHistory[size]; } }; }