summaryrefslogtreecommitdiff
path: root/core/java
diff options
context:
space:
mode:
Diffstat (limited to 'core/java')
-rw-r--r--core/java/android/view/textclassifier/DefaultLogger.java2
-rw-r--r--core/java/android/view/textclassifier/Logger.java48
-rw-r--r--core/java/android/view/textclassifier/SelectionEvent.java249
-rw-r--r--core/java/android/view/textclassifier/TextClassificationContext.java123
-rw-r--r--core/java/android/view/textclassifier/TextClassificationManager.java64
-rw-r--r--core/java/android/view/textclassifier/TextClassificationSession.java217
-rw-r--r--core/java/android/view/textclassifier/TextClassificationSessionFactory.java36
-rw-r--r--core/java/android/view/textclassifier/TextClassificationSessionId.java125
-rw-r--r--core/java/android/view/textclassifier/TextClassifier.java93
-rw-r--r--core/java/android/view/textclassifier/TextClassifierImpl.java14
-rw-r--r--core/java/android/widget/SelectionActionModeHelper.java66
-rw-r--r--core/java/android/widget/TextView.java55
12 files changed, 1007 insertions, 85 deletions
diff --git a/core/java/android/view/textclassifier/DefaultLogger.java b/core/java/android/view/textclassifier/DefaultLogger.java
index b2f4e399da5b..46ff44246280 100644
--- a/core/java/android/view/textclassifier/DefaultLogger.java
+++ b/core/java/android/view/textclassifier/DefaultLogger.java
@@ -39,7 +39,7 @@ import java.util.StringJoiner;
public final class DefaultLogger extends Logger {
private static final String LOG_TAG = "DefaultLogger";
- private static final String CLASSIFIER_ID = "androidtc";
+ 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;
diff --git a/core/java/android/view/textclassifier/Logger.java b/core/java/android/view/textclassifier/Logger.java
index 9c92fd4543e2..c29d3e64a8b7 100644
--- a/core/java/android/view/textclassifier/Logger.java
+++ b/core/java/android/view/textclassifier/Logger.java
@@ -18,56 +18,25 @@ package android.view.textclassifier;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.StringDef;
import android.content.Context;
-import android.util.Log;
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.
+ * @hide
*/
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.InvocationMethod int mInvocationMethod;
private SelectionEvent mPrevEvent;
private SelectionEvent mSmartEvent;
@@ -108,7 +77,6 @@ public abstract class Logger {
return false;
}
-
/**
* Returns a token iterator for tokenizing text for logging purposes.
*/
@@ -299,6 +267,9 @@ public abstract class Logger {
// Selection did not change. Ignore event.
return;
}
+ break;
+ default:
+ // do nothing.
}
event.setEventTime(now);
@@ -325,9 +296,9 @@ public abstract class Logger {
}
}
- private String startNewSession() {
+ private TextClassificationSessionId startNewSession() {
endSession();
- return UUID.randomUUID().toString();
+ return new TextClassificationSessionId();
}
private void endSession() {
@@ -372,12 +343,12 @@ public abstract class Logger {
/**
* @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}
+ * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}
* @param widgetVersion a string version info for the widget the logger logs for
*/
public Config(
@NonNull Context context,
- @WidgetType String widgetType,
+ @TextClassifier.WidgetType String widgetType,
@Nullable String widgetVersion) {
mPackageName = Preconditions.checkNotNull(context).getPackageName();
mWidgetType = widgetType;
@@ -392,7 +363,8 @@ public abstract class Logger {
}
/**
- * Returns the name for the widget being logged for. e.g. {@link #WIDGET_TEXTVIEW}.
+ * Returns the name for the widget being logged for. e.g.
+ * {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}.
*/
public String getWidgetType() {
return mWidgetType;
diff --git a/core/java/android/view/textclassifier/SelectionEvent.java b/core/java/android/view/textclassifier/SelectionEvent.java
index 7ac094eda031..5a4d2cf1d481 100644
--- a/core/java/android/view/textclassifier/SelectionEvent.java
+++ b/core/java/android/view/textclassifier/SelectionEvent.java
@@ -17,10 +17,12 @@
package android.view.textclassifier;
import android.annotation.IntDef;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Parcel;
import android.os.Parcelable;
import android.view.textclassifier.TextClassifier.EntityType;
+import android.view.textclassifier.TextClassifier.WidgetType;
import com.android.internal.util.Preconditions;
@@ -103,30 +105,33 @@ public final class SelectionEvent implements Parcelable {
/** @hide */
@Retention(RetentionPolicy.SOURCE)
- @IntDef({INVOCATION_MANUAL, INVOCATION_LINK})
+ @IntDef({INVOCATION_MANUAL, INVOCATION_LINK, INVOCATION_UNKNOWN})
public @interface InvocationMethod {}
/** Selection was invoked by the user long pressing, double tapping, or dragging to select. */
public static final int INVOCATION_MANUAL = 1;
/** Selection was invoked by the user tapping on a link. */
public static final int INVOCATION_LINK = 2;
+ /** Unknown invocation method */
+ public static final int INVOCATION_UNKNOWN = 0;
+
+ private static final String NO_SIGNATURE = "";
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;
- private final @InvocationMethod int mInvocationMethod;
- // These fields should only be set by creator of a SelectionEvent.
- private String mSignature;
+ private @EventType int mEventType;
+ private String mPackageName = "";
+ private String mWidgetType = TextClassifier.WIDGET_TYPE_UNKNOWN;
+ private @InvocationMethod int mInvocationMethod;
+ @Nullable private String mWidgetVersion;
+ private String mSignature; // TODO: Rename to resultId.
private long mEventTime;
private long mDurationSinceSessionStart;
private long mDurationSincePreviousEvent;
private int mEventIndex;
- @Nullable private String mSessionId;
+ @Nullable private TextClassificationSessionId mSessionId;
private int mStart;
private int mEnd;
private int mSmartStart;
@@ -135,20 +140,29 @@ public final class SelectionEvent implements Parcelable {
SelectionEvent(
int start, int end,
@EventType int eventType, @EntityType String entityType,
- @InvocationMethod int invocationMethod, String signature, Logger.Config config) {
+ @InvocationMethod int invocationMethod, String signature) {
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());
mInvocationMethod = invocationMethod;
}
+ SelectionEvent(
+ int start, int end,
+ @EventType int eventType, @EntityType String entityType,
+ @InvocationMethod int invocationMethod, String signature, Logger.Config config) {
+ this(start, end, eventType, entityType, invocationMethod, signature);
+ Preconditions.checkNotNull(config);
+ setTextClassificationSessionContext(
+ new TextClassificationContext.Builder(
+ config.getPackageName(), config.getWidgetType())
+ .setWidgetVersion(config.getWidgetVersion())
+ .build());
+ }
+
private SelectionEvent(Parcel in) {
mAbsoluteStart = in.readInt();
mAbsoluteEnd = in.readInt();
@@ -163,7 +177,8 @@ public final class SelectionEvent implements Parcelable {
mDurationSinceSessionStart = in.readLong();
mDurationSincePreviousEvent = in.readLong();
mEventIndex = in.readInt();
- mSessionId = in.readInt() > 0 ? in.readString() : null;
+ mSessionId = in.readInt() > 0
+ ? TextClassificationSessionId.CREATOR.createFromParcel(in) : null;
mStart = in.readInt();
mEnd = in.readInt();
mSmartStart = in.readInt();
@@ -190,7 +205,7 @@ public final class SelectionEvent implements Parcelable {
dest.writeInt(mEventIndex);
dest.writeInt(mSessionId != null ? 1 : 0);
if (mSessionId != null) {
- dest.writeString(mSessionId);
+ mSessionId.writeToParcel(dest, flags);
}
dest.writeInt(mStart);
dest.writeInt(mEnd);
@@ -203,6 +218,156 @@ public final class SelectionEvent implements Parcelable {
return 0;
}
+ /**
+ * Creates a "selection started" event.
+ *
+ * @param invocationMethod the way the selection was triggered
+ * @param start the index of the selected text
+ */
+ @NonNull
+ public static SelectionEvent createSelectionStartedEvent(
+ @SelectionEvent.InvocationMethod int invocationMethod, int start) {
+ return new SelectionEvent(
+ start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED,
+ TextClassifier.TYPE_UNKNOWN, invocationMethod, NO_SIGNATURE);
+ }
+
+ /**
+ * Creates a "selection modified" event.
+ * Use when the user modifies the selection.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ *
+ * @throws IllegalArgumentException if end is less than start
+ */
+ @NonNull
+ public static SelectionEvent createSelectionModifiedEvent(int start, int end) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ return new SelectionEvent(
+ start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
+ TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN, NO_SIGNATURE);
+ }
+
+ /**
+ * Creates a "selection modified" event.
+ * Use when the user modifies the selection and the selection's entity type is known.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ * @param classification the TextClassification object returned by the TextClassifier that
+ * classified the selected text
+ *
+ * @throws IllegalArgumentException if end is less than start
+ */
+ @NonNull
+ public static SelectionEvent createSelectionModifiedEvent(
+ int start, int end, @NonNull TextClassification classification) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ Preconditions.checkNotNull(classification);
+ final String entityType = classification.getEntityCount() > 0
+ ? classification.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ return new SelectionEvent(
+ start, end, SelectionEvent.EVENT_SELECTION_MODIFIED,
+ entityType, INVOCATION_UNKNOWN, classification.getSignature());
+ }
+
+ /**
+ * Creates a "selection modified" event.
+ * Use when a TextClassifier modifies the selection.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ * @param selection the TextSelection object returned by the TextClassifier for the
+ * specified selection
+ *
+ * @throws IllegalArgumentException if end is less than start
+ */
+ @NonNull
+ public static SelectionEvent createSelectionModifiedEvent(
+ int start, int end, @NonNull TextSelection selection) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ Preconditions.checkNotNull(selection);
+ final String entityType = selection.getEntityCount() > 0
+ ? selection.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ return new SelectionEvent(
+ start, end, SelectionEvent.EVENT_AUTO_SELECTION,
+ entityType, INVOCATION_UNKNOWN, selection.getSignature());
+ }
+
+ /**
+ * Creates an event specifying an action taken on a selection.
+ * Use when the user clicks on an action to act on the selected text.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (exclusive) index of the selection
+ * @param actionType the action that was performed on the selection
+ *
+ * @throws IllegalArgumentException if end is less than start
+ */
+ @NonNull
+ public static SelectionEvent createSelectionActionEvent(
+ int start, int end, @SelectionEvent.ActionType int actionType) {
+ Preconditions.checkArgument(end >= start, "end cannot be less than start");
+ checkActionType(actionType);
+ return new SelectionEvent(
+ start, end, actionType, TextClassifier.TYPE_UNKNOWN, INVOCATION_UNKNOWN,
+ NO_SIGNATURE);
+ }
+
+ /**
+ * Creates an event specifying an action taken on a selection.
+ * Use when the user clicks on an action to act on the selected text and the selection's
+ * entity type is known.
+ *
+ * @param start the start (inclusive) index of the selection
+ * @param end the end (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 end is less than start
+ * @throws IllegalArgumentException If actionType is not a valid SelectionEvent actionType
+ */
+ @NonNull
+ public static SelectionEvent createSelectionActionEvent(
+ 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);
+ final String entityType = classification.getEntityCount() > 0
+ ? classification.getEntity(0)
+ : TextClassifier.TYPE_UNKNOWN;
+ return new SelectionEvent(start, end, actionType, entityType, INVOCATION_UNKNOWN,
+ classification.getSignature());
+ }
+
+ /**
+ * @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));
+ }
+ }
+
int getAbsoluteStart() {
return mAbsoluteStart;
}
@@ -214,15 +379,24 @@ public final class SelectionEvent implements Parcelable {
/**
* Returns the type of event that was triggered. e.g. {@link #ACTION_COPY}.
*/
+ @EventType
public int getEventType() {
return mEventType;
}
/**
+ * Sets the event type.
+ */
+ void setEventType(@EventType int eventType) {
+ mEventType = eventType;
+ }
+
+ /**
* Returns the type of entity that is associated with this event. e.g.
* {@link android.view.textclassifier.TextClassifier#TYPE_EMAIL}.
*/
@EntityType
+ @NonNull
public String getEntityType() {
return mEntityType;
}
@@ -230,6 +404,7 @@ public final class SelectionEvent implements Parcelable {
/**
* Returns the package name of the app that this event originated in.
*/
+ @NonNull
public String getPackageName() {
return mPackageName;
}
@@ -237,6 +412,8 @@ public final class SelectionEvent implements Parcelable {
/**
* Returns the type of widget that was involved in triggering this event.
*/
+ @WidgetType
+ @NonNull
public String getWidgetType() {
return mWidgetType;
}
@@ -244,11 +421,21 @@ public final class SelectionEvent implements Parcelable {
/**
* Returns a string version info for the widget this event was triggered in.
*/
+ @Nullable
public String getWidgetVersion() {
return mWidgetVersion;
}
/**
+ * Sets the {@link TextClassificationContext} for this event.
+ */
+ void setTextClassificationSessionContext(TextClassificationContext context) {
+ mPackageName = context.getPackageName();
+ mWidgetType = context.getWidgetType();
+ mWidgetVersion = context.getWidgetVersion();
+ }
+
+ /**
* Returns the way the selection mode was invoked.
*/
public @InvocationMethod int getInvocationMethod() {
@@ -256,6 +443,13 @@ public final class SelectionEvent implements Parcelable {
}
/**
+ * Sets the invocationMethod for this event.
+ */
+ void setInvocationMethod(@InvocationMethod int invocationMethod) {
+ mInvocationMethod = invocationMethod;
+ }
+
+ /**
* Returns the signature of the text classifier result associated with this event.
*/
public String getSignature() {
@@ -320,17 +514,18 @@ public final class SelectionEvent implements Parcelable {
/**
* Returns the selection session id.
*/
- public String getSessionId() {
+ @Nullable
+ public TextClassificationSessionId getSessionId() {
return mSessionId;
}
- SelectionEvent setSessionId(String id) {
+ SelectionEvent setSessionId(TextClassificationSessionId id) {
mSessionId = id;
return this;
}
/**
- * Returns the start index of this events token relative to the index of the start selection
+ * Returns the start index of this events relative to the index of the start selection
* event in the selection session.
*/
public int getStart() {
@@ -343,7 +538,7 @@ public final class SelectionEvent implements Parcelable {
}
/**
- * Returns the end index of this events token relative to the index of the start selection
+ * Returns the end index of this events relative to the index of the start selection
* event in the selection session.
*/
public int getEnd() {
@@ -356,7 +551,7 @@ public final class SelectionEvent implements Parcelable {
}
/**
- * Returns the start index of this events token relative to the index of the smart selection
+ * Returns the start index of this events relative to the index of the smart selection
* event in the selection session.
*/
public int getSmartStart() {
@@ -369,7 +564,7 @@ public final class SelectionEvent implements Parcelable {
}
/**
- * Returns the end index of this events token relative to the index of the smart selection
+ * Returns the end index of this events relative to the index of the smart selection
* event in the selection session.
*/
public int getSmartEnd() {
@@ -382,7 +577,15 @@ public final class SelectionEvent implements Parcelable {
}
boolean isTerminal() {
- switch (mEventType) {
+ return isTerminal(mEventType);
+ }
+
+ /**
+ * Returns true if the eventType is a terminal event type. Otherwise returns false.
+ * A terminal event is an event that ends a selection interaction.
+ */
+ public static boolean isTerminal(@EventType int eventType) {
+ switch (eventType) {
case ACTION_OVERTYPE: // fall through
case ACTION_COPY: // fall through
case ACTION_PASTE: // fall through
diff --git a/core/java/android/view/textclassifier/TextClassificationContext.java b/core/java/android/view/textclassifier/TextClassificationContext.java
new file mode 100644
index 000000000000..a88f2f66d359
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextClassificationContext.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.view.textclassifier.TextClassifier.WidgetType;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.Locale;
+
+/**
+ * A representation of the context in which text classification would be performed.
+ * @see TextClassificationManager#createTextClassificationSession(TextClassificationContext)
+ */
+public final class TextClassificationContext {
+
+ private final String mPackageName;
+ private final String mWidgetType;
+ @Nullable private final String mWidgetVersion;
+
+ private TextClassificationContext(
+ String packageName,
+ String widgetType,
+ String widgetVersion) {
+ mPackageName = Preconditions.checkNotNull(packageName);
+ mWidgetType = Preconditions.checkNotNull(widgetType);
+ mWidgetVersion = widgetVersion;
+ }
+
+ /**
+ * Returns the package name for the calling package.
+ */
+ @NonNull
+ public String getPackageName() {
+ return mPackageName;
+ }
+
+ /**
+ * Returns the widget type for this classification context.
+ */
+ @NonNull
+ @WidgetType
+ public String getWidgetType() {
+ return mWidgetType;
+ }
+
+ /**
+ * Returns a custom version string for the widget type.
+ *
+ * @see #getWidgetType()
+ */
+ @Nullable
+ public String getWidgetVersion() {
+ return mWidgetVersion;
+ }
+
+ @Override
+ public String toString() {
+ return String.format(Locale.US, "TextClassificationContext{"
+ + "packageName=%s, widgetType=%s, widgetVersion=%s}",
+ mPackageName, mWidgetType, mWidgetVersion);
+ }
+
+ /**
+ * A builder for building a TextClassification context.
+ */
+ public static final class Builder {
+
+ private final String mPackageName;
+ private final String mWidgetType;
+
+ @Nullable private String mWidgetVersion;
+
+ /**
+ * Initializes a new builder for text classification context objects.
+ *
+ * @param packageName the name of the calling package
+ * @param widgetType the type of widget e.g. {@link TextClassifier#WIDGET_TYPE_TEXTVIEW}
+ *
+ * @return this builder
+ */
+ public Builder(@NonNull String packageName, @NonNull @WidgetType String widgetType) {
+ mPackageName = Preconditions.checkNotNull(packageName);
+ mWidgetType = Preconditions.checkNotNull(widgetType);
+ }
+
+ /**
+ * Sets an optional custom version string for the widget type.
+ *
+ * @return this builder
+ */
+ public Builder setWidgetVersion(@Nullable String widgetVersion) {
+ mWidgetVersion = widgetVersion;
+ return this;
+ }
+
+ /**
+ * Builds the text classification context object.
+ *
+ * @return the built TextClassificationContext object
+ */
+ @NonNull
+ public TextClassificationContext build() {
+ return new TextClassificationContext(mPackageName, mWidgetType, mWidgetVersion);
+ }
+ }
+}
diff --git a/core/java/android/view/textclassifier/TextClassificationManager.java b/core/java/android/view/textclassifier/TextClassificationManager.java
index a7f1ca1aa239..262d9b852b82 100644
--- a/core/java/android/view/textclassifier/TextClassificationManager.java
+++ b/core/java/android/view/textclassifier/TextClassificationManager.java
@@ -16,6 +16,7 @@
package android.view.textclassifier;
+import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
@@ -36,6 +37,9 @@ public final class TextClassificationManager {
private static final String LOG_TAG = "TextClassificationManager";
private final Object mLock = new Object();
+ private final TextClassificationSessionFactory mDefaultSessionFactory =
+ classificationContext -> new TextClassificationSession(
+ classificationContext, getTextClassifier());
private final Context mContext;
private final TextClassificationConstants mSettings;
@@ -46,12 +50,15 @@ public final class TextClassificationManager {
private TextClassifier mLocalTextClassifier;
@GuardedBy("mLock")
private TextClassifier mSystemTextClassifier;
+ @GuardedBy("mLock")
+ private TextClassificationSessionFactory mSessionFactory;
/** @hide */
public TextClassificationManager(Context context) {
mContext = Preconditions.checkNotNull(context);
mSettings = TextClassificationConstants.loadFromString(Settings.Global.getString(
context.getContentResolver(), Settings.Global.TEXT_CLASSIFIER_CONSTANTS));
+ mSessionFactory = mDefaultSessionFactory;
}
/**
@@ -61,6 +68,7 @@ public final class TextClassificationManager {
*
* @see #setTextClassifier(TextClassifier)
*/
+ @NonNull
public TextClassifier getTextClassifier() {
synchronized (mLock) {
if (mTextClassifier == null) {
@@ -93,7 +101,6 @@ public final class TextClassificationManager {
* @see TextClassifier#SYSTEM
* @hide
*/
- // TODO: Expose as system API.
public TextClassifier getTextClassifier(@TextClassifierType int type) {
switch (type) {
case TextClassifier.LOCAL:
@@ -108,6 +115,61 @@ public final class TextClassificationManager {
return mSettings;
}
+ /**
+ * Call this method to start a text classification session with the given context.
+ * A session is created with a context helping the classifier better understand
+ * what the user needs and consists of queries and feedback events. The queries
+ * are directly related to providing useful functionality to the user and the events
+ * are a feedback loop back to the classifier helping it learn and better serve
+ * future queries.
+ *
+ * <p> All interactions with the returned classifier are considered part of a single
+ * session and are logically grouped. For example, when a text widget is focused
+ * all user interactions around text editing (selection, editing, etc) can be
+ * grouped together to allow the classifier get better.
+ *
+ * @param classificationContext The context in which classification would occur
+ *
+ * @return An instance to perform classification in the given context
+ */
+ @NonNull
+ public TextClassifier createTextClassificationSession(
+ @NonNull TextClassificationContext classificationContext) {
+ Preconditions.checkNotNull(classificationContext);
+ final TextClassifier textClassifier =
+ mSessionFactory.createTextClassificationSession(classificationContext);
+ Preconditions.checkNotNull(textClassifier, "Session Factory should never return null");
+ return textClassifier;
+ }
+
+ /**
+ * @see #createTextClassificationSession(TextClassificationContext, TextClassifier)
+ * @hide
+ */
+ public TextClassifier createTextClassificationSession(
+ TextClassificationContext classificationContext, TextClassifier textClassifier) {
+ Preconditions.checkNotNull(classificationContext);
+ Preconditions.checkNotNull(textClassifier);
+ return new TextClassificationSession(classificationContext, textClassifier);
+ }
+
+ /**
+ * Sets a TextClassificationSessionFactory to be used to create session-aware TextClassifiers.
+ *
+ * @param factory the textClassification session factory. If this is null, the default factory
+ * will be used.
+ */
+ public void setTextClassificationSessionFactory(
+ @Nullable TextClassificationSessionFactory factory) {
+ synchronized (mLock) {
+ if (factory != null) {
+ mSessionFactory = factory;
+ } else {
+ mSessionFactory = mDefaultSessionFactory;
+ }
+ }
+ }
+
private TextClassifier getSystemTextClassifier() {
synchronized (mLock) {
if (mSystemTextClassifier == null && isSystemTextClassifierEnabled()) {
diff --git a/core/java/android/view/textclassifier/TextClassificationSession.java b/core/java/android/view/textclassifier/TextClassificationSession.java
new file mode 100644
index 000000000000..6938e1ae11c4
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextClassificationSession.java
@@ -0,0 +1,217 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.annotation.WorkerThread;
+import android.view.textclassifier.DefaultLogger.SignatureParser;
+import android.view.textclassifier.SelectionEvent.InvocationMethod;
+
+import com.android.internal.util.Preconditions;
+
+/**
+ * Session-aware TextClassifier.
+ */
+@WorkerThread
+final class TextClassificationSession implements TextClassifier {
+
+ /* package */ static final boolean DEBUG_LOG_ENABLED = true;
+ private static final String LOG_TAG = "TextClassificationSession";
+
+ private final TextClassifier mDelegate;
+ private final SelectionEventHelper mEventHelper;
+
+ private boolean mDestroyed;
+
+ TextClassificationSession(TextClassificationContext context, TextClassifier delegate) {
+ mDelegate = Preconditions.checkNotNull(delegate);
+ mEventHelper = new SelectionEventHelper(new TextClassificationSessionId(), context);
+ }
+
+ @Override
+ public TextSelection suggestSelection(CharSequence text, int selectionStartIndex,
+ int selectionEndIndex, TextSelection.Options options) {
+ checkDestroyed();
+ return mDelegate.suggestSelection(text, selectionStartIndex, selectionEndIndex, options);
+ }
+
+ @Override
+ public TextClassification classifyText(CharSequence text, int startIndex, int endIndex,
+ TextClassification.Options options) {
+ checkDestroyed();
+ return mDelegate.classifyText(text, startIndex, endIndex, options);
+ }
+
+ @Override
+ public TextLinks generateLinks(CharSequence text, TextLinks.Options options) {
+ checkDestroyed();
+ return mDelegate.generateLinks(text, options);
+ }
+
+ @Override
+ public void onSelectionEvent(SelectionEvent event) {
+ checkDestroyed();
+ Preconditions.checkNotNull(event);
+ if (mEventHelper.sanitizeEvent(event)) {
+ mDelegate.onSelectionEvent(event);
+ }
+ }
+
+ @Override
+ public void destroy() {
+ mEventHelper.endSession();
+ mDestroyed = true;
+ }
+
+ @Override
+ public boolean isDestroyed() {
+ return mDestroyed;
+ }
+
+ /**
+ * @throws IllegalStateException if this TextClassification session has been destroyed.
+ * @see #isDestroyed()
+ * @see #destroy()
+ */
+ private void checkDestroyed() {
+ if (mDestroyed) {
+ throw new IllegalStateException("This TextClassification session has been destroyed");
+ }
+ }
+
+ /**
+ * Helper class for updating SelectionEvent fields.
+ */
+ private static final class SelectionEventHelper {
+
+ private final TextClassificationSessionId mSessionId;
+ private final TextClassificationContext mContext;
+
+ @InvocationMethod
+ private int mInvocationMethod = SelectionEvent.INVOCATION_UNKNOWN;
+ private SelectionEvent mPrevEvent;
+ private SelectionEvent mSmartEvent;
+ private SelectionEvent mStartEvent;
+
+ SelectionEventHelper(
+ TextClassificationSessionId sessionId, TextClassificationContext context) {
+ mSessionId = Preconditions.checkNotNull(sessionId);
+ mContext = Preconditions.checkNotNull(context);
+ }
+
+ /**
+ * Updates the necessary fields in the event for the current session.
+ *
+ * @return true if the event should be reported. false if the event should be ignored
+ */
+ boolean sanitizeEvent(SelectionEvent event) {
+ updateInvocationMethod(event);
+ modifyAutoSelectionEventType(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 false;
+ }
+
+ final long now = System.currentTimeMillis();
+ switch (event.getEventType()) {
+ case SelectionEvent.EVENT_SELECTION_STARTED:
+ Preconditions.checkArgument(
+ event.getAbsoluteEnd() == event.getAbsoluteStart() + 1);
+ event.setSessionId(mSessionId);
+ 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 false;
+ }
+ break;
+ default:
+ // do nothing.
+ }
+
+ 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);
+ }
+ mPrevEvent = event;
+ return true;
+ }
+
+ void endSession() {
+ mPrevEvent = null;
+ mSmartEvent = null;
+ mStartEvent = null;
+ }
+
+ private void updateInvocationMethod(SelectionEvent event) {
+ event.setTextClassificationSessionContext(mContext);
+ if (event.getInvocationMethod() == SelectionEvent.INVOCATION_UNKNOWN) {
+ event.setInvocationMethod(mInvocationMethod);
+ } else {
+ mInvocationMethod = event.getInvocationMethod();
+ }
+ }
+
+ private void modifyAutoSelectionEventType(SelectionEvent event) {
+ switch (event.getEventType()) {
+ case SelectionEvent.EVENT_SMART_SELECTION_SINGLE: // fall through
+ case SelectionEvent.EVENT_SMART_SELECTION_MULTI: // fall through
+ case SelectionEvent.EVENT_AUTO_SELECTION:
+ if (isPlatformLocalTextClassifierSmartSelection(event.getSignature())) {
+ if (event.getAbsoluteEnd() - event.getAbsoluteStart() > 1) {
+ event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_MULTI);
+ } else {
+ event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_SINGLE);
+ }
+ } else {
+ event.setEventType(SelectionEvent.EVENT_AUTO_SELECTION);
+ }
+ return;
+ default:
+ return;
+ }
+ }
+
+ private static boolean isPlatformLocalTextClassifierSmartSelection(String signature) {
+ return DefaultLogger.CLASSIFIER_ID.equals(SignatureParser.getClassifierId(signature));
+ }
+ }
+}
diff --git a/core/java/android/view/textclassifier/TextClassificationSessionFactory.java b/core/java/android/view/textclassifier/TextClassificationSessionFactory.java
new file mode 100644
index 000000000000..c0914b6c943a
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextClassificationSessionFactory.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2018 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;
+
+import android.annotation.NonNull;
+
+/**
+ * An interface for creating a session-aware TextClassifier.
+ *
+ * @see TextClassificationManager#createTextClassificationSession(TextClassificationContext)
+ */
+public interface TextClassificationSessionFactory {
+
+ /**
+ * Creates and returns a session-aware TextClassifier.
+ *
+ * @param classificationContext the classification context
+ */
+ @NonNull
+ TextClassifier createTextClassificationSession(
+ @NonNull TextClassificationContext classificationContext);
+}
diff --git a/core/java/android/view/textclassifier/TextClassificationSessionId.java b/core/java/android/view/textclassifier/TextClassificationSessionId.java
new file mode 100644
index 000000000000..3e4dc1c52b4d
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextClassificationSessionId.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2013 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;
+
+import android.annotation.NonNull;
+import android.os.Parcel;
+import android.os.Parcelable;
+
+import com.android.internal.util.Preconditions;
+
+import java.util.UUID;
+
+/**
+ * This class represents the id of a text classification session.
+ */
+public final class TextClassificationSessionId implements Parcelable {
+ private final @NonNull String mValue;
+
+ /**
+ * Creates a new instance.
+ *
+ * @hide
+ */
+ public TextClassificationSessionId() {
+ this(UUID.randomUUID().toString());
+ }
+
+ /**
+ * Creates a new instance.
+ *
+ * @param value The internal value.
+ *
+ * @hide
+ */
+ public TextClassificationSessionId(@NonNull String value) {
+ mValue = value;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + mValue.hashCode();
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj) {
+ return true;
+ }
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ TextClassificationSessionId other = (TextClassificationSessionId) obj;
+ if (!mValue.equals(other.mValue)) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void writeToParcel(Parcel parcel, int flags) {
+ parcel.writeString(mValue);
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ /**
+ * Flattens this id to a string.
+ *
+ * @return The flattened id.
+ *
+ * @hide
+ */
+ public @NonNull String flattenToString() {
+ return mValue;
+ }
+
+ /**
+ * Unflattens a print job id from a string.
+ *
+ * @param string The string.
+ * @return The unflattened id, or null if the string is malformed.
+ *
+ * @hide
+ */
+ public static @NonNull TextClassificationSessionId unflattenFromString(@NonNull String string) {
+ return new TextClassificationSessionId(string);
+ }
+
+ public static final Parcelable.Creator<TextClassificationSessionId> CREATOR =
+ new Parcelable.Creator<TextClassificationSessionId>() {
+ @Override
+ public TextClassificationSessionId createFromParcel(Parcel parcel) {
+ return new TextClassificationSessionId(
+ Preconditions.checkNotNull(parcel.readString()));
+ }
+
+ @Override
+ public TextClassificationSessionId[] newArray(int size) {
+ return new TextClassificationSessionId[size];
+ }
+ };
+}
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index 98fa574f076b..2048f2b49182 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -112,6 +112,38 @@ public interface TextClassifier {
@StringDef(prefix = { "HINT_" }, value = {HINT_TEXT_IS_EDITABLE, HINT_TEXT_IS_NOT_EDITABLE})
@interface Hints {}
+ /** @hide */
+ @Retention(RetentionPolicy.SOURCE)
+ @StringDef({WIDGET_TYPE_TEXTVIEW, WIDGET_TYPE_WEBVIEW, WIDGET_TYPE_EDITTEXT,
+ WIDGET_TYPE_EDIT_WEBVIEW, WIDGET_TYPE_CUSTOM_TEXTVIEW, WIDGET_TYPE_CUSTOM_EDITTEXT,
+ WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW, WIDGET_TYPE_UNKNOWN})
+ @interface WidgetType {}
+
+ /** The widget involved in the text classification session is a standard
+ * {@link android.widget.TextView}. */
+ String WIDGET_TYPE_TEXTVIEW = "textview";
+ /** The widget involved in the text classification session is a standard
+ * {@link android.widget.EditText}. */
+ String WIDGET_TYPE_EDITTEXT = "edittext";
+ /** The widget involved in the text classification session is a standard non-selectable
+ * {@link android.widget.TextView}. */
+ String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview";
+ /** The widget involved in the text classification session is a standard
+ * {@link android.webkit.WebView}. */
+ String WIDGET_TYPE_WEBVIEW = "webview";
+ /** The widget involved in the text classification session is a standard editable
+ * {@link android.webkit.WebView}. */
+ String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview";
+ /** The widget involved in the text classification session is a custom text widget. */
+ String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview";
+ /** The widget involved in the text classification session is a custom editable text widget. */
+ String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit";
+ /** The widget involved in the text classification session is a custom non-selectable text
+ * widget. */
+ String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
+ /** The widget involved in the text classification session is of an unknown/unspecified type. */
+ String WIDGET_TYPE_UNKNOWN = "unknown";
+
/**
* No-op TextClassifier.
* This may be used to turn off TextClassifier features.
@@ -124,6 +156,9 @@ public interface TextClassifier {
*
* <p><strong>NOTE: </strong>Call on a worker thread.
*
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
* @param text text providing context for the selected text (which is specified
* by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
* @param selectionStartIndex start index of the selected part of text
@@ -156,6 +191,9 @@ public interface TextClassifier {
*
* <p><strong>NOTE: </strong>Call on a worker thread.
*
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
* @param text text providing context for the selected text (which is specified
* by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
* @param selectionStartIndex start index of the selected part of text
@@ -185,6 +223,9 @@ public interface TextClassifier {
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method
* calls this method, a stack overflow error will happen.
+ *
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
*/
@WorkerThread
@NonNull
@@ -205,6 +246,9 @@ public interface TextClassifier {
*
* <p><strong>NOTE: </strong>Call on a worker thread.
*
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
* @param text text providing context for the text to classify (which is specified
* by the sub sequence starting at startIndex and ending at endIndex)
* @param startIndex start index of the text to classify
@@ -237,6 +281,9 @@ public interface TextClassifier {
* {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method
* calls this method, a stack overflow error will happen.
*
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
* @param text text providing context for the text to classify (which is specified
* by the sub sequence starting at startIndex and ending at endIndex)
* @param startIndex start index of the text to classify
@@ -265,6 +312,9 @@ public interface TextClassifier {
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method
* calls this method, a stack overflow error will happen.
+ *
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
*/
@WorkerThread
@NonNull
@@ -285,6 +335,9 @@ public interface TextClassifier {
*
* <p><strong>NOTE: </strong>Call on a worker thread.
*
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
* @param text the text to generate annotations for
* @param options configuration for link generation
*
@@ -295,6 +348,7 @@ public interface TextClassifier {
* @see #getMaxGenerateLinksTextLength()
*/
@WorkerThread
+ @NonNull
default TextLinks generateLinks(
@NonNull CharSequence text, @Nullable TextLinks.Options options) {
Utils.validate(text, false);
@@ -311,6 +365,9 @@ public interface TextClassifier {
* {@link #generateLinks(CharSequence, TextLinks.Options)}. If that method calls this method,
* a stack overflow error will happen.
*
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
* @param text the text to generate annotations for
*
* @throws IllegalArgumentException if text is null or the text is too long for the
@@ -320,6 +377,7 @@ public interface TextClassifier {
* @see #getMaxGenerateLinksTextLength()
*/
@WorkerThread
+ @NonNull
default TextLinks generateLinks(@NonNull CharSequence text) {
return generateLinks(text, null);
}
@@ -327,6 +385,9 @@ public interface TextClassifier {
/**
* Returns the maximal length of text that can be processed by generateLinks.
*
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
* @see #generateLinks(CharSequence)
* @see #generateLinks(CharSequence, TextLinks.Options)
*/
@@ -339,6 +400,7 @@ public interface TextClassifier {
* Returns a helper for logging TextClassifier related events.
*
* @param config logger configuration
+ * @hide
*/
@WorkerThread
default Logger getLogger(@NonNull Logger.Config config) {
@@ -347,6 +409,37 @@ public interface TextClassifier {
}
/**
+ * Reports a selection event.
+ *
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ */
+ default void onSelectionEvent(@NonNull SelectionEvent event) {}
+
+ /**
+ * Destroys this TextClassifier.
+ *
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to its methods should
+ * throw an {@link IllegalStateException}. See {@link #isDestroyed()}.
+ *
+ * <p>Subsequent calls to this method are no-ops.
+ */
+ default void destroy() {}
+
+ /**
+ * Returns whether or not this TextClassifier has been destroyed.
+ *
+ * <strong>NOTE: </strong>If a TextClassifier has been destroyed, caller should not interact
+ * with the classifier and an attempt to do so would throw an {@link IllegalStateException}.
+ * However, this method should never throw an {@link IllegalStateException}.
+ *
+ * @see #destroy()
+ */
+ default boolean isDestroyed() {
+ return false;
+ }
+
+ /**
* Configuration object for specifying what entities to identify.
*
* Configs are initially based on a predefined preset, and can be modified from there.
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index c2fb032d3e60..316442728c73 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -93,6 +93,8 @@ public final class TextClassifierImpl implements TextClassifier {
private Logger.Config mLoggerConfig;
@GuardedBy("mLoggerLock") // Do not access outside this lock.
private Logger mLogger;
+ @GuardedBy("mLoggerLock") // Do not access outside this lock.
+ private Logger mLogger2; // This is the new logger. Will replace mLogger.
private final TextClassificationConstants mSettings;
@@ -299,6 +301,18 @@ public final class TextClassifierImpl implements TextClassifier {
return mLogger;
}
+ @Override
+ public void onSelectionEvent(SelectionEvent event) {
+ Preconditions.checkNotNull(event);
+ synchronized (mLoggerLock) {
+ if (mLogger2 == null) {
+ mLogger2 = new DefaultLogger(
+ new Logger.Config(mContext, WIDGET_TYPE_UNKNOWN, null));
+ }
+ mLogger2.writeEvent(event);
+ }
+ }
+
private TextClassifierImplNative getNative(LocaleList localeList)
throws FileNotFoundException {
synchronized (mLock) {
diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java
index 6e855ba3f8f7..9e4e6d6ac355 100644
--- a/core/java/android/widget/SelectionActionModeHelper.java
+++ b/core/java/android/widget/SelectionActionModeHelper.java
@@ -35,6 +35,7 @@ import android.util.Log;
import android.view.ActionMode;
import android.view.textclassifier.Logger;
import android.view.textclassifier.SelectionEvent;
+import android.view.textclassifier.SelectionEvent.InvocationMethod;
import android.view.textclassifier.TextClassification;
import android.view.textclassifier.TextClassificationConstants;
import android.view.textclassifier.TextClassificationManager;
@@ -86,7 +87,7 @@ public final class SelectionActionModeHelper {
mTextClassificationSettings = TextClassificationManager.getSettings(mTextView.getContext());
mTextClassificationHelper = new TextClassificationHelper(
mTextView.getContext(),
- mTextView.getTextClassifier(),
+ mTextView::getTextClassifier,
getText(mTextView),
0, 1, mTextView.getTextLocales());
mSelectionTracker = new SelectionTracker(mTextView);
@@ -218,7 +219,7 @@ public final class SelectionActionModeHelper {
private boolean skipTextClassification() {
// No need to make an async call for a no-op TextClassifier.
- final boolean noOpTextClassifier = mTextView.getTextClassifier() == TextClassifier.NO_OP;
+ final boolean noOpTextClassifier = mTextView.usesNoOpTextClassifier();
// Do not call the TextClassifier if there is no selection.
final boolean noSelection = mTextView.getSelectionEnd() == mTextView.getSelectionStart();
// Do not call the TextClassifier if this is a password field.
@@ -444,7 +445,7 @@ public final class SelectionActionModeHelper {
selectionEnd = mTextView.getSelectionEnd();
}
mTextClassificationHelper.init(
- mTextView.getTextClassifier(),
+ mTextView::getTextClassifier,
getText(mTextView),
selectionStart, selectionEnd,
mTextView.getTextLocales());
@@ -657,6 +658,7 @@ public final class SelectionActionModeHelper {
private static final String LOG_TAG = "SelectionMetricsLogger";
private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
+ private final Supplier<TextClassifier> mTextClassificationSession;
private final Logger mLogger;
private final boolean mEditTextLogger;
private final BreakIterator mTokenIterator;
@@ -665,26 +667,27 @@ public final class SelectionActionModeHelper {
SelectionMetricsLogger(TextView textView) {
Preconditions.checkNotNull(textView);
+ mTextClassificationSession = textView::getTextClassificationSession;
mLogger = textView.getTextClassifier().getLogger(
new Logger.Config(textView.getContext(), getWidetType(textView), null));
mEditTextLogger = textView.isTextEditable();
mTokenIterator = mLogger.getTokenIterator(textView.getTextLocale());
}
- @Logger.WidgetType
+ @TextClassifier.WidgetType
private static String getWidetType(TextView textView) {
if (textView.isTextEditable()) {
- return Logger.WIDGET_EDITTEXT;
+ return TextClassifier.WIDGET_TYPE_EDITTEXT;
}
if (textView.isTextSelectable()) {
- return Logger.WIDGET_TEXTVIEW;
+ return TextClassifier.WIDGET_TYPE_TEXTVIEW;
}
- return Logger.WIDGET_UNSELECTABLE_TEXTVIEW;
+ return TextClassifier.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW;
}
public void logSelectionStarted(
CharSequence text, int index,
- @SelectionEvent.InvocationMethod int invocationMethod) {
+ @InvocationMethod int invocationMethod) {
try {
Preconditions.checkNotNull(text);
Preconditions.checkArgumentInRange(index, 0, text.length(), "index");
@@ -694,9 +697,12 @@ public final class SelectionActionModeHelper {
mTokenIterator.setText(mText);
mStartIndex = index;
mLogger.logSelectionStartedEvent(invocationMethod, 0);
+ // TODO: Remove the above legacy logging.
+ mTextClassificationSession.get().onSelectionEvent(
+ SelectionEvent.createSelectionStartedEvent(invocationMethod, 0));
} catch (Exception e) {
// Avoid crashes due to logging.
- Log.d(LOG_TAG, e.getMessage());
+ Log.e(LOG_TAG, "" + e.getMessage(), e);
}
}
@@ -709,16 +715,28 @@ public final class SelectionActionModeHelper {
if (selection != null) {
mLogger.logSelectionModifiedEvent(
wordIndices[0], wordIndices[1], selection);
+ // TODO: Remove the above legacy logging.
+ mTextClassificationSession.get().onSelectionEvent(
+ SelectionEvent.createSelectionModifiedEvent(
+ wordIndices[0], wordIndices[1], selection));
} else if (classification != null) {
mLogger.logSelectionModifiedEvent(
wordIndices[0], wordIndices[1], classification);
+ // TODO: Remove the above legacy logging.
+ mTextClassificationSession.get().onSelectionEvent(
+ SelectionEvent.createSelectionModifiedEvent(
+ wordIndices[0], wordIndices[1], classification));
} else {
mLogger.logSelectionModifiedEvent(
wordIndices[0], wordIndices[1]);
+ // TODO: Remove the above legacy logging.
+ mTextClassificationSession.get().onSelectionEvent(
+ SelectionEvent.createSelectionModifiedEvent(
+ wordIndices[0], wordIndices[1]));
}
} catch (Exception e) {
// Avoid crashes due to logging.
- Log.d(LOG_TAG, e.getMessage());
+ Log.e(LOG_TAG, "" + e.getMessage(), e);
}
}
@@ -733,13 +751,25 @@ public final class SelectionActionModeHelper {
if (classification != null) {
mLogger.logSelectionActionEvent(
wordIndices[0], wordIndices[1], action, classification);
+ // TODO: Remove the above legacy logging.
+ mTextClassificationSession.get().onSelectionEvent(
+ SelectionEvent.createSelectionActionEvent(
+ wordIndices[0], wordIndices[1], action, classification));
} else {
mLogger.logSelectionActionEvent(
wordIndices[0], wordIndices[1], action);
+ // TODO: Remove the above legacy logging.
+ mTextClassificationSession.get().onSelectionEvent(
+ SelectionEvent.createSelectionActionEvent(
+ wordIndices[0], wordIndices[1], action));
}
} catch (Exception e) {
// Avoid crashes due to logging.
- Log.d(LOG_TAG, e.getMessage());
+ Log.e(LOG_TAG, "" + e.getMessage(), e);
+ } finally {
+ if (SelectionEvent.isTerminal(action)) {
+ mTextClassificationSession.get().destroy();
+ }
}
}
@@ -880,7 +910,7 @@ public final class SelectionActionModeHelper {
private final Context mContext;
private final boolean mDarkLaunchEnabled;
- private TextClassifier mTextClassifier;
+ private Supplier<TextClassifier> mTextClassifier;
/** The original TextView text. **/
private String mText;
@@ -912,7 +942,7 @@ public final class SelectionActionModeHelper {
/** Whether the TextClassifier has been initialized. */
private boolean mHot;
- TextClassificationHelper(Context context, TextClassifier textClassifier,
+ TextClassificationHelper(Context context, Supplier<TextClassifier> textClassifier,
CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
init(textClassifier, text, selectionStart, selectionEnd, locales);
mContext = Preconditions.checkNotNull(context);
@@ -921,7 +951,7 @@ public final class SelectionActionModeHelper {
}
@UiThread
- public void init(TextClassifier textClassifier, CharSequence text,
+ public void init(Supplier<TextClassifier> textClassifier, CharSequence text,
int selectionStart, int selectionEnd, LocaleList locales) {
mTextClassifier = Preconditions.checkNotNull(textClassifier);
mText = Preconditions.checkNotNull(text).toString();
@@ -946,11 +976,11 @@ public final class SelectionActionModeHelper {
trimText();
final TextSelection selection;
if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) {
- selection = mTextClassifier.suggestSelection(
+ selection = mTextClassifier.get().suggestSelection(
mTrimmedText, mRelativeStart, mRelativeEnd, mSelectionOptions);
} else {
// Use old APIs.
- selection = mTextClassifier.suggestSelection(
+ selection = mTextClassifier.get().suggestSelection(
mTrimmedText, mRelativeStart, mRelativeEnd,
mSelectionOptions.getDefaultLocales());
}
@@ -995,11 +1025,11 @@ public final class SelectionActionModeHelper {
trimText();
final TextClassification classification;
if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) {
- classification = mTextClassifier.classifyText(
+ classification = mTextClassifier.get().classifyText(
mTrimmedText, mRelativeStart, mRelativeEnd, mClassificationOptions);
} else {
// Use old APIs.
- classification = mTextClassifier.classifyText(
+ classification = mTextClassifier.get().classifyText(
mTrimmedText, mRelativeStart, mRelativeEnd,
mClassificationOptions.getDefaultLocales());
}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index c366a9129d34..6f25577d73ef 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -163,6 +163,7 @@ import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassificationContext;
import android.view.textclassifier.TextClassificationManager;
import android.view.textclassifier.TextClassifier;
import android.view.textclassifier.TextLinks;
@@ -427,6 +428,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private boolean mPreDrawListenerDetached;
private TextClassifier mTextClassifier;
+ private TextClassifier mTextClassificationSession;
// A flag to prevent repeated movements from escaping the enclosing text view. The idea here is
// that if a user is holding down a movement key to traverse text, we shouldn't also traverse
@@ -11509,18 +11511,63 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@NonNull
public TextClassifier getTextClassifier() {
if (mTextClassifier == null) {
- TextClassificationManager tcm =
+ final TextClassificationManager tcm =
mContext.getSystemService(TextClassificationManager.class);
if (tcm != null) {
- mTextClassifier = tcm.getTextClassifier();
- } else {
- mTextClassifier = TextClassifier.NO_OP;
+ return tcm.getTextClassifier();
}
+ return TextClassifier.NO_OP;
}
return mTextClassifier;
}
/**
+ * Returns a session-aware text classifier.
+ */
+ @NonNull
+ TextClassifier getTextClassificationSession() {
+ if (mTextClassificationSession == null || mTextClassificationSession.isDestroyed()) {
+ final TextClassificationManager tcm =
+ mContext.getSystemService(TextClassificationManager.class);
+ if (tcm != null) {
+ final String widgetType;
+ if (isTextEditable()) {
+ widgetType = TextClassifier.WIDGET_TYPE_EDITTEXT;
+ } else if (isTextSelectable()) {
+ widgetType = TextClassifier.WIDGET_TYPE_TEXTVIEW;
+ } else {
+ widgetType = TextClassifier.WIDGET_TYPE_UNSELECTABLE_TEXTVIEW;
+ }
+ // TODO: Tagged this widgetType with a * so it we can monitor if it reports
+ // SelectionEvents exactly as the older Logger does. Remove once investigations
+ // are complete.
+ final TextClassificationContext textClassificationContext =
+ new TextClassificationContext.Builder(
+ mContext.getPackageName(), "*" + widgetType)
+ .build();
+ if (mTextClassifier != null) {
+ mTextClassificationSession = tcm.createTextClassificationSession(
+ textClassificationContext, mTextClassifier);
+ } else {
+ mTextClassificationSession = tcm.createTextClassificationSession(
+ textClassificationContext);
+ }
+ } else {
+ mTextClassificationSession = TextClassifier.NO_OP;
+ }
+ }
+ return mTextClassificationSession;
+ }
+
+ /**
+ * Returns true if this TextView uses a no-op TextClassifier.
+ */
+ boolean usesNoOpTextClassifier() {
+ return getTextClassifier() == TextClassifier.NO_OP;
+ }
+
+
+ /**
* Starts an ActionMode for the specified TextLinkSpan.
*
* @return Whether or not we're attempting to start the action mode.