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 
17 package android.view.textclassifier.logging;
18 
19 import android.annotation.IntDef;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.content.Context;
23 import android.metrics.LogMaker;
24 import android.util.Log;
25 import android.view.textclassifier.TextClassification;
26 import android.view.textclassifier.TextClassifier;
27 import android.view.textclassifier.TextSelection;
28 
29 import com.android.internal.logging.MetricsLogger;
30 import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
31 import com.android.internal.util.Preconditions;
32 
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.util.Objects;
36 import java.util.UUID;
37 
38 /**
39  * A selection event tracker.
40  * @hide
41  */
42 //TODO: Do not allow any crashes from this class.
43 public final class SmartSelectionEventTracker {
44 
45     private static final String LOG_TAG = "SmartSelectEventTracker";
46     private static final boolean DEBUG_LOG_ENABLED = true;
47 
48     private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START;
49     private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS;
50     private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX;
51     private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE;
52     private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION;
53     private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL;
54     private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE;
55     private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START;
56     private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END;
57     private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START;
58     private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END;
59     private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID;
60 
61     private static final String ZERO = "0";
62     private static final String TEXTVIEW = "textview";
63     private static final String EDITTEXT = "edittext";
64     private static final String UNSELECTABLE_TEXTVIEW = "nosel-textview";
65     private static final String WEBVIEW = "webview";
66     private static final String EDIT_WEBVIEW = "edit-webview";
67     private static final String CUSTOM_TEXTVIEW = "customview";
68     private static final String CUSTOM_EDITTEXT = "customedit";
69     private static final String CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
70     private static final String UNKNOWN = "unknown";
71 
72     @Retention(RetentionPolicy.SOURCE)
73     @IntDef({WidgetType.UNSPECIFIED, WidgetType.TEXTVIEW, WidgetType.WEBVIEW,
74             WidgetType.EDITTEXT, WidgetType.EDIT_WEBVIEW})
75     public @interface WidgetType {
76         int UNSPECIFIED = 0;
77         int TEXTVIEW = 1;
78         int WEBVIEW = 2;
79         int EDITTEXT = 3;
80         int EDIT_WEBVIEW = 4;
81         int UNSELECTABLE_TEXTVIEW = 5;
82         int CUSTOM_TEXTVIEW = 6;
83         int CUSTOM_EDITTEXT = 7;
84         int CUSTOM_UNSELECTABLE_TEXTVIEW = 8;
85     }
86 
87     private final MetricsLogger mMetricsLogger = new MetricsLogger();
88     private final int mWidgetType;
89     @Nullable private final String mWidgetVersion;
90     private final Context mContext;
91 
92     @Nullable private String mSessionId;
93     private final int[] mSmartIndices = new int[2];
94     private final int[] mPrevIndices = new int[2];
95     private int mOrigStart;
96     private int mIndex;
97     private long mSessionStartTime;
98     private long mLastEventTime;
99     private boolean mSmartSelectionTriggered;
100     private String mModelName;
101 
SmartSelectionEventTracker(@onNull Context context, @WidgetType int widgetType)102     public SmartSelectionEventTracker(@NonNull Context context, @WidgetType int widgetType) {
103         mWidgetType = widgetType;
104         mWidgetVersion = null;
105         mContext = Preconditions.checkNotNull(context);
106     }
107 
SmartSelectionEventTracker( @onNull Context context, @WidgetType int widgetType, @Nullable String widgetVersion)108     public SmartSelectionEventTracker(
109             @NonNull Context context, @WidgetType int widgetType, @Nullable String widgetVersion) {
110         mWidgetType = widgetType;
111         mWidgetVersion = widgetVersion;
112         mContext = Preconditions.checkNotNull(context);
113     }
114 
115     /**
116      * Logs a selection event.
117      *
118      * @param event the selection event
119      */
logEvent(@onNull SelectionEvent event)120     public void logEvent(@NonNull SelectionEvent event) {
121         Preconditions.checkNotNull(event);
122 
123         if (event.mEventType != SelectionEvent.EventType.SELECTION_STARTED && mSessionId == null
124                 && DEBUG_LOG_ENABLED) {
125             Log.d(LOG_TAG, "Selection session not yet started. Ignoring event");
126             return;
127         }
128 
129         final long now = System.currentTimeMillis();
130         switch (event.mEventType) {
131             case SelectionEvent.EventType.SELECTION_STARTED:
132                 mSessionId = startNewSession();
133                 Preconditions.checkArgument(event.mEnd == event.mStart + 1);
134                 mOrigStart = event.mStart;
135                 mSessionStartTime = now;
136                 break;
137             case SelectionEvent.EventType.SMART_SELECTION_SINGLE:  // fall through
138             case SelectionEvent.EventType.SMART_SELECTION_MULTI:
139                 mSmartSelectionTriggered = true;
140                 mModelName = getModelName(event);
141                 mSmartIndices[0] = event.mStart;
142                 mSmartIndices[1] = event.mEnd;
143                 break;
144             case SelectionEvent.EventType.SELECTION_MODIFIED:  // fall through
145             case SelectionEvent.EventType.AUTO_SELECTION:
146                 if (mPrevIndices[0] == event.mStart && mPrevIndices[1] == event.mEnd) {
147                     // Selection did not change. Ignore event.
148                     return;
149                 }
150         }
151         writeEvent(event, now);
152 
153         if (event.isTerminal()) {
154             endSession();
155         }
156     }
157 
writeEvent(SelectionEvent event, long now)158     private void writeEvent(SelectionEvent event, long now) {
159         final long prevEventDelta = mLastEventTime == 0 ? 0 : now - mLastEventTime;
160         final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION)
161                 .setType(getLogType(event))
162                 .setSubtype(MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL)
163                 .setPackageName(mContext.getPackageName())
164                 .addTaggedData(START_EVENT_DELTA, now - mSessionStartTime)
165                 .addTaggedData(PREV_EVENT_DELTA, prevEventDelta)
166                 .addTaggedData(INDEX, mIndex)
167                 .addTaggedData(WIDGET_TYPE, getWidgetTypeName())
168                 .addTaggedData(WIDGET_VERSION, mWidgetVersion)
169                 .addTaggedData(MODEL_NAME, mModelName)
170                 .addTaggedData(ENTITY_TYPE, event.mEntityType)
171                 .addTaggedData(SMART_START, getSmartRangeDelta(mSmartIndices[0]))
172                 .addTaggedData(SMART_END, getSmartRangeDelta(mSmartIndices[1]))
173                 .addTaggedData(EVENT_START, getRangeDelta(event.mStart))
174                 .addTaggedData(EVENT_END, getRangeDelta(event.mEnd))
175                 .addTaggedData(SESSION_ID, mSessionId);
176         mMetricsLogger.write(log);
177         debugLog(log);
178         mLastEventTime = now;
179         mPrevIndices[0] = event.mStart;
180         mPrevIndices[1] = event.mEnd;
181         mIndex++;
182     }
183 
startNewSession()184     private String startNewSession() {
185         endSession();
186         mSessionId = createSessionId();
187         return mSessionId;
188     }
189 
endSession()190     private void endSession() {
191         // Reset fields.
192         mOrigStart = 0;
193         mSmartIndices[0] = mSmartIndices[1] = 0;
194         mPrevIndices[0] = mPrevIndices[1] = 0;
195         mIndex = 0;
196         mSessionStartTime = 0;
197         mLastEventTime = 0;
198         mSmartSelectionTriggered = false;
199         mModelName = getModelName(null);
200         mSessionId = null;
201     }
202 
getLogType(SelectionEvent event)203     private static int getLogType(SelectionEvent event) {
204         switch (event.mEventType) {
205             case SelectionEvent.ActionType.OVERTYPE:
206                 return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE;
207             case SelectionEvent.ActionType.COPY:
208                 return MetricsEvent.ACTION_TEXT_SELECTION_COPY;
209             case SelectionEvent.ActionType.PASTE:
210                 return MetricsEvent.ACTION_TEXT_SELECTION_PASTE;
211             case SelectionEvent.ActionType.CUT:
212                 return MetricsEvent.ACTION_TEXT_SELECTION_CUT;
213             case SelectionEvent.ActionType.SHARE:
214                 return MetricsEvent.ACTION_TEXT_SELECTION_SHARE;
215             case SelectionEvent.ActionType.SMART_SHARE:
216                 return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE;
217             case SelectionEvent.ActionType.DRAG:
218                 return MetricsEvent.ACTION_TEXT_SELECTION_DRAG;
219             case SelectionEvent.ActionType.ABANDON:
220                 return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON;
221             case SelectionEvent.ActionType.OTHER:
222                 return MetricsEvent.ACTION_TEXT_SELECTION_OTHER;
223             case SelectionEvent.ActionType.SELECT_ALL:
224                 return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL;
225             case SelectionEvent.ActionType.RESET:
226                 return MetricsEvent.ACTION_TEXT_SELECTION_RESET;
227             case SelectionEvent.EventType.SELECTION_STARTED:
228                 return MetricsEvent.ACTION_TEXT_SELECTION_START;
229             case SelectionEvent.EventType.SELECTION_MODIFIED:
230                 return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY;
231             case SelectionEvent.EventType.SMART_SELECTION_SINGLE:
232                 return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE;
233             case SelectionEvent.EventType.SMART_SELECTION_MULTI:
234                 return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI;
235             case SelectionEvent.EventType.AUTO_SELECTION:
236                 return MetricsEvent.ACTION_TEXT_SELECTION_AUTO;
237             default:
238                 return MetricsEvent.VIEW_UNKNOWN;
239         }
240     }
241 
getLogTypeString(int logType)242     private static String getLogTypeString(int logType) {
243         switch (logType) {
244             case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE:
245                 return "OVERTYPE";
246             case MetricsEvent.ACTION_TEXT_SELECTION_COPY:
247                 return "COPY";
248             case MetricsEvent.ACTION_TEXT_SELECTION_PASTE:
249                 return "PASTE";
250             case MetricsEvent.ACTION_TEXT_SELECTION_CUT:
251                 return "CUT";
252             case MetricsEvent.ACTION_TEXT_SELECTION_SHARE:
253                 return "SHARE";
254             case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE:
255                 return "SMART_SHARE";
256             case MetricsEvent.ACTION_TEXT_SELECTION_DRAG:
257                 return "DRAG";
258             case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON:
259                 return "ABANDON";
260             case MetricsEvent.ACTION_TEXT_SELECTION_OTHER:
261                 return "OTHER";
262             case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL:
263                 return "SELECT_ALL";
264             case MetricsEvent.ACTION_TEXT_SELECTION_RESET:
265                 return "RESET";
266             case MetricsEvent.ACTION_TEXT_SELECTION_START:
267                 return "SELECTION_STARTED";
268             case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY:
269                 return "SELECTION_MODIFIED";
270             case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE:
271                 return "SMART_SELECTION_SINGLE";
272             case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI:
273                 return "SMART_SELECTION_MULTI";
274             case MetricsEvent.ACTION_TEXT_SELECTION_AUTO:
275                 return "AUTO_SELECTION";
276             default:
277                 return UNKNOWN;
278         }
279     }
280 
getRangeDelta(int offset)281     private int getRangeDelta(int offset) {
282         return offset - mOrigStart;
283     }
284 
getSmartRangeDelta(int offset)285     private int getSmartRangeDelta(int offset) {
286         return mSmartSelectionTriggered ? getRangeDelta(offset) : 0;
287     }
288 
getWidgetTypeName()289     private String getWidgetTypeName() {
290         switch (mWidgetType) {
291             case WidgetType.TEXTVIEW:
292                 return TEXTVIEW;
293             case WidgetType.WEBVIEW:
294                 return WEBVIEW;
295             case WidgetType.EDITTEXT:
296                 return EDITTEXT;
297             case WidgetType.EDIT_WEBVIEW:
298                 return EDIT_WEBVIEW;
299             case WidgetType.UNSELECTABLE_TEXTVIEW:
300                 return UNSELECTABLE_TEXTVIEW;
301             case WidgetType.CUSTOM_TEXTVIEW:
302                 return CUSTOM_TEXTVIEW;
303             case WidgetType.CUSTOM_EDITTEXT:
304                 return CUSTOM_EDITTEXT;
305             case WidgetType.CUSTOM_UNSELECTABLE_TEXTVIEW:
306                 return CUSTOM_UNSELECTABLE_TEXTVIEW;
307             default:
308                 return UNKNOWN;
309         }
310     }
311 
getModelName(@ullable SelectionEvent event)312     private String getModelName(@Nullable SelectionEvent event) {
313         return event == null
314                 ? SelectionEvent.NO_VERSION_TAG
315                 : Objects.toString(event.mVersionTag, SelectionEvent.NO_VERSION_TAG);
316     }
317 
createSessionId()318     private static String createSessionId() {
319         return UUID.randomUUID().toString();
320     }
321 
debugLog(LogMaker log)322     private static void debugLog(LogMaker log) {
323         if (!DEBUG_LOG_ENABLED) return;
324 
325         final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN);
326         final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), "");
327         final String widget = widgetVersion.isEmpty()
328                 ? widgetType : widgetType + "-" + widgetVersion;
329         final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO));
330         if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) {
331             String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), "");
332             sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1);
333             Log.d(LOG_TAG, String.format("New selection session: %s (%s)", widget, sessionId));
334         }
335 
336         final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN);
337         final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN);
338         final String type = getLogTypeString(log.getType());
339         final int smartStart = Integer.parseInt(
340                 Objects.toString(log.getTaggedData(SMART_START), ZERO));
341         final int smartEnd = Integer.parseInt(
342                 Objects.toString(log.getTaggedData(SMART_END), ZERO));
343         final int eventStart = Integer.parseInt(
344                 Objects.toString(log.getTaggedData(EVENT_START), ZERO));
345         final int eventEnd = Integer.parseInt(
346                 Objects.toString(log.getTaggedData(EVENT_END), ZERO));
347 
348         Log.d(LOG_TAG, String.format("%2d: %s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)",
349                 index, type, entity, eventStart, eventEnd, smartStart, smartEnd, widget, model));
350     }
351 
352     /**
353      * A selection event.
354      * Specify index parameters as word token indices.
355      */
356     public static final class SelectionEvent {
357 
358         /**
359          * Use this to specify an indeterminate positive index.
360          */
361         public static final int OUT_OF_BOUNDS = Integer.MAX_VALUE;
362 
363         /**
364          * Use this to specify an indeterminate negative index.
365          */
366         public static final int OUT_OF_BOUNDS_NEGATIVE = Integer.MIN_VALUE;
367 
368         private static final String NO_VERSION_TAG = "";
369 
370         @Retention(RetentionPolicy.SOURCE)
371         @IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT,
372                 ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON,
373                 ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET})
374         public @interface ActionType {
375         /** User typed over the selection. */
376         int OVERTYPE = 100;
377         /** User copied the selection. */
378         int COPY = 101;
379         /** User pasted over the selection. */
380         int PASTE = 102;
381         /** User cut the selection. */
382         int CUT = 103;
383         /** User shared the selection. */
384         int SHARE = 104;
385         /** User clicked the textAssist menu item. */
386         int SMART_SHARE = 105;
387         /** User dragged+dropped the selection. */
388         int DRAG = 106;
389         /** User abandoned the selection. */
390         int ABANDON = 107;
391         /** User performed an action on the selection. */
392         int OTHER = 108;
393 
394         /* Non-terminal actions. */
395         /** User activated Select All */
396         int SELECT_ALL = 200;
397         /** User reset the smart selection. */
398         int RESET = 201;
399         }
400 
401         @Retention(RetentionPolicy.SOURCE)
402         @IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT,
403                 ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON,
404                 ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET,
405                 EventType.SELECTION_STARTED, EventType.SELECTION_MODIFIED,
406                 EventType.SMART_SELECTION_SINGLE, EventType.SMART_SELECTION_MULTI,
407                 EventType.AUTO_SELECTION})
408         private @interface EventType {
409         /** User started a new selection. */
410         int SELECTION_STARTED = 1;
411         /** User modified an existing selection. */
412         int SELECTION_MODIFIED = 2;
413         /** Smart selection triggered for a single token (word). */
414         int SMART_SELECTION_SINGLE = 3;
415         /** Smart selection triggered spanning multiple tokens (words). */
416         int SMART_SELECTION_MULTI = 4;
417         /** Something else other than User or the default TextClassifier triggered a selection. */
418         int AUTO_SELECTION = 5;
419         }
420 
421         private final int mStart;
422         private final int mEnd;
423         private @EventType int mEventType;
424         private final @TextClassifier.EntityType String mEntityType;
425         private final String mVersionTag;
426 
SelectionEvent( int start, int end, int eventType, @TextClassifier.EntityType String entityType, String versionTag)427         private SelectionEvent(
428                 int start, int end, int eventType,
429                 @TextClassifier.EntityType String entityType, String versionTag) {
430             Preconditions.checkArgument(end >= start, "end cannot be less than start");
431             mStart = start;
432             mEnd = end;
433             mEventType = eventType;
434             mEntityType = Preconditions.checkNotNull(entityType);
435             mVersionTag = Preconditions.checkNotNull(versionTag);
436         }
437 
438         /**
439          * Creates a "selection started" event.
440          *
441          * @param start  the word index of the selected word
442          */
selectionStarted(int start)443         public static SelectionEvent selectionStarted(int start) {
444             return new SelectionEvent(
445                     start, start + 1, EventType.SELECTION_STARTED,
446                     TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG);
447         }
448 
449         /**
450          * Creates a "selection modified" event.
451          * Use when the user modifies the selection.
452          *
453          * @param start  the start word (inclusive) index of the selection
454          * @param end  the end word (exclusive) index of the selection
455          */
selectionModified(int start, int end)456         public static SelectionEvent selectionModified(int start, int end) {
457             return new SelectionEvent(
458                     start, end, EventType.SELECTION_MODIFIED,
459                     TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG);
460         }
461 
462         /**
463          * Creates a "selection modified" event.
464          * Use when the user modifies the selection and the selection's entity type is known.
465          *
466          * @param start  the start word (inclusive) index of the selection
467          * @param end  the end word (exclusive) index of the selection
468          * @param classification  the TextClassification object returned by the TextClassifier that
469          *      classified the selected text
470          */
selectionModified( int start, int end, @NonNull TextClassification classification)471         public static SelectionEvent selectionModified(
472                 int start, int end, @NonNull TextClassification classification) {
473             final String entityType = classification.getEntityCount() > 0
474                     ? classification.getEntity(0)
475                     : TextClassifier.TYPE_UNKNOWN;
476             final String versionTag = getVersionInfo(classification.getId());
477             return new SelectionEvent(
478                     start, end, EventType.SELECTION_MODIFIED, entityType, versionTag);
479         }
480 
481         /**
482          * Creates a "selection modified" event.
483          * Use when a TextClassifier modifies the selection.
484          *
485          * @param start  the start word (inclusive) index of the selection
486          * @param end  the end word (exclusive) index of the selection
487          * @param selection  the TextSelection object returned by the TextClassifier for the
488          *      specified selection
489          */
selectionModified( int start, int end, @NonNull TextSelection selection)490         public static SelectionEvent selectionModified(
491                 int start, int end, @NonNull TextSelection selection) {
492             final boolean smartSelection = getSourceClassifier(selection.getId())
493                     .equals(TextClassifier.DEFAULT_LOG_TAG);
494             final int eventType;
495             if (smartSelection) {
496                 eventType = end - start > 1
497                         ? EventType.SMART_SELECTION_MULTI
498                         : EventType.SMART_SELECTION_SINGLE;
499 
500             } else {
501                 eventType = EventType.AUTO_SELECTION;
502             }
503             final String entityType = selection.getEntityCount() > 0
504                     ? selection.getEntity(0)
505                     : TextClassifier.TYPE_UNKNOWN;
506             final String versionTag = getVersionInfo(selection.getId());
507             return new SelectionEvent(start, end, eventType, entityType, versionTag);
508         }
509 
510         /**
511          * Creates an event specifying an action taken on a selection.
512          * Use when the user clicks on an action to act on the selected text.
513          *
514          * @param start  the start word (inclusive) index of the selection
515          * @param end  the end word (exclusive) index of the selection
516          * @param actionType  the action that was performed on the selection
517          */
selectionAction( int start, int end, @ActionType int actionType)518         public static SelectionEvent selectionAction(
519                 int start, int end, @ActionType int actionType) {
520             return new SelectionEvent(
521                     start, end, actionType, TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG);
522         }
523 
524         /**
525          * Creates an event specifying an action taken on a selection.
526          * Use when the user clicks on an action to act on the selected text and the selection's
527          * entity type is known.
528          *
529          * @param start  the start word (inclusive) index of the selection
530          * @param end  the end word (exclusive) index of the selection
531          * @param actionType  the action that was performed on the selection
532          * @param classification  the TextClassification object returned by the TextClassifier that
533          *      classified the selected text
534          */
selectionAction( int start, int end, @ActionType int actionType, @NonNull TextClassification classification)535         public static SelectionEvent selectionAction(
536                 int start, int end, @ActionType int actionType,
537                 @NonNull TextClassification classification) {
538             final String entityType = classification.getEntityCount() > 0
539                     ? classification.getEntity(0)
540                     : TextClassifier.TYPE_UNKNOWN;
541             final String versionTag = getVersionInfo(classification.getId());
542             return new SelectionEvent(start, end, actionType, entityType, versionTag);
543         }
544 
getVersionInfo(String signature)545         private static String getVersionInfo(String signature) {
546             final int start = signature.indexOf("|");
547             final int end = signature.indexOf("|", start);
548             if (start >= 0 && end >= start) {
549                 return signature.substring(start, end);
550             }
551             return "";
552         }
553 
getSourceClassifier(String signature)554         private static String getSourceClassifier(String signature) {
555             final int end = signature.indexOf("|");
556             if (end >= 0) {
557                 return signature.substring(0, end);
558             }
559             return "";
560         }
561 
isTerminal()562         private boolean isTerminal() {
563             switch (mEventType) {
564                 case ActionType.OVERTYPE:  // fall through
565                 case ActionType.COPY:  // fall through
566                 case ActionType.PASTE:  // fall through
567                 case ActionType.CUT:  // fall through
568                 case ActionType.SHARE:  // fall through
569                 case ActionType.SMART_SHARE:  // fall through
570                 case ActionType.DRAG:  // fall through
571                 case ActionType.ABANDON:  // fall through
572                 case ActionType.OTHER:  // fall through
573                     return true;
574                 default:
575                     return false;
576             }
577         }
578     }
579 }
580