Home | History | Annotate | Download | only in logging
      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 
    102     public SmartSelectionEventTracker(@NonNull Context context, @WidgetType int widgetType) {
    103         mWidgetType = widgetType;
    104         mWidgetVersion = null;
    105         mContext = Preconditions.checkNotNull(context);
    106     }
    107 
    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      */
    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 
    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 
    184     private String startNewSession() {
    185         endSession();
    186         mSessionId = createSessionId();
    187         return mSessionId;
    188     }
    189 
    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 
    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 
    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 
    281     private int getRangeDelta(int offset) {
    282         return offset - mOrigStart;
    283     }
    284 
    285     private int getSmartRangeDelta(int offset) {
    286         return mSmartSelectionTriggered ? getRangeDelta(offset) : 0;
    287     }
    288 
    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 
    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 
    318     private static String createSessionId() {
    319         return UUID.randomUUID().toString();
    320     }
    321 
    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 
    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          */
    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          */
    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          */
    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          */
    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          */
    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          */
    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 
    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 
    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 
    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