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.view.textclassifier;
  
  import android.annotation.IntDef;
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.UserIdInt;
  import android.os.Parcel;
  import android.os.Parcelable;
  import android.os.UserHandle;
  import android.view.textclassifier.TextClassifier.EntityType;
  import android.view.textclassifier.TextClassifier.WidgetType;
  
  import com.android.internal.annotations.VisibleForTesting;
  import com.android.internal.util.Preconditions;
  
  import java.lang.annotation.Retention;
  import java.lang.annotation.RetentionPolicy;
  import java.util.Locale;
  import java.util.Objects;
  
  /**
   * A selection event.
   * Specify index parameters as word token indices.
   */
  public final class SelectionEvent implements Parcelable {
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT,
              ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON,
              ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET})
      // NOTE: ActionType values should not be lower than 100 to avoid colliding with the other
      // EventTypes declared below.
      public @interface ActionType {
          /*
           * Terminal event types range: [100,200).
           * Non-terminal event types range: [200,300).
           */
      }
  
      /** User typed over the selection. */
      public static final int ACTION_OVERTYPE = 100;
      /** User copied the selection. */
      public static final int ACTION_COPY = 101;
      /** User pasted over the selection. */
      public static final int ACTION_PASTE = 102;
      /** User cut the selection. */
      public static final int ACTION_CUT = 103;
      /** User shared the selection. */
      public static final int ACTION_SHARE = 104;
      /** User clicked the textAssist menu item. */
      public static final int ACTION_SMART_SHARE = 105;
      /** User dragged+dropped the selection. */
      public static final int ACTION_DRAG = 106;
      /** User abandoned the selection. */
      public static final int ACTION_ABANDON = 107;
      /** User performed an action on the selection. */
      public static final int ACTION_OTHER = 108;
  
      // Non-terminal actions.
      /** User activated Select All */
      public static final int ACTION_SELECT_ALL = 200;
      /** User reset the smart selection. */
      public static final int ACTION_RESET = 201;
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT,
              ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON,
              ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET,
              EVENT_SELECTION_STARTED, EVENT_SELECTION_MODIFIED,
              EVENT_SMART_SELECTION_SINGLE, EVENT_SMART_SELECTION_MULTI,
              EVENT_AUTO_SELECTION})
      // NOTE: EventTypes declared here must be less than 100 to avoid colliding with the
      // ActionTypes declared above.
      public @interface EventType {
          /*
           * Range: 1 -> 99.
           */
      }
  
      /** User started a new selection. */
      public static final int EVENT_SELECTION_STARTED = 1;
      /** User modified an existing selection. */
      public static final int EVENT_SELECTION_MODIFIED = 2;
      /** Smart selection triggered for a single token (word). */
      public static final int EVENT_SMART_SELECTION_SINGLE = 3;
      /** Smart selection triggered spanning multiple tokens (words). */
      public static final int EVENT_SMART_SELECTION_MULTI = 4;
      /** Something else other than User or the default TextClassifier triggered a selection. */
      public static final int EVENT_AUTO_SELECTION = 5;
  
      /** @hide */
      @Retention(RetentionPolicy.SOURCE)
      @IntDef({INVOCATION_MANUAL, INVOCATION_LINK, INVOCATION_UNKNOWN})
      public @interface InvocationMethod {}
  
      /** Selection was invoked by the user long pressing, double tapping, or dragging to select. */
      public static final int INVOCATION_MANUAL = 1;
      /** Selection was invoked by the user tapping on a link. */
      public static final int INVOCATION_LINK = 2;
      /** Unknown invocation method */
      public static final int INVOCATION_UNKNOWN = 0;
  
      static final String NO_SIGNATURE = "";
  
      private final int mAbsoluteStart;
      private final int mAbsoluteEnd;
      private final @EntityType String mEntityType;
  
      private @EventType int mEventType;
      private String mPackageName = "";
      private String mWidgetType = TextClassifier.WIDGET_TYPE_UNKNOWN;
      private @InvocationMethod int mInvocationMethod;
      @Nullable private String mWidgetVersion;
      private @UserIdInt int mUserId = UserHandle.USER_NULL;
      @Nullable private String mResultId;
      private long mEventTime;
      private long mDurationSinceSessionStart;
      private long mDurationSincePreviousEvent;
      private int mEventIndex;
      @Nullable private TextClassificationSessionId mSessionId;
      private int mStart;
      private int mEnd;
      private int mSmartStart;
      private int mSmartEnd;
  
      SelectionEvent(
              int start, int end,
              @EventType int eventType, @EntityType String entityType,
              @InvocationMethod int invocationMethod, @Nullable String resultId) {
          Preconditions.checkArgument(end >= start, "end cannot be less than start");
          mAbsoluteStart = start;
          mAbsoluteEnd = end;
          mEventType = eventType;
          mEntityType = Preconditions.checkNotNull(entityType);
          mResultId = resultId;
          mInvocationMethod = invocationMethod;
      }
  
      private SelectionEvent(Parcel in) {
          mAbsoluteStart = in.readInt();
          mAbsoluteEnd = in.readInt();
          mEventType = in.readInt();
          mEntityType = in.readString();
          mWidgetVersion = in.readInt() > 0 ? in.readString() : null;
          mPackageName = in.readString();
          mWidgetType = in.readString();
          mInvocationMethod = in.readInt();
          mResultId = in.readString();
          mEventTime = in.readLong();
          mDurationSinceSessionStart = in.readLong();
          mDurationSincePreviousEvent = in.readLong();
          mEventIndex = in.readInt();
          mSessionId = in.readInt() > 0
                  ? TextClassificationSessionId.CREATOR.createFromParcel(in) : null;
          mStart = in.readInt();
          mEnd = in.readInt();
          mSmartStart = in.readInt();
          mSmartEnd = in.readInt();
          mUserId = in.readInt();
      }
  
      @Override
      public void writeToParcel(Parcel dest, int flags) {
          dest.writeInt(mAbsoluteStart);
          dest.writeInt(mAbsoluteEnd);
          dest.writeInt(mEventType);
          dest.writeString(mEntityType);
          dest.writeInt(mWidgetVersion != null ? 1 : 0);
          if (mWidgetVersion != null) {
              dest.writeString(mWidgetVersion);
          }
          dest.writeString(mPackageName);
          dest.writeString(mWidgetType);
          dest.writeInt(mInvocationMethod);
          dest.writeString(mResultId);
          dest.writeLong(mEventTime);
          dest.writeLong(mDurationSinceSessionStart);
          dest.writeLong(mDurationSincePreviousEvent);
          dest.writeInt(mEventIndex);
          dest.writeInt(mSessionId != null ? 1 : 0);
          if (mSessionId != null) {
              mSessionId.writeToParcel(dest, flags);
          }
          dest.writeInt(mStart);
          dest.writeInt(mEnd);
          dest.writeInt(mSmartStart);
          dest.writeInt(mSmartEnd);
          dest.writeInt(mUserId);
      }
  
      @Override
      public int describeContents() {
          return 0;
      }
  
      /**
       * Creates a "selection started" event.
       *
       * @param invocationMethod  the way the selection was triggered
       * @param start  the index of the selected text
       */
      @NonNull
      public static SelectionEvent createSelectionStartedEvent(
              @SelectionEvent.InvocationMethod int invocationMethod, int start) {
          return new SelectionEvent(
                  start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED,
                  TextClassifier.TYPE_UNKNOWN, invocationMethod, NO_SIGNATURE);
      }
  
      /**
       * Creates a "selection modified" event.
       * Use when the user modifies the selection.
       *
       * @param start  the start (inclusive) index of the selection
       * @param end  the end (exclusive) index of the selection
       *
       * @throws IllegalArgumentException if end is less than start
       */
      @NonNull
      public static SelectionEvent createSelectionModifiedEvent(int start, int end) {
          Preconditions.checkArgument(end >= start, "end cannot be less than start");
          return new SelectionEvent(
                  start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
                  TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN, NO_SIGNATURE);
      }
  
      /**
       * Creates a "selection modified" event.
       * Use when the user modifies the selection and the selection's entity type is known.
       *
       * @param start  the start (inclusive) index of the selection
       * @param end  the end (exclusive) index of the selection
       * @param classification  the TextClassification object returned by the TextClassifier that
       *      classified the selected text
       *
       * @throws IllegalArgumentException if end is less than start
       */
      @NonNull
      public static SelectionEvent createSelectionModifiedEvent(
              int start, int end, @NonNull TextClassification classification) {
          Preconditions.checkArgument(end >= start, "end cannot be less than start");
          Preconditions.checkNotNull(classification);
          final String entityType = classification.getEntityCount() > 0
                  ? classification.getEntity(0)
                  : TextClassifier.TYPE_UNKNOWN;
          return new SelectionEvent(
                  start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
                  entityType, INVOCATION_UNKNOWN, classification.getId());
      }
  
      /**
       * Creates a "selection modified" event.
       * Use when a TextClassifier modifies the selection.
       *
       * @param start  the start (inclusive) index of the selection
       * @param end  the end (exclusive) index of the selection
       * @param selection  the TextSelection object returned by the TextClassifier for the
       *      specified selection
       *
       * @throws IllegalArgumentException if end is less than start
       */
      @NonNull
      public static SelectionEvent createSelectionModifiedEvent(
              int start, int end, @NonNull TextSelection selection) {
          Preconditions.checkArgument(end >= start, "end cannot be less than start");
          Preconditions.checkNotNull(selection);
          final String entityType = selection.getEntityCount() > 0
                  ? selection.getEntity(0)
                  : TextClassifier.TYPE_UNKNOWN;
          return new SelectionEvent(
                  start, end, SelectionEvent.EVENT_AUTO_SELECTION,
                  entityType, INVOCATION_UNKNOWN, selection.getId());
      }
  
      /**
       * Creates an event specifying an action taken on a selection.
       * Use when the user clicks on an action to act on the selected text.
       *
       * @param start  the start (inclusive) index of the selection
       * @param end  the end (exclusive) index of the selection
       * @param actionType  the action that was performed on the selection
       *
       * @throws IllegalArgumentException if end is less than start
       */
      @NonNull
      public static SelectionEvent createSelectionActionEvent(
              int start, int end, @SelectionEvent.ActionType int actionType) {
          Preconditions.checkArgument(end >= start, "end cannot be less than start");
          checkActionType(actionType);
          return new SelectionEvent(
                  start, end, actionType, TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN,
                  NO_SIGNATURE);
      }
  
      /**
       * Creates an event specifying an action taken on a selection.
       * Use when the user clicks on an action to act on the selected text and the selection's
       * entity type is known.
       *
       * @param start  the start (inclusive) index of the selection
       * @param end  the end (exclusive) index of the selection
       * @param actionType  the action that was performed on the selection
       * @param classification  the TextClassification object returned by the TextClassifier that
       *      classified the selected text
       *
       * @throws IllegalArgumentException if end is less than start
       * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType
       */
      @NonNull
      public static SelectionEvent createSelectionActionEvent(
              int start, int end, @SelectionEvent.ActionType int actionType,
              @NonNull TextClassification classification) {
          Preconditions.checkArgument(end >= start, "end cannot be less than start");
          Preconditions.checkNotNull(classification);
          checkActionType(actionType);
          final String entityType = classification.getEntityCount() > 0
                  ? classification.getEntity(0)
                  : TextClassifier.TYPE_UNKNOWN;
          return new SelectionEvent(start, end, actionType, entityType, INVOCATION_UNKNOWN,
                  classification.getId());
      }
  
      /**
       * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType}
       */
      private static void checkActionType(@SelectionEvent.EventType int eventType)
              throws IllegalArgumentException {
          switch (eventType) {
              case SelectionEvent.ACTION_OVERTYPE:  // fall through
              case SelectionEvent.ACTION_COPY:  // fall through
              case SelectionEvent.ACTION_PASTE:  // fall through
              case SelectionEvent.ACTION_CUT:  // fall through
              case SelectionEvent.ACTION_SHARE:  // fall through
              case SelectionEvent.ACTION_SMART_SHARE:  // fall through
              case SelectionEvent.ACTION_DRAG:  // fall through
              case SelectionEvent.ACTION_ABANDON:  // fall through
              case SelectionEvent.ACTION_SELECT_ALL:  // fall through
              case SelectionEvent.ACTION_RESET:  // fall through
              case SelectionEvent.ACTION_OTHER:  // fall through
                  return;
              default:
                  throw new IllegalArgumentException(
                          String.format(Locale.US, "%d is not an eventType", eventType));
          }
      }
  
      int getAbsoluteStart() {
          return mAbsoluteStart;
      }
  
      int getAbsoluteEnd() {
          return mAbsoluteEnd;
      }
  
      /**
       * Returns the type of event that was triggered. e.g. {@link #ACTION_COPY}.
       */
      @EventType
      public int getEventType() {
          return mEventType;
      }
  
      /**
       * Sets the event type.
       * @hide
       */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public void setEventType(@EventType int eventType) {
          mEventType = eventType;
      }
  
      /**
       * Returns the type of entity that is associated with this event. e.g.
       * {@link android.view.textclassifier.TextClassifier#TYPE_EMAIL}.
       */
      @EntityType
      @NonNull
      public String getEntityType() {
          return mEntityType;
      }
  
      /**
       * Returns the package name of the app that this event originated in.
       */
      @NonNull
      public String getPackageName() {
          return mPackageName;
      }
  
      /**
       * Sets the id of this event's user.
       * <p>
       * Package-private for SystemTextClassifier's use.
       */
      void setUserId(@UserIdInt int userId) {
          mUserId = userId;
      }
  
      /**
       * Returns the id of this event's user.
       * @hide
       */
      @UserIdInt
      public int getUserId() {
          return mUserId;
      }
  
      /**
       * Returns the type of widget that was involved in triggering this event.
       */
      @WidgetType
      @NonNull
      public String getWidgetType() {
          return mWidgetType;
      }
  
      /**
       * Returns a string version info for the widget this event was triggered in.
       */
      @Nullable
      public String getWidgetVersion() {
          return mWidgetVersion;
      }
  
      /**
       * Sets the {@link TextClassificationContext} for this event.
       * @hide
       */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public void setTextClassificationSessionContext(TextClassificationContext context) {
          mPackageName = context.getPackageName();
          mWidgetType = context.getWidgetType();
          mWidgetVersion = context.getWidgetVersion();
          mUserId = context.getUserId();
      }
  
      /**
       * Returns the way the selection mode was invoked.
       */
      public @InvocationMethod int getInvocationMethod() {
          return mInvocationMethod;
      }
  
      /**
       * Sets the invocationMethod for this event.
       * @hide
       */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public void setInvocationMethod(@InvocationMethod int invocationMethod) {
          mInvocationMethod = invocationMethod;
      }
  
      /**
       * Returns the id of the text classifier result associated with this event.
       */
      @Nullable
      public String getResultId() {
          return mResultId;
      }
  
      SelectionEvent setResultId(@Nullable String resultId) {
          mResultId = resultId;
          return this;
      }
  
      /**
       * Returns the time this event was triggered.
       */
      public long getEventTime() {
          return mEventTime;
      }
  
      SelectionEvent setEventTime(long timeMs) {
          mEventTime = timeMs;
          return this;
      }
  
      /**
       * Returns the duration in ms between when this event was triggered and when the first event in
       * the selection session was triggered.
       */
      public long getDurationSinceSessionStart() {
          return mDurationSinceSessionStart;
      }
  
      SelectionEvent setDurationSinceSessionStart(long durationMs) {
          mDurationSinceSessionStart = durationMs;
          return this;
      }
  
      /**
       * Returns the duration in ms between when this event was triggered and when the previous event
       * in the selection session was triggered.
       */
      public long getDurationSincePreviousEvent() {
          return mDurationSincePreviousEvent;
      }
  
      SelectionEvent setDurationSincePreviousEvent(long durationMs) {
          this.mDurationSincePreviousEvent = durationMs;
          return this;
      }
  
      /**
       * Returns the index (e.g. 1st event, 2nd event, etc.) of this event in the selection session.
       */
      public int getEventIndex() {
          return mEventIndex;
      }
  
      /** @hide */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public SelectionEvent setEventIndex(int index) {
          mEventIndex = index;
          return this;
      }
  
      /**
       * Returns the selection session id.
       */
      @Nullable
      public TextClassificationSessionId getSessionId() {
          return mSessionId;
      }
  
      /** @hide */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public SelectionEvent setSessionId(@Nullable TextClassificationSessionId id) {
          mSessionId = id;
          return this;
      }
  
      /**
       * Returns the start index of this events relative to the index of the start selection
       * event in the selection session.
       */
      public int getStart() {
          return mStart;
      }
  
      /** @hide */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public SelectionEvent setStart(int start) {
          mStart = start;
          return this;
      }
  
      /**
       * Returns the end index of this events relative to the index of the start selection
       * event in the selection session.
       */
      public int getEnd() {
          return mEnd;
      }
  
      /** @hide */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public SelectionEvent setEnd(int end) {
          mEnd = end;
          return this;
      }
  
      /**
       * Returns the start index of this events relative to the index of the smart selection
       * event in the selection session.
       */
      public int getSmartStart() {
          return mSmartStart;
      }
  
      /** @hide */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public SelectionEvent setSmartStart(int start) {
          this.mSmartStart = start;
          return this;
      }
  
      /**
       * Returns the end index of this events relative to the index of the smart selection
       * event in the selection session.
       */
      public int getSmartEnd() {
          return mSmartEnd;
      }
  
      /** @hide */
      @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
      public SelectionEvent setSmartEnd(int end) {
          mSmartEnd = end;
          return this;
      }
  
      boolean isTerminal() {
          return isTerminal(mEventType);
      }
  
      /**
       * Returns true if the eventType is a terminal event type. Otherwise returns false.
       * A terminal event is an event that ends a selection interaction.
       */
      public static boolean isTerminal(@EventType int eventType) {
          switch (eventType) {
              case ACTION_OVERTYPE:  // fall through
              case ACTION_COPY:  // fall through
              case ACTION_PASTE:  // fall through
              case ACTION_CUT:  // fall through
              case ACTION_SHARE:  // fall through
              case ACTION_SMART_SHARE:  // fall through
              case ACTION_DRAG:  // fall through
              case ACTION_ABANDON:  // fall through
              case ACTION_OTHER:  // fall through
                  return true;
              default:
                  return false;
          }
      }
  
      @Override
      public int hashCode() {
          return Objects.hash(mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
                  mWidgetVersion, mPackageName, mUserId, mWidgetType, mInvocationMethod, mResultId,
                  mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent,
                  mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
      }
  
      @Override
      public boolean equals(Object obj) {
          if (this == obj) {
              return true;
          }
          if (!(obj instanceof SelectionEvent)) {
              return false;
          }
  
          final SelectionEvent other = (SelectionEvent) obj;
          return mAbsoluteStart == other.mAbsoluteStart
                  && mAbsoluteEnd == other.mAbsoluteEnd
                  && mEventType == other.mEventType
                  && Objects.equals(mEntityType, other.mEntityType)
                  && Objects.equals(mWidgetVersion, other.mWidgetVersion)
                  && Objects.equals(mPackageName, other.mPackageName)
                  && mUserId == other.mUserId
                  && Objects.equals(mWidgetType, other.mWidgetType)
                  && mInvocationMethod == other.mInvocationMethod
                  && Objects.equals(mResultId, other.mResultId)
                  && mEventTime == other.mEventTime
                  && mDurationSinceSessionStart == other.mDurationSinceSessionStart
                  && mDurationSincePreviousEvent == other.mDurationSincePreviousEvent
                  && mEventIndex == other.mEventIndex
                  && Objects.equals(mSessionId, other.mSessionId)
                  && mStart == other.mStart
                  && mEnd == other.mEnd
                  && mSmartStart == other.mSmartStart
                  && mSmartEnd == other.mSmartEnd;
      }
  
      @Override
      public String toString() {
          return String.format(Locale.US,
                  "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, "
                          + "widgetVersion=%s, packageName=%s, widgetType=%s, invocationMethod=%s, "
                          + "userId=%d, resultId=%s, eventTime=%d, durationSinceSessionStart=%d, "
                          + "durationSincePreviousEvent=%d, eventIndex=%d,"
                          + "sessionId=%s, start=%d, end=%d, smartStart=%d, smartEnd=%d}",
                  mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
                  mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod,
                  mUserId, mResultId, mEventTime, mDurationSinceSessionStart,
                  mDurationSincePreviousEvent, mEventIndex,
                  mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
      }
  
      public static final @android.annotation.NonNull Creator<SelectionEvent> CREATOR = new Creator<SelectionEvent>() {
          @Override
          public SelectionEvent createFromParcel(Parcel in) {
              return new SelectionEvent(in);
          }
  
          @Override
          public SelectionEvent[] newArray(int size) {
              return new SelectionEvent[size];
          }
      };
  }
  