SelectionEvent.java revision 5a03094ebc91df1c64a2232be648ac3ed26657ce
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    private SelectionEvent(Parcel in) {
154        mAbsoluteStart = in.readInt();
155        mAbsoluteEnd = in.readInt();
156        mEventType = in.readInt();
157        mEntityType = in.readString();
158        mWidgetVersion = in.readInt() > 0 ? in.readString() : null;
159        mPackageName = in.readString();
160        mWidgetType = in.readString();
161        mInvocationMethod = in.readInt();
162        mResultId = in.readString();
163        mEventTime = in.readLong();
164        mDurationSinceSessionStart = in.readLong();
165        mDurationSincePreviousEvent = in.readLong();
166        mEventIndex = in.readInt();
167        mSessionId = in.readInt() > 0
168                ? TextClassificationSessionId.CREATOR.createFromParcel(in) : null;
169        mStart = in.readInt();
170        mEnd = in.readInt();
171        mSmartStart = in.readInt();
172        mSmartEnd = in.readInt();
173    }
174
175    @Override
176    public void writeToParcel(Parcel dest, int flags) {
177        dest.writeInt(mAbsoluteStart);
178        dest.writeInt(mAbsoluteEnd);
179        dest.writeInt(mEventType);
180        dest.writeString(mEntityType);
181        dest.writeInt(mWidgetVersion != null ? 1 : 0);
182        if (mWidgetVersion != null) {
183            dest.writeString(mWidgetVersion);
184        }
185        dest.writeString(mPackageName);
186        dest.writeString(mWidgetType);
187        dest.writeInt(mInvocationMethod);
188        dest.writeString(mResultId);
189        dest.writeLong(mEventTime);
190        dest.writeLong(mDurationSinceSessionStart);
191        dest.writeLong(mDurationSincePreviousEvent);
192        dest.writeInt(mEventIndex);
193        dest.writeInt(mSessionId != null ? 1 : 0);
194        if (mSessionId != null) {
195            mSessionId.writeToParcel(dest, flags);
196        }
197        dest.writeInt(mStart);
198        dest.writeInt(mEnd);
199        dest.writeInt(mSmartStart);
200        dest.writeInt(mSmartEnd);
201    }
202
203    @Override
204    public int describeContents() {
205        return 0;
206    }
207
208    /**
209     * Creates a "selection started" event.
210     *
211     * @param invocationMethod  the way the selection was triggered
212     * @param start  the index of the selected text
213     */
214    @NonNull
215    public static SelectionEvent createSelectionStartedEvent(
216            @SelectionEvent.InvocationMethod int invocationMethod, int start) {
217        return new SelectionEvent(
218                start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED,
219                TextClassifier.TYPE_UNKNOWN, invocationMethod, NO_SIGNATURE);
220    }
221
222    /**
223     * Creates a "selection modified" event.
224     * Use when the user modifies the selection.
225     *
226     * @param start  the start (inclusive) index of the selection
227     * @param end  the end (exclusive) index of the selection
228     *
229     * @throws IllegalArgumentException if end is less than start
230     */
231    @NonNull
232    public static SelectionEvent createSelectionModifiedEvent(int start, int end) {
233        Preconditions.checkArgument(end >= start, "end cannot be less than start");
234        return new SelectionEvent(
235                start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
236                TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN, NO_SIGNATURE);
237    }
238
239    /**
240     * Creates a "selection modified" event.
241     * Use when the user modifies the selection and the selection's entity type is known.
242     *
243     * @param start  the start (inclusive) index of the selection
244     * @param end  the end (exclusive) index of the selection
245     * @param classification  the TextClassification object returned by the TextClassifier that
246     *      classified the selected text
247     *
248     * @throws IllegalArgumentException if end is less than start
249     */
250    @NonNull
251    public static SelectionEvent createSelectionModifiedEvent(
252            int start, int end, @NonNull TextClassification classification) {
253        Preconditions.checkArgument(end >= start, "end cannot be less than start");
254        Preconditions.checkNotNull(classification);
255        final String entityType = classification.getEntityCount() > 0
256                ? classification.getEntity(0)
257                : TextClassifier.TYPE_UNKNOWN;
258        return new SelectionEvent(
259                start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
260                entityType, INVOCATION_UNKNOWN, classification.getId());
261    }
262
263    /**
264     * Creates a "selection modified" event.
265     * Use when a TextClassifier modifies the selection.
266     *
267     * @param start  the start (inclusive) index of the selection
268     * @param end  the end (exclusive) index of the selection
269     * @param selection  the TextSelection object returned by the TextClassifier for the
270     *      specified selection
271     *
272     * @throws IllegalArgumentException if end is less than start
273     */
274    @NonNull
275    public static SelectionEvent createSelectionModifiedEvent(
276            int start, int end, @NonNull TextSelection selection) {
277        Preconditions.checkArgument(end >= start, "end cannot be less than start");
278        Preconditions.checkNotNull(selection);
279        final String entityType = selection.getEntityCount() > 0
280                ? selection.getEntity(0)
281                : TextClassifier.TYPE_UNKNOWN;
282        return new SelectionEvent(
283                start, end, SelectionEvent.EVENT_AUTO_SELECTION,
284                entityType, INVOCATION_UNKNOWN, selection.getId());
285    }
286
287    /**
288     * Creates an event specifying an action taken on a selection.
289     * Use when the user clicks on an action to act on the selected text.
290     *
291     * @param start  the start (inclusive) index of the selection
292     * @param end  the end (exclusive) index of the selection
293     * @param actionType  the action that was performed on the selection
294     *
295     * @throws IllegalArgumentException if end is less than start
296     */
297    @NonNull
298    public static SelectionEvent createSelectionActionEvent(
299            int start, int end, @SelectionEvent.ActionType int actionType) {
300        Preconditions.checkArgument(end >= start, "end cannot be less than start");
301        checkActionType(actionType);
302        return new SelectionEvent(
303                start, end, actionType, TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN,
304                NO_SIGNATURE);
305    }
306
307    /**
308     * Creates an event specifying an action taken on a selection.
309     * Use when the user clicks on an action to act on the selected text and the selection's
310     * entity type is known.
311     *
312     * @param start  the start (inclusive) index of the selection
313     * @param end  the end (exclusive) index of the selection
314     * @param actionType  the action that was performed on the selection
315     * @param classification  the TextClassification object returned by the TextClassifier that
316     *      classified the selected text
317     *
318     * @throws IllegalArgumentException if end is less than start
319     * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType
320     */
321    @NonNull
322    public static SelectionEvent createSelectionActionEvent(
323            int start, int end, @SelectionEvent.ActionType int actionType,
324            @NonNull TextClassification classification) {
325        Preconditions.checkArgument(end >= start, "end cannot be less than start");
326        Preconditions.checkNotNull(classification);
327        checkActionType(actionType);
328        final String entityType = classification.getEntityCount() > 0
329                ? classification.getEntity(0)
330                : TextClassifier.TYPE_UNKNOWN;
331        return new SelectionEvent(start, end, actionType, entityType, INVOCATION_UNKNOWN,
332                classification.getId());
333    }
334
335    /**
336     * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType}
337     */
338    private static void checkActionType(@SelectionEvent.EventType int eventType)
339            throws IllegalArgumentException {
340        switch (eventType) {
341            case SelectionEvent.ACTION_OVERTYPE:  // fall through
342            case SelectionEvent.ACTION_COPY:  // fall through
343            case SelectionEvent.ACTION_PASTE:  // fall through
344            case SelectionEvent.ACTION_CUT:  // fall through
345            case SelectionEvent.ACTION_SHARE:  // fall through
346            case SelectionEvent.ACTION_SMART_SHARE:  // fall through
347            case SelectionEvent.ACTION_DRAG:  // fall through
348            case SelectionEvent.ACTION_ABANDON:  // fall through
349            case SelectionEvent.ACTION_SELECT_ALL:  // fall through
350            case SelectionEvent.ACTION_RESET:  // fall through
351                return;
352            default:
353                throw new IllegalArgumentException(
354                        String.format(Locale.US, "%d is not an eventType", eventType));
355        }
356    }
357
358    int getAbsoluteStart() {
359        return mAbsoluteStart;
360    }
361
362    int getAbsoluteEnd() {
363        return mAbsoluteEnd;
364    }
365
366    /**
367     * Returns the type of event that was triggered. e.g. {@link #ACTION_COPY}.
368     */
369    @EventType
370    public int getEventType() {
371        return mEventType;
372    }
373
374    /**
375     * Sets the event type.
376     */
377    void setEventType(@EventType int eventType) {
378        mEventType = eventType;
379    }
380
381    /**
382     * Returns the type of entity that is associated with this event. e.g.
383     * {@link android.view.textclassifier.TextClassifier#TYPE_EMAIL}.
384     */
385    @EntityType
386    @NonNull
387    public String getEntityType() {
388        return mEntityType;
389    }
390
391    /**
392     * Returns the package name of the app that this event originated in.
393     */
394    @NonNull
395    public String getPackageName() {
396        return mPackageName;
397    }
398
399    /**
400     * Returns the type of widget that was involved in triggering this event.
401     */
402    @WidgetType
403    @NonNull
404    public String getWidgetType() {
405        return mWidgetType;
406    }
407
408    /**
409     * Returns a string version info for the widget this event was triggered in.
410     */
411    @Nullable
412    public String getWidgetVersion() {
413        return mWidgetVersion;
414    }
415
416    /**
417     * Sets the {@link TextClassificationContext} for this event.
418     */
419    void setTextClassificationSessionContext(TextClassificationContext context) {
420        mPackageName = context.getPackageName();
421        mWidgetType = context.getWidgetType();
422        mWidgetVersion = context.getWidgetVersion();
423    }
424
425    /**
426     * Returns the way the selection mode was invoked.
427     */
428    public @InvocationMethod int getInvocationMethod() {
429        return mInvocationMethod;
430    }
431
432    /**
433     * Sets the invocationMethod for this event.
434     */
435    void setInvocationMethod(@InvocationMethod int invocationMethod) {
436        mInvocationMethod = invocationMethod;
437    }
438
439    /**
440     * Returns the id of the text classifier result associated with this event.
441     */
442    @Nullable
443    public String getResultId() {
444        return mResultId;
445    }
446
447    SelectionEvent setResultId(@Nullable String resultId) {
448        mResultId = resultId;
449        return this;
450    }
451
452    /**
453     * Returns the time this event was triggered.
454     */
455    public long getEventTime() {
456        return mEventTime;
457    }
458
459    SelectionEvent setEventTime(long timeMs) {
460        mEventTime = timeMs;
461        return this;
462    }
463
464    /**
465     * Returns the duration in ms between when this event was triggered and when the first event in
466     * the selection session was triggered.
467     */
468    public long getDurationSinceSessionStart() {
469        return mDurationSinceSessionStart;
470    }
471
472    SelectionEvent setDurationSinceSessionStart(long durationMs) {
473        mDurationSinceSessionStart = durationMs;
474        return this;
475    }
476
477    /**
478     * Returns the duration in ms between when this event was triggered and when the previous event
479     * in the selection session was triggered.
480     */
481    public long getDurationSincePreviousEvent() {
482        return mDurationSincePreviousEvent;
483    }
484
485    SelectionEvent setDurationSincePreviousEvent(long durationMs) {
486        this.mDurationSincePreviousEvent = durationMs;
487        return this;
488    }
489
490    /**
491     * Returns the index (e.g. 1st event, 2nd event, etc.) of this event in the selection session.
492     */
493    public int getEventIndex() {
494        return mEventIndex;
495    }
496
497    SelectionEvent setEventIndex(int index) {
498        mEventIndex = index;
499        return this;
500    }
501
502    /**
503     * Returns the selection session id.
504     */
505    @Nullable
506    public TextClassificationSessionId getSessionId() {
507        return mSessionId;
508    }
509
510    SelectionEvent setSessionId(TextClassificationSessionId id) {
511        mSessionId = id;
512        return this;
513    }
514
515    /**
516     * Returns the start index of this events relative to the index of the start selection
517     * event in the selection session.
518     */
519    public int getStart() {
520        return mStart;
521    }
522
523    SelectionEvent setStart(int start) {
524        mStart = start;
525        return this;
526    }
527
528    /**
529     * Returns the end index of this events relative to the index of the start selection
530     * event in the selection session.
531     */
532    public int getEnd() {
533        return mEnd;
534    }
535
536    SelectionEvent setEnd(int end) {
537        mEnd = end;
538        return this;
539    }
540
541    /**
542     * Returns the start index of this events relative to the index of the smart selection
543     * event in the selection session.
544     */
545    public int getSmartStart() {
546        return mSmartStart;
547    }
548
549    SelectionEvent setSmartStart(int start) {
550        this.mSmartStart = start;
551        return this;
552    }
553
554    /**
555     * Returns the end index of this events relative to the index of the smart selection
556     * event in the selection session.
557     */
558    public int getSmartEnd() {
559        return mSmartEnd;
560    }
561
562    SelectionEvent setSmartEnd(int end) {
563        mSmartEnd = end;
564        return this;
565    }
566
567    boolean isTerminal() {
568        return isTerminal(mEventType);
569    }
570
571    /**
572     * Returns true if the eventType is a terminal event type. Otherwise returns false.
573     * A terminal event is an event that ends a selection interaction.
574     */
575    public static boolean isTerminal(@EventType int eventType) {
576        switch (eventType) {
577            case ACTION_OVERTYPE:  // fall through
578            case ACTION_COPY:  // fall through
579            case ACTION_PASTE:  // fall through
580            case ACTION_CUT:  // fall through
581            case ACTION_SHARE:  // fall through
582            case ACTION_SMART_SHARE:  // fall through
583            case ACTION_DRAG:  // fall through
584            case ACTION_ABANDON:  // fall through
585            case ACTION_OTHER:  // fall through
586                return true;
587            default:
588                return false;
589        }
590    }
591
592    @Override
593    public int hashCode() {
594        return Objects.hash(mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
595                mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, mResultId,
596                mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent,
597                mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
598    }
599
600    @Override
601    public boolean equals(Object obj) {
602        if (this == obj) {
603            return true;
604        }
605        if (!(obj instanceof SelectionEvent)) {
606            return false;
607        }
608
609        final SelectionEvent other = (SelectionEvent) obj;
610        return mAbsoluteStart == other.mAbsoluteStart
611                && mAbsoluteEnd == other.mAbsoluteEnd
612                && mEventType == other.mEventType
613                && Objects.equals(mEntityType, other.mEntityType)
614                && Objects.equals(mWidgetVersion, other.mWidgetVersion)
615                && Objects.equals(mPackageName, other.mPackageName)
616                && Objects.equals(mWidgetType, other.mWidgetType)
617                && mInvocationMethod == other.mInvocationMethod
618                && Objects.equals(mResultId, other.mResultId)
619                && mEventTime == other.mEventTime
620                && mDurationSinceSessionStart == other.mDurationSinceSessionStart
621                && mDurationSincePreviousEvent == other.mDurationSincePreviousEvent
622                && mEventIndex == other.mEventIndex
623                && Objects.equals(mSessionId, other.mSessionId)
624                && mStart == other.mStart
625                && mEnd == other.mEnd
626                && mSmartStart == other.mSmartStart
627                && mSmartEnd == other.mSmartEnd;
628    }
629
630    @Override
631    public String toString() {
632        return String.format(Locale.US,
633                "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, "
634                        + "widgetVersion=%s, packageName=%s, widgetType=%s, invocationMethod=%s, "
635                        + "resultId=%s, eventTime=%d, durationSinceSessionStart=%d, "
636                        + "durationSincePreviousEvent=%d, eventIndex=%d,"
637                        + "sessionId=%s, start=%d, end=%d, smartStart=%d, smartEnd=%d}",
638                mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType,
639                mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod,
640                mResultId, mEventTime, mDurationSinceSessionStart,
641                mDurationSincePreviousEvent, mEventIndex,
642                mSessionId, mStart, mEnd, mSmartStart, mSmartEnd);
643    }
644
645    public static final Creator<SelectionEvent> CREATOR = new Creator<SelectionEvent>() {
646        @Override
647        public SelectionEvent createFromParcel(Parcel in) {
648            return new SelectionEvent(in);
649        }
650
651        @Override
652        public SelectionEvent[] newArray(int size) {
653            return new SelectionEvent[size];
654        }
655    };
656}