| /* |
| * 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 android.annotation.IntDef; |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.annotation.TestApi; |
| import android.content.IntentSender; |
| import android.os.Bundle; |
| import android.os.Parcel; |
| 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 |
| * {@link AutofillService#onFillRequest(FillRequest, android.os.CancellationSignal, FillCallback)} |
| * call. |
| * |
| * <p>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. |
| * |
| * <p>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 { |
| /** |
| * Not in parcel. The UID of the {@link AutofillService} that created the {@link FillResponse}. |
| */ |
| private final int mServiceUid; |
| |
| /** |
| * 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<Event> mEvents; |
| |
| /** |
| * Gets the UID of the {@link AutofillService} that created the {@link FillResponse}. |
| * |
| * @return The UID of the {@link AutofillService} |
| * |
| * @hide |
| */ |
| public int getServiceUid() { |
| return mServiceUid; |
| } |
| |
| /** @hide */ |
| public int getSessionId() { |
| return mSessionId; |
| } |
| |
| /** |
| * Returns the client state set in the previous {@link FillResponse}. |
| * |
| * <p><b>Note: </b>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<Event> getEvents() { |
| return mEvents; |
| } |
| |
| /** |
| * @hide |
| */ |
| public void addEvent(Event event) { |
| if (mEvents == null) { |
| mEvents = new ArrayList<>(1); |
| } |
| mEvents.add(event); |
| } |
| |
| /** |
| * @hide |
| */ |
| public FillEventHistory(int serviceUid, int sessionId, @Nullable Bundle clientState) { |
| mClientState = clientState; |
| mServiceUid = serviceUid; |
| mSessionId = sessionId; |
| } |
| |
| @Override |
| public String toString() { |
| return mEvents == null ? "no events" : mEvents.toString(); |
| } |
| |
| @Override |
| public int describeContents() { |
| return 0; |
| } |
| |
| @Override |
| public void writeToParcel(Parcel dest, int flags) { |
| dest.writeBundle(mClientState); |
| if (mEvents == null) { |
| dest.writeInt(0); |
| } else { |
| dest.writeInt(mEvents.size()); |
| |
| int numEvents = mEvents.size(); |
| for (int i = 0; i < numEvents; i++) { |
| Event event = mEvents.get(i); |
| 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)); |
| } |
| } |
| dest.writeString(event.mDetectedRemoteId); |
| if (event.mDetectedRemoteId != null) { |
| dest.writeInt(event.mDetectedFieldScore); |
| } |
| } |
| } |
| } |
| |
| /** |
| * Description of an event that occured 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()}. |
| * |
| * <p><b>Note: </b>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. |
| * |
| * <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. |
| */ |
| // TODO(b/67867469): update with field detection behavior |
| 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_CONTEXT_COMMITTED}) |
| @Retention(RetentionPolicy.SOURCE) |
| @interface EventIds{} |
| |
| @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<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; |
| |
| @Nullable private final String mDetectedRemoteId; |
| private final int mDetectedFieldScore; |
| |
| /** |
| * 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. |
| * |
| * <p><b>Note: </b>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. |
| * |
| * <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; |
| } |
| |
| /** |
| * Gets the results of the last {@link FieldsDetection} request. |
| * |
| * @return map of edit-distance match ({@code 0} means full match, |
| * {@code 1} means 1 character different, etc...) by remote id (as set in the |
| * {@link FieldsDetection} constructor), or {@code null} if none of the user-input values |
| * matched the requested detection. |
| * |
| * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the |
| * service requested {@link FillResponse.Builder#setFieldsDetection(FieldsDetection) fields |
| * detection}. |
| * |
| * TODO(b/67867469): |
| * - improve javadoc |
| * - refine score meaning (for example, should 1 be different of -1?) |
| * - mention when it's set |
| * - unhide |
| * - unhide / remove testApi |
| * - add @NonNull / check it / add unit tests |
| * |
| * @hide |
| */ |
| @TestApi |
| @NonNull public Map<String, Integer> getDetectedFields() { |
| if (mDetectedRemoteId == null || mDetectedFieldScore == -1) { |
| return Collections.emptyMap(); |
| } |
| |
| final ArrayMap<String, Integer> map = new ArrayMap<>(1); |
| map.put(mDetectedRemoteId, mDetectedFieldScore); |
| return map; |
| } |
| |
| /** |
| * Returns which fields were available on datasets provided by the service but manually |
| * entered by the user. |
| * |
| * <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 |
| */ |
| // TODO(b/67867469): document detection field parameters once stable |
| public Event(int eventType, @Nullable String datasetId, @Nullable Bundle clientState, |
| @Nullable List<String> selectedDatasetIds, |
| @Nullable ArraySet<String> ignoredDatasetIds, |
| @Nullable ArrayList<AutofillId> changedFieldIds, |
| @Nullable ArrayList<String> changedDatasetIds, |
| @Nullable ArrayList<AutofillId> manuallyFilledFieldIds, |
| @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds, |
| @Nullable String detectedRemoteId, int detectedFieldScore) { |
| 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; |
| mDetectedRemoteId = detectedRemoteId; |
| mDetectedFieldScore = detectedFieldScore; |
| } |
| |
| @Override |
| public String toString() { |
| return "FillEvent [datasetId=" + mDatasetId |
| + ", type=" + mEventType |
| + ", selectedDatasets=" + mSelectedDatasetIds |
| + ", ignoredDatasetIds=" + mIgnoredDatasetIds |
| + ", changedFieldIds=" + mChangedFieldIds |
| + ", changedDatasetsIds=" + mChangedDatasetIds |
| + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds |
| + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds |
| + ", detectedRemoteId=" + mDetectedRemoteId |
| + ", detectedFieldScore=" + mDetectedFieldScore |
| + "]"; |
| } |
| } |
| |
| public static final Parcelable.Creator<FillEventHistory> CREATOR = |
| new Parcelable.Creator<FillEventHistory>() { |
| @Override |
| public FillEventHistory createFromParcel(Parcel parcel) { |
| FillEventHistory selection = new FillEventHistory(0, 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<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; |
| } |
| final String detectedRemoteId = parcel.readString(); |
| final int detectedFieldScore = detectedRemoteId == null ? -1 |
| : parcel.readInt(); |
| |
| selection.addEvent(new Event(eventType, datasetId, clientState, |
| selectedDatasetIds, ignoredDatasets, |
| changedFieldIds, changedDatasetIds, |
| manuallyFilledFieldIds, manuallyFilledDatasetIds, |
| detectedRemoteId, detectedFieldScore)); |
| } |
| return selection; |
| } |
| |
| @Override |
| public FillEventHistory[] newArray(int size) { |
| return new FillEventHistory[size]; |
| } |
| }; |
| } |