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