diff options
| author | 2017-08-15 15:05:11 +0100 | |
|---|---|---|
| committer | 2017-08-30 13:38:51 +0100 | |
| commit | 692b196cc12f6852b0bb9009c882a69b67dda4d8 (patch) | |
| tree | e969c49578392b35fccd9cc39ed29d716710a934 | |
| parent | 102e9e78589c97b2a8f493ef4c190d8127bf67cf (diff) | |
Introduce SmartSelectionEventTracker.
This will be used for logging text selection interaction.
Bug: 64914512
Test: No test. Everything builds fine.
Change-Id: Idb28864e0fc969be05d81855b2e7cd8389bd835e
5 files changed, 637 insertions, 10 deletions
diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java index d1e0ae5917f4..fa7b9a59c669 100644 --- a/core/java/android/view/textclassifier/TextClassification.java +++ b/core/java/android/view/textclassifier/TextClassification.java @@ -48,6 +48,7 @@ public final class TextClassification { @NonNull private final EntityConfidence<String> mEntityConfidence; @NonNull private final List<String> mEntities; private int mLogType; + @NonNull private final String mVersionInfo; private TextClassification( @Nullable String text, @@ -56,7 +57,8 @@ public final class TextClassification { @Nullable Intent intent, @Nullable OnClickListener onClickListener, @NonNull EntityConfidence<String> entityConfidence, - int logType) { + int logType, + @NonNull String versionInfo) { mText = text; mIcon = icon; mLabel = label; @@ -65,6 +67,7 @@ public final class TextClassification { mEntityConfidence = new EntityConfidence<>(entityConfidence); mEntities = mEntityConfidence.getEntities(); mLogType = logType; + mVersionInfo = versionInfo; } /** @@ -145,6 +148,15 @@ public final class TextClassification { return mLogType; } + /** + * Returns information about the classifier model used to generate this TextClassification. + * @hide + */ + @NonNull + public String getVersionInfo() { + return mVersionInfo; + } + @Override public String toString() { return String.format("TextClassification {" @@ -179,6 +191,7 @@ public final class TextClassification { @NonNull private final EntityConfidence<String> mEntityConfidence = new EntityConfidence<>(); private int mLogType; + @NonNull private String mVersionInfo = ""; /** * Sets the classified text. @@ -244,11 +257,21 @@ public final class TextClassification { } /** + * Sets information about the classifier model used to generate this TextClassification. + * @hide + */ + Builder setVersionInfo(@NonNull String versionInfo) { + mVersionInfo = Preconditions.checkNotNull(mVersionInfo); + return this; + } + + /** * Builds and returns a {@link TextClassification} object. */ public TextClassification build() { return new TextClassification( - mText, mIcon, mLabel, mIntent, mOnClickListener, mEntityConfidence, mLogType); + mText, mIcon, mLabel, mIntent, mOnClickListener, mEntityConfidence, + mLogType, mVersionInfo); } } } diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index ab1d034bb916..bb1e693fbf43 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -34,6 +34,11 @@ import java.lang.annotation.RetentionPolicy; */ public interface TextClassifier { + /** @hide */ + String DEFAULT_LOG_TAG = "TextClassifierImpl"; + + /** @hide */ + String TYPE_UNKNOWN = ""; // TODO: Make this public API. String TYPE_OTHER = "other"; String TYPE_EMAIL = "email"; String TYPE_PHONE = "phone"; @@ -43,7 +48,7 @@ public interface TextClassifier { /** @hide */ @Retention(RetentionPolicy.SOURCE) @StringDef({ - TYPE_OTHER, TYPE_EMAIL, TYPE_PHONE, TYPE_ADDRESS, TYPE_URL + TYPE_UNKNOWN, TYPE_OTHER, TYPE_EMAIL, TYPE_PHONE, TYPE_ADDRESS, TYPE_URL }) @interface EntityType {} diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index 290d811d01ac..7e93b78c4809 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -70,7 +70,7 @@ import java.util.regex.Pattern; */ final class TextClassifierImpl implements TextClassifier { - private static final String LOG_TAG = "TextClassifierImpl"; + private static final String LOG_TAG = DEFAULT_LOG_TAG; private static final String MODEL_DIR = "/etc/textclassifier/"; private static final String MODEL_FILE_REGEX = "textclassifier\\.smartselection\\.(.*)\\.model"; private static final String UPDATED_MODEL_FILE_PATH = @@ -86,6 +86,8 @@ final class TextClassifierImpl implements TextClassifier { @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. private Locale mLocale; @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. + private int mVersion; + @GuardedBy("mSmartSelectionLock") // Do not access outside this lock. private SmartSelection mSmartSelection; TextClassifierImpl(Context context) { @@ -108,8 +110,7 @@ final class TextClassifierImpl implements TextClassifier { if (start <= end && start >= 0 && end <= string.length() && start <= selectionStartIndex && end >= selectionEndIndex) { - final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end) - .setLogSource(LOG_TAG); + final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end); final SmartSelection.ClassificationResult[] results = smartSelection.classifyText( string, start, end, @@ -118,7 +119,10 @@ final class TextClassifierImpl implements TextClassifier { for (int i = 0; i < size; i++) { tsBuilder.setEntityType(results[i].mCollection, results[i].mScore); } - return tsBuilder.build(); + return tsBuilder + .setLogSource(LOG_TAG) + .setVersionInfo(getVersionInfo()) + .build(); } else { // We can not trust the result. Log the issue and ignore the result. Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result."); @@ -202,6 +206,16 @@ final class TextClassifierImpl implements TextClassifier { } } + @NonNull + private String getVersionInfo() { + synchronized (mSmartSelectionLock) { + if (mLocale != null) { + return String.format("%s_v%d", mLocale.toLanguageTag(), mVersion); + } + return ""; + } + } + @GuardedBy("mSmartSelectionLock") // Do not call outside this lock. private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException { ParcelFileDescriptor updateFd; @@ -256,9 +270,11 @@ final class TextClassifierImpl implements TextClassifier { final int factoryVersion = SmartSelection.getVersion(factoryFd.getFd()); if (updateVersion > factoryVersion) { closeAndLogError(factoryFd); + mVersion = updateVersion; return updateFd; } else { closeAndLogError(updateFd); + mVersion = factoryVersion; return factoryFd; } } @@ -374,7 +390,7 @@ final class TextClassifierImpl implements TextClassifier { builder.setLabel(label != null ? label.toString() : null); } } - return builder.build(); + return builder.setVersionInfo(getVersionInfo()).build(); } private static int getHintFlags(CharSequence text, int start, int end) { diff --git a/core/java/android/view/textclassifier/TextSelection.java b/core/java/android/view/textclassifier/TextSelection.java index 9a66693a93fa..085dd32966b0 100644 --- a/core/java/android/view/textclassifier/TextSelection.java +++ b/core/java/android/view/textclassifier/TextSelection.java @@ -35,15 +35,17 @@ public final class TextSelection { @NonNull private final EntityConfidence<String> mEntityConfidence; @NonNull private final List<String> mEntities; @NonNull private final String mLogSource; + @NonNull private final String mVersionInfo; private TextSelection( int startIndex, int endIndex, @NonNull EntityConfidence<String> entityConfidence, - @NonNull String logSource) { + @NonNull String logSource, @NonNull String versionInfo) { mStartIndex = startIndex; mEndIndex = endIndex; mEntityConfidence = new EntityConfidence<>(entityConfidence); mEntities = mEntityConfidence.getEntities(); mLogSource = logSource; + mVersionInfo = versionInfo; } /** @@ -94,10 +96,20 @@ public final class TextSelection { * Returns a tag for the source classifier used to generate this result. * @hide */ + @NonNull public String getSourceClassifier() { return mLogSource; } + /** + * Returns information about the classifier model used to generate this TextSelection. + * @hide + */ + @NonNull + public String getVersionInfo() { + return mVersionInfo; + } + @Override public String toString() { return String.format("TextSelection {%d, %d, %s}", @@ -114,6 +126,7 @@ public final class TextSelection { @NonNull private final EntityConfidence<String> mEntityConfidence = new EntityConfidence<>(); @NonNull private String mLogSource = ""; + @NonNull private String mVersionInfo = ""; /** * Creates a builder used to build {@link TextSelection} objects. @@ -152,10 +165,20 @@ public final class TextSelection { } /** + * Sets information about the classifier model used to generate this TextSelection. + * @hide + */ + Builder setVersionInfo(@NonNull String versionInfo) { + mVersionInfo = Preconditions.checkNotNull(mVersionInfo); + return this; + } + + /** * Builds and returns {@link TextSelection} object. */ public TextSelection build() { - return new TextSelection(mStartIndex, mEndIndex, mEntityConfidence, mLogSource); + return new TextSelection( + mStartIndex, mEndIndex, mEntityConfidence, mLogSource, mVersionInfo); } } } diff --git a/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java new file mode 100644 index 000000000000..45baf912ebb2 --- /dev/null +++ b/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java @@ -0,0 +1,560 @@ +/* + * Copyright (C) 2017 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.textclassifier.logging; + +import android.annotation.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.metrics.LogMaker; +import android.util.Log; +import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextSelection; + +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Objects; +import java.util.UUID; + +/** + * A selection event tracker. + * @hide + */ +//TODO: Do not allow any crashes from this class. +public final class SmartSelectionEventTracker { + + private static final String LOG_TAG = "SmartSelectionEventTracker"; + private static final boolean DEBUG_LOG_ENABLED = true; + + private static final int START_EVENT_DELTA = MetricsEvent.NOTIFICATION_SINCE_CREATE_MILLIS; + private static final int PREV_EVENT_DELTA = MetricsEvent.NOTIFICATION_SINCE_UPDATE_MILLIS; + private static final int ENTITY_TYPE = MetricsEvent.NOTIFICATION_TAG; + private static final int INDEX = MetricsEvent.NOTIFICATION_SHADE_INDEX; + private static final int TAG = MetricsEvent.FIELD_CLASS_NAME; + private static final int SMART_INDICES = MetricsEvent.FIELD_GESTURE_LENGTH; + private static final int EVENT_INDICES = MetricsEvent.FIELD_CONTEXT; + private static final int SESSION_ID = MetricsEvent.FIELD_INSTANT_APP_LAUNCH_TOKEN; + + private static final String ZERO = "0"; + private static final String TEXTVIEW = "textview"; + private static final String EDITTEXT = "edittext"; + private static final String WEBVIEW = "webview"; + private static final String EDIT_WEBVIEW = "edit-webview"; + private static final String UNKNOWN = "unknown"; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({WidgetType.UNSPECIFIED, WidgetType.TEXTVIEW, WidgetType.WEBVIEW, + WidgetType.EDITTEXT, WidgetType.EDIT_WEBVIEW}) + public @interface WidgetType { + int UNSPECIFIED = 0; + int TEXTVIEW = 1; + int WEBVIEW = 2; + int EDITTEXT = 3; + int EDIT_WEBVIEW = 4; + } + + private final MetricsLogger mMetricsLogger = new MetricsLogger(); + private final int mWidgetType; + private final Context mContext; + + @Nullable private String mSessionId; + private final int[] mSmartIndices = new int[2]; + private final int[] mPrevIndices = new int[2]; + private int mOrigStart; + private int mIndex; + private long mSessionStartTime; + private long mLastEventTime; + private boolean mSmartSelectionTriggered; + + public SmartSelectionEventTracker(@NonNull Context context, @WidgetType int widgetType) { + mWidgetType = widgetType; + mContext = Preconditions.checkNotNull(context); + } + + /** + * Logs a selection event. + * + * @param event the selection event + */ + public void logEvent(@NonNull SelectionEvent event) { + Preconditions.checkNotNull(event); + + if (event.mEventType != SelectionEvent.EventType.SELECTION_STARTED && mSessionId == null) { + Log.d(LOG_TAG, "Selection session not yet started. Ignoring event"); + return; + } + + final long now = System.currentTimeMillis(); + switch (event.mEventType) { + case SelectionEvent.EventType.SELECTION_STARTED: + mSessionId = startNewSession(); + Preconditions.checkArgument(event.mEnd == event.mStart + 1); + mOrigStart = event.mStart; + mSessionStartTime = now; + break; + case SelectionEvent.EventType.SMART_SELECTION_SINGLE: // fall through + case SelectionEvent.EventType.SMART_SELECTION_MULTI: + mSmartSelectionTriggered = true; + mSmartIndices[0] = event.mStart; + mSmartIndices[1] = event.mEnd; + break; + case SelectionEvent.EventType.SELECTION_MODIFIED: // fall through + case SelectionEvent.EventType.AUTO_SELECTION: + if (mPrevIndices[0] == event.mStart && mPrevIndices[1] == event.mEnd) { + // Selection did not change. Ignore event. + return; + } + } + writeEvent(event, now); + + if (event.isTerminal()) { + endSession(); + } + } + + private void writeEvent(SelectionEvent event, long now) { + final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST) + .setType(getLogType(event)) + .setSubtype(event.mEventType) + .setPackageName(mContext.getPackageName()) + .setTimestamp(now) + .addTaggedData(START_EVENT_DELTA, now - mSessionStartTime) + .addTaggedData(PREV_EVENT_DELTA, now - mLastEventTime) + .addTaggedData(ENTITY_TYPE, event.mEntityType) + .addTaggedData(INDEX, mIndex) + .addTaggedData(TAG, getTag(event)) + .addTaggedData(SMART_INDICES, getSmartDelta()) + .addTaggedData(EVENT_INDICES, getEventDelta(event)) + .addTaggedData(SESSION_ID, mSessionId); + mMetricsLogger.write(log); + debugLog(log); + mLastEventTime = now; + mPrevIndices[0] = event.mStart; + mPrevIndices[1] = event.mEnd; + mIndex++; + } + + private String startNewSession() { + endSession(); + mSessionId = createSessionId(); + return mSessionId; + } + + private void endSession() { + // Reset fields. + mOrigStart = 0; + mSmartIndices[0] = mSmartIndices[1] = 0; + mPrevIndices[0] = mPrevIndices[1] = 0; + mIndex = 0; + mSessionStartTime = 0; + mLastEventTime = 0; + mSmartSelectionTriggered = false; + mSessionId = null; + } + + private int getLogType(SelectionEvent event) { + switch (event.mEventType) { + case SelectionEvent.EventType.SELECTION_STARTED: // fall through + case SelectionEvent.EventType.SMART_SELECTION_SINGLE: // fall through + case SelectionEvent.EventType.SMART_SELECTION_MULTI: // fall through + case SelectionEvent.EventType.AUTO_SELECTION: + return MetricsEvent.TYPE_OPEN; + case SelectionEvent.ActionType.ABANDON: + return MetricsEvent.TYPE_CLOSE; + } + if (event.isActionType()) { + if (event.isTerminal() && mSmartSelectionTriggered) { + if (matchesSmartSelectionBounds(event)) { + // Smart selection accepted. + return MetricsEvent.TYPE_SUCCESS; + } else if (containsOriginalSelection(event)) { + // Smart selection rejected. + return MetricsEvent.TYPE_FAILURE; + } + // User changed the original selection entirely. + } + return MetricsEvent.TYPE_ACTION; + } else { + return MetricsEvent.TYPE_UPDATE; + } + } + + private boolean matchesSmartSelectionBounds(SelectionEvent event) { + return event.mStart == mSmartIndices[0] && event.mEnd == mSmartIndices[1]; + } + + private boolean containsOriginalSelection(SelectionEvent event) { + return event.mStart <= mOrigStart && event.mEnd > mOrigStart; + } + + private int getSmartDelta() { + if (mSmartSelectionTriggered) { + return (clamp(mSmartIndices[0] - mOrigStart) << 16) + | (clamp(mSmartIndices[1] - mOrigStart) & 0xffff); + } + // If no smart selection, return start selection indices (i.e. [0, 1]) + return /* (0 << 16) | */ (1 & 0xffff); + } + + private int getEventDelta(SelectionEvent event) { + return (clamp(event.mStart - mOrigStart) << 16) + | (clamp(event.mEnd - mOrigStart) & 0xffff); + } + + private String getTag(SelectionEvent event) { + final String widgetType; + switch (mWidgetType) { + case WidgetType.TEXTVIEW: + widgetType = TEXTVIEW; + break; + case WidgetType.WEBVIEW: + widgetType = WEBVIEW; + break; + case WidgetType.EDITTEXT: + widgetType = EDITTEXT; + break; + case WidgetType.EDIT_WEBVIEW: + widgetType = EDIT_WEBVIEW; + break; + default: + widgetType = UNKNOWN; + } + final String version = Objects.toString(event.mVersionTag, SelectionEvent.NO_VERSION_TAG); + return String.format("%s/%s", widgetType, version); + } + + private static String createSessionId() { + return UUID.randomUUID().toString(); + } + + private static int clamp(int val) { + return Math.max(Math.min(val, Short.MAX_VALUE), Short.MIN_VALUE); + } + + private static void debugLog(LogMaker log) { + if (!DEBUG_LOG_ENABLED) return; + + final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO)); + + final String event; + switch (log.getSubtype()) { + case SelectionEvent.ActionType.OVERTYPE: + event = "OVERTYPE"; + break; + case SelectionEvent.ActionType.COPY: + event = "COPY"; + break; + case SelectionEvent.ActionType.PASTE: + event = "PASTE"; + break; + case SelectionEvent.ActionType.CUT: + event = "CUT"; + break; + case SelectionEvent.ActionType.SHARE: + event = "SHARE"; + break; + case SelectionEvent.ActionType.SMART_SHARE: + event = "SMART_SHARE"; + break; + case SelectionEvent.ActionType.DRAG: + event = "DRAG"; + break; + case SelectionEvent.ActionType.ABANDON: + event = "ABANDON"; + break; + case SelectionEvent.ActionType.OTHER: + event = "OTHER"; + break; + case SelectionEvent.ActionType.SELECT_ALL: + event = "SELECT_ALL"; + break; + case SelectionEvent.ActionType.RESET: + event = "RESET"; + break; + case SelectionEvent.EventType.SELECTION_STARTED: + final String tag = Objects.toString(log.getTaggedData(TAG), "tag"); + String sessionId = Objects.toString(log.getTaggedData(SESSION_ID), ""); + sessionId = sessionId.substring(sessionId.lastIndexOf("-") + 1); + Log.d(LOG_TAG, String.format("New selection session: %s(%s)", tag, sessionId)); + event = "SELECTION_STARTED"; + break; + case SelectionEvent.EventType.SELECTION_MODIFIED: + event = "SELECTION_MODIFIED"; + break; + case SelectionEvent.EventType.SMART_SELECTION_SINGLE: + event = "SMART_SELECTION_SINGLE"; + break; + case SelectionEvent.EventType.SMART_SELECTION_MULTI: + event = "SMART_SELECTION_MULTI"; + break; + case SelectionEvent.EventType.AUTO_SELECTION: + event = "AUTO_SELECTION"; + break; + default: + event = "UNKNOWN"; + } + + final int smartIndices = Integer.parseInt( + Objects.toString(log.getTaggedData(SMART_INDICES), ZERO)); + final int smartStart = (short) ((smartIndices & 0xffff0000) >> 16); + final int smartEnd = (short) (smartIndices & 0xffff); + + final int eventIndices = Integer.parseInt( + Objects.toString(log.getTaggedData(EVENT_INDICES), ZERO)); + final int eventStart = (short) ((eventIndices & 0xffff0000) >> 16); + final int eventEnd = (short) (eventIndices & 0xffff); + + final String entity = Objects.toString( + log.getTaggedData(ENTITY_TYPE), TextClassifier.TYPE_UNKNOWN); + + Log.d(LOG_TAG, String.format("%2d: %s, context=%d,%d - old=%d,%d [%s]", + index, event, eventStart, eventEnd, smartStart, smartEnd, entity)); + } + + /** + * A selection event. + * Specify index parameters as word token indices. + */ + public static final class SelectionEvent { + + /** + * Use this to specify an indeterminate positive index. + */ + public static final int OUT_OF_BOUNDS = Short.MAX_VALUE; + + /** + * Use this to specify an indeterminate negative index. + */ + public static final int OUT_OF_BOUNDS_NEGATIVE = Short.MIN_VALUE; + + private static final String NO_VERSION_TAG = ""; + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT, + ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON, + ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET}) + public @interface ActionType { + /** User typed over the selection. */ + int OVERTYPE = 100; + /** User copied the selection. */ + int COPY = 101; + /** User pasted over the selection. */ + int PASTE = 102; + /** User cut the selection. */ + int CUT = 103; + /** User shared the selection. */ + int SHARE = 104; + /** User clicked the textAssist menu item. */ + int SMART_SHARE = 105; + /** User dragged+dropped the selection. */ + int DRAG = 106; + /** User abandoned the selection. */ + int ABANDON = 107; + /** User performed an action on the selection. */ + int OTHER = 108; + + /* Non-terminal actions. */ + /** User activated Select All */ + int SELECT_ALL = 200; + /** User reset the smart selection. */ + int RESET = 201; + } + + @Retention(RetentionPolicy.SOURCE) + @IntDef({ActionType.OVERTYPE, ActionType.COPY, ActionType.PASTE, ActionType.CUT, + ActionType.SHARE, ActionType.SMART_SHARE, ActionType.DRAG, ActionType.ABANDON, + ActionType.OTHER, ActionType.SELECT_ALL, ActionType.RESET, + EventType.SELECTION_STARTED, EventType.SELECTION_MODIFIED, + EventType.SMART_SELECTION_SINGLE, EventType.SMART_SELECTION_MULTI, + EventType.AUTO_SELECTION}) + private @interface EventType { + /** User started a new selection. */ + int SELECTION_STARTED = 1; + /** User modified an existing selection. */ + int SELECTION_MODIFIED = 2; + /** Smart selection triggered for a single token (word). */ + int SMART_SELECTION_SINGLE = 3; + /** Smart selection triggered spanning multiple tokens (words). */ + int SMART_SELECTION_MULTI = 4; + /** Something else other than User or the default TextClassifier triggered a selection. */ + int AUTO_SELECTION = 5; + } + + private final int mStart; + private final int mEnd; + private @EventType int mEventType; + private final @TextClassifier.EntityType String mEntityType; + private final String mVersionTag; + + private SelectionEvent( + int start, int end, int eventType, + @TextClassifier.EntityType String entityType, String versionTag) { + Preconditions.checkArgument(end >= start, "end cannot be less than start"); + mStart = start; + mEnd = end; + mEventType = eventType; + mEntityType = Preconditions.checkNotNull(entityType); + mVersionTag = Preconditions.checkNotNull(versionTag); + } + + /** + * Creates a "selection started" event. + * + * @param start the word index of the selected word + */ + public static SelectionEvent selectionStarted(int start) { + return new SelectionEvent( + start, start + 1, EventType.SELECTION_STARTED, + TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG); + } + + /** + * Creates a "selection modified" event. + * Use when the user modifies the selection. + * + * @param start the start word (inclusive) index of the selection + * @param end the end word (exclusive) index of the selection + */ + public static SelectionEvent selectionModified(int start, int end) { + return new SelectionEvent( + start, end, EventType.SELECTION_MODIFIED, + TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG); + } + + /** + * Creates a "selection modified" event. + * Use when the user modifies the selection and the selection's entity type is known. + * + * @param start the start word (inclusive) index of the selection + * @param end the end word (exclusive) index of the selection + * @param classification the TextClassification object returned by the TextClassifier that + * classified the selected text + */ + public static SelectionEvent selectionModified( + int start, int end, @NonNull TextClassification classification) { + final String entityType = classification.getEntityCount() > 0 + ? classification.getEntity(0) + : TextClassifier.TYPE_UNKNOWN; + final String versionTag = classification.getVersionInfo(); + return new SelectionEvent( + start, end, EventType.SELECTION_MODIFIED, entityType, versionTag); + } + + /** + * Creates a "selection modified" event. + * Use when a TextClassifier modifies the selection. + * + * @param start the start word (inclusive) index of the selection + * @param end the end word (exclusive) index of the selection + * @param selection the TextSelection object returned by the TextClassifier for the + * specified selection + */ + public static SelectionEvent selectionModified( + int start, int end, @NonNull TextSelection selection) { + final boolean smartSelection = selection.getSourceClassifier() + .equals(TextClassifier.DEFAULT_LOG_TAG); + final int eventType; + if (smartSelection) { + eventType = end - start > 1 + ? EventType.SMART_SELECTION_MULTI + : EventType.SMART_SELECTION_SINGLE; + + } else { + eventType = EventType.AUTO_SELECTION; + } + final String entityType = selection.getEntityCount() > 0 + ? selection.getEntity(0) + : TextClassifier.TYPE_UNKNOWN; + final String versionTag = selection.getVersionInfo(); + return new SelectionEvent(start, end, eventType, entityType, versionTag); + } + + /** + * Creates an event specifying an action taken on a selection. + * Use when the user clicks on an action to act on the selected text. + * + * @param start the start word (inclusive) index of the selection + * @param end the end word (exclusive) index of the selection + * @param actionType the action that was performed on the selection + */ + public static SelectionEvent selectionAction( + int start, int end, @ActionType int actionType) { + return new SelectionEvent( + start, end, actionType, TextClassifier.TYPE_UNKNOWN, NO_VERSION_TAG); + } + + /** + * Creates an event specifying an action taken on a selection. + * Use when the user clicks on an action to act on the selected text and the selection's + * entity type is known. + * + * @param start the start word (inclusive) index of the selection + * @param end the end word (exclusive) index of the selection + * @param actionType the action that was performed on the selection + * @param classification the TextClassification object returned by the TextClassifier that + * classified the selected text + */ + public static SelectionEvent selectionAction( + int start, int end, @ActionType int actionType, + @NonNull TextClassification classification) { + final String entityType = classification.getEntityCount() > 0 + ? classification.getEntity(0) + : TextClassifier.TYPE_UNKNOWN; + final String versionTag = classification.getVersionInfo(); + return new SelectionEvent(start, end, actionType, entityType, versionTag); + } + + private boolean isActionType() { + switch (mEventType) { + case ActionType.OVERTYPE: // fall through + case ActionType.COPY: // fall through + case ActionType.PASTE: // fall through + case ActionType.CUT: // fall through + case ActionType.SHARE: // fall through + case ActionType.SMART_SHARE: // fall through + case ActionType.DRAG: // fall through + case ActionType.ABANDON: // fall through + case ActionType.SELECT_ALL: // fall through + case ActionType.RESET: // fall through + return true; + default: + return false; + } + } + + private boolean isTerminal() { + switch (mEventType) { + case ActionType.OVERTYPE: // fall through + case ActionType.COPY: // fall through + case ActionType.PASTE: // fall through + case ActionType.CUT: // fall through + case ActionType.SHARE: // fall through + case ActionType.SMART_SHARE: // fall through + case ActionType.DRAG: // fall through + case ActionType.ABANDON: // fall through + return true; + default: + return false; + } + } + } +} |