content: /*
   * 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.
   *
   * <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 {
      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<Event> mEvents;
  
      /** @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 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);
                  }
              }
          }
      }
  
      /**
       * 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()}.
           *   <li>Which fields match the {@link UserData} set by the service.
           * </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(prefix = { "TYPE_" }, 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 AutofillId[] mDetectedFieldIds;
          @Nullable private final FieldClassification[] mDetectedFieldClassifications;
  
          /**
           * 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 <a href="AutofillService.html#FieldClassification">field classification</a>
           * results.
           *
           * <p><b>Note: </b>Only set on events of type {@link #TYPE_CONTEXT_COMMITTED}, when the
           * service requested {@link FillResponse.Builder#setFieldClassificationIds(AutofillId...)
           * field classification}.
           */
          @NonNull public Map<AutofillId, FieldClassification> getFieldsClassification() {
              if (mDetectedFieldIds == null) {
                  return Collections.emptyMap();
              }
              final int size = mDetectedFieldIds.length;
              final ArrayMap<AutofillId, FieldClassification> 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.
           *
           * <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.
           */
          @NonNull 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}.
           * @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<String> selectedDatasetIds,
                  @Nullable ArraySet<String> ignoredDatasetIds,
                  @Nullable ArrayList<AutofillId> changedFieldIds,
                  @Nullable ArrayList<String> changedDatasetIds,
                  @Nullable ArrayList<AutofillId> manuallyFilledFieldIds,
                  @Nullable ArrayList<ArrayList<String>> manuallyFilledDatasetIds,
                  @Nullable AutofillId[] detectedFieldIds,
                  @Nullable FieldClassification[] detectedFieldClassifications) {
              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;
  
              mDetectedFieldIds = detectedFieldIds;
              mDetectedFieldClassifications = detectedFieldClassifications;
          }
  
          @Override
          public String toString() {
              return "FillEvent [datasetId=" + mDatasetId
                      + ", type=" + mEventType
                      + ", selectedDatasets=" + mSelectedDatasetIds
                      + ", ignoredDatasetIds=" + mIgnoredDatasetIds
                      + ", changedFieldIds=" + mChangedFieldIds
                      + ", changedDatasetsIds=" + mChangedDatasetIds
                      + ", manuallyFilledFieldIds=" + mManuallyFilledFieldIds
                      + ", manuallyFilledDatasetIds=" + mManuallyFilledDatasetIds
                      + ", detectedFieldIds=" + Arrays.toString(mDetectedFieldIds)
                      + ", detectedFieldClassifications ="
                          + Arrays.toString(mDetectedFieldClassifications)
                      + "]";
          }
      }
  
      public static final @android.annotation.NonNull Parcelable.Creator<FillEventHistory> CREATOR =
              new Parcelable.Creator<FillEventHistory>() {
                  @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<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 AutofillId[] detectedFieldIds = parcel.readParcelableArray(null,
                                  AutofillId.class);
                          final FieldClassification[] detectedFieldClassifications =
                                  (detectedFieldIds != null)
                                  ? FieldClassification.readArrayFromParcel(parcel)
                                  : null;
  
                          selection.addEvent(new Event(eventType, datasetId, clientState,
                                  selectedDatasetIds, ignoredDatasets,
                                  changedFieldIds, changedDatasetIds,
                                  manuallyFilledFieldIds, manuallyFilledDatasetIds,
                                  detectedFieldIds, detectedFieldClassifications));
                      }
                      return selection;
                  }
  
                  @Override
                  public FillEventHistory[] newArray(int size) {
                      return new FillEventHistory[size];
                  }
              };
  }
  