diff options
| author | 2017-12-05 07:33:41 +0000 | |
|---|---|---|
| committer | 2018-01-31 10:09:54 +0000 | |
| commit | 3bb443613820c7e54512cef9659ef2e9428243c6 (patch) | |
| tree | fbe9358dca81716b6bdd4e3daa36ba1b75c930cd | |
| parent | 7c691c606c0e68eea5ddea4a910232df68501332 (diff) | |
Implement TextClassifier.getLogger API
- Introduces getLogger() API.
- A logger should run in the client's process. This helps us manage
sessions specific to a client.
- The logger exposes a tokenizer that clients may use to tokenize
strings for logging purposes.
- Logger subclasses need to provide a writeEvent() implementation.
- SelectionEvent is serializable over IPC.
- Logger takes care of the session management. It writes session
specific information into the SelectionEvent.
- We still keep the SmartSelectionEventTracker for now so clients
can slowly move off of it. The plan is to delete it.
- The plan is to include support other event types. e.g. link events.
Bug: 64914512
Bug: 67609167
Test: See topic
Change-Id: Ic9470cf8f969add8a4c6570f78603d0b118956cd
7 files changed, 1172 insertions, 55 deletions
diff --git a/api/current.txt b/api/current.txt index f5da9b05e6b6..4eb360e2000f 100644 --- a/api/current.txt +++ b/api/current.txt @@ -50096,6 +50096,7 @@ package android.view.textclassifier { method public default android.view.textclassifier.TextLinks generateLinks(java.lang.CharSequence, android.view.textclassifier.TextLinks.Options); method public default android.view.textclassifier.TextLinks generateLinks(java.lang.CharSequence); method public default java.util.Collection<java.lang.String> getEntitiesForPreset(int); + method public default android.view.textclassifier.logging.Logger getLogger(android.view.textclassifier.logging.Logger.Config); method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int, android.view.textclassifier.TextSelection.Options); method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int); method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int, android.os.LocaleList); @@ -50191,6 +50192,75 @@ package android.view.textclassifier { } +package android.view.textclassifier.logging { + + public abstract class Logger { + ctor public Logger(android.view.textclassifier.logging.Logger.Config); + method public java.text.BreakIterator getTokenIterator(java.util.Locale); + method public boolean isSmartSelection(java.lang.String); + method public final void logSelectionActionEvent(int, int, int); + method public final void logSelectionActionEvent(int, int, int, android.view.textclassifier.TextClassification); + method public final void logSelectionModifiedEvent(int, int); + method public final void logSelectionModifiedEvent(int, int, android.view.textclassifier.TextClassification); + method public final void logSelectionModifiedEvent(int, int, android.view.textclassifier.TextSelection); + method public final void logSelectionStartedEvent(int); + method public abstract void writeEvent(android.view.textclassifier.logging.SelectionEvent); + field public static final int OUT_OF_BOUNDS = 2147483647; // 0x7fffffff + field public static final int OUT_OF_BOUNDS_NEGATIVE = -2147483648; // 0x80000000 + field public static final java.lang.String WIDGET_CUSTOM_EDITTEXT = "customedit"; + field public static final java.lang.String WIDGET_CUSTOM_TEXTVIEW = "customview"; + field public static final java.lang.String WIDGET_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview"; + field public static final java.lang.String WIDGET_EDITTEXT = "edittext"; + field public static final java.lang.String WIDGET_EDIT_WEBVIEW = "edit-webview"; + field public static final java.lang.String WIDGET_TEXTVIEW = "textview"; + field public static final java.lang.String WIDGET_UNKNOWN = "unknown"; + field public static final java.lang.String WIDGET_UNSELECTABLE_TEXTVIEW = "nosel-textview"; + field public static final java.lang.String WIDGET_WEBVIEW = "webview"; + } + + public static final class Logger.Config { + ctor public Logger.Config(android.content.Context, java.lang.String, java.lang.String); + method public java.lang.String getPackageName(); + method public java.lang.String getWidgetType(); + method public java.lang.String getWidgetVersion(); + } + + public final class SelectionEvent { + method public long getDurationSincePreviousEvent(); + method public long getDurationSinceSessionStart(); + method public int getEnd(); + method public java.lang.String getEntityType(); + method public int getEventIndex(); + method public long getEventTime(); + method public int getEventType(); + method public java.lang.String getPackageName(); + method public java.lang.String getSessionId(); + method public java.lang.String getSignature(); + method public int getSmartEnd(); + method public int getSmartStart(); + method public int getStart(); + method public java.lang.String getWidgetType(); + method public java.lang.String getWidgetVersion(); + field public static final int ACTION_ABANDON = 107; // 0x6b + field public static final int ACTION_COPY = 101; // 0x65 + field public static final int ACTION_CUT = 103; // 0x67 + field public static final int ACTION_DRAG = 106; // 0x6a + field public static final int ACTION_OTHER = 108; // 0x6c + field public static final int ACTION_OVERTYPE = 100; // 0x64 + field public static final int ACTION_PASTE = 102; // 0x66 + field public static final int ACTION_RESET = 201; // 0xc9 + field public static final int ACTION_SELECT_ALL = 200; // 0xc8 + field public static final int ACTION_SHARE = 104; // 0x68 + field public static final int ACTION_SMART_SHARE = 105; // 0x69 + field public static final int EVENT_AUTO_SELECTION = 5; // 0x5 + field public static final int EVENT_SELECTION_MODIFIED = 2; // 0x2 + field public static final int EVENT_SELECTION_STARTED = 1; // 0x1 + field public static final int EVENT_SMART_SELECTION_MULTI = 4; // 0x4 + field public static final int EVENT_SMART_SELECTION_SINGLE = 3; // 0x3 + } + +} + package android.view.textservice { public final class SentenceSuggestionsInfo implements android.os.Parcelable { diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index 5dd9ac62fbbd..9f75c4a80ca2 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -28,6 +28,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.ArraySet; import android.util.Slog; +import android.view.textclassifier.logging.Logger; import com.android.internal.util.Preconditions; @@ -319,14 +320,15 @@ public interface TextClassifier { } /** - * Logs a TextClassifier event. + * Returns a helper for logging TextClassifier related events. * - * @param source the text classifier used to generate this event - * @param event the text classifier related event - * @hide + * @param config logger configuration */ @WorkerThread - default void logEvent(String source, String event) {} + default Logger getLogger(@NonNull Logger.Config config) { + Preconditions.checkNotNull(config); + return Logger.DISABLED; + } /** * Returns this TextClassifier's settings. diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index 4da5bbf69a78..4d4c171c1cf6 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -35,6 +35,8 @@ import android.provider.ContactsContract; import android.provider.Settings; import android.text.util.Linkify; import android.util.Patterns; +import android.view.textclassifier.logging.DefaultLogger; +import android.view.textclassifier.logging.Logger; import com.android.internal.annotations.GuardedBy; import com.android.internal.logging.MetricsLogger; @@ -43,6 +45,7 @@ import com.android.internal.util.Preconditions; import java.io.File; import java.io.FileNotFoundException; import java.io.IOException; +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; import java.util.Calendar; @@ -104,6 +107,12 @@ public final class TextClassifierImpl implements TextClassifier { @GuardedBy("mLock") // Do not access outside this lock. private SmartSelection mSmartSelection; + private final Object mLoggerLock = new Object(); + @GuardedBy("mLoggerLock") // Do not access outside this lock. + private WeakReference<Logger.Config> mLoggerConfig = new WeakReference<>(null); + @GuardedBy("mLoggerLock") // Do not access outside this lock. + private Logger mLogger; // Should never be null if mLoggerConfig.get() is not null. + private TextClassifierConstants mSettings; public TextClassifierImpl(Context context) { @@ -245,11 +254,15 @@ public final class TextClassifierImpl implements TextClassifier { } } - /** @hide */ @Override - public void logEvent(String source, String event) { - if (LOG_TAG.equals(source)) { - mMetricsLogger.count(event, 1); + public Logger getLogger(@NonNull Logger.Config config) { + Preconditions.checkNotNull(config); + synchronized (mLoggerLock) { + if (mLoggerConfig.get() == null || !mLoggerConfig.get().equals(config)) { + mLoggerConfig = new WeakReference<>(config); + mLogger = new DefaultLogger(config); + } + return mLogger; } } @@ -285,11 +298,7 @@ public final class TextClassifierImpl implements TextClassifier { private String getSignature(String text, int start, int end) { synchronized (mLock) { - final String versionInfo = (mLocale != null) - ? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion) - : ""; - final int hash = Objects.hash(text, start, end, mContext.getPackageName()); - return String.format(Locale.US, "%s|%s|%d", LOG_TAG, versionInfo, hash); + return DefaultLogger.createSignature(text, start, end, mContext, mVersion, mLocale); } } @@ -328,7 +337,7 @@ public final class TextClassifierImpl implements TextClassifier { return factoryFd; } else { throw new FileNotFoundException( - String.format("No model file found for %s", locale)); + String.format(Locale.US, "No model file found for %s", locale)); } } @@ -342,7 +351,7 @@ public final class TextClassifierImpl implements TextClassifier { } else { closeAndLogError(updateFd); throw new FileNotFoundException( - String.format("No model file found for %s", locale)); + String.format(Locale.US, "No model file found for %s", locale)); } } diff --git a/core/java/android/view/textclassifier/logging/DefaultLogger.java b/core/java/android/view/textclassifier/logging/DefaultLogger.java new file mode 100644 index 000000000000..6b848351cbf6 --- /dev/null +++ b/core/java/android/view/textclassifier/logging/DefaultLogger.java @@ -0,0 +1,263 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.metrics.LogMaker; +import android.util.Log; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.logging.MetricsLogger; +import com.android.internal.logging.nano.MetricsProto.MetricsEvent; +import com.android.internal.util.Preconditions; + +import java.util.Locale; +import java.util.Objects; + +/** + * Default Logger. + * Used internally by TextClassifierImpl. + * @hide + */ +public final class DefaultLogger extends Logger { + + private static final String LOG_TAG = "DefaultLogger"; + private static final String CLASSIFIER_ID = "androidtc"; + + private static final int START_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_START; + private static final int PREV_EVENT_DELTA = MetricsEvent.FIELD_SELECTION_SINCE_PREVIOUS; + private static final int INDEX = MetricsEvent.FIELD_SELECTION_SESSION_INDEX; + private static final int WIDGET_TYPE = MetricsEvent.FIELD_SELECTION_WIDGET_TYPE; + private static final int WIDGET_VERSION = MetricsEvent.FIELD_SELECTION_WIDGET_VERSION; + private static final int MODEL_NAME = MetricsEvent.FIELD_TEXTCLASSIFIER_MODEL; + private static final int ENTITY_TYPE = MetricsEvent.FIELD_SELECTION_ENTITY_TYPE; + private static final int SMART_START = MetricsEvent.FIELD_SELECTION_SMART_RANGE_START; + private static final int SMART_END = MetricsEvent.FIELD_SELECTION_SMART_RANGE_END; + private static final int EVENT_START = MetricsEvent.FIELD_SELECTION_RANGE_START; + private static final int EVENT_END = MetricsEvent.FIELD_SELECTION_RANGE_END; + private static final int SESSION_ID = MetricsEvent.FIELD_SELECTION_SESSION_ID; + + private static final String ZERO = "0"; + private static final String UNKNOWN = "unknown"; + + private final MetricsLogger mMetricsLogger; + + public DefaultLogger(@NonNull Config config) { + super(config); + mMetricsLogger = new MetricsLogger(); + } + + @VisibleForTesting + public DefaultLogger(@NonNull Config config, @NonNull MetricsLogger metricsLogger) { + super(config); + mMetricsLogger = Preconditions.checkNotNull(metricsLogger); + } + + @Override + public boolean isSmartSelection(@NonNull String signature) { + return CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature)); + } + + @Override + public void writeEvent(@NonNull SelectionEvent event) { + Preconditions.checkNotNull(event); + final LogMaker log = new LogMaker(MetricsEvent.TEXT_SELECTION_SESSION) + .setType(getLogType(event)) + .setSubtype(MetricsEvent.TEXT_SELECTION_INVOCATION_MANUAL) + .setPackageName(event.getPackageName()) + .addTaggedData(START_EVENT_DELTA, event.getDurationSinceSessionStart()) + .addTaggedData(PREV_EVENT_DELTA, event.getDurationSincePreviousEvent()) + .addTaggedData(INDEX, event.getEventIndex()) + .addTaggedData(WIDGET_TYPE, event.getWidgetType()) + .addTaggedData(WIDGET_VERSION, event.getWidgetVersion()) + .addTaggedData(MODEL_NAME, SignatureParser.getModelName(event.getSignature())) + .addTaggedData(ENTITY_TYPE, event.getEntityType()) + .addTaggedData(SMART_START, event.getSmartStart()) + .addTaggedData(SMART_END, event.getSmartEnd()) + .addTaggedData(EVENT_START, event.getStart()) + .addTaggedData(EVENT_END, event.getEnd()) + .addTaggedData(SESSION_ID, event.getSessionId()); + mMetricsLogger.write(log); + debugLog(log); + } + + private static int getLogType(SelectionEvent event) { + switch (event.getEventType()) { + case SelectionEvent.ACTION_OVERTYPE: + return MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE; + case SelectionEvent.ACTION_COPY: + return MetricsEvent.ACTION_TEXT_SELECTION_COPY; + case SelectionEvent.ACTION_PASTE: + return MetricsEvent.ACTION_TEXT_SELECTION_PASTE; + case SelectionEvent.ACTION_CUT: + return MetricsEvent.ACTION_TEXT_SELECTION_CUT; + case SelectionEvent.ACTION_SHARE: + return MetricsEvent.ACTION_TEXT_SELECTION_SHARE; + case SelectionEvent.ACTION_SMART_SHARE: + return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE; + case SelectionEvent.ACTION_DRAG: + return MetricsEvent.ACTION_TEXT_SELECTION_DRAG; + case SelectionEvent.ACTION_ABANDON: + return MetricsEvent.ACTION_TEXT_SELECTION_ABANDON; + case SelectionEvent.ACTION_OTHER: + return MetricsEvent.ACTION_TEXT_SELECTION_OTHER; + case SelectionEvent.ACTION_SELECT_ALL: + return MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL; + case SelectionEvent.ACTION_RESET: + return MetricsEvent.ACTION_TEXT_SELECTION_RESET; + case SelectionEvent.EVENT_SELECTION_STARTED: + return MetricsEvent.ACTION_TEXT_SELECTION_START; + case SelectionEvent.EVENT_SELECTION_MODIFIED: + return MetricsEvent.ACTION_TEXT_SELECTION_MODIFY; + case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: + return MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE; + case SelectionEvent.EVENT_SMART_SELECTION_MULTI: + return MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI; + case SelectionEvent.EVENT_AUTO_SELECTION: + return MetricsEvent.ACTION_TEXT_SELECTION_AUTO; + default: + return MetricsEvent.VIEW_UNKNOWN; + } + } + + private static String getLogTypeString(int logType) { + switch (logType) { + case MetricsEvent.ACTION_TEXT_SELECTION_OVERTYPE: + return "OVERTYPE"; + case MetricsEvent.ACTION_TEXT_SELECTION_COPY: + return "COPY"; + case MetricsEvent.ACTION_TEXT_SELECTION_PASTE: + return "PASTE"; + case MetricsEvent.ACTION_TEXT_SELECTION_CUT: + return "CUT"; + case MetricsEvent.ACTION_TEXT_SELECTION_SHARE: + return "SHARE"; + case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SHARE: + return "SMART_SHARE"; + case MetricsEvent.ACTION_TEXT_SELECTION_DRAG: + return "DRAG"; + case MetricsEvent.ACTION_TEXT_SELECTION_ABANDON: + return "ABANDON"; + case MetricsEvent.ACTION_TEXT_SELECTION_OTHER: + return "OTHER"; + case MetricsEvent.ACTION_TEXT_SELECTION_SELECT_ALL: + return "SELECT_ALL"; + case MetricsEvent.ACTION_TEXT_SELECTION_RESET: + return "RESET"; + case MetricsEvent.ACTION_TEXT_SELECTION_START: + return "SELECTION_STARTED"; + case MetricsEvent.ACTION_TEXT_SELECTION_MODIFY: + return "SELECTION_MODIFIED"; + case MetricsEvent.ACTION_TEXT_SELECTION_SMART_SINGLE: + return "SMART_SELECTION_SINGLE"; + case MetricsEvent.ACTION_TEXT_SELECTION_SMART_MULTI: + return "SMART_SELECTION_MULTI"; + case MetricsEvent.ACTION_TEXT_SELECTION_AUTO: + return "AUTO_SELECTION"; + default: + return UNKNOWN; + } + } + + private static void debugLog(LogMaker log) { + if (!DEBUG_LOG_ENABLED) return; + + final String widgetType = Objects.toString(log.getTaggedData(WIDGET_TYPE), UNKNOWN); + final String widgetVersion = Objects.toString(log.getTaggedData(WIDGET_VERSION), ""); + final String widget = widgetVersion.isEmpty() + ? widgetType : widgetType + "-" + widgetVersion; + final int index = Integer.parseInt(Objects.toString(log.getTaggedData(INDEX), ZERO)); + if (log.getType() == MetricsEvent.ACTION_TEXT_SELECTION_START) { + 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)", widget, sessionId)); + } + + final String model = Objects.toString(log.getTaggedData(MODEL_NAME), UNKNOWN); + final String entity = Objects.toString(log.getTaggedData(ENTITY_TYPE), UNKNOWN); + final String type = getLogTypeString(log.getType()); + final int smartStart = Integer.parseInt( + Objects.toString(log.getTaggedData(SMART_START), ZERO)); + final int smartEnd = Integer.parseInt( + Objects.toString(log.getTaggedData(SMART_END), ZERO)); + final int eventStart = Integer.parseInt( + Objects.toString(log.getTaggedData(EVENT_START), ZERO)); + final int eventEnd = Integer.parseInt( + Objects.toString(log.getTaggedData(EVENT_END), ZERO)); + + Log.d(LOG_TAG, String.format("%2d: %s/%s, range=%d,%d - smart_range=%d,%d (%s/%s)", + index, type, entity, eventStart, eventEnd, smartStart, smartEnd, widget, model)); + } + + /** + * Creates a signature string that may be used to tag TextClassifier results. + */ + public static String createSignature( + String text, int start, int end, Context context, int modelVersion, + @Nullable Locale locale) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(context); + final String modelName = (locale != null) + ? String.format(Locale.US, "%s_v%d", locale.toLanguageTag(), modelVersion) + : ""; + final int hash = Objects.hash(text, start, end, context.getPackageName()); + return SignatureParser.createSignature(CLASSIFIER_ID, modelName, hash); + } + + /** + * Helper for creating and parsing signature strings for + * {@link android.view.textclassifier.TextClassifierImpl}. + */ + @VisibleForTesting + public static final class SignatureParser { + + static String createSignature(String classifierId, String modelName, int hash) { + return String.format(Locale.US, "%s|%s|%d", classifierId, modelName, hash); + } + + static String getClassifierId(String signature) { + Preconditions.checkNotNull(signature); + final int end = signature.indexOf("|"); + if (end >= 0) { + return signature.substring(0, end); + } + return ""; + } + + static String getModelName(String signature) { + Preconditions.checkNotNull(signature); + final int start = signature.indexOf("|"); + final int end = signature.indexOf("|", start); + if (start >= 0 && end >= start) { + return signature.substring(start, end); + } + return ""; + } + + static int getHash(String signature) { + Preconditions.checkNotNull(signature); + final int index1 = signature.indexOf("|"); + final int index2 = signature.indexOf("|", index1); + if (index2 > 0) { + return Integer.parseInt(signature.substring(index2)); + } + return 0; + } + } +} diff --git a/core/java/android/view/textclassifier/logging/Logger.java b/core/java/android/view/textclassifier/logging/Logger.java new file mode 100644 index 000000000000..40e4d8ce1a77 --- /dev/null +++ b/core/java/android/view/textclassifier/logging/Logger.java @@ -0,0 +1,429 @@ +/* + * 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.NonNull; +import android.annotation.Nullable; +import android.annotation.StringDef; +import android.content.Context; +import android.util.Log; +import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextSelection; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.text.BreakIterator; +import java.util.Locale; +import java.util.Objects; +import java.util.UUID; + +/** + * A helper for logging TextClassifier related events. + */ +public abstract class Logger { + + /** + * Use this to specify an indeterminate positive index. + */ + public static final int OUT_OF_BOUNDS = Integer.MAX_VALUE; + + /** + * Use this to specify an indeterminate negative index. + */ + public static final int OUT_OF_BOUNDS_NEGATIVE = Integer.MIN_VALUE; + + private static final String LOG_TAG = "Logger"; + /* package */ static final boolean DEBUG_LOG_ENABLED = true; + + private static final String NO_SIGNATURE = ""; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @StringDef({WIDGET_TEXTVIEW, WIDGET_WEBVIEW, WIDGET_EDITTEXT, + WIDGET_EDIT_WEBVIEW, WIDGET_CUSTOM_TEXTVIEW, WIDGET_CUSTOM_EDITTEXT, + WIDGET_CUSTOM_UNSELECTABLE_TEXTVIEW, WIDGET_UNKNOWN}) + public @interface WidgetType {} + + public static final String WIDGET_TEXTVIEW = "textview"; + public static final String WIDGET_EDITTEXT = "edittext"; + public static final String WIDGET_UNSELECTABLE_TEXTVIEW = "nosel-textview"; + public static final String WIDGET_WEBVIEW = "webview"; + public static final String WIDGET_EDIT_WEBVIEW = "edit-webview"; + public static final String WIDGET_CUSTOM_TEXTVIEW = "customview"; + public static final String WIDGET_CUSTOM_EDITTEXT = "customedit"; + public static final String WIDGET_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview"; + public static final String WIDGET_UNKNOWN = "unknown"; + + private SelectionEvent mPrevEvent; + private SelectionEvent mSmartEvent; + private SelectionEvent mStartEvent; + + /** + * Logger that does not log anything. + * @hide + */ + public static final Logger DISABLED = new Logger() { + @Override + public void writeEvent(SelectionEvent event) {} + }; + + @Nullable + private final Config mConfig; + + public Logger(Config config) { + mConfig = Preconditions.checkNotNull(config); + } + + private Logger() { + mConfig = null; + } + + /** + * Writes the selection event. + * + * <p><strong>NOTE: </strong>This method is designed for subclasses. + * Apps should not call it directly. + */ + public abstract void writeEvent(@NonNull SelectionEvent event); + + /** + * Returns true if the signature matches that of a smart selection event (i.e. + * {@link SelectionEvent#EVENT_SMART_SELECTION_SINGLE} or + * {@link SelectionEvent#EVENT_SMART_SELECTION_MULTI}). + * Returns false otherwise. + */ + public boolean isSmartSelection(@NonNull String signature) { + return false; + } + + + /** + * Returns a token iterator for tokenizing text for logging purposes. + */ + public BreakIterator getTokenIterator(@NonNull Locale locale) { + return BreakIterator.getWordInstance(Preconditions.checkNotNull(locale)); + } + + /** + * Logs a "selection started" event. + * + * @param start the token index of the selected token + */ + public final void logSelectionStartedEvent(int start) { + if (mConfig == null) { + return; + } + + logEvent(new SelectionEvent( + start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED, + TextClassifier.TYPE_UNKNOWN, NO_SIGNATURE, mConfig)); + } + + /** + * Logs a "selection modified" event. + * Use when the user modifies the selection. + * + * @param start the start token (inclusive) index of the selection + * @param end the end token (exclusive) index of the selection + */ + public final void logSelectionModifiedEvent(int start, int end) { + Preconditions.checkArgument(end >= start, "end cannot be less than start"); + + if (mConfig == null) { + return; + } + + logEvent(new SelectionEvent( + start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, + TextClassifier.TYPE_UNKNOWN, NO_SIGNATURE, mConfig)); + } + + /** + * Logs a "selection modified" event. + * Use when the user modifies the selection and the selection's entity type is known. + * + * @param start the start token (inclusive) index of the selection + * @param end the end token (exclusive) index of the selection + * @param classification the TextClassification object returned by the TextClassifier that + * classified the selected text + */ + public final void logSelectionModifiedEvent( + int start, int end, @NonNull TextClassification classification) { + Preconditions.checkArgument(end >= start, "end cannot be less than start"); + Preconditions.checkNotNull(classification); + + if (mConfig == null) { + return; + } + + final String entityType = classification.getEntityCount() > 0 + ? classification.getEntity(0) + : TextClassifier.TYPE_UNKNOWN; + final String signature = classification.getSignature(); + logEvent(new SelectionEvent( + start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, + entityType, signature, mConfig)); + } + + /** + * Logs a "selection modified" event. + * Use when a TextClassifier modifies the selection. + * + * @param start the start token (inclusive) index of the selection + * @param end the end token (exclusive) index of the selection + * @param selection the TextSelection object returned by the TextClassifier for the + * specified selection + */ + public final void logSelectionModifiedEvent( + int start, int end, @NonNull TextSelection selection) { + Preconditions.checkArgument(end >= start, "end cannot be less than start"); + Preconditions.checkNotNull(selection); + + if (mConfig == null) { + return; + } + + final int eventType; + if (isSmartSelection(selection.getSignature())) { + eventType = end - start > 1 + ? SelectionEvent.EVENT_SMART_SELECTION_MULTI + : SelectionEvent.EVENT_SMART_SELECTION_SINGLE; + + } else { + eventType = SelectionEvent.EVENT_AUTO_SELECTION; + } + final String entityType = selection.getEntityCount() > 0 + ? selection.getEntity(0) + : TextClassifier.TYPE_UNKNOWN; + final String signature = selection.getSignature(); + logEvent(new SelectionEvent(start, end, eventType, entityType, signature, mConfig)); + } + + /** + * Logs 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 token (inclusive) index of the selection + * @param end the end token (exclusive) index of the selection + * @param actionType the action that was performed on the selection + */ + public final void logSelectionActionEvent( + int start, int end, @SelectionEvent.ActionType int actionType) { + Preconditions.checkArgument(end >= start, "end cannot be less than start"); + checkActionType(actionType); + + if (mConfig == null) { + return; + } + + logEvent(new SelectionEvent( + start, end, actionType, TextClassifier.TYPE_UNKNOWN, NO_SIGNATURE, mConfig)); + } + + /** + * Logs 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 token (inclusive) index of the selection + * @param end the end token (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 + * + * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType + */ + public final void logSelectionActionEvent( + int start, int end, @SelectionEvent.ActionType int actionType, + @NonNull TextClassification classification) { + Preconditions.checkArgument(end >= start, "end cannot be less than start"); + Preconditions.checkNotNull(classification); + checkActionType(actionType); + + if (mConfig == null) { + return; + } + + final String entityType = classification.getEntityCount() > 0 + ? classification.getEntity(0) + : TextClassifier.TYPE_UNKNOWN; + final String signature = classification.getSignature(); + logEvent(new SelectionEvent(start, end, actionType, entityType, signature, mConfig)); + } + + private void logEvent(@NonNull SelectionEvent event) { + Preconditions.checkNotNull(event); + + if (event.getEventType() != SelectionEvent.EVENT_SELECTION_STARTED + && mStartEvent == null) { + if (DEBUG_LOG_ENABLED) { + Log.d(LOG_TAG, "Selection session not yet started. Ignoring event"); + } + return; + } + + final long now = System.currentTimeMillis(); + switch (event.getEventType()) { + case SelectionEvent.EVENT_SELECTION_STARTED: + Preconditions.checkArgument(event.getAbsoluteEnd() == event.getAbsoluteStart() + 1); + event.setSessionId(startNewSession()); + mStartEvent = event; + break; + case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through + case SelectionEvent.EVENT_SMART_SELECTION_MULTI: + mSmartEvent = event; + break; + case SelectionEvent.EVENT_SELECTION_MODIFIED: // fall through + case SelectionEvent.EVENT_AUTO_SELECTION: + if (mPrevEvent != null + && mPrevEvent.getAbsoluteStart() == event.getAbsoluteStart() + && mPrevEvent.getAbsoluteEnd() == event.getAbsoluteEnd()) { + // Selection did not change. Ignore event. + return; + } + } + + event.setEventTime(now); + if (mStartEvent != null) { + event.setSessionId(mStartEvent.getSessionId()) + .setDurationSinceSessionStart(now - mStartEvent.getEventTime()) + .setStart(event.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) + .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); + } + if (mSmartEvent != null) { + event.setSignature(mSmartEvent.getSignature()) + .setSmartStart(mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) + .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); + } + if (mPrevEvent != null) { + event.setDurationSincePreviousEvent(now - mPrevEvent.getEventTime()) + .setEventIndex(mPrevEvent.getEventIndex() + 1); + } + writeEvent(event); + mPrevEvent = event; + + if (event.isTerminal()) { + endSession(); + } + } + + private String startNewSession() { + endSession(); + return UUID.randomUUID().toString(); + } + + private void endSession() { + mPrevEvent = null; + mSmartEvent = null; + mStartEvent = null; + } + + /** + * @throws IllegalArgumentException If eventType is not an {@link SelectionEvent.ActionType} + */ + private static void checkActionType(@SelectionEvent.EventType int eventType) + throws IllegalArgumentException { + switch (eventType) { + case SelectionEvent.ACTION_OVERTYPE: // fall through + case SelectionEvent.ACTION_COPY: // fall through + case SelectionEvent.ACTION_PASTE: // fall through + case SelectionEvent.ACTION_CUT: // fall through + case SelectionEvent.ACTION_SHARE: // fall through + case SelectionEvent.ACTION_SMART_SHARE: // fall through + case SelectionEvent.ACTION_DRAG: // fall through + case SelectionEvent.ACTION_ABANDON: // fall through + case SelectionEvent.ACTION_SELECT_ALL: // fall through + case SelectionEvent.ACTION_RESET: // fall through + return; + default: + throw new IllegalArgumentException( + String.format(Locale.US, "%d is not an eventType", eventType)); + } + } + + + /** + * A Logger config. + */ + public static final class Config { + + private final String mPackageName; + private final String mWidgetType; + @Nullable private final String mWidgetVersion; + + /** + * @param context Context of the widget the logger logs for + * @param widgetType a name for the widget being logged for. e.g. + * {@link #WIDGET_TEXTVIEW} + * @param widgetVersion a string version info for the widget the logger logs for + */ + public Config( + @NonNull Context context, + @WidgetType String widgetType, + @Nullable String widgetVersion) { + mPackageName = Preconditions.checkNotNull(context).getPackageName(); + mWidgetType = widgetType; + mWidgetVersion = widgetVersion; + } + + /** + * Returns the package name of the application the logger logs for. + */ + public String getPackageName() { + return mPackageName; + } + + /** + * Returns the name for the widget being logged for. e.g. {@link #WIDGET_TEXTVIEW}. + */ + public String getWidgetType() { + return mWidgetType; + } + + /** + * Returns string version info for the logger. This is specific to the text classifier. + */ + @Nullable + public String getWidgetVersion() { + return mWidgetVersion; + } + + @Override + public int hashCode() { + return Objects.hash(mPackageName, mWidgetType, mWidgetVersion); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + + if (!(obj instanceof Config)) { + return false; + } + + final Config other = (Config) obj; + return Objects.equals(mPackageName, other.mPackageName) + && Objects.equals(mWidgetType, other.mWidgetType) + && Objects.equals(mWidgetVersion, other.mWidgetType); + } + } +} diff --git a/core/java/android/view/textclassifier/logging/SelectionEvent.java b/core/java/android/view/textclassifier/logging/SelectionEvent.java new file mode 100644 index 000000000000..f40b65571142 --- /dev/null +++ b/core/java/android/view/textclassifier/logging/SelectionEvent.java @@ -0,0 +1,337 @@ +/* + * 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.Nullable; +import android.view.textclassifier.TextClassifier.EntityType; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.util.Locale; + +/** + * A selection event. + * Specify index parameters as word token indices. + */ +public final class SelectionEvent { + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT, + ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON, + ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET}) + // NOTE: ActionType values should not be lower than 100 to avoid colliding with the other + // EventTypes declared below. + public @interface ActionType { + /* + * Terminal event types range: [100,200). + * Non-terminal event types range: [200,300). + */ + } + + /** User typed over the selection. */ + public static final int ACTION_OVERTYPE = 100; + /** User copied the selection. */ + public static final int ACTION_COPY = 101; + /** User pasted over the selection. */ + public static final int ACTION_PASTE = 102; + /** User cut the selection. */ + public static final int ACTION_CUT = 103; + /** User shared the selection. */ + public static final int ACTION_SHARE = 104; + /** User clicked the textAssist menu item. */ + public static final int ACTION_SMART_SHARE = 105; + /** User dragged+dropped the selection. */ + public static final int ACTION_DRAG = 106; + /** User abandoned the selection. */ + public static final int ACTION_ABANDON = 107; + /** User performed an action on the selection. */ + public static final int ACTION_OTHER = 108; + + // Non-terminal actions. + /** User activated Select All */ + public static final int ACTION_SELECT_ALL = 200; + /** User reset the smart selection. */ + public static final int ACTION_RESET = 201; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({ACTION_OVERTYPE, ACTION_COPY, ACTION_PASTE, ACTION_CUT, + ACTION_SHARE, ACTION_SMART_SHARE, ACTION_DRAG, ACTION_ABANDON, + ACTION_OTHER, ACTION_SELECT_ALL, ACTION_RESET, + EVENT_SELECTION_STARTED, EVENT_SELECTION_MODIFIED, + EVENT_SMART_SELECTION_SINGLE, EVENT_SMART_SELECTION_MULTI, + EVENT_AUTO_SELECTION}) + // NOTE: EventTypes declared here must be less than 100 to avoid colliding with the + // ActionTypes declared above. + public @interface EventType { + /* + * Range: 1 -> 99. + */ + } + + /** User started a new selection. */ + public static final int EVENT_SELECTION_STARTED = 1; + /** User modified an existing selection. */ + public static final int EVENT_SELECTION_MODIFIED = 2; + /** Smart selection triggered for a single token (word). */ + public static final int EVENT_SMART_SELECTION_SINGLE = 3; + /** Smart selection triggered spanning multiple tokens (words). */ + public static final int EVENT_SMART_SELECTION_MULTI = 4; + /** Something else other than User or the default TextClassifier triggered a selection. */ + public static final int EVENT_AUTO_SELECTION = 5; + + private final int mAbsoluteStart; + private final int mAbsoluteEnd; + private final @EventType int mEventType; + private final @EntityType String mEntityType; + @Nullable private final String mWidgetVersion; + private final String mPackageName; + private final String mWidgetType; + + // These fields should only be set by creator of a SelectionEvent. + private String mSignature; + private long mEventTime; + private long mDurationSinceSessionStart; + private long mDurationSinceLastEvent; + private int mEventIndex; + private String mSessionId; + private int mStart; + private int mEnd; + private int mSmartStart; + private int mSmartEnd; + + SelectionEvent( + int start, int end, + @EventType int eventType, @EntityType String entityType, + String signature, Logger.Config config) { + Preconditions.checkArgument(end >= start, "end cannot be less than start"); + mAbsoluteStart = start; + mAbsoluteEnd = end; + mEventType = eventType; + mEntityType = Preconditions.checkNotNull(entityType); + mSignature = Preconditions.checkNotNull(signature); + Preconditions.checkNotNull(config); + mWidgetVersion = config.getWidgetVersion(); + mPackageName = Preconditions.checkNotNull(config.getPackageName()); + mWidgetType = Preconditions.checkNotNull(config.getWidgetType()); + } + + int getAbsoluteStart() { + return mAbsoluteStart; + } + + int getAbsoluteEnd() { + return mAbsoluteEnd; + } + + /** + * Returns the type of event that was triggered. e.g. {@link #ACTION_COPY}. + */ + public int getEventType() { + return mEventType; + } + + /** + * Returns the type of entity that is associated with this event. e.g. + * {@link android.view.textclassifier.TextClassifier#TYPE_EMAIL}. + */ + @EntityType + public String getEntityType() { + return mEntityType; + } + + /** + * Returns the package name of the app that this event originated in. + */ + public String getPackageName() { + return mPackageName; + } + + /** + * Returns the type of widget that was involved in triggering this event. + */ + public String getWidgetType() { + return mWidgetType; + } + + /** + * Returns a string version info for the widget this event was triggered in. + */ + public String getWidgetVersion() { + return mWidgetVersion; + } + + /** + * Returns the signature of the text classifier result associated with this event. + */ + public String getSignature() { + return mSignature; + } + + SelectionEvent setSignature(String signature) { + mSignature = Preconditions.checkNotNull(signature); + return this; + } + + /** + * Returns the time this event was triggered. + */ + public long getEventTime() { + return mEventTime; + } + + SelectionEvent setEventTime(long timeMs) { + mEventTime = timeMs; + return this; + } + + /** + * Returns the duration in ms between when this event was triggered and when the first event in + * the selection session was triggered. + */ + public long getDurationSinceSessionStart() { + return mDurationSinceSessionStart; + } + + SelectionEvent setDurationSinceSessionStart(long durationMs) { + mDurationSinceSessionStart = durationMs; + return this; + } + + /** + * Returns the duration in ms between when this event was triggered and when the previous event + * in the selection session was triggered. + */ + public long getDurationSincePreviousEvent() { + return mDurationSinceLastEvent; + } + + SelectionEvent setDurationSincePreviousEvent(long durationMs) { + this.mDurationSinceLastEvent = durationMs; + return this; + } + + /** + * Returns the index (e.g. 1st event, 2nd event, etc.) of this event in the selection session. + */ + public int getEventIndex() { + return mEventIndex; + } + + SelectionEvent setEventIndex(int index) { + mEventIndex = index; + return this; + } + + /** + * Returns the selection session id. + */ + public String getSessionId() { + return mSessionId; + } + + SelectionEvent setSessionId(String id) { + mSessionId = id; + return this; + } + + /** + * Returns the start index of this events token relative to the index of the start selection + * event in the selection session. + */ + public int getStart() { + return mStart; + } + + SelectionEvent setStart(int start) { + mStart = start; + return this; + } + + /** + * Returns the end index of this events token relative to the index of the start selection + * event in the selection session. + */ + public int getEnd() { + return mEnd; + } + + SelectionEvent setEnd(int end) { + mEnd = end; + return this; + } + + /** + * Returns the start index of this events token relative to the index of the smart selection + * event in the selection session. + */ + public int getSmartStart() { + return mSmartStart; + } + + SelectionEvent setSmartStart(int start) { + this.mSmartStart = start; + return this; + } + + /** + * Returns the end index of this events token relative to the index of the smart selection + * event in the selection session. + */ + public int getSmartEnd() { + return mSmartEnd; + } + + SelectionEvent setSmartEnd(int end) { + mSmartEnd = end; + return this; + } + + boolean isTerminal() { + switch (mEventType) { + case ACTION_OVERTYPE: // fall through + case ACTION_COPY: // fall through + case ACTION_PASTE: // fall through + case ACTION_CUT: // fall through + case ACTION_SHARE: // fall through + case ACTION_SMART_SHARE: // fall through + case ACTION_DRAG: // fall through + case ACTION_ABANDON: // fall through + case ACTION_OTHER: // fall through + return true; + default: + return false; + } + } + + @Override + public String toString() { + return String.format(Locale.US, + "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, " + + "widgetVersion=%s, packageName=%s, widgetType=%s, signature=%s, " + + "eventTime=%d, durationSinceSessionStart=%d, durationSinceLastEvent=%d, " + + "eventIndex=%d, sessionId=%s, start=%d, end=%d, smartStart=%d, smartEnd=%d}", + mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType, + mWidgetVersion, mPackageName, mWidgetType, mSignature, + mEventTime, mDurationSinceSessionStart, mDurationSinceLastEvent, + mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd); + } +} diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index 3f5584e6d3fb..2e354c1eee1f 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -37,8 +37,8 @@ import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; -import android.view.textclassifier.logging.SmartSelectionEventTracker; -import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent; +import android.view.textclassifier.logging.Logger; +import android.view.textclassifier.logging.SelectionEvent; import android.widget.Editor.SelectionModifierCursorController; import com.android.internal.annotations.VisibleForTesting; @@ -173,7 +173,7 @@ public final class SelectionActionModeHelper { public void onSelectionDrag() { mSelectionTracker.onSelectionAction( mTextView.getSelectionStart(), mTextView.getSelectionEnd(), - SelectionEvent.ActionType.DRAG, mTextClassification); + SelectionEvent.ACTION_DRAG, mTextClassification); } public void onTextChanged(int start, int end) { @@ -575,7 +575,7 @@ public final class SelectionActionModeHelper { mSelectionEnd = editor.getTextView().getSelectionEnd(); mLogger.logSelectionAction( textView.getSelectionStart(), textView.getSelectionEnd(), - SelectionEvent.ActionType.RESET, null /* classification */); + SelectionEvent.ACTION_RESET, null /* classification */); } return selected; } @@ -584,7 +584,7 @@ public final class SelectionActionModeHelper { public void onTextChanged(int start, int end, TextClassification classification) { if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) { - onSelectionAction(start, end, SelectionEvent.ActionType.OVERTYPE, classification); + onSelectionAction(start, end, SelectionEvent.ACTION_OVERTYPE, classification); } } @@ -623,7 +623,7 @@ public final class SelectionActionModeHelper { if (mIsPending) { mLogger.logSelectionAction( mSelectionStart, mSelectionEnd, - SelectionEvent.ActionType.ABANDON, null /* classification */); + SelectionEvent.ACTION_ABANDON, null /* classification */); mSelectionStart = mSelectionEnd = -1; mIsPending = false; } @@ -650,22 +650,29 @@ public final class SelectionActionModeHelper { private static final String LOG_TAG = "SelectionMetricsLogger"; private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+"); - private final SmartSelectionEventTracker mDelegate; + private final Logger mLogger; private final boolean mEditTextLogger; - private final BreakIterator mWordIterator; + private final BreakIterator mTokenIterator; private int mStartIndex; private String mText; SelectionMetricsLogger(TextView textView) { Preconditions.checkNotNull(textView); - final @SmartSelectionEventTracker.WidgetType int widgetType = textView.isTextEditable() - ? SmartSelectionEventTracker.WidgetType.EDITTEXT - : (textView.isTextSelectable() - ? SmartSelectionEventTracker.WidgetType.TEXTVIEW - : SmartSelectionEventTracker.WidgetType.UNSELECTABLE_TEXTVIEW); - mDelegate = new SmartSelectionEventTracker(textView.getContext(), widgetType); + mLogger = textView.getTextClassifier().getLogger( + new Logger.Config(textView.getContext(), getWidetType(textView), null)); mEditTextLogger = textView.isTextEditable(); - mWordIterator = BreakIterator.getWordInstance(textView.getTextLocale()); + mTokenIterator = mLogger.getTokenIterator(textView.getTextLocale()); + } + + @Logger.WidgetType + private static String getWidetType(TextView textView) { + if (textView.isTextEditable()) { + return Logger.WIDGET_EDITTEXT; + } + if (textView.isTextSelectable()) { + return Logger.WIDGET_TEXTVIEW; + } + return Logger.WIDGET_UNSELECTABLE_TEXTVIEW; } public void logSelectionStarted(CharSequence text, int index) { @@ -675,9 +682,9 @@ public final class SelectionActionModeHelper { if (mText == null || !mText.contentEquals(text)) { mText = text.toString(); } - mWordIterator.setText(mText); + mTokenIterator.setText(mText); mStartIndex = index; - mDelegate.logEvent(SelectionEvent.selectionStarted(0)); + mLogger.logSelectionStartedEvent(0); } catch (Exception e) { // Avoid crashes due to logging. Log.d(LOG_TAG, e.getMessage()); @@ -691,14 +698,14 @@ public final class SelectionActionModeHelper { Preconditions.checkArgumentInRange(end, start, mText.length(), "end"); int[] wordIndices = getWordDelta(start, end); if (selection != null) { - mDelegate.logEvent(SelectionEvent.selectionModified( - wordIndices[0], wordIndices[1], selection)); + mLogger.logSelectionModifiedEvent( + wordIndices[0], wordIndices[1], selection); } else if (classification != null) { - mDelegate.logEvent(SelectionEvent.selectionModified( - wordIndices[0], wordIndices[1], classification)); + mLogger.logSelectionModifiedEvent( + wordIndices[0], wordIndices[1], classification); } else { - mDelegate.logEvent(SelectionEvent.selectionModified( - wordIndices[0], wordIndices[1])); + mLogger.logSelectionModifiedEvent( + wordIndices[0], wordIndices[1]); } } catch (Exception e) { // Avoid crashes due to logging. @@ -715,11 +722,11 @@ public final class SelectionActionModeHelper { Preconditions.checkArgumentInRange(end, start, mText.length(), "end"); int[] wordIndices = getWordDelta(start, end); if (classification != null) { - mDelegate.logEvent(SelectionEvent.selectionAction( - wordIndices[0], wordIndices[1], action, classification)); + mLogger.logSelectionActionEvent( + wordIndices[0], wordIndices[1], action, classification); } else { - mDelegate.logEvent(SelectionEvent.selectionAction( - wordIndices[0], wordIndices[1], action)); + mLogger.logSelectionActionEvent( + wordIndices[0], wordIndices[1], action); } } catch (Exception e) { // Avoid crashes due to logging. @@ -742,10 +749,10 @@ public final class SelectionActionModeHelper { wordIndices[0] = countWordsBackward(start); // For the selection start index, avoid counting a partial word backwards. - if (!mWordIterator.isBoundary(start) + if (!mTokenIterator.isBoundary(start) && !isWhitespace( - mWordIterator.preceding(start), - mWordIterator.following(start))) { + mTokenIterator.preceding(start), + mTokenIterator.following(start))) { // We counted a partial word. Remove it. wordIndices[0]--; } @@ -767,7 +774,7 @@ public final class SelectionActionModeHelper { int wordCount = 0; int offset = from; while (offset > mStartIndex) { - int start = mWordIterator.preceding(offset); + int start = mTokenIterator.preceding(offset); if (!isWhitespace(start, offset)) { wordCount++; } @@ -781,7 +788,7 @@ public final class SelectionActionModeHelper { int wordCount = 0; int offset = from; while (offset < mStartIndex) { - int end = mWordIterator.following(offset); + int end = mTokenIterator.following(offset); if (!isWhitespace(offset, end)) { wordCount++; } @@ -1022,20 +1029,20 @@ public final class SelectionActionModeHelper { private static int getActionType(int menuItemId) { switch (menuItemId) { case TextView.ID_SELECT_ALL: - return SelectionEvent.ActionType.SELECT_ALL; + return SelectionEvent.ACTION_SELECT_ALL; case TextView.ID_CUT: - return SelectionEvent.ActionType.CUT; + return SelectionEvent.ACTION_CUT; case TextView.ID_COPY: - return SelectionEvent.ActionType.COPY; + return SelectionEvent.ACTION_COPY; case TextView.ID_PASTE: // fall through case TextView.ID_PASTE_AS_PLAIN_TEXT: - return SelectionEvent.ActionType.PASTE; + return SelectionEvent.ACTION_PASTE; case TextView.ID_SHARE: - return SelectionEvent.ActionType.SHARE; + return SelectionEvent.ACTION_SHARE; case TextView.ID_ASSIST: - return SelectionEvent.ActionType.SMART_SHARE; + return SelectionEvent.ACTION_SMART_SHARE; default: - return SelectionEvent.ActionType.OTHER; + return SelectionEvent.ACTION_OTHER; } } |