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