summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Abodunrinwa Toki <toki@google.com> 2017-12-05 07:33:41 +0000
committer Abodunrinwa Toki <toki@google.com> 2018-01-31 10:09:54 +0000
commit3bb443613820c7e54512cef9659ef2e9428243c6 (patch)
treefbe9358dca81716b6bdd4e3daa36ba1b75c930cd
parent7c691c606c0e68eea5ddea4a910232df68501332 (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
-rw-r--r--api/current.txt70
-rw-r--r--core/java/android/view/textclassifier/TextClassifier.java12
-rw-r--r--core/java/android/view/textclassifier/TextClassifierImpl.java31
-rw-r--r--core/java/android/view/textclassifier/logging/DefaultLogger.java263
-rw-r--r--core/java/android/view/textclassifier/logging/Logger.java429
-rw-r--r--core/java/android/view/textclassifier/logging/SelectionEvent.java337
-rw-r--r--core/java/android/widget/SelectionActionModeHelper.java85
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;
}
}