SelectionEvent.java revision 35b3057627387102496b647c90740af4dd6b833d
1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.view.textclassifier;
18
19import android.annotation.IntDef;
20import android.annotation.NonNull;
21import android.annotation.Nullable;
22import android.os.Parcel;
23import android.os.Parcelable;
24import android.view.textclassifier.TextClassifier.EntityType;
25import android.view.textclassifier.TextClassifier.WidgetType;
26
27import com.android.internal.util.Preconditions;
28
29import java.lang.annotation.Retention;
30import java.lang.annotation.RetentionPolicy;
31import java.util.Locale;
32import java.util.Objects;
33
34/**
35 * A selection event.
36 * Specify index parameters as word token indices.
37 */
38public final class SelectionEvent implements Parcelable {
39
40    /** @hide */
41    @Retention(RetentionPolicy.SOURCE)
42    @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT,
43            ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON,
44            ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET})
45    // NOTE: ActionType values should not be lower than 100 to avoid colliding with the other
46    // EventTypes declared below.
47    public @interface ActionType {
48        /*
49         * Terminal event types range: [100,200).
50         * Non-terminal event types range: [200,300).
51         */
52    }
53
54    /** User typed over the selection. */
55    public static final int ACTION_OVERTYPE = 100;
56    /** User copied the selection. */
57    public static final int ACTION_COPY = 101;
58    /** User pasted over the selection. */
59    public static final int ACTION_PASTE = 102;
60    /** User cut the selection. */
61    public static final int ACTION_CUT = 103;
62    /** User shared the selection. */
63    public static final int ACTION_SHARE = 104;
64    /** User clicked the textAssist menu item. */
65    public static final int ACTION_SMART_SHARE = 105;
66    /** User dragged+dropped the selection. */
67    public static final int ACTION_DRAG = 106;
68    /** User abandoned the selection. */
69    public static final int ACTION_ABANDON = 107;
70    /** User performed an action on the selection. */
71    public static final int ACTION_OTHER = 108;
72
73    // Non-terminal actions.
74    /** User activated Select All */
75    public static final int ACTION_SELECT_ALL = 200;
76    /** User reset the smart selection. */
77    public static final int ACTION_RESET = 201;
78
79    /** @hide */
80    @Retention(RetentionPolicy.SOURCE)
81    @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT,
82            ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON,
83            ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET,
84            EVENT_SELECTION_STARTED, EVENT_SELECTION_MODIFIED,
85            EVENT_SMART_SELECTION_SINGLE, EVENT_SMART_SELECTION_MULTI,
86            EVENT_AUTO_SELECTION})
87    // NOTE: EventTypes declared here must be less than 100 to avoid colliding with the
88    // ActionTypes declared above.
89    public @interface EventType {
90        /*
91         * Range: 1 -> 99.
92         */
93    }
94
95    /** User started a new selection. */
96    public static final int EVENT_SELECTION_STARTED = 1;
97    /** User modified an existing selection. */
98    public static final int EVENT_SELECTION_MODIFIED = 2;
99    /** Smart selection triggered for a single token (word). */
100    public static final int EVENT_SMART_SELECTION_SINGLE = 3;
101    /** Smart selection triggered spanning multiple tokens (words). */
102    public static final int EVENT_SMART_SELECTION_MULTI = 4;
103    /** Something else other than User or the default TextClassifier triggered a selection. */
104    public static final int EVENT_AUTO_SELECTION = 5;
105
106    /** @hide */
107    @Retention(RetentionPolicy.SOURCE)
108    @IntDef({INVOCATION_MANUAL, INVOCATION_LINK, INVOCATION_UNKNOWN})
109    public @interface InvocationMethod {}
110
111    /** Selection was invoked by the user long pressing, double tapping, or dragging to select. */
112    public static final int INVOCATION_MANUAL = 1;
113    /** Selection was invoked by the user tapping on a link. */
114    public static final int INVOCATION_LINK = 2;
115    /** Unknown invocation method */
116    public static final int INVOCATION_UNKNOWN = 0;
117
118    private static final String NO_SIGNATURE = "";
119
120    private final int mAbsoluteStart;
121    private final int mAbsoluteEnd;
122    private final @EntityType String mEntityType;
123
124    private @EventType int mEventType;
125    private String mPackageName = "";
126    private String mWidgetType = TextClassifier.WIDGET_TYPE_UNKNOWN;
127    private @InvocationMethod int mInvocationMethod;
128    @Nullable private String mWidgetVersion;
129    @Nullable private String mResultId;
130    private long mEventTime;
131    private long mDurationSinceSessionStart;
132    private long mDurationSincePreviousEvent;
133    private int mEventIndex;
134    @Nullable private TextClassificationSessionId mSessionId;
135    private int mStart;
136    private int mEnd;
137    private int mSmartStart;
138    private int mSmartEnd;
139
140    SelectionEvent(
141            int start, int end,
142            @EventType int eventType, @EntityType String entityType,
143            @InvocationMethod int invocationMethod, @Nullable String resultId) {
144        Preconditions.checkArgument(end >= start, "end cannot be less than start");
145        mAbsoluteStart = start;
146        mAbsoluteEnd = end;
147        mEventType = eventType;
148        mEntityType = Preconditions.checkNotNull(entityType);
149        mResultId = resultId;
150        mInvocationMethod = invocationMethod;
151    }
152
153    SelectionEvent(
154            int start, int end,
155            @EventType int eventType, @EntityType String entityType,
156            @InvocationMethod int invocationMethod, @Nullable String resultId,
157            Logger.Config config) {
158        this(start, end, eventType, entityType, invocationMethod, resultId);
159        Preconditions.checkNotNull(config);
160        setTextClassificationSessionContext(
161                new TextClassificationContext.Builder(
162                        config.getPackageName(), config.getWidgetType())
163                        .setWidgetVersion(config.getWidgetVersion())
164                        .build());
165    }
166
167    private SelectionEvent(Parcel in) {
168        mAbsoluteStart = in.readInt();
169        mAbsoluteEnd = in.readInt();
170        mEventType = in.readInt();
171        mEntityType = in.readString();
172        mWidgetVersion = in.readInt() > 0 ? in.readString() : null;
173        mPackageName = in.readString();
174        mWidgetType = in.readString();
175        mInvocationMethod = in.readInt();
176        mResultId = in.readString();
177        mEventTime = in.readLong();
178        mDurationSinceSessionStart = in.readLong();
179        mDurationSincePreviousEvent = in.readLong();
180        mEventIndex = in.readInt();
181        mSessionId = in.readInt() > 0
182                ? TextClassificationSessionId.CREATOR.createFromParcel(in) : null;
183        mStart = in.readInt();
184        mEnd = in.readInt();
185        mSmartStart = in.readInt();
186        mSmartEnd = in.readInt();
187    }
188
189    @Override
190    public void writeToParcel(Parcel dest, int flags) {
191        dest.writeInt(mAbsoluteStart);
192        dest.writeInt(mAbsoluteEnd);
193        dest.writeInt(mEventType);
194        dest.writeString(mEntityType);
195        dest.writeInt(mWidgetVersion != null ? 1 : 0);
196        if (mWidgetVersion != null) {
197            dest.writeString(mWidgetVersion);
198        }
199        dest.writeString(mPackageName);
200        dest.writeString(mWidgetType);
201        dest.writeInt(mInvocationMethod);
202        dest.writeString(mResultId);
203        dest.writeLong(mEventTime);
204        dest.writeLong(mDurationSinceSessionStart);
205        dest.writeLong(mDurationSincePreviousEvent);
206        dest.writeInt(mEventIndex);
207        dest.writeInt(mSessionId != null ? 1 : 0);
208        if (mSessionId != null) {
209            mSessionId.writeToParcel(dest, flags);
210        }
211        dest.writeInt(mStart);
212        dest.writeInt(mEnd);
213        dest.writeInt(mSmartStart);
214        dest.writeInt(mSmartEnd);
215    }
216
217    @Override
218    public int describeContents() {
219        return 0;
220    }
221
222    /**
223     * Creates a "selection started" event.
224     *
225     * @param invocationMethod  the way the selection was triggered
226     * @param start  the index of the selected text
227     */
228    @NonNull
229    public static SelectionEvent createSelectionStartedEvent(
230            @SelectionEvent.InvocationMethod int invocationMethod, int start) {
231        return new SelectionEvent(
232                start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED,
233                TextClassifier.TYPE_UNKNOWN, invocationMethod, NO_SIGNATURE);
234    }
235
236    /**
237     * Creates a "selection modified" event.
238     * Use when the user modifies the selection.
239     *
240     * @param start  the start (inclusive) index of the selection
241     * @param end  the end (exclusive) index of the selection
242     *
243     * @throws IllegalArgumentException if end is less than start
244     */
245    @NonNull
246    public static SelectionEvent createSelectionModifiedEvent(int start, int end) {
247        Preconditions.checkArgument(end >= start, "end cannot be less than start");
248        return new SelectionEvent(
249                start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
250                TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN, NO_SIGNATURE);
251    }
252
253    /**
254     * Creates a "selection modified" event.
255     * Use when the user modifies the selection and the selection's entity type is known.
256     *
257     * @param start  the start (inclusive) index of the selection
258     * @param end  the end (exclusive) index of the selection
259     * @param classification  the TextClassification object returned by the TextClassifier that
260     *      classified the selected text
261     *
262     * @throws IllegalArgumentException if end is less than start
263     */
264    @NonNull
265    public static SelectionEvent createSelectionModifiedEvent(
266            int start, int end, @NonNull TextClassification classification) {
267        Preconditions.checkArgument(end >= start, "end cannot be less than start");
268        Preconditions.checkNotNull(classification);
269        final String entityType = classification.getEntityCount() > 0
270                ? classification.getEntity(0)
271                : TextClassifier.TYPE_UNKNOWN;
272        return new SelectionEvent(
273                start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
274                entityType, INVOCATION_UNKNOWN, classification.getId());
275    }
276
277    /**
278     * Creates a "selection modified" event.
279     * Use when a TextClassifier modifies the selection.
280     *
281     * @param start  the start (inclusive) index of the selection
282     * @param end  the end (exclusive) index of the selection
283     * @param selection  the TextSelection object returned by the TextClassifier for the
284     *      specified selection
285     *
286     * @throws IllegalArgumentException if end is less than start
287     */
288    @NonNull
289    public static SelectionEvent createSelectionModifiedEvent(
290            int start, int end, @NonNull TextSelection selection) {
291        Preconditions.checkArgument(end >= start, "end cannot be less than start");
292        Preconditions.checkNotNull(selection);
293        final String entityType = selection.getEntityCount() > 0
294                ? selection.getEntity(0)
295                : TextClassifier.TYPE_UNKNOWN;
296        return new SelectionEvent(
297                start, end, SelectionEvent.EVENT_AUTO_SELECTION,
298                entityType, INVOCATION_UNKNOWN, selection.getId());
299    }
300
301    /**
302     * Creates an event specifying an action taken on a selection.
303     * Use when the user clicks on an action to act on the selected text.
304     *
305     * @param start  the start (inclusive) index of the selection
306     * @param end  the end (exclusive) index of the selection
307     * @param actionType  the action that was performed on the selection
308     *
309     * @throws IllegalArgumentException if end is less than start
310     */
311    @NonNull
312    public static SelectionEvent createSelectionActionEvent(
313            int start, int end, @SelectionEvent.ActionType int actionType) {
314        Preconditions.checkArgument(end >= start, "end cannot be less than start");
315        checkActionType(actionType);
316        return new SelectionEvent(
317                start, end, actionType, TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN,
318                NO_SIGNATURE);
319    }
320
321    /**
322     * Creates an event specifying an action taken on a selection.
323     * Use when the user clicks on an action to act on the selected text and the selection's
324     * entity type is known.
325     *
326     * @param start  the start (inclusive) index of the selection
327     * @param end  the end (exclusive) index of the selection
328     * @param actionType  the action that was performed on the selection
329     * @param classification  the TextClassification object returned by the TextClassifier that
330     *      classified the selected text
331     *
332     * @throws IllegalArgumentException if end is less than start
333     * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType
334     */
335    @NonNull
336    public static SelectionEvent createSelectionActionEvent(
337            int start, int end, @SelectionEvent.ActionType int actionType,
338            @NonNull TextClassification classification) {
339        Preconditions.checkArgument(end >= start, "end cannot be less than start");
340        Preconditions.checkNotNull(classification);
341        checkActionType(actionType);
342        final String entityType = classification.getEntityCount() > 0
343                ? classification.getEntity(0)
344                : TextClassifier.TYPE_UNKNOWN;
345        return new SelectionEvent(start, end, actionType, entityType, INVOCATION_UNKNOWN,
346                classification.getId());
347    }
348
349    /**
350     * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType}
351     */
352    private static void checkActionType(@SelectionEvent.EventType int eventType)
353            throws IllegalArgumentException {
354        switch (eventType) {
355            case SelectionEvent.ACTION_OVERTYPE:  // fall through
356            case SelectionEvent.ACTION_COPY:  // fall through
357            case SelectionEvent.ACTION_PASTE:  // fall through
358            case SelectionEvent.ACTION_CUT:  // fall through
359            case SelectionEvent.ACTION_SHARE:  // fall through
360            case SelectionEvent.ACTION_SMART_SHARE:  // fall through
361            case SelectionEvent.ACTION_DRAG:  // fall through
362            case SelectionEvent.ACTION_ABANDON:  // fall through
363            case SelectionEvent.ACTION_SELECT_ALL:  // fall through
364            case SelectionEvent.ACTION_RESET:  // fall through
365            case SelectionEvent.ACTION_OTHER:  // fall through
366                return;
367            default:
368                throw new IllegalArgumentException(
369                        String.format(Locale.US, "%d is not an eventType", eventType));
370        }
371    }
372
373    int getAbsoluteStart() {
374        return mAbsoluteStart;
375    }
376
377    int getAbsoluteEnd() {
378        return mAbsoluteEnd;
379    }
380
381    /**
382     * Returns the type of event that was triggered. e.g. {@link #ACTION_COPY}.
383     */
384    @EventType
385    public int getEventType() {
386        return mEventType;
387    }
388
389    /**
390     * Sets the event type.
391     */
392    void setEventType(@EventType int eventType) {
393        mEventType = eventType;
394    }
395
396    /**
397     * Returns the type of entity that is associated with this event. e.g.
398     * {@link android.view.textclassifier.TextClassifier#TYPE_EMAIL}.
399     */
400    @EntityType
401    @NonNull
402    public String getEntityType() {
403        return mEntityType;
404    }
405
406    /**
407     * Returns the package name of the app that this event originated in.
408     */
409    @NonNull
410    public String getPackageName() {
411        return mPackageName;
412    }
413
414    /**
415     * Returns the type of widget that was involved in triggering this event.
416     */
417    @WidgetType
418    @NonNull
419    public String getWidgetType() {
420        return mWidgetType;
421    }
422
423    /**
424     * Returns a string version info for the widget this event was triggered in.
425     */
426    @Nullable
427    public String getWidgetVersion() {
428        return mWidgetVersion;
429    }
430
431    /**
432     * Sets the {@link TextClassificationContext} for this event.
433     */
434    void setTextClassificationSessionContext(TextClassificationContext context) {
435        mPackageName = context.getPackageName();
436        mWidgetType = context.getWidgetType();
437        mWidgetVersion = context.getWidgetVersion();
438    }
439
440    /**
441     * Returns the way the selection mode was invoked.
442     */
443    public @InvocationMethod int getInvocationMethod() {
444        return mInvocationMethod;
445    }
446
447    /**
448     * Sets the invocationMethod for this event.
449     */
450    void setInvocationMethod(@InvocationMethod int invocationMethod) {
451        mInvocationMethod = invocationMethod;
452    }
453
454    /**
455     * Returns the id of the text classifier result associated with this event.
456     */
457    @Nullable
458    public String getResultId() {
459        return mResultId;
460    }
461
462    SelectionEvent setResultId(@Nullable String resultId) {
463        mResultId = resultId;
464        return this;
465    }
466
467    /**
468     * Returns the time this event was triggered.
469     */
470    public long getEventTime() {
471        return mEventTime;
472    }
473
474    SelectionEvent setEventTime(long timeMs) {
475        mEventTime = timeMs;
476        return this;
477    }
478
479    /**
480     * Returns the duration in ms between when this event was triggered and when the first event in
481     * the selection session was triggered.
482     */
483    public long getDurationSinceSessionStart() {
484        return mDurationSinceSessionStart;
485    }
486
487    SelectionEvent setDurationSinceSessionStart(long durationMs) {
488        mDurationSinceSessionStart = durationMs;
489        return this;
490    }
491
492    /**
493     * Returns the duration in ms between when this event was triggered and when the previous event
494     * in the selection session was triggered.
495     */
496    public long getDurationSincePreviousEvent() {
497        return mDurationSincePreviousEvent;
498    }
499
500    SelectionEvent setDurationSincePreviousEvent(long durationMs) {
501        this.mDurationSincePreviousEvent = durationMs;
502        return this;
503    }
504
505    /**
506     * Returns the index (e.g. 1st event, 2nd event, etc.) of this event in the selection session.
507     */
508    public int getEventIndex() {
509        return mEventIndex;
510    }
511
512    SelectionEvent setEventIndex(int index) {
513        mEventIndex = index;
514        return this;
515    }
516
517    /**
518     * Returns the selection session id.
519     */
520    @Nullable
521    public TextClassificationSessionId getSessionId() {
522        return mSessionId;
523    }
524
525    SelectionEvent setSessionId(TextClassificationSessionId id) {
526        mSessionId = id;
527        return this;
528    }
529
530    /**
531     * Returns the start index of this events relative to the index of the start selection
532     * event in the selection session.
533     */
534    public int getStart() {
535        return mStart;
536    }
537
538    SelectionEvent setStart(int start) {
539        mStart = start;
540        return this;
541    }
542
543    /**
544     * Returns the end index of this events relative to the index of the start selection
545     * event in the selection session.
546     */
547    public int getEnd() {
548        return mEnd;
549    }
550
551    SelectionEvent setEnd(int end) {
552        mEnd = end;
553        return this;
554    }
555
556    /**
557     * Returns the start index of this events relative to the index of the smart selection
558     * event in the selection session.
559     */
560    public int getSmartStart() {
561        return mSmartStart;
562    }
563
564    SelectionEvent setSmartStart(int start) {
565        this.mSmartStart = start;
566        return this;
567    }
568
569    /**
570     * Returns the end index of this events relative to the index of the smart selection
571     * event in the selection session.
572     */
573    public int getSmartEnd() {
574        return mSmartEnd;
575    }
576
577    SelectionEvent setSmartEnd(int end) {
578        mSmartEnd = end;
579        return this;
580    }
581
582    boolean isTerminal() {
583        return isTerminal(mEventType);
584    }
585
586    /**
587     * Returns true if the eventType is a terminal event type. Otherwise returns false.
588     * A terminal event is an event that ends a selection interaction.
589     */
590    public static boolean isTerminal(@EventType int eventType) {
591        switch (eventType) {
592            case ACTION_OVERTYPE:  // fall through
593            case ACTION_COPY:  // fall through
594            case ACTION_PASTE:  // fall through
595            case ACTION_CUT:  // fall through
596            case ACTION_SHARE:  // fall through
597            case ACTION_SMART_SHARE:  // fall through
598            case ACTION_DRAG:  // fall through
599            case ACTION_ABANDON:  // fall through
600            case ACTION_OTHER:  // fall through
601                return true;
602            default:
603                return false;
604        }
605    }
606
607    @Override
608    public int hashCode() {
609        return Objects.hash(mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
610                mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, mResultId,
611                mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent,
612                mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
613    }
614
615    @Override
616    public boolean equals(Object obj) {
617        if (this == obj) {
618            return true;
619        }
620        if (!(obj instanceof SelectionEvent)) {
621            return false;
622        }
623
624        final SelectionEvent other = (SelectionEvent) obj;
625        return mAbsoluteStart == other.mAbsoluteStart
626                && mAbsoluteEnd == other.mAbsoluteEnd
627                && mEventType == other.mEventType
628                && Objects.equals(mEntityType, other.mEntityType)
629                && Objects.equals(mWidgetVersion, other.mWidgetVersion)
630                && Objects.equals(mPackageName, other.mPackageName)
631                && Objects.equals(mWidgetType, other.mWidgetType)
632                && mInvocationMethod == other.mInvocationMethod
633                && Objects.equals(mResultId, other.mResultId)
634                && mEventTime == other.mEventTime
635                && mDurationSinceSessionStart == other.mDurationSinceSessionStart
636                && mDurationSincePreviousEvent == other.mDurationSincePreviousEvent
637                && mEventIndex == other.mEventIndex
638                && Objects.equals(mSessionId, other.mSessionId)
639                && mStart == other.mStart
640                && mEnd == other.mEnd
641                && mSmartStart == other.mSmartStart
642                && mSmartEnd == other.mSmartEnd;
643    }
644
645    @Override
646    public String toString() {
647        return String.format(Locale.US,
648                "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, "
649                        + "widgetVersion=%s, packageName=%s, widgetType=%s, invocationMethod=%s, "
650                        + "resultId=%s, eventTime=%d, durationSinceSessionStart=%d, "
651                        + "durationSincePreviousEvent=%d, eventIndex=%d,"
652                        + "sessionId=%s, start=%d, end=%d, smartStart=%d, smartEnd=%d}",
653                mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
654                mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod,
655                mResultId, mEventTime, mDurationSinceSessionStart,
656                mDurationSincePreviousEvent, mEventIndex,
657                mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
658    }
659
660    public static final Creator<SelectionEvent> CREATOR = new Creator<SelectionEvent>() {
661        @Override
662        public SelectionEvent createFromParcel(Parcel in) {
663            return new SelectionEvent(in);
664        }
665
666        @Override
667        public SelectionEvent[] newArray(int size) {
668            return new SelectionEvent[size];
669        }
670    };
671}
672