/* * 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.os.Parcel; import android.os.Parcelable; import android.view.textclassifier.TextClassifier.EntityType; import android.view.textclassifier.TextClassifier.WidgetType; 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; private 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; @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(); } @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); } @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. */ 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; } /** * 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. */ void setTextClassificationSessionContext(TextClassificationContext context) { mPackageName = context.getPackageName(); mWidgetType = context.getWidgetType(); mWidgetVersion = context.getWidgetVersion(); } /** * Returns the way the selection mode was invoked. */ public @InvocationMethod int getInvocationMethod() { return mInvocationMethod; } /** * Sets the invocationMethod for this event. */ 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; } SelectionEvent setEventIndex(int index) { mEventIndex = index; return this; } /** * Returns the selection session id. */ @Nullable public TextClassificationSessionId getSessionId() { return mSessionId; } SelectionEvent setSessionId(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; } 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; } 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; } 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; } 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, 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) && 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, " + "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, mResultId, mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent, mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd); } public static final Creator CREATOR = new Creator() { @Override public SelectionEvent createFromParcel(Parcel in) { return new SelectionEvent(in); } @Override public SelectionEvent[] newArray(int size) { return new SelectionEvent[size]; } }; }