Merge "Implement Stateful TextClassifier APIs." into pi-dev am: 119d10deb9

am: 12dea84530

Change-Id: I248b3ffff26747f3cb95b6caf44bfcbe8dae10a3
This commit is contained in:
Abodunrinwa Toki
2018-03-29 00:10:10 +00:00
committed by android-build-merger
14 changed files with 1057 additions and 120 deletions

View File

@@ -50317,38 +50317,13 @@ package android.view.inputmethod {
package android.view.textclassifier {
public abstract class Logger {
ctor public Logger(android.view.textclassifier.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, int);
method public abstract void writeEvent(android.view.textclassifier.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 implements android.os.Parcelable {
method public static android.view.textclassifier.SelectionEvent createSelectionActionEvent(int, int, int);
method public static android.view.textclassifier.SelectionEvent createSelectionActionEvent(int, int, int, android.view.textclassifier.TextClassification);
method public static android.view.textclassifier.SelectionEvent createSelectionModifiedEvent(int, int);
method public static android.view.textclassifier.SelectionEvent createSelectionModifiedEvent(int, int, android.view.textclassifier.TextClassification);
method public static android.view.textclassifier.SelectionEvent createSelectionModifiedEvent(int, int, android.view.textclassifier.TextSelection);
method public static android.view.textclassifier.SelectionEvent createSelectionStartedEvent(int, int);
method public int describeContents();
method public long getDurationSincePreviousEvent();
method public long getDurationSinceSessionStart();
@@ -50359,13 +50334,14 @@ package android.view.textclassifier {
method public int getEventType();
method public int getInvocationMethod();
method public java.lang.String getPackageName();
method public java.lang.String getSessionId();
method public android.view.textclassifier.TextClassificationSessionId 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();
method public static boolean isTerminal(int);
method public void writeToParcel(android.os.Parcel, int);
field public static final int ACTION_ABANDON = 107; // 0x6b
field public static final int ACTION_COPY = 101; // 0x65
@@ -50386,6 +50362,7 @@ package android.view.textclassifier {
field public static final int EVENT_SMART_SELECTION_SINGLE = 3; // 0x3
field public static final int INVOCATION_LINK = 2; // 0x2
field public static final int INVOCATION_MANUAL = 1; // 0x1
field public static final int INVOCATION_UNKNOWN = 0; // 0x0
}
public final class TextClassification implements android.os.Parcelable {
@@ -50428,19 +50405,45 @@ package android.view.textclassifier {
field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextClassification.Options> CREATOR;
}
public final class TextClassificationContext {
method public java.lang.String getPackageName();
method public java.lang.String getWidgetType();
method public java.lang.String getWidgetVersion();
}
public static final class TextClassificationContext.Builder {
ctor public TextClassificationContext.Builder(java.lang.String, java.lang.String);
method public android.view.textclassifier.TextClassificationContext build();
method public android.view.textclassifier.TextClassificationContext.Builder setWidgetVersion(java.lang.String);
}
public final class TextClassificationManager {
method public android.view.textclassifier.TextClassifier createTextClassificationSession(android.view.textclassifier.TextClassificationContext);
method public android.view.textclassifier.TextClassifier getTextClassifier();
method public void setTextClassificationSessionFactory(android.view.textclassifier.TextClassificationSessionFactory);
method public void setTextClassifier(android.view.textclassifier.TextClassifier);
}
public abstract interface TextClassificationSessionFactory {
method public abstract android.view.textclassifier.TextClassifier createTextClassificationSession(android.view.textclassifier.TextClassificationContext);
}
public final class TextClassificationSessionId implements android.os.Parcelable {
method public int describeContents();
method public void writeToParcel(android.os.Parcel, int);
field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextClassificationSessionId> CREATOR;
}
public abstract interface TextClassifier {
method public default android.view.textclassifier.TextClassification classifyText(java.lang.CharSequence, int, int, android.view.textclassifier.TextClassification.Options);
method public default android.view.textclassifier.TextClassification classifyText(java.lang.CharSequence, int, int);
method public default android.view.textclassifier.TextClassification classifyText(java.lang.CharSequence, int, int, android.os.LocaleList);
method public default void destroy();
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 android.view.textclassifier.Logger getLogger(android.view.textclassifier.Logger.Config);
method public default int getMaxGenerateLinksTextLength();
method public default boolean isDestroyed();
method public default void onSelectionEvent(android.view.textclassifier.SelectionEvent);
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);
@@ -50456,6 +50459,15 @@ package android.view.textclassifier {
field public static final java.lang.String TYPE_PHONE = "phone";
field public static final java.lang.String TYPE_UNKNOWN = "";
field public static final java.lang.String TYPE_URL = "url";
field public static final java.lang.String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit";
field public static final java.lang.String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview";
field public static final java.lang.String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview";
field public static final java.lang.String WIDGET_TYPE_EDITTEXT = "edittext";
field public static final java.lang.String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview";
field public static final java.lang.String WIDGET_TYPE_TEXTVIEW = "textview";
field public static final java.lang.String WIDGET_TYPE_UNKNOWN = "unknown";
field public static final java.lang.String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview";
field public static final java.lang.String WIDGET_TYPE_WEBVIEW = "webview";
}
public static final class TextClassifier.EntityConfig implements android.os.Parcelable {

View File

@@ -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;

View File

@@ -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;

View File

@@ -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,10 +421,20 @@ 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.
*/
@@ -255,6 +442,13 @@ public final class SelectionEvent implements Parcelable {
return mInvocationMethod;
}
/**
* 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.
*/
@@ -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

View File

@@ -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);
}
}
}

View File

@@ -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()) {

View File

@@ -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));
}
}
}

View File

@@ -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);
}

View File

@@ -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];
}
};
}

View File

@@ -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) {
@@ -346,6 +408,37 @@ public interface TextClassifier {
return Logger.DISABLED;
}
/**
* 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.
*

View File

@@ -94,6 +94,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;
@@ -300,6 +302,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) {

View File

@@ -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);
@@ -221,7 +222,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.
@@ -447,7 +448,7 @@ public final class SelectionActionModeHelper {
selectionEnd = mTextView.getSelectionEnd();
}
mTextClassificationHelper.init(
mTextView.getTextClassifier(),
mTextView::getTextClassifier,
getText(mTextView),
selectionStart, selectionEnd,
mTextView.getTextLocales());
@@ -660,6 +661,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;
@@ -668,26 +670,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");
@@ -697,9 +700,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);
}
}
@@ -712,16 +718,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);
}
}
@@ -736,13 +754,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();
}
}
}
@@ -887,7 +917,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;
@@ -919,7 +949,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);
@@ -928,7 +958,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();
@@ -953,11 +983,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());
}
@@ -1006,11 +1036,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());
}

View File

@@ -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
@@ -11543,17 +11545,62 @@ 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.
*

View File

@@ -33,8 +33,11 @@ public class SelectionEventTest {
@Test
public void testParcel() {
final SelectionEvent[] captured = new SelectionEvent[1];
final Logger logger = new Logger(new Logger.Config(
InstrumentationRegistry.getTargetContext(), Logger.WIDGET_TEXTVIEW, null)) {
final Logger logger = new Logger(
new Logger.Config(
InstrumentationRegistry.getTargetContext(),
TextClassifier.WIDGET_TYPE_TEXTVIEW,
null)) {
@Override
public void writeEvent(SelectionEvent event) {
captured[0] = event;