diff options
Diffstat (limited to 'core/java')
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. |
