diff options
Diffstat (limited to 'core/java')
26 files changed, 1196 insertions, 821 deletions
diff --git a/core/java/android/service/textclassifier/ITextClassifierService.aidl b/core/java/android/service/textclassifier/ITextClassifierService.aidl index 25e9d454efb5..7ac72c7c8e83 100644 --- a/core/java/android/service/textclassifier/ITextClassifierService.aidl +++ b/core/java/android/service/textclassifier/ITextClassifierService.aidl @@ -21,30 +21,41 @@ import android.service.textclassifier.ITextLinksCallback; import android.service.textclassifier.ITextSelectionCallback; import android.view.textclassifier.SelectionEvent; import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassificationContext; +import android.view.textclassifier.TextClassificationSessionId; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; /** * TextClassifierService binder interface. - * See TextClassifier (and TextClassifier.Logger) for interface documentation. + * See TextClassifier for interface documentation. * {@hide} */ oneway interface ITextClassifierService { void onSuggestSelection( - in CharSequence text, int selectionStartIndex, int selectionEndIndex, - in TextSelection.Options options, - in ITextSelectionCallback c); + in TextClassificationSessionId sessionId, + in TextSelection.Request request, + in ITextSelectionCallback callback); void onClassifyText( - in CharSequence text, int startIndex, int endIndex, - in TextClassification.Options options, - in ITextClassificationCallback c); + in TextClassificationSessionId sessionId, + in TextClassification.Request request, + in ITextClassificationCallback callback); void onGenerateLinks( - in CharSequence text, - in TextLinks.Options options, - in ITextLinksCallback c); + in TextClassificationSessionId sessionId, + in TextLinks.Request request, + in ITextLinksCallback callback); - void onSelectionEvent(in SelectionEvent event); + void onSelectionEvent( + in TextClassificationSessionId sessionId, + in SelectionEvent event); + + void onCreateTextClassificationSession( + in TextClassificationContext context, + in TextClassificationSessionId sessionId); + + void onDestroyTextClassificationSession( + in TextClassificationSessionId sessionId); } diff --git a/core/java/android/service/textclassifier/TextClassifierService.java b/core/java/android/service/textclassifier/TextClassifierService.java index 88e29b0d3d11..f1bb72cfacf1 100644 --- a/core/java/android/service/textclassifier/TextClassifierService.java +++ b/core/java/android/service/textclassifier/TextClassifierService.java @@ -17,7 +17,6 @@ package android.service.textclassifier; import android.Manifest; -import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.SystemApi; @@ -35,11 +34,15 @@ import android.text.TextUtils; import android.util.Slog; import android.view.textclassifier.SelectionEvent; import android.view.textclassifier.TextClassification; +import android.view.textclassifier.TextClassificationContext; import android.view.textclassifier.TextClassificationManager; +import android.view.textclassifier.TextClassificationSessionId; import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; +import com.android.internal.util.Preconditions; + /** * Abstract base class for the TextClassifier service. * @@ -88,11 +91,13 @@ public abstract class TextClassifierService extends Service { /** {@inheritDoc} */ @Override public void onSuggestSelection( - CharSequence text, int selectionStartIndex, int selectionEndIndex, - TextSelection.Options options, ITextSelectionCallback callback) + TextClassificationSessionId sessionId, + TextSelection.Request request, ITextSelectionCallback callback) throws RemoteException { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(callback); TextClassifierService.this.onSuggestSelection( - text, selectionStartIndex, selectionEndIndex, options, mCancellationSignal, + sessionId, request, mCancellationSignal, new Callback<TextSelection>() { @Override public void onSuccess(TextSelection result) { @@ -119,11 +124,13 @@ public abstract class TextClassifierService extends Service { /** {@inheritDoc} */ @Override public void onClassifyText( - CharSequence text, int startIndex, int endIndex, - TextClassification.Options options, ITextClassificationCallback callback) + TextClassificationSessionId sessionId, + TextClassification.Request request, ITextClassificationCallback callback) throws RemoteException { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(callback); TextClassifierService.this.onClassifyText( - text, startIndex, endIndex, options, mCancellationSignal, + sessionId, request, mCancellationSignal, new Callback<TextClassification>() { @Override public void onSuccess(TextClassification result) { @@ -148,10 +155,13 @@ public abstract class TextClassifierService extends Service { /** {@inheritDoc} */ @Override public void onGenerateLinks( - CharSequence text, TextLinks.Options options, ITextLinksCallback callback) + TextClassificationSessionId sessionId, + TextLinks.Request request, ITextLinksCallback callback) throws RemoteException { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(callback); TextClassifierService.this.onGenerateLinks( - text, options, mCancellationSignal, + sessionId, request, mCancellationSignal, new Callback<TextLinks>() { @Override public void onSuccess(TextLinks result) { @@ -175,8 +185,28 @@ public abstract class TextClassifierService extends Service { /** {@inheritDoc} */ @Override - public void onSelectionEvent(SelectionEvent event) throws RemoteException { - TextClassifierService.this.onSelectionEvent(event); + public void onSelectionEvent( + TextClassificationSessionId sessionId, + SelectionEvent event) throws RemoteException { + Preconditions.checkNotNull(event); + TextClassifierService.this.onSelectionEvent(sessionId, event); + } + + /** {@inheritDoc} */ + @Override + public void onCreateTextClassificationSession( + TextClassificationContext context, TextClassificationSessionId sessionId) + throws RemoteException { + Preconditions.checkNotNull(context); + Preconditions.checkNotNull(sessionId); + TextClassifierService.this.onCreateTextClassificationSession(context, sessionId); + } + + /** {@inheritDoc} */ + @Override + public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) + throws RemoteException { + TextClassifierService.this.onDestroyTextClassificationSession(sessionId); } }; @@ -193,19 +223,14 @@ public abstract class TextClassifierService extends Service { * Returns suggested text selection start and end indices, recognized entity types, and their * associated confidence scores. The entity types are ordered from highest to lowest scoring. * - * @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 - * @param selectionEndIndex end index of the selected part of text - * @param options optional input parameters + * @param sessionId the session id + * @param request the text selection request * @param cancellationSignal object to watch for canceling the current operation * @param callback the callback to return the result to */ public abstract void onSuggestSelection( - @NonNull CharSequence text, - @IntRange(from = 0) int selectionStartIndex, - @IntRange(from = 0) int selectionEndIndex, - @Nullable TextSelection.Options options, + @Nullable TextClassificationSessionId sessionId, + @NonNull TextSelection.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextSelection> callback); @@ -213,19 +238,14 @@ public abstract class TextClassifierService extends Service { * Classifies the specified text and returns a {@link TextClassification} object that can be * used to generate a widget for handling the classified text. * - * @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 - * @param endIndex end index of the text to classify - * @param options optional input parameters + * @param sessionId the session id + * @param request the text classification request * @param cancellationSignal object to watch for canceling the current operation * @param callback the callback to return the result to */ public abstract void onClassifyText( - @NonNull CharSequence text, - @IntRange(from = 0) int startIndex, - @IntRange(from = 0) int endIndex, - @Nullable TextClassification.Options options, + @Nullable TextClassificationSessionId sessionId, + @NonNull TextClassification.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextClassification> callback); @@ -233,14 +253,14 @@ public abstract class TextClassifierService extends Service { * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with * links information. * - * @param text the text to generate annotations for - * @param options configuration for link generation + * @param sessionId the session id + * @param request the text classification request * @param cancellationSignal object to watch for canceling the current operation * @param callback the callback to return the result to */ public abstract void onGenerateLinks( - @NonNull CharSequence text, - @Nullable TextLinks.Options options, + @Nullable TextClassificationSessionId sessionId, + @NonNull TextLinks.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextLinks> callback); @@ -250,8 +270,30 @@ public abstract class TextClassifierService extends Service { * happened. * * <p>The default implementation ignores the event. + * + * @param sessionId the session id + * @param event the selection event + */ + public void onSelectionEvent( + @Nullable TextClassificationSessionId sessionId, @NonNull SelectionEvent event) {} + + /** + * Creates a new text classification session for the specified context. + * + * @param context the text classification context + * @param sessionId the session's Id + */ + public abstract void onCreateTextClassificationSession( + @NonNull TextClassificationContext context, + @NonNull TextClassificationSessionId sessionId); + + /** + * Destroys the text classification session identified by the specified sessionId. + * + * @param sessionId the id of the session to destroy */ - public void onSelectionEvent(@NonNull SelectionEvent event) {} + public abstract void onDestroyTextClassificationSession( + @NonNull TextClassificationSessionId sessionId); /** * Returns a TextClassifier that runs in this service's process. diff --git a/core/java/android/text/util/Linkify.java b/core/java/android/text/util/Linkify.java index 435e32456343..9c6a3f5f8c72 100644 --- a/core/java/android/text/util/Linkify.java +++ b/core/java/android/text/util/Linkify.java @@ -33,6 +33,7 @@ import android.util.Patterns; import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextLinks.TextLinkSpan; +import android.view.textclassifier.TextLinksParams; import android.webkit.WebView; import android.widget.TextView; @@ -55,7 +56,6 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.Executor; import java.util.concurrent.Future; import java.util.function.Consumer; -import java.util.function.Function; import java.util.function.Supplier; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -502,15 +502,16 @@ public class Linkify { * calling thread. * * @param textView TextView whose text is to be marked-up with links - * @param options optional parameters to specify how to generate the links + * @param params optional parameters to specify how to generate the links * * @return a future that may be used to interrupt or query the background task + * @hide */ @UiThread public static Future<Void> addLinksAsync( @NonNull TextView textView, - @Nullable TextLinks.Options options) { - return addLinksAsync(textView, options, null /* executor */, null /* callback */); + @Nullable TextLinksParams params) { + return addLinksAsync(textView, params, null /* executor */, null /* callback */); } /** @@ -525,16 +526,42 @@ public class Linkify { * calling thread. * * @param textView TextView whose text is to be marked-up with links - * @param options optional parameters to specify how to generate the links + * @param mask mask to define which kinds of links will be generated + * + * @return a future that may be used to interrupt or query the background task + * @hide + */ + @UiThread + public static Future<Void> addLinksAsync( + @NonNull TextView textView, + @LinkifyMask int mask) { + return addLinksAsync(textView, TextLinksParams.fromLinkMask(mask), + null /* executor */, null /* callback */); + } + + /** + * Scans the text of the provided TextView and turns all occurrences of the entity types + * specified by {@code options} into clickable links. If links are found, this method + * removes any pre-existing {@link TextLinkSpan} attached to the text (to avoid + * problems if you call it repeatedly on the same text) and sets the movement method for the + * TextView to LinkMovementMethod. + * + * <p><strong>Note:</strong> This method returns immediately but generates the links with + * the specified classifier on a background thread. The generated links are applied on the + * calling thread. + * + * @param textView TextView whose text is to be marked-up with links + * @param params optional parameters to specify how to generate the links * @param executor Executor that runs the background task * @param callback Callback that receives the final status of the background task execution * * @return a future that may be used to interrupt or query the background task + * @hide */ @UiThread public static Future<Void> addLinksAsync( @NonNull TextView textView, - @Nullable TextLinks.Options options, + @Nullable TextLinksParams params, @Nullable Executor executor, @Nullable Consumer<Integer> callback) { Preconditions.checkNotNull(textView); @@ -548,7 +575,7 @@ public class Linkify { } }; return addLinksAsync(spannable, textView.getTextClassifier(), - options, executor, callback, modifyTextView); + params, executor, callback, modifyTextView); } /** @@ -566,15 +593,16 @@ public class Linkify { * * @param text Spannable whose text is to be marked-up with links * @param classifier the TextClassifier to use to generate the links - * @param options optional parameters to specify how to generate the links + * @param params optional parameters to specify how to generate the links * * @return a future that may be used to interrupt or query the background task + * @hide */ public static Future<Void> addLinksAsync( @NonNull Spannable text, @NonNull TextClassifier classifier, - @Nullable TextLinks.Options options) { - return addLinksAsync(text, classifier, options, null /* executor */, null /* callback */); + @Nullable TextLinksParams params) { + return addLinksAsync(text, classifier, params, null /* executor */, null /* callback */); } /** @@ -595,12 +623,13 @@ public class Linkify { * @param mask mask to define which kinds of links will be generated * * @return a future that may be used to interrupt or query the background task + * @hide */ public static Future<Void> addLinksAsync( @NonNull Spannable text, @NonNull TextClassifier classifier, @LinkifyMask int mask) { - return addLinksAsync(text, classifier, TextLinks.Options.fromLinkMask(mask), + return addLinksAsync(text, classifier, TextLinksParams.fromLinkMask(mask), null /* executor */, null /* callback */); } @@ -619,39 +648,46 @@ public class Linkify { * * @param text Spannable whose text is to be marked-up with links * @param classifier the TextClassifier to use to generate the links - * @param options optional parameters to specify how to generate the links + * @param params optional parameters to specify how to generate the links * @param executor Executor that runs the background task * @param callback Callback that receives the final status of the background task execution * * @return a future that may be used to interrupt or query the background task + * @hide */ public static Future<Void> addLinksAsync( @NonNull Spannable text, @NonNull TextClassifier classifier, - @Nullable TextLinks.Options options, + @Nullable TextLinksParams params, @Nullable Executor executor, @Nullable Consumer<Integer> callback) { - return addLinksAsync(text, classifier, options, executor, callback, + return addLinksAsync(text, classifier, params, executor, callback, null /* modifyTextView */); } private static Future<Void> addLinksAsync( @NonNull Spannable text, @NonNull TextClassifier classifier, - @Nullable TextLinks.Options options, + @Nullable TextLinksParams params, @Nullable Executor executor, @Nullable Consumer<Integer> callback, @Nullable Runnable modifyTextView) { Preconditions.checkNotNull(text); Preconditions.checkNotNull(classifier); + // TODO: This is a bug. We shouldnot call getMaxGenerateLinksTextLength() on the UI thread. // The input text may exceed the maximum length the text classifier can handle. In such // cases, we process the text up to the maximum length. final CharSequence truncatedText = text.subSequence( 0, Math.min(text.length(), classifier.getMaxGenerateLinksTextLength())); - final Supplier<TextLinks> supplier = () -> - classifier.generateLinks(truncatedText, options.setLegacyFallback(true)); + final TextClassifier.EntityConfig entityConfig = (params == null) + ? null : params.getEntityConfig(); + final TextLinks.Request request = new TextLinks.Request.Builder(truncatedText) + .setLegacyFallback(true) + .setEntityConfig(entityConfig) + .build(); + final Supplier<TextLinks> supplier = () -> classifier.generateLinks(request); final Consumer<TextLinks> consumer = links -> { if (links.getLinks().isEmpty()) { if (callback != null) { @@ -661,17 +697,13 @@ public class Linkify { } // Remove spans only for the part of the text we generated links for. - final TextLinkSpan[] old = text.getSpans(0, truncatedText.length(), TextLinkSpan.class); + final TextLinkSpan[] old = + text.getSpans(0, truncatedText.length(), TextLinkSpan.class); for (int i = old.length - 1; i >= 0; i--) { text.removeSpan(old[i]); } - final Function<TextLinks.TextLink, TextLinkSpan> spanFactory = (options == null) - ? null : options.getSpanFactory(); - final @TextLinks.ApplyStrategy int applyStrategy = (options == null) - ? TextLinks.APPLY_STRATEGY_IGNORE : options.getApplyStrategy(); - final @TextLinks.Status int result = links.apply(text, applyStrategy, spanFactory, - true /*allowPrefix*/); + final @TextLinks.Status int result = params.apply(text, links); if (result == TextLinks.STATUS_LINKS_APPLIED) { if (modifyTextView != null) { modifyTextView.run(); diff --git a/core/java/android/view/textclassifier/DefaultLogger.java b/core/java/android/view/textclassifier/DefaultLogger.java index 46ff44246280..203ca5606818 100644 --- a/core/java/android/view/textclassifier/DefaultLogger.java +++ b/core/java/android/view/textclassifier/DefaultLogger.java @@ -87,7 +87,7 @@ public final class DefaultLogger extends Logger { .addTaggedData(INDEX, event.getEventIndex()) .addTaggedData(WIDGET_TYPE, event.getWidgetType()) .addTaggedData(WIDGET_VERSION, event.getWidgetVersion()) - .addTaggedData(MODEL_NAME, SignatureParser.getModelName(event.getSignature())) + .addTaggedData(MODEL_NAME, SignatureParser.getModelName(event.getResultId())) .addTaggedData(ENTITY_TYPE, event.getEntityType()) .addTaggedData(SMART_START, event.getSmartStart()) .addTaggedData(SMART_END, event.getSmartEnd()) @@ -231,9 +231,9 @@ public final class DefaultLogger extends Logger { } /** - * Creates a signature string that may be used to tag TextClassifier results. + * Creates a string id that may be used to identify a TextClassifier result. */ - public static String createSignature( + public static String createId( String text, int start, int end, Context context, int modelVersion, List<Locale> locales) { Preconditions.checkNotNull(text); @@ -250,7 +250,7 @@ public final class DefaultLogger extends Logger { } /** - * Helper for creating and parsing signature strings for + * Helper for creating and parsing string ids for * {@link android.view.textclassifier.TextClassifierImpl}. */ @VisibleForTesting diff --git a/core/java/android/view/textclassifier/LinksInfo.java b/core/java/android/view/textclassifier/LinksInfo.java deleted file mode 100644 index 754c9e90ed4b..000000000000 --- a/core/java/android/view/textclassifier/LinksInfo.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (C) 2017 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package android.view.textclassifier; - -import android.annotation.NonNull; - -/** - * Link information that can be applied to text. See: {@link #apply(CharSequence)}. - * Typical implementations of this interface will annotate spannable text with e.g - * {@link android.text.style.ClickableSpan}s or other annotations. - * @hide - */ -public interface LinksInfo { - - /** - * @hide - */ - LinksInfo NO_OP = text -> false; - - /** - * Applies link annotations to the specified text. - * These annotations are not guaranteed to be applied. For example, the annotations may not be - * applied if the text has changed from what it was when the link spec was generated for it. - * - * @return Whether or not the link annotations were successfully applied. - */ - boolean apply(@NonNull CharSequence text); -} diff --git a/core/java/android/view/textclassifier/Log.java b/core/java/android/view/textclassifier/Log.java index 83ca15df92f4..ef19ee56a843 100644 --- a/core/java/android/view/textclassifier/Log.java +++ b/core/java/android/view/textclassifier/Log.java @@ -35,6 +35,10 @@ final class Log { Slog.d(tag, msg); } + public static void w(String tag, String msg) { + Slog.w(tag, msg); + } + public static void e(String tag, String msg, Throwable tr) { if (ENABLE_FULL_LOGGING) { Slog.e(tag, msg, tr); diff --git a/core/java/android/view/textclassifier/Logger.java b/core/java/android/view/textclassifier/Logger.java index c29d3e64a8b7..f03906a0b5ea 100644 --- a/core/java/android/view/textclassifier/Logger.java +++ b/core/java/android/view/textclassifier/Logger.java @@ -35,8 +35,6 @@ public abstract class Logger { private static final String LOG_TAG = "Logger"; /* package */ static final boolean DEBUG_LOG_ENABLED = true; - private static final String NO_SIGNATURE = ""; - private @SelectionEvent.InvocationMethod int mInvocationMethod; private SelectionEvent mPrevEvent; private SelectionEvent mSmartEvent; @@ -68,12 +66,12 @@ public abstract class Logger { public abstract void writeEvent(@NonNull SelectionEvent event); /** - * Returns true if the signature matches that of a smart selection event (i.e. + * Returns true if the resultId matches that of a smart selection event (i.e. * {@link SelectionEvent#EVENT_SMART_SELECTION_SINGLE} or * {@link SelectionEvent#EVENT_SMART_SELECTION_MULTI}). * Returns false otherwise. */ - public boolean isSmartSelection(@NonNull String signature) { + public boolean isSmartSelection(@NonNull String resultId) { return false; } @@ -99,7 +97,7 @@ public abstract class Logger { mInvocationMethod = invocationMethod; logEvent(new SelectionEvent( start, start + 1, SelectionEvent.EVENT_SELECTION_STARTED, - TextClassifier.TYPE_UNKNOWN, mInvocationMethod, NO_SIGNATURE, mConfig)); + TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig)); } /** @@ -118,7 +116,7 @@ public abstract class Logger { logEvent(new SelectionEvent( start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, - TextClassifier.TYPE_UNKNOWN, mInvocationMethod, NO_SIGNATURE, mConfig)); + TextClassifier.TYPE_UNKNOWN, mInvocationMethod, null, mConfig)); } /** @@ -142,10 +140,9 @@ public abstract class Logger { final String entityType = classification.getEntityCount() > 0 ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String signature = classification.getSignature(); logEvent(new SelectionEvent( start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, - entityType, mInvocationMethod, signature, mConfig)); + entityType, mInvocationMethod, classification.getId(), mConfig)); } /** @@ -167,7 +164,7 @@ public abstract class Logger { } final int eventType; - if (isSmartSelection(selection.getSignature())) { + if (isSmartSelection(selection.getId())) { eventType = end - start > 1 ? SelectionEvent.EVENT_SMART_SELECTION_MULTI : SelectionEvent.EVENT_SMART_SELECTION_SINGLE; @@ -178,9 +175,8 @@ public abstract class Logger { final String entityType = selection.getEntityCount() > 0 ? selection.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String signature = selection.getSignature(); - logEvent(new SelectionEvent(start, end, eventType, entityType, mInvocationMethod, signature, - mConfig)); + logEvent(new SelectionEvent(start, end, eventType, entityType, mInvocationMethod, + selection.getId(), mConfig)); } /** @@ -202,7 +198,7 @@ public abstract class Logger { logEvent(new SelectionEvent( start, end, actionType, TextClassifier.TYPE_UNKNOWN, mInvocationMethod, - NO_SIGNATURE, mConfig)); + null, mConfig)); } /** @@ -232,9 +228,8 @@ public abstract class Logger { final String entityType = classification.getEntityCount() > 0 ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String signature = classification.getSignature(); logEvent(new SelectionEvent(start, end, actionType, entityType, mInvocationMethod, - signature, mConfig)); + classification.getId(), mConfig)); } private void logEvent(@NonNull SelectionEvent event) { @@ -280,7 +275,7 @@ public abstract class Logger { .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); } if (mSmartEvent != null) { - event.setSignature(mSmartEvent.getSignature()) + event.setResultId(mSmartEvent.getResultId()) .setSmartStart(mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); } diff --git a/core/java/android/view/textclassifier/SelectionEvent.java b/core/java/android/view/textclassifier/SelectionEvent.java index 5a4d2cf1d481..1e978ccf5390 100644 --- a/core/java/android/view/textclassifier/SelectionEvent.java +++ b/core/java/android/view/textclassifier/SelectionEvent.java @@ -126,7 +126,7 @@ public final class SelectionEvent implements Parcelable { private String mWidgetType = TextClassifier.WIDGET_TYPE_UNKNOWN; private @InvocationMethod int mInvocationMethod; @Nullable private String mWidgetVersion; - private String mSignature; // TODO: Rename to resultId. + @Nullable private String mResultId; private long mEventTime; private long mDurationSinceSessionStart; private long mDurationSincePreviousEvent; @@ -140,21 +140,22 @@ public final class SelectionEvent implements Parcelable { SelectionEvent( int start, int end, @EventType int eventType, @EntityType String entityType, - @InvocationMethod int invocationMethod, String signature) { + @InvocationMethod int invocationMethod, @Nullable String resultId) { Preconditions.checkArgument(end >= start, "end cannot be less than start"); mAbsoluteStart = start; mAbsoluteEnd = end; mEventType = eventType; mEntityType = Preconditions.checkNotNull(entityType); - mSignature = Preconditions.checkNotNull(signature); + mResultId = resultId; 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); + @InvocationMethod int invocationMethod, @Nullable String resultId, + Logger.Config config) { + this(start, end, eventType, entityType, invocationMethod, resultId); Preconditions.checkNotNull(config); setTextClassificationSessionContext( new TextClassificationContext.Builder( @@ -172,7 +173,7 @@ public final class SelectionEvent implements Parcelable { mPackageName = in.readString(); mWidgetType = in.readString(); mInvocationMethod = in.readInt(); - mSignature = in.readString(); + mResultId = in.readString(); mEventTime = in.readLong(); mDurationSinceSessionStart = in.readLong(); mDurationSincePreviousEvent = in.readLong(); @@ -198,7 +199,7 @@ public final class SelectionEvent implements Parcelable { dest.writeString(mPackageName); dest.writeString(mWidgetType); dest.writeInt(mInvocationMethod); - dest.writeString(mSignature); + dest.writeString(mResultId); dest.writeLong(mEventTime); dest.writeLong(mDurationSinceSessionStart); dest.writeLong(mDurationSincePreviousEvent); @@ -270,7 +271,7 @@ public final class SelectionEvent implements Parcelable { : TextClassifier.TYPE_UNKNOWN; return new SelectionEvent( start, end, SelectionEvent.EVENT_SELECTION_MODIFIED, - entityType, INVOCATION_UNKNOWN, classification.getSignature()); + entityType, INVOCATION_UNKNOWN, classification.getId()); } /** @@ -294,7 +295,7 @@ public final class SelectionEvent implements Parcelable { : TextClassifier.TYPE_UNKNOWN; return new SelectionEvent( start, end, SelectionEvent.EVENT_AUTO_SELECTION, - entityType, INVOCATION_UNKNOWN, selection.getSignature()); + entityType, INVOCATION_UNKNOWN, selection.getId()); } /** @@ -342,7 +343,7 @@ public final class SelectionEvent implements Parcelable { ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; return new SelectionEvent(start, end, actionType, entityType, INVOCATION_UNKNOWN, - classification.getSignature()); + classification.getId()); } /** @@ -450,14 +451,15 @@ public final class SelectionEvent implements Parcelable { } /** - * Returns the signature of the text classifier result associated with this event. + * Returns the id of the text classifier result associated with this event. */ - public String getSignature() { - return mSignature; + @Nullable + public String getResultId() { + return mResultId; } - SelectionEvent setSignature(String signature) { - mSignature = Preconditions.checkNotNull(signature); + SelectionEvent setResultId(@Nullable String resultId) { + mResultId = resultId; return this; } @@ -604,7 +606,7 @@ public final class SelectionEvent implements Parcelable { @Override public int hashCode() { return Objects.hash(mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType, - mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, mSignature, + mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, mResultId, mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent, mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd); } @@ -627,7 +629,7 @@ public final class SelectionEvent implements Parcelable { && Objects.equals(mPackageName, other.mPackageName) && Objects.equals(mWidgetType, other.mWidgetType) && mInvocationMethod == other.mInvocationMethod - && Objects.equals(mSignature, other.mSignature) + && Objects.equals(mResultId, other.mResultId) && mEventTime == other.mEventTime && mDurationSinceSessionStart == other.mDurationSinceSessionStart && mDurationSincePreviousEvent == other.mDurationSincePreviousEvent @@ -642,15 +644,16 @@ public final class SelectionEvent implements Parcelable { @Override public String toString() { return String.format(Locale.US, - "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, " - + "widgetVersion=%s, packageName=%s, widgetType=%s, invocationMethod=%s, " - + "signature=%s, eventTime=%d, durationSinceSessionStart=%d, " - + "durationSincePreviousEvent=%d, eventIndex=%d, sessionId=%s, start=%d, end=%d, " - + "smartStart=%d, smartEnd=%d}", + "SelectionEvent {absoluteStart=%d, absoluteEnd=%d, eventType=%d, entityType=%s, " + + "widgetVersion=%s, packageName=%s, widgetType=%s, invocationMethod=%s, " + + "resultId=%s, eventTime=%d, durationSinceSessionStart=%d, " + + "durationSincePreviousEvent=%d, eventIndex=%d," + + "sessionId=%s, start=%d, end=%d, smartStart=%d, smartEnd=%d}", mAbsoluteStart, mAbsoluteEnd, mEventType, mEntityType, - mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, mSignature, - mEventTime, mDurationSinceSessionStart, mDurationSincePreviousEvent, - mEventIndex, mSessionId, mStart, mEnd, mSmartStart, mSmartEnd); + mWidgetVersion, mPackageName, mWidgetType, mInvocationMethod, + mResultId, mEventTime, mDurationSinceSessionStart, + mDurationSincePreviousEvent, mEventIndex, + mSessionId, mStart, mEnd, mSmartStart, mSmartEnd); } public static final Creator<SelectionEvent> CREATOR = new Creator<SelectionEvent>() { @@ -664,4 +667,4 @@ public final class SelectionEvent implements Parcelable { return new SelectionEvent[size]; } }; -} +}
\ No newline at end of file diff --git a/core/java/android/view/textclassifier/SystemTextClassifier.java b/core/java/android/view/textclassifier/SystemTextClassifier.java index 2a24dd731b4d..45fd6bfb1949 100644 --- a/core/java/android/view/textclassifier/SystemTextClassifier.java +++ b/core/java/android/view/textclassifier/SystemTextClassifier.java @@ -16,7 +16,6 @@ package android.view.textclassifier; -import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.Nullable; import android.annotation.WorkerThread; @@ -56,6 +55,8 @@ public final class SystemTextClassifier implements TextClassifier { private Logger.Config mLoggerConfig; @GuardedBy("mLoggerLock") private Logger mLogger; + @GuardedBy("mLoggerLock") + private TextClassificationSessionId mSessionId; public SystemTextClassifier(Context context, TextClassificationConstants settings) throws ServiceManager.ServiceNotFoundException { @@ -71,26 +72,20 @@ public final class SystemTextClassifier implements TextClassifier { */ @Override @WorkerThread - public TextSelection suggestSelection( - @NonNull CharSequence text, - @IntRange(from = 0) int selectionStartIndex, - @IntRange(from = 0) int selectionEndIndex, - @Nullable TextSelection.Options options) { - Utils.validate(text, selectionStartIndex, selectionEndIndex, false /* allowInMainThread */); + public TextSelection suggestSelection(TextSelection.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); try { final TextSelectionCallback callback = new TextSelectionCallback(); - mManagerService.onSuggestSelection( - text, selectionStartIndex, selectionEndIndex, options, callback); + mManagerService.onSuggestSelection(mSessionId, request, callback); final TextSelection selection = callback.mReceiver.get(); if (selection != null) { return selection; } - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } catch (InterruptedException e) { - Log.d(LOG_TAG, e.getMessage()); + } catch (RemoteException | InterruptedException e) { + Log.e(LOG_TAG, "Error suggesting selection for text. Using fallback.", e); } - return mFallback.suggestSelection(text, selectionStartIndex, selectionEndIndex, options); + return mFallback.suggestSelection(request); } /** @@ -98,25 +93,20 @@ public final class SystemTextClassifier implements TextClassifier { */ @Override @WorkerThread - public TextClassification classifyText( - @NonNull CharSequence text, - @IntRange(from = 0) int startIndex, - @IntRange(from = 0) int endIndex, - @Nullable TextClassification.Options options) { - Utils.validate(text, startIndex, endIndex, false /* allowInMainThread */); + public TextClassification classifyText(TextClassification.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); try { final TextClassificationCallback callback = new TextClassificationCallback(); - mManagerService.onClassifyText(text, startIndex, endIndex, options, callback); + mManagerService.onClassifyText(mSessionId, request, callback); final TextClassification classification = callback.mReceiver.get(); if (classification != null) { return classification; } - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } catch (InterruptedException e) { - Log.d(LOG_TAG, e.getMessage()); + } catch (RemoteException | InterruptedException e) { + Log.e(LOG_TAG, "Error classifying text. Using fallback.", e); } - return mFallback.classifyText(text, startIndex, endIndex, options); + return mFallback.classifyText(request); } /** @@ -124,33 +114,26 @@ public final class SystemTextClassifier implements TextClassifier { */ @Override @WorkerThread - public TextLinks generateLinks( - @NonNull CharSequence text, @Nullable TextLinks.Options options) { - Utils.validate(text, false /* allowInMainThread */); + public TextLinks generateLinks(@NonNull TextLinks.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); - final boolean legacyFallback = options != null && options.isLegacyFallback(); - if (!mSettings.isSmartLinkifyEnabled() && legacyFallback) { - return Utils.generateLegacyLinks(text, options); + if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) { + return Utils.generateLegacyLinks(request); } try { - if (options == null) { - options = new TextLinks.Options().setCallingPackageName(mPackageName); - } else if (!mPackageName.equals(options.getCallingPackageName())) { - options.setCallingPackageName(mPackageName); - } + request.setCallingPackageName(mPackageName); final TextLinksCallback callback = new TextLinksCallback(); - mManagerService.onGenerateLinks(text, options, callback); + mManagerService.onGenerateLinks(mSessionId, request, callback); final TextLinks links = callback.mReceiver.get(); if (links != null) { return links; } - } catch (RemoteException e) { - e.rethrowAsRuntimeException(); - } catch (InterruptedException e) { - Log.d(LOG_TAG, e.getMessage()); + } catch (RemoteException | InterruptedException e) { + Log.e(LOG_TAG, "Error generating links. Using fallback.", e); } - return mFallback.generateLinks(text, options); + return mFallback.generateLinks(request); } /** @@ -173,9 +156,9 @@ public final class SystemTextClassifier implements TextClassifier { @Override public void writeEvent(SelectionEvent event) { try { - mManagerService.onSelectionEvent(event); + mManagerService.onSelectionEvent(mSessionId, event); } catch (RemoteException e) { - e.rethrowAsRuntimeException(); + Log.e(LOG_TAG, "Error reporting selection event.", e); } } }; @@ -184,6 +167,34 @@ public final class SystemTextClassifier implements TextClassifier { return mLogger; } + @Override + public void destroy() { + try { + if (mSessionId != null) { + mManagerService.onDestroyTextClassificationSession(mSessionId); + } + } catch (RemoteException e) { + Log.e(LOG_TAG, "Error destroying classification session.", e); + } + } + + /** + * Attempts to initialize a new classification session. + * + * @param classificationContext the classification context + * @param sessionId the session's id + */ + void initializeRemoteSession( + @NonNull TextClassificationContext classificationContext, + @NonNull TextClassificationSessionId sessionId) { + mSessionId = Preconditions.checkNotNull(sessionId); + try { + mManagerService.onCreateTextClassificationSession(classificationContext, mSessionId); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Error starting a new classification session.", e); + } + } + private static final class TextSelectionCallback extends ITextSelectionCallback.Stub { final ResponseReceiver<TextSelection> mReceiver = new ResponseReceiver<>(); diff --git a/core/java/android/view/textclassifier/TextClassification.aidl b/core/java/android/view/textclassifier/TextClassification.aidl index 9fefe5d4176a..bfb143c4d0ac 100644 --- a/core/java/android/view/textclassifier/TextClassification.aidl +++ b/core/java/android/view/textclassifier/TextClassification.aidl @@ -17,4 +17,4 @@ package android.view.textclassifier; parcelable TextClassification; -parcelable TextClassification.Options;
\ No newline at end of file +parcelable TextClassification.Request;
\ No newline at end of file diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java index b413d48c6c78..37a5d9a10743 100644 --- a/core/java/android/view/textclassifier/TextClassification.java +++ b/core/java/android/view/textclassifier/TextClassification.java @@ -38,6 +38,7 @@ import android.os.Parcelable; import android.util.ArrayMap; import android.view.View.OnClickListener; import android.view.textclassifier.TextClassifier.EntityType; +import android.view.textclassifier.TextClassifier.Utils; import com.android.internal.util.Preconditions; @@ -123,7 +124,7 @@ public final class TextClassification implements Parcelable { @Nullable private final OnClickListener mLegacyOnClickListener; @NonNull private final List<RemoteAction> mActions; @NonNull private final EntityConfidence mEntityConfidence; - @NonNull private final String mSignature; + @Nullable private final String mId; private TextClassification( @Nullable String text, @@ -133,7 +134,7 @@ public final class TextClassification implements Parcelable { @Nullable OnClickListener legacyOnClickListener, @NonNull List<RemoteAction> actions, @NonNull Map<String, Float> entityConfidence, - @NonNull String signature) { + @Nullable String id) { mText = text; mLegacyIcon = legacyIcon; mLegacyLabel = legacyLabel; @@ -141,7 +142,7 @@ public final class TextClassification implements Parcelable { mLegacyOnClickListener = legacyOnClickListener; mActions = Collections.unmodifiableList(actions); mEntityConfidence = new EntityConfidence(entityConfidence); - mSignature = signature; + mId = id; } /** @@ -237,20 +238,18 @@ public final class TextClassification implements Parcelable { } /** - * Returns the signature for this object. - * The TextClassifier that generates this object may use it as a way to internally identify - * this object. + * Returns the id, if one exists, for this object. */ - @NonNull - public String getSignature() { - return mSignature; + @Nullable + public String getId() { + return mId; } @Override public String toString() { return String.format(Locale.US, - "TextClassification {text=%s, entities=%s, actions=%s, signature=%s}", - mText, mEntityConfidence, mActions, mSignature); + "TextClassification {text=%s, entities=%s, actions=%s, id=%s}", + mText, mEntityConfidence, mActions, mId); } /** @@ -264,7 +263,7 @@ public final class TextClassification implements Parcelable { try { intent.send(); } catch (PendingIntent.CanceledException e) { - Log.e(LOG_TAG, "Error creating OnClickListener from PendingIntent", e); + Log.e(LOG_TAG, "Error sending PendingIntent", e); } }; } @@ -289,25 +288,6 @@ public final class TextClassification implements Parcelable { } } - /** - * Triggers the specified intent. - * - * @throws IllegalArgumentException if context or intent is null - * @hide - */ - public static void fireIntent(@NonNull final Context context, @NonNull final Intent intent) { - switch (getIntentType(intent, context)) { - case IntentType.ACTIVITY: - context.startActivity(intent); - return; - case IntentType.SERVICE: - context.startService(intent); - return; - default: - return; - } - } - @IntentType private static int getIntentType(@NonNull Intent intent, @NonNull Context context) { Preconditions.checkArgument(context != null); @@ -402,11 +382,12 @@ public final class TextClassification implements Parcelable { @Nullable String mLegacyLabel; @Nullable Intent mLegacyIntent; @Nullable OnClickListener mLegacyOnClickListener; - @NonNull private String mSignature = ""; + @Nullable private String mId; /** * Sets the classified text. */ + @NonNull public Builder setText(@Nullable String text) { mText = text; return this; @@ -421,6 +402,7 @@ public final class TextClassification implements Parcelable { * 0 implies the entity does not exist for the classified text. * Values greater than 1 are clamped to 1. */ + @NonNull public Builder setEntityType( @NonNull @EntityType String type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { @@ -433,6 +415,7 @@ public final class TextClassification implements Parcelable { * order of likelihood that the user will use them, with the most likely action being added * first. */ + @NonNull public Builder addAction(@NonNull RemoteAction action) { Preconditions.checkArgument(action != null); mActions.add(action); @@ -446,6 +429,7 @@ public final class TextClassification implements Parcelable { * @deprecated Use {@link #addAction(RemoteAction)} instead. */ @Deprecated + @NonNull public Builder setIcon(@Nullable Drawable icon) { mLegacyIcon = icon; return this; @@ -458,6 +442,7 @@ public final class TextClassification implements Parcelable { * @deprecated Use {@link #addAction(RemoteAction)} instead. */ @Deprecated + @NonNull public Builder setLabel(@Nullable String label) { mLegacyLabel = label; return this; @@ -470,6 +455,7 @@ public final class TextClassification implements Parcelable { * @deprecated Use {@link #addAction(RemoteAction)} instead. */ @Deprecated + @NonNull public Builder setIntent(@Nullable Intent intent) { mLegacyIntent = intent; return this; @@ -482,58 +468,79 @@ public final class TextClassification implements Parcelable { * * @deprecated Use {@link #addAction(RemoteAction)} instead. */ + @Deprecated + @NonNull public Builder setOnClickListener(@Nullable OnClickListener onClickListener) { mLegacyOnClickListener = onClickListener; return this; } /** - * Sets a signature for the TextClassification object. - * The TextClassifier that generates the TextClassification object may use it as a way to - * internally identify the TextClassification object. + * Sets an id for the TextClassification object. */ - public Builder setSignature(@NonNull String signature) { - mSignature = Preconditions.checkNotNull(signature); + @NonNull + public Builder setId(@Nullable String id) { + mId = id; return this; } /** * Builds and returns a {@link TextClassification} object. */ + @NonNull public TextClassification build() { return new TextClassification(mText, mLegacyIcon, mLegacyLabel, mLegacyIntent, - mLegacyOnClickListener, mActions, mEntityConfidence, mSignature); + mLegacyOnClickListener, mActions, mEntityConfidence, mId); } } /** - * Optional input parameters for generating TextClassification. + * A request object for generating TextClassification. */ - public static final class Options implements Parcelable { - - private @Nullable LocaleList mDefaultLocales; - private @Nullable ZonedDateTime mReferenceTime; + public static final class Request implements Parcelable { + + private final CharSequence mText; + private final int mStartIndex; + private final int mEndIndex; + @Nullable private final LocaleList mDefaultLocales; + @Nullable private final ZonedDateTime mReferenceTime; + + private Request( + CharSequence text, + int startIndex, + int endIndex, + LocaleList defaultLocales, + ZonedDateTime referenceTime) { + mText = text; + mStartIndex = startIndex; + mEndIndex = endIndex; + mDefaultLocales = defaultLocales; + mReferenceTime = referenceTime; + } - public Options() {} + /** + * Returns the text providing context for the text to classify (which is specified + * by the sub sequence starting at startIndex and ending at endIndex) + */ + @NonNull + public CharSequence getText() { + return mText; + } /** - * @param defaultLocales ordered list of locale preferences that may be used to disambiguate - * the provided text. If no locale preferences exist, set this to null or an empty - * locale list. + * Returns start index of the text to classify. */ - public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { - mDefaultLocales = defaultLocales; - return this; + @IntRange(from = 0) + public int getStartIndex() { + return mStartIndex; } /** - * @param referenceTime reference time based on which relative dates (e.g. "tomorrow" should - * be interpreted. This should usually be the time when the text was originally - * composed. If no reference time is set, now is used. + * Returns end index of the text to classify. */ - public Options setReferenceTime(ZonedDateTime referenceTime) { - mReferenceTime = referenceTime; - return this; + @IntRange(from = 0) + public int getEndIndex() { + return mEndIndex; } /** @@ -554,6 +561,69 @@ public final class TextClassification implements Parcelable { return mReferenceTime; } + /** + * A builder for building TextClassification requests. + */ + public static final class Builder { + + private final CharSequence mText; + private final int mStartIndex; + private final int mEndIndex; + + @Nullable private LocaleList mDefaultLocales; + @Nullable private ZonedDateTime mReferenceTime; + + /** + * @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 + * @param endIndex end index of the text to classify + */ + public Builder( + @NonNull CharSequence text, + @IntRange(from = 0) int startIndex, + @IntRange(from = 0) int endIndex) { + Utils.checkArgument(text, startIndex, endIndex); + mText = text; + mStartIndex = startIndex; + mEndIndex = endIndex; + } + + /** + * @param defaultLocales ordered list of locale preferences that may be used to + * disambiguate the provided text. If no locale preferences exist, set this to null + * or an empty locale list. + * + * @return this builder + */ + @NonNull + public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** + * @param referenceTime reference time based on which relative dates (e.g. "tomorrow" + * should be interpreted. This should usually be the time when the text was + * originally composed. If no reference time is set, now is used. + * + * @return this builder + */ + @NonNull + public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) { + mReferenceTime = referenceTime; + return this; + } + + /** + * Builds and returns the request object. + */ + @NonNull + public Request build() { + return new Request(mText, mStartIndex, mEndIndex, mDefaultLocales, mReferenceTime); + } + } + @Override public int describeContents() { return 0; @@ -561,6 +631,9 @@ public final class TextClassification implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mText.toString()); + dest.writeInt(mStartIndex); + dest.writeInt(mEndIndex); dest.writeInt(mDefaultLocales != null ? 1 : 0); if (mDefaultLocales != null) { mDefaultLocales.writeToParcel(dest, flags); @@ -571,26 +644,25 @@ public final class TextClassification implements Parcelable { } } - public static final Parcelable.Creator<Options> CREATOR = - new Parcelable.Creator<Options>() { + public static final Parcelable.Creator<Request> CREATOR = + new Parcelable.Creator<Request>() { @Override - public Options createFromParcel(Parcel in) { - return new Options(in); + public Request createFromParcel(Parcel in) { + return new Request(in); } @Override - public Options[] newArray(int size) { - return new Options[size]; + public Request[] newArray(int size) { + return new Request[size]; } }; - private Options(Parcel in) { - if (in.readInt() > 0) { - mDefaultLocales = LocaleList.CREATOR.createFromParcel(in); - } - if (in.readInt() > 0) { - mReferenceTime = ZonedDateTime.parse(in.readString()); - } + private Request(Parcel in) { + mText = in.readString(); + mStartIndex = in.readInt(); + mEndIndex = in.readInt(); + mDefaultLocales = in.readInt() == 0 ? null : LocaleList.CREATOR.createFromParcel(in); + mReferenceTime = in.readInt() == 0 ? null : ZonedDateTime.parse(in.readString()); } } @@ -615,7 +687,7 @@ public final class TextClassification implements Parcelable { // mOnClickListener is not parcelable. dest.writeTypedList(mActions); mEntityConfidence.writeToParcel(dest, flags); - dest.writeString(mSignature); + dest.writeString(mId); } public static final Parcelable.Creator<TextClassification> CREATOR = @@ -647,6 +719,6 @@ public final class TextClassification implements Parcelable { mLegacyOnClickListener = null; // not parcelable mActions = in.createTypedArrayList(RemoteAction.CREATOR); mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in); - mSignature = in.readString(); + mId = in.readString(); } } diff --git a/core/java/android/view/textclassifier/TextClassificationContext.aidl b/core/java/android/view/textclassifier/TextClassificationContext.aidl new file mode 100644 index 000000000000..0d6b03318ab9 --- /dev/null +++ b/core/java/android/view/textclassifier/TextClassificationContext.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +parcelable TextClassificationContext; diff --git a/core/java/android/view/textclassifier/TextClassificationContext.java b/core/java/android/view/textclassifier/TextClassificationContext.java index a88f2f66d359..a15411f0416e 100644 --- a/core/java/android/view/textclassifier/TextClassificationContext.java +++ b/core/java/android/view/textclassifier/TextClassificationContext.java @@ -18,6 +18,8 @@ package android.view.textclassifier; import android.annotation.NonNull; import android.annotation.Nullable; +import android.os.Parcel; +import android.os.Parcelable; import android.view.textclassifier.TextClassifier.WidgetType; import com.android.internal.util.Preconditions; @@ -28,7 +30,7 @@ import java.util.Locale; * A representation of the context in which text classification would be performed. * @see TextClassificationManager#createTextClassificationSession(TextClassificationContext) */ -public final class TextClassificationContext { +public final class TextClassificationContext implements Parcelable { private final String mPackageName; private final String mWidgetType; @@ -120,4 +122,35 @@ public final class TextClassificationContext { return new TextClassificationContext(mPackageName, mWidgetType, mWidgetVersion); } } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int flags) { + parcel.writeString(mPackageName); + parcel.writeString(mWidgetType); + parcel.writeString(mWidgetVersion); + } + + private TextClassificationContext(Parcel in) { + mPackageName = in.readString(); + mWidgetType = in.readString(); + mWidgetVersion = in.readString(); + } + + public static final Parcelable.Creator<TextClassificationContext> CREATOR = + new Parcelable.Creator<TextClassificationContext>() { + @Override + public TextClassificationContext createFromParcel(Parcel parcel) { + return new TextClassificationContext(parcel); + } + + @Override + public TextClassificationContext[] newArray(int size) { + return new TextClassificationContext[size]; + } + }; } diff --git a/core/java/android/view/textclassifier/TextClassificationSession.java b/core/java/android/view/textclassifier/TextClassificationSession.java index 6938e1ae11c4..e8e300a94de4 100644 --- a/core/java/android/view/textclassifier/TextClassificationSession.java +++ b/core/java/android/view/textclassifier/TextClassificationSession.java @@ -33,32 +33,42 @@ final class TextClassificationSession implements TextClassifier { private final TextClassifier mDelegate; private final SelectionEventHelper mEventHelper; + private final TextClassificationSessionId mSessionId; + private final TextClassificationContext mClassificationContext; private boolean mDestroyed; TextClassificationSession(TextClassificationContext context, TextClassifier delegate) { + mClassificationContext = Preconditions.checkNotNull(context); mDelegate = Preconditions.checkNotNull(delegate); - mEventHelper = new SelectionEventHelper(new TextClassificationSessionId(), context); + mSessionId = new TextClassificationSessionId(); + mEventHelper = new SelectionEventHelper(mSessionId, mClassificationContext); + initializeRemoteSession(); } @Override - public TextSelection suggestSelection(CharSequence text, int selectionStartIndex, - int selectionEndIndex, TextSelection.Options options) { + public TextSelection suggestSelection(TextSelection.Request request) { checkDestroyed(); - return mDelegate.suggestSelection(text, selectionStartIndex, selectionEndIndex, options); + return mDelegate.suggestSelection(request); + } + + private void initializeRemoteSession() { + if (mDelegate instanceof SystemTextClassifier) { + ((SystemTextClassifier) mDelegate).initializeRemoteSession( + mClassificationContext, mSessionId); + } } @Override - public TextClassification classifyText(CharSequence text, int startIndex, int endIndex, - TextClassification.Options options) { + public TextClassification classifyText(TextClassification.Request request) { checkDestroyed(); - return mDelegate.classifyText(text, startIndex, endIndex, options); + return mDelegate.classifyText(request); } @Override - public TextLinks generateLinks(CharSequence text, TextLinks.Options options) { + public TextLinks generateLinks(TextLinks.Request request) { checkDestroyed(); - return mDelegate.generateLinks(text, options); + return mDelegate.generateLinks(request); } @Override @@ -73,6 +83,7 @@ final class TextClassificationSession implements TextClassifier { @Override public void destroy() { mEventHelper.endSession(); + mDelegate.destroy(); mDestroyed = true; } @@ -162,7 +173,7 @@ final class TextClassificationSession implements TextClassifier { .setEnd(event.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); } if (mSmartEvent != null) { - event.setSignature(mSmartEvent.getSignature()) + event.setResultId(mSmartEvent.getResultId()) .setSmartStart( mSmartEvent.getAbsoluteStart() - mStartEvent.getAbsoluteStart()) .setSmartEnd(mSmartEvent.getAbsoluteEnd() - mStartEvent.getAbsoluteStart()); @@ -195,7 +206,7 @@ final class TextClassificationSession implements TextClassifier { 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 (isPlatformLocalTextClassifierSmartSelection(event.getResultId())) { if (event.getAbsoluteEnd() - event.getAbsoluteStart() > 1) { event.setEventType(SelectionEvent.EVENT_SMART_SELECTION_MULTI); } else { diff --git a/core/java/android/view/textclassifier/TextClassificationSessionId.aidl b/core/java/android/view/textclassifier/TextClassificationSessionId.aidl new file mode 100644 index 000000000000..1bbf2da09be5 --- /dev/null +++ b/core/java/android/view/textclassifier/TextClassificationSessionId.aidl @@ -0,0 +1,19 @@ +/* + * 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; + +parcelable TextClassificationSessionId; diff --git a/core/java/android/view/textclassifier/TextClassificationSessionId.java b/core/java/android/view/textclassifier/TextClassificationSessionId.java index 3e4dc1c52b4d..1378bd9c9035 100644 --- a/core/java/android/view/textclassifier/TextClassificationSessionId.java +++ b/core/java/android/view/textclassifier/TextClassificationSessionId.java @@ -22,6 +22,7 @@ import android.os.Parcelable; import com.android.internal.util.Preconditions; +import java.util.Locale; import java.util.UUID; /** @@ -77,6 +78,11 @@ public final class TextClassificationSessionId implements Parcelable { } @Override + public String toString() { + return String.format(Locale.US, "TextClassificationSessionId {%s}", mValue); + } + + @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeString(mValue); } diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index 2048f2b49182..54261be33607 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -33,7 +33,6 @@ import android.text.util.Linkify; import android.text.util.Linkify.LinkifyMask; import android.util.ArrayMap; import android.util.ArraySet; -import android.util.Slog; import com.android.internal.util.Preconditions; @@ -156,76 +155,44 @@ 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 + * <p><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 - * @param selectionEndIndex end index of the selected part of text - * @param options optional input parameters - * - * @throws IllegalArgumentException if text is null; selectionStartIndex is negative; - * selectionEndIndex is greater than text.length() or not greater than selectionStartIndex - * - * @see #suggestSelection(CharSequence, int, int) + * @param request the text selection request */ @WorkerThread @NonNull - default TextSelection suggestSelection( - @NonNull CharSequence text, - @IntRange(from = 0) int selectionStartIndex, - @IntRange(from = 0) int selectionEndIndex, - @Nullable TextSelection.Options options) { - Utils.validate(text, selectionStartIndex, selectionEndIndex, false); - return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build(); + default TextSelection suggestSelection(@NonNull TextSelection.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); + return new TextSelection.Builder(request.getStartIndex(), request.getEndIndex()).build(); } /** * Returns suggested text selection start and end indices, recognized entity types, and their * associated confidence scores. The entity types are ordered from highest to lowest scoring. * - * <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. - * * <p><strong>NOTE: </strong>Call on a worker thread. * - * <strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should + * <p><strong>NOTE: </strong>If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. * + * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls + * {@link #suggestSelection(TextSelection.Request)}. If that method calls this method, + * a stack overflow error will happen. + * * @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 * @param selectionEndIndex end index of the selected part of text + * @param defaultLocales ordered list of locale preferences that may be used to + * disambiguate the provided text. If no locale preferences exist, set this to null + * or an empty locale list. * * @throws IllegalArgumentException if text is null; selectionStartIndex is negative; * selectionEndIndex is greater than text.length() or not greater than selectionStartIndex * - * @see #suggestSelection(CharSequence, int, int, TextSelection.Options) - */ - @WorkerThread - @NonNull - default TextSelection suggestSelection( - @NonNull CharSequence text, - @IntRange(from = 0) int selectionStartIndex, - @IntRange(from = 0) int selectionEndIndex) { - return suggestSelection(text, selectionStartIndex, selectionEndIndex, - (TextSelection.Options) null); - } - - /** - * See {@link #suggestSelection(CharSequence, int, int)} or - * {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. - * - * <p><strong>NOTE: </strong>Call on a worker thread. - * - * <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()}. + * @see #suggestSelection(TextSelection.Request) */ @WorkerThread @NonNull @@ -234,10 +201,11 @@ public interface TextClassifier { @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, @Nullable LocaleList defaultLocales) { - final TextSelection.Options options = (defaultLocales != null) - ? new TextSelection.Options().setDefaultLocales(defaultLocales) - : null; - return suggestSelection(text, selectionStartIndex, selectionEndIndex, options); + final TextSelection.Request request = new TextSelection.Request.Builder( + text, selectionStartIndex, selectionEndIndex) + .setDefaultLocales(defaultLocales) + .build(); + return suggestSelection(request); } /** @@ -249,25 +217,13 @@ public interface TextClassifier { * <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 - * @param endIndex end index of the text to classify - * @param options optional input parameters - * - * @throws IllegalArgumentException if text is null; startIndex is negative; - * endIndex is greater than text.length() or not greater than startIndex - * - * @see #classifyText(CharSequence, int, int) + * @param request the text classification request */ @WorkerThread @NonNull - default TextClassification classifyText( - @NonNull CharSequence text, - @IntRange(from = 0) int startIndex, - @IntRange(from = 0) int endIndex, - @Nullable TextClassification.Options options) { - Utils.validate(text, startIndex, endIndex, false); + default TextClassification classifyText(@NonNull TextClassification.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); return TextClassification.EMPTY; } @@ -278,8 +234,8 @@ public interface TextClassifier { * <p><strong>NOTE: </strong>Call on a worker thread. * * <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. + * {@link #classifyText(TextClassification.Request)}. 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()}. @@ -288,33 +244,14 @@ public interface TextClassifier { * by the sub sequence starting at startIndex and ending at endIndex) * @param startIndex start index of the text to classify * @param endIndex end index of the text to classify + * @param defaultLocales ordered list of locale preferences that may be used to + * disambiguate the provided text. If no locale preferences exist, set this to null + * or an empty locale list. * * @throws IllegalArgumentException if text is null; startIndex is negative; * endIndex is greater than text.length() or not greater than startIndex * - * @see #classifyText(CharSequence, int, int, TextClassification.Options) - */ - @WorkerThread - @NonNull - default TextClassification classifyText( - @NonNull CharSequence text, - @IntRange(from = 0) int startIndex, - @IntRange(from = 0) int endIndex) { - return classifyText(text, startIndex, endIndex, (TextClassification.Options) null); - } - - /** - * See {@link #classifyText(CharSequence, int, int, TextClassification.Options)} or - * {@link #classifyText(CharSequence, int, int)}. - * - * <p><strong>NOTE: </strong>Call on a worker thread. - * - * <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()}. + * @see #classifyText(TextClassification.Request) */ @WorkerThread @NonNull @@ -323,36 +260,11 @@ public interface TextClassifier { @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, @Nullable LocaleList defaultLocales) { - final TextClassification.Options options = (defaultLocales != null) - ? new TextClassification.Options().setDefaultLocales(defaultLocales) - : null; - return classifyText(text, startIndex, endIndex, options); - } - - /** - * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with - * links information. - * - * <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 - * - * @throws IllegalArgumentException if text is null or the text is too long for the - * TextClassifier implementation. - * - * @see #generateLinks(CharSequence) - * @see #getMaxGenerateLinksTextLength() - */ - @WorkerThread - @NonNull - default TextLinks generateLinks( - @NonNull CharSequence text, @Nullable TextLinks.Options options) { - Utils.validate(text, false); - return new TextLinks.Builder(text.toString()).build(); + final TextClassification.Request request = new TextClassification.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(defaultLocales) + .build(); + return classifyText(request); } /** @@ -361,25 +273,19 @@ public interface TextClassifier { * * <p><strong>NOTE: </strong>Call on a worker thread. * - * <p><b>NOTE:</b> Do not implement. The default implementation of this method calls - * {@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 + * @param request the text links request * - * @throws IllegalArgumentException if text is null or the text is too long for the - * TextClassifier implementation. - * - * @see #generateLinks(CharSequence, TextLinks.Options) * @see #getMaxGenerateLinksTextLength() */ @WorkerThread @NonNull - default TextLinks generateLinks(@NonNull CharSequence text) { - return generateLinks(text, null); + default TextLinks generateLinks(@NonNull TextLinks.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); + return new TextLinks.Builder(request.getText().toString()).build(); } /** @@ -388,8 +294,7 @@ public interface TextClassifier { * <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) + * @see #generateLinks(TextLinks.Request) */ @WorkerThread default int getMaxGenerateLinksTextLength() { @@ -467,7 +372,7 @@ public interface TextClassifier { * * @param hints Hints for the TextClassifier to determine what types of entities to find. */ - public static EntityConfig create(@Nullable Collection<String> hints) { + public static EntityConfig createWithHints(@Nullable Collection<String> hints) { return new EntityConfig(/* useHints */ true, hints, /* includedEntityTypes */null, /* excludedEntityTypes */ null); } @@ -495,7 +400,8 @@ public interface TextClassifier { * @param entityTypes Complete set of entities, e.g. {@link #TYPE_URL} to find. * */ - public static EntityConfig createWithEntityList(@Nullable Collection<String> entityTypes) { + public static EntityConfig createWithExplicitEntityList( + @Nullable Collection<String> entityTypes) { return new EntityConfig(/* useHints */ false, /* hints */ null, /* includedEntityTypes */ entityTypes, /* excludedEntityTypes */ null); } @@ -584,42 +490,25 @@ public interface TextClassifier { * endIndex is greater than text.length() or is not greater than startIndex; * options is null */ - public static void validate( - @NonNull CharSequence text, int startIndex, int endIndex, - boolean allowInMainThread) { + static void checkArgument(@NonNull CharSequence text, int startIndex, int endIndex) { Preconditions.checkArgument(text != null); Preconditions.checkArgument(startIndex >= 0); Preconditions.checkArgument(endIndex <= text.length()); Preconditions.checkArgument(endIndex > startIndex); - checkMainThread(allowInMainThread); } - /** - * @throws IllegalArgumentException if text is null or options is null - */ - public static void validate(@NonNull CharSequence text, boolean allowInMainThread) { - Preconditions.checkArgument(text != null); - checkMainThread(allowInMainThread); - } - - /** - * @throws IllegalArgumentException if text is null; the text is too long or options is null - */ - public static void validate(@NonNull CharSequence text, int maxLength, - boolean allowInMainThread) { - validate(text, allowInMainThread); + static void checkTextLength(CharSequence text, int maxLength) { Preconditions.checkArgumentInRange(text.length(), 0, maxLength, "text.length()"); } /** * Generates links using legacy {@link Linkify}. */ - public static TextLinks generateLegacyLinks( - @NonNull CharSequence text, @NonNull TextLinks.Options options) { - final String string = Preconditions.checkNotNull(text).toString(); + public static TextLinks generateLegacyLinks(@NonNull TextLinks.Request request) { + final String string = request.getText().toString(); final TextLinks.Builder links = new TextLinks.Builder(string); - final List<String> entities = Preconditions.checkNotNull(options).getEntityConfig() + final List<String> entities = request.getEntityConfig() .resolveEntityListModifications(Collections.emptyList()); if (entities.contains(TextClassifier.TYPE_URL)) { addLinks(links, string, TextClassifier.TYPE_URL); @@ -670,9 +559,9 @@ public interface TextClassifier { return scores; } - private static void checkMainThread(boolean allowInMainThread) { - if (!allowInMainThread && Looper.myLooper() == Looper.getMainLooper()) { - Slog.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread"); + static void checkMainThread() { + if (Looper.myLooper() == Looper.getMainLooper()) { + Log.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread"); } } } diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index 8d1ed0eb68cb..7e3748ae76cd 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -112,35 +112,32 @@ public final class TextClassifierImpl implements TextClassifier { /** @inheritDoc */ @Override @WorkerThread - public TextSelection suggestSelection( - @NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex, - @Nullable TextSelection.Options options) { - Utils.validate(text, selectionStartIndex, selectionEndIndex, false /* allowInMainThread */); + public TextSelection suggestSelection(TextSelection.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); try { - final int rangeLength = selectionEndIndex - selectionStartIndex; - if (text.length() > 0 + final int rangeLength = request.getEndIndex() - request.getStartIndex(); + final String string = request.getText().toString(); + if (string.length() > 0 && rangeLength <= mSettings.getSuggestSelectionMaxRangeLength()) { - final LocaleList locales = (options == null) ? null : options.getDefaultLocales(); - final String localesString = concatenateLocales(locales); + final String localesString = concatenateLocales(request.getDefaultLocales()); final ZonedDateTime refTime = ZonedDateTime.now(); - final boolean darkLaunchAllowed = options != null && options.isDarkLaunchAllowed(); - final TextClassifierImplNative nativeImpl = getNative(locales); - final String string = text.toString(); + final TextClassifierImplNative nativeImpl = getNative(request.getDefaultLocales()); final int start; final int end; - if (mSettings.isModelDarkLaunchEnabled() && !darkLaunchAllowed) { - start = selectionStartIndex; - end = selectionEndIndex; + if (mSettings.isModelDarkLaunchEnabled() && !request.isDarkLaunchAllowed()) { + start = request.getStartIndex(); + end = request.getEndIndex(); } else { final int[] startEnd = nativeImpl.suggestSelection( - string, selectionStartIndex, selectionEndIndex, + string, request.getStartIndex(), request.getEndIndex(), new TextClassifierImplNative.SelectionOptions(localesString)); start = startEnd[0]; end = startEnd[1]; } if (start < end && start >= 0 && end <= string.length() - && start <= selectionStartIndex && end >= selectionEndIndex) { + && start <= request.getStartIndex() && end >= request.getEndIndex()) { final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end); final TextClassifierImplNative.ClassificationResult[] results = nativeImpl.classifyText( @@ -153,9 +150,8 @@ public final class TextClassifierImpl implements TextClassifier { for (int i = 0; i < size; i++) { tsBuilder.setEntityType(results[i].getCollection(), results[i].getScore()); } - return tsBuilder - .setSignature( - getSignature(string, selectionStartIndex, selectionEndIndex)) + return tsBuilder.setId(createId( + string, request.getStartIndex(), request.getEndIndex())) .build(); } else { // We can not trust the result. Log the issue and ignore the result. @@ -169,37 +165,34 @@ public final class TextClassifierImpl implements TextClassifier { t); } // Getting here means something went wrong, return a NO_OP result. - return mFallback.suggestSelection( - text, selectionStartIndex, selectionEndIndex, options); + return mFallback.suggestSelection(request); } /** @inheritDoc */ @Override @WorkerThread - public TextClassification classifyText( - @NonNull CharSequence text, int startIndex, int endIndex, - @Nullable TextClassification.Options options) { - Utils.validate(text, startIndex, endIndex, false /* allowInMainThread */); + public TextClassification classifyText(TextClassification.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); try { - final int rangeLength = endIndex - startIndex; - if (text.length() > 0 && rangeLength <= mSettings.getClassifyTextMaxRangeLength()) { - final String string = text.toString(); - final LocaleList locales = (options == null) ? null : options.getDefaultLocales(); - final String localesString = concatenateLocales(locales); - final ZonedDateTime refTime = - (options != null && options.getReferenceTime() != null) - ? options.getReferenceTime() : ZonedDateTime.now(); - + final int rangeLength = request.getEndIndex() - request.getStartIndex(); + final String string = request.getText().toString(); + if (string.length() > 0 && rangeLength <= mSettings.getClassifyTextMaxRangeLength()) { + final String localesString = concatenateLocales(request.getDefaultLocales()); + final ZonedDateTime refTime = request.getReferenceTime() != null + ? request.getReferenceTime() : ZonedDateTime.now(); final TextClassifierImplNative.ClassificationResult[] results = - getNative(locales) - .classifyText(string, startIndex, endIndex, + getNative(request.getDefaultLocales()) + .classifyText( + string, request.getStartIndex(), request.getEndIndex(), new TextClassifierImplNative.ClassificationOptions( refTime.toInstant().toEpochMilli(), refTime.getZone().getId(), localesString)); if (results.length > 0) { return createClassificationResult( - results, string, startIndex, endIndex, refTime.toInstant()); + results, string, + request.getStartIndex(), request.getEndIndex(), refTime.toInstant()); } } } catch (Throwable t) { @@ -207,42 +200,40 @@ public final class TextClassifierImpl implements TextClassifier { Log.e(LOG_TAG, "Error getting text classification info.", t); } // Getting here means something went wrong, return a NO_OP result. - return mFallback.classifyText(text, startIndex, endIndex, options); + return mFallback.classifyText(request); } /** @inheritDoc */ @Override @WorkerThread - public TextLinks generateLinks( - @NonNull CharSequence text, @Nullable TextLinks.Options options) { - Utils.validate(text, getMaxGenerateLinksTextLength(), false /* allowInMainThread */); + public TextLinks generateLinks(@NonNull TextLinks.Request request) { + Preconditions.checkNotNull(request); + Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength()); + Utils.checkMainThread(); - final boolean legacyFallback = options != null && options.isLegacyFallback(); - if (!mSettings.isSmartLinkifyEnabled() && legacyFallback) { - return Utils.generateLegacyLinks(text, options); + if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) { + return Utils.generateLegacyLinks(request); } - final String textString = text.toString(); + final String textString = request.getText().toString(); final TextLinks.Builder builder = new TextLinks.Builder(textString); try { final long startTimeMs = System.currentTimeMillis(); - final LocaleList defaultLocales = options != null ? options.getDefaultLocales() : null; final ZonedDateTime refTime = ZonedDateTime.now(); - final Collection<String> entitiesToIdentify = - options != null && options.getEntityConfig() != null - ? options.getEntityConfig().resolveEntityListModifications( - getEntitiesForHints(options.getEntityConfig().getHints())) - : mSettings.getEntityListDefault(); + final Collection<String> entitiesToIdentify = request.getEntityConfig() != null + ? request.getEntityConfig().resolveEntityListModifications( + getEntitiesForHints(request.getEntityConfig().getHints())) + : mSettings.getEntityListDefault(); final TextClassifierImplNative nativeImpl = - getNative(defaultLocales); + getNative(request.getDefaultLocales()); final TextClassifierImplNative.AnnotatedSpan[] annotations = nativeImpl.annotate( textString, new TextClassifierImplNative.AnnotationOptions( refTime.toInstant().toEpochMilli(), - refTime.getZone().getId(), - concatenateLocales(defaultLocales))); + refTime.getZone().getId(), + concatenateLocales(request.getDefaultLocales()))); for (TextClassifierImplNative.AnnotatedSpan span : annotations) { final TextClassifierImplNative.ClassificationResult[] results = span.getClassification(); @@ -258,18 +249,17 @@ public final class TextClassifierImpl implements TextClassifier { } final TextLinks links = builder.build(); final long endTimeMs = System.currentTimeMillis(); - final String callingPackageName = - options == null || options.getCallingPackageName() == null - ? mContext.getPackageName() // local (in process) TC. - : options.getCallingPackageName(); + final String callingPackageName = request.getCallingPackageName() == null + ? mContext.getPackageName() // local (in process) TC. + : request.getCallingPackageName(); mGenerateLinksLogger.logGenerateLinks( - text, links, callingPackageName, endTimeMs - startTimeMs); + request.getText(), links, callingPackageName, endTimeMs - startTimeMs); return links; } catch (Throwable t) { // Avoid throwing from this method. Log the error. Log.e(LOG_TAG, "Error getting links info.", t); } - return mFallback.generateLinks(text, options); + return mFallback.generateLinks(request); } /** @inheritDoc */ @@ -339,9 +329,9 @@ public final class TextClassifierImpl implements TextClassifier { } } - private String getSignature(String text, int start, int end) { + private String createId(String text, int start, int end) { synchronized (mLock) { - return DefaultLogger.createSignature(text, start, end, mContext, mModel.getVersion(), + return DefaultLogger.createId(text, start, end, mContext, mModel.getVersion(), mModel.getSupportedLocales()); } } @@ -455,7 +445,7 @@ public final class TextClassifierImpl implements TextClassifier { builder.addAction(action); } - return builder.setSignature(getSignature(text, start, end)).build(); + return builder.setId(createId(text, start, end)).build(); } /** @@ -512,7 +502,7 @@ public final class TextClassifierImpl implements TextClassifier { return mPath; } - /** A name to use for signature generation. Effectively the name of the model file. */ + /** A name to use for id generation. Effectively the name of the model file. */ String getName() { return mName; } diff --git a/core/java/android/view/textclassifier/TextLinks.aidl b/core/java/android/view/textclassifier/TextLinks.aidl index 1bbb79845528..5de2c77758c2 100644 --- a/core/java/android/view/textclassifier/TextLinks.aidl +++ b/core/java/android/view/textclassifier/TextLinks.aidl @@ -17,4 +17,4 @@ package android.view.textclassifier; parcelable TextLinks; -parcelable TextLinks.Options;
\ No newline at end of file +parcelable TextLinks.Request;
\ No newline at end of file diff --git a/core/java/android/view/textclassifier/TextLinks.java b/core/java/android/view/textclassifier/TextLinks.java index 38a7d9aa1d2b..17c7b13cdc85 100644 --- a/core/java/android/view/textclassifier/TextLinks.java +++ b/core/java/android/view/textclassifier/TextLinks.java @@ -25,10 +25,9 @@ import android.os.LocaleList; import android.os.Parcel; import android.os.Parcelable; import android.text.Spannable; +import android.text.method.MovementMethod; import android.text.style.ClickableSpan; import android.text.style.URLSpan; -import android.text.util.Linkify; -import android.text.util.Linkify.LinkifyMask; import android.view.View; import android.view.textclassifier.TextClassifier.EntityType; import android.widget.TextView; @@ -43,6 +42,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.function.Function; @@ -79,15 +79,15 @@ public final class TextLinks implements Parcelable { public @interface ApplyStrategy {} /** - * Do not replace {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to - * be applied to. Do not apply the TextLinkSpan. - */ + * Do not replace {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to + * be applied to. Do not apply the TextLinkSpan. + */ public static final int APPLY_STRATEGY_IGNORE = 0; /** - * Replace any {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to be - * applied to. - */ + * Replace any {@link ClickableSpan}s that exist where the {@link TextLinkSpan} needs to be + * applied to. + */ public static final int APPLY_STRATEGY_REPLACE = 1; private final String mFullText; @@ -99,70 +99,54 @@ public final class TextLinks implements Parcelable { } /** + * Returns the text that was used to generate these links. + * @hide + */ + @NonNull + public String getText() { + return mFullText; + } + + /** * Returns an unmodifiable Collection of the links. */ + @NonNull public Collection<TextLink> getLinks() { return mLinks; } /** * Annotates the given text with the generated links. It will fail if the provided text doesn't - * match the original text used to crete the TextLinks. + * match the original text used to create the TextLinks. + * + * <p><strong>NOTE: </strong>It may be necessary to set a LinkMovementMethod on the TextView + * widget to properly handle links. See {@link TextView#setMovementMethod(MovementMethod)} * * @param text the text to apply the links to. Must match the original text - * @param applyStrategy strategy for resolving link conflicts - * @param spanFactory a factory to generate spans from TextLinks. Will use a default if null - * @param allowPrefix whether to allow applying links only to a prefix of the text. + * @param applyStrategy the apply strategy used to determine how to apply links to text. + * e.g {@link TextLinks#APPLY_STRATEGY_IGNORE} + * @param spanFactory a custom span factory for converting TextLinks to TextLinkSpans. + * Set to {@code null} to use the default span factory. * * @return a status code indicating whether or not the links were successfully applied - * - * @hide + * e.g. {@link #STATUS_LINKS_APPLIED} */ @Status public int apply( @NonNull Spannable text, @ApplyStrategy int applyStrategy, - @Nullable Function<TextLink, TextLinkSpan> spanFactory, - boolean allowPrefix) { + @Nullable Function<TextLink, TextLinkSpan> spanFactory) { Preconditions.checkNotNull(text); - checkValidApplyStrategy(applyStrategy); - final String textString = text.toString(); - if (!mFullText.equals(textString) && !(allowPrefix && textString.startsWith(mFullText))) { - return STATUS_DIFFERENT_TEXT; - } - if (mLinks.isEmpty()) { - return STATUS_NO_LINKS_FOUND; - } + return new TextLinksParams.Builder() + .setApplyStrategy(applyStrategy) + .setSpanFactory(spanFactory) + .build() + .apply(text, this); + } - if (spanFactory == null) { - spanFactory = DEFAULT_SPAN_FACTORY; - } - int applyCount = 0; - for (TextLink link : mLinks) { - final TextLinkSpan span = spanFactory.apply(link); - if (span != null) { - final ClickableSpan[] existingSpans = text.getSpans( - link.getStart(), link.getEnd(), ClickableSpan.class); - if (existingSpans.length > 0) { - if (applyStrategy == APPLY_STRATEGY_REPLACE) { - for (ClickableSpan existingSpan : existingSpans) { - text.removeSpan(existingSpan); - } - text.setSpan(span, link.getStart(), link.getEnd(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - applyCount++; - } - } else { - text.setSpan(span, link.getStart(), link.getEnd(), - Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - applyCount++; - } - } - } - if (applyCount == 0) { - return STATUS_NO_LINKS_APPLIED; - } - return STATUS_LINKS_APPLIED; + @Override + public String toString() { + return String.format(Locale.US, "TextLinks{fullText=%s, links=%s}", mFullText, mLinks); } @Override @@ -271,6 +255,13 @@ public final class TextLinks implements Parcelable { } @Override + public String toString() { + return String.format(Locale.US, + "TextLink{start=%s, end=%s, entityScores=%s, urlSpan=%s}", + mStart, mEnd, mEntityScores, mUrlSpan); + } + + @Override public int describeContents() { return 0; } @@ -304,108 +295,35 @@ public final class TextLinks implements Parcelable { } /** - * Optional input parameters for generating TextLinks. + * A request object for generating TextLinks. */ - public static final class Options implements Parcelable { - - private LocaleList mDefaultLocales; - private TextClassifier.EntityConfig mEntityConfig; - private boolean mLegacyFallback; - - private @ApplyStrategy int mApplyStrategy; - private Function<TextLink, TextLinkSpan> mSpanFactory; + public static final class Request implements Parcelable { + private final CharSequence mText; + @Nullable private final LocaleList mDefaultLocales; + @Nullable private final TextClassifier.EntityConfig mEntityConfig; + private final boolean mLegacyFallback; private String mCallingPackageName; - /** - * Returns a new options object based on the specified link mask. - */ - public static Options fromLinkMask(@LinkifyMask int mask) { - final List<String> entitiesToFind = new ArrayList<>(); - - if ((mask & Linkify.WEB_URLS) != 0) { - entitiesToFind.add(TextClassifier.TYPE_URL); - } - if ((mask & Linkify.EMAIL_ADDRESSES) != 0) { - entitiesToFind.add(TextClassifier.TYPE_EMAIL); - } - if ((mask & Linkify.PHONE_NUMBERS) != 0) { - entitiesToFind.add(TextClassifier.TYPE_PHONE); - } - if ((mask & Linkify.MAP_ADDRESSES) != 0) { - entitiesToFind.add(TextClassifier.TYPE_ADDRESS); - } - - return new Options().setEntityConfig( - TextClassifier.EntityConfig.createWithEntityList(entitiesToFind)); - } - - public Options() {} - - /** - * @param defaultLocales ordered list of locale preferences that may be used to - * disambiguate the provided text. If no locale preferences exist, - * set this to null or an empty locale list. - */ - public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { + private Request( + CharSequence text, + LocaleList defaultLocales, + TextClassifier.EntityConfig entityConfig, + boolean legacyFallback, + String callingPackageName) { + mText = text; mDefaultLocales = defaultLocales; - return this; - } - - /** - * Sets the entity configuration to use. This determines what types of entities the - * TextClassifier will look for. - * - * @param entityConfig EntityConfig to use - */ - public Options setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { mEntityConfig = entityConfig; - return this; - } - - /** - * Sets whether the TextClassifier can fallback to legacy links if smart linkify is - * disabled. - * <strong>Note: </strong>This is not parcelled. - * @hide - */ - public Options setLegacyFallback(boolean legacyFallback) { mLegacyFallback = legacyFallback; - return this; - } - - /** - * Sets a strategy for resolving conflicts when applying generated links to text that - * already have links. - * - * @throws IllegalArgumentException if applyStrategy is not valid - * - * @see #APPLY_STRATEGY_IGNORE - * @see #APPLY_STRATEGY_REPLACE - */ - public Options setApplyStrategy(@ApplyStrategy int applyStrategy) { - checkValidApplyStrategy(applyStrategy); - mApplyStrategy = applyStrategy; - return this; - } - - /** - * Sets a factory for converting a TextLink to a TextLinkSpan. - * - * <p><strong>Note: </strong>This is not parceled over IPC. - */ - public Options setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) { - mSpanFactory = spanFactory; - return this; + mCallingPackageName = callingPackageName; } /** - * Sets the name of the package that requested the links to get generated. - * @hide + * Returns the text to generate links for. */ - public Options setCallingPackageName(@Nullable String callingPackageName) { - mCallingPackageName = callingPackageName; - return this; + @NonNull + public CharSequence getText() { + return mText; } /** @@ -437,26 +355,91 @@ public final class TextLinks implements Parcelable { } /** - * @return the strategy for resolving conflictswhen applying generated links to text that - * already have links - * - * @see #APPLY_STRATEGY_IGNORE - * @see #APPLY_STRATEGY_REPLACE + * Sets the name of the package that requested the links to get generated. */ - @ApplyStrategy - public int getApplyStrategy() { - return mApplyStrategy; + void setCallingPackageName(@Nullable String callingPackageName) { + mCallingPackageName = callingPackageName; } /** - * Returns a factory for converting a TextLink to a TextLinkSpan. - * - * <p><strong>Note: </strong>This is not parcelable and will always return null if read - * from a parcel + * A builder for building TextLinks requests. */ - @Nullable - public Function<TextLink, TextLinkSpan> getSpanFactory() { - return mSpanFactory; + public static final class Builder { + + private final CharSequence mText; + + @Nullable private LocaleList mDefaultLocales; + @Nullable private TextClassifier.EntityConfig mEntityConfig; + private boolean mLegacyFallback = true; // Use legacy fall back by default. + private String mCallingPackageName; + + public Builder(@NonNull CharSequence text) { + mText = Preconditions.checkNotNull(text); + } + + /** + * @param defaultLocales ordered list of locale preferences that may be used to + * disambiguate the provided text. If no locale preferences exist, + * set this to null or an empty locale list. + * @return this builder + */ + @NonNull + public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** + * Sets the entity configuration to use. This determines what types of entities the + * TextClassifier will look for. + * Set to {@code null} for the default entity config and teh TextClassifier will + * automatically determine what links to generate. + * + * @return this builder + */ + @NonNull + public Builder setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { + mEntityConfig = entityConfig; + return this; + } + + /** + * Sets whether the TextClassifier can fallback to legacy links if smart linkify is + * disabled. + * + * <p><strong>Note: </strong>This is not parcelled. + * + * @return this builder + * @hide + */ + @NonNull + public Builder setLegacyFallback(boolean legacyFallback) { + mLegacyFallback = legacyFallback; + return this; + } + + /** + * Sets the name of the package that requested the links to get generated. + * + * @return this builder + * @hide + */ + @NonNull + public Builder setCallingPackageName(@Nullable String callingPackageName) { + mCallingPackageName = callingPackageName; + return this; + } + + /** + * Builds and returns the request object. + */ + @NonNull + public Request build() { + return new Request( + mText, mDefaultLocales, mEntityConfig, + mLegacyFallback, mCallingPackageName); + } + } /** @@ -476,6 +459,7 @@ public final class TextLinks implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mText.toString()); dest.writeInt(mDefaultLocales != null ? 1 : 0); if (mDefaultLocales != null) { mDefaultLocales.writeToParcel(dest, flags); @@ -484,42 +468,33 @@ public final class TextLinks implements Parcelable { if (mEntityConfig != null) { mEntityConfig.writeToParcel(dest, flags); } - dest.writeInt(mApplyStrategy); dest.writeString(mCallingPackageName); } - public static final Parcelable.Creator<Options> CREATOR = - new Parcelable.Creator<Options>() { + public static final Parcelable.Creator<Request> CREATOR = + new Parcelable.Creator<Request>() { @Override - public Options createFromParcel(Parcel in) { - return new Options(in); + public Request createFromParcel(Parcel in) { + return new Request(in); } @Override - public Options[] newArray(int size) { - return new Options[size]; + public Request[] newArray(int size) { + return new Request[size]; } }; - private Options(Parcel in) { - if (in.readInt() > 0) { - mDefaultLocales = LocaleList.CREATOR.createFromParcel(in); - } - if (in.readInt() > 0) { - mEntityConfig = TextClassifier.EntityConfig.CREATOR.createFromParcel(in); - } - mApplyStrategy = in.readInt(); + private Request(Parcel in) { + mText = in.readString(); + mDefaultLocales = in.readInt() == 0 ? null : LocaleList.CREATOR.createFromParcel(in); + mEntityConfig = in.readInt() == 0 + ? null : TextClassifier.EntityConfig.CREATOR.createFromParcel(in); + mLegacyFallback = true; mCallingPackageName = in.readString(); } } /** - * A function to create spans from TextLinks. - */ - private static final Function<TextLink, TextLinkSpan> DEFAULT_SPAN_FACTORY = - textLink -> new TextLinkSpan(textLink); - - /** * A ClickableSpan for a TextLink. * * <p>Applies only to TextViews. @@ -596,6 +571,7 @@ public final class TextLinks implements Parcelable { * * @throws IllegalArgumentException if entityScores is null or empty. */ + @NonNull public Builder addLink(int start, int end, Map<String, Float> entityScores) { mLinks.add(new TextLink(start, end, entityScores, null)); return this; @@ -605,6 +581,7 @@ public final class TextLinks implements Parcelable { * @see #addLink(int, int, Map) * @param urlSpan An optional URLSpan to delegate to. NOTE: Not parcelled. */ + @NonNull Builder addLink(int start, int end, Map<String, Float> entityScores, @Nullable URLSpan urlSpan) { mLinks.add(new TextLink(start, end, entityScores, urlSpan)); @@ -614,6 +591,7 @@ public final class TextLinks implements Parcelable { /** * Removes all {@link TextLink}s. */ + @NonNull public Builder clearTextLinks() { mLinks.clear(); return this; @@ -624,18 +602,9 @@ public final class TextLinks implements Parcelable { * * @return the constructed TextLinks */ + @NonNull public TextLinks build() { return new TextLinks(mFullText, mLinks); } } - - /** - * @throws IllegalArgumentException if the value is invalid - */ - private static void checkValidApplyStrategy(int applyStrategy) { - if (applyStrategy != APPLY_STRATEGY_IGNORE && applyStrategy != APPLY_STRATEGY_REPLACE) { - throw new IllegalArgumentException( - "Invalid apply strategy. See TextLinks.ApplyStrategy for options."); - } - } } diff --git a/core/java/android/view/textclassifier/TextLinksParams.java b/core/java/android/view/textclassifier/TextLinksParams.java new file mode 100644 index 000000000000..be4c3bcdff67 --- /dev/null +++ b/core/java/android/view/textclassifier/TextLinksParams.java @@ -0,0 +1,206 @@ +/* + * Copyright 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.text.Spannable; +import android.text.style.ClickableSpan; +import android.text.util.Linkify; +import android.text.util.Linkify.LinkifyMask; +import android.view.textclassifier.TextLinks.TextLink; +import android.view.textclassifier.TextLinks.TextLinkSpan; + +import com.android.internal.util.Preconditions; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; + +/** + * Parameters for generating and applying links. + * @hide + */ +public final class TextLinksParams { + + /** + * A function to create spans from TextLinks. + */ + private static final Function<TextLink, TextLinkSpan> DEFAULT_SPAN_FACTORY = + textLink -> new TextLinkSpan(textLink); + + @TextLinks.ApplyStrategy + private final int mApplyStrategy; + private final Function<TextLink, TextLinkSpan> mSpanFactory; + private final TextClassifier.EntityConfig mEntityConfig; + + private TextLinksParams( + @TextLinks.ApplyStrategy int applyStrategy, + Function<TextLink, TextLinkSpan> spanFactory) { + mApplyStrategy = applyStrategy; + mSpanFactory = spanFactory; + mEntityConfig = TextClassifier.EntityConfig.createWithHints(null); + } + + /** + * Returns a new TextLinksParams object based on the specified link mask. + * + * @param mask the link mask + * e.g. {@link LinkifyMask#PHONE_NUMBERS} | {@link LinkifyMask#EMAIL_ADDRESSES} + * @hide + */ + @NonNull + public static TextLinksParams fromLinkMask(@LinkifyMask int mask) { + final List<String> entitiesToFind = new ArrayList<>(); + if ((mask & Linkify.WEB_URLS) != 0) { + entitiesToFind.add(TextClassifier.TYPE_URL); + } + if ((mask & Linkify.EMAIL_ADDRESSES) != 0) { + entitiesToFind.add(TextClassifier.TYPE_EMAIL); + } + if ((mask & Linkify.PHONE_NUMBERS) != 0) { + entitiesToFind.add(TextClassifier.TYPE_PHONE); + } + if ((mask & Linkify.MAP_ADDRESSES) != 0) { + entitiesToFind.add(TextClassifier.TYPE_ADDRESS); + } + return new TextLinksParams.Builder().setEntityConfig( + TextClassifier.EntityConfig.createWithExplicitEntityList(entitiesToFind)) + .build(); + } + + /** + * Returns the entity config used to determine what entity types to generate. + */ + @NonNull + public TextClassifier.EntityConfig getEntityConfig() { + return mEntityConfig; + } + + /** + * Annotates the given text with the generated links. It will fail if the provided text doesn't + * match the original text used to crete the TextLinks. + * + * @param text the text to apply the links to. Must match the original text + * @param textLinks the links to apply to the text + * + * @return a status code indicating whether or not the links were successfully applied + * @hide + */ + @TextLinks.Status + public int apply(@NonNull Spannable text, @NonNull TextLinks textLinks) { + Preconditions.checkNotNull(text); + Preconditions.checkNotNull(textLinks); + + final String textString = text.toString(); + if (!textString.startsWith(textLinks.getText())) { + return TextLinks.STATUS_DIFFERENT_TEXT; + } + if (textLinks.getLinks().isEmpty()) { + return TextLinks.STATUS_NO_LINKS_FOUND; + } + + int applyCount = 0; + for (TextLink link : textLinks.getLinks()) { + final TextLinkSpan span = mSpanFactory.apply(link); + if (span != null) { + final ClickableSpan[] existingSpans = text.getSpans( + link.getStart(), link.getEnd(), ClickableSpan.class); + if (existingSpans.length > 0) { + if (mApplyStrategy == TextLinks.APPLY_STRATEGY_REPLACE) { + for (ClickableSpan existingSpan : existingSpans) { + text.removeSpan(existingSpan); + } + text.setSpan(span, link.getStart(), link.getEnd(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + applyCount++; + } + } else { + text.setSpan(span, link.getStart(), link.getEnd(), + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + applyCount++; + } + } + } + if (applyCount == 0) { + return TextLinks.STATUS_NO_LINKS_APPLIED; + } + return TextLinks.STATUS_LINKS_APPLIED; + } + + /** + * A builder for building TextLinksParams. + */ + public static final class Builder { + + @TextLinks.ApplyStrategy + private int mApplyStrategy = TextLinks.APPLY_STRATEGY_IGNORE; + private Function<TextLink, TextLinkSpan> mSpanFactory = DEFAULT_SPAN_FACTORY; + + /** + * Sets the apply strategy used to determine how to apply links to text. + * e.g {@link TextLinks#APPLY_STRATEGY_IGNORE} + * + * @return this builder + */ + public Builder setApplyStrategy(@TextLinks.ApplyStrategy int applyStrategy) { + mApplyStrategy = checkApplyStrategy(applyStrategy); + return this; + } + + /** + * Sets a custom span factory for converting TextLinks to TextLinkSpans. + * Set to {@code null} to use the default span factory. + * + * @return this builder + */ + public Builder setSpanFactory(@Nullable Function<TextLink, TextLinkSpan> spanFactory) { + mSpanFactory = spanFactory == null ? DEFAULT_SPAN_FACTORY : spanFactory; + return this; + } + + /** + * Sets the entity configuration used to determine what entity types to generate. + * Set to {@code null} for the default entity config which will automatically determine + * what links to generate. + * + * @return this builder + */ + public Builder setEntityConfig(@Nullable TextClassifier.EntityConfig entityConfig) { + return this; + } + + /** + * Builds and returns a TextLinksParams object. + */ + public TextLinksParams build() { + return new TextLinksParams(mApplyStrategy, mSpanFactory); + } + } + + /** @throws IllegalArgumentException if the value is invalid */ + @TextLinks.ApplyStrategy + private static int checkApplyStrategy(int applyStrategy) { + if (applyStrategy != TextLinks.APPLY_STRATEGY_IGNORE + && applyStrategy != TextLinks.APPLY_STRATEGY_REPLACE) { + throw new IllegalArgumentException( + "Invalid apply strategy. See TextLinksParams.ApplyStrategy for options."); + } + return applyStrategy; + } +} + diff --git a/core/java/android/view/textclassifier/TextSelection.aidl b/core/java/android/view/textclassifier/TextSelection.aidl index dab1aefa3532..b2fd9beeafe3 100644 --- a/core/java/android/view/textclassifier/TextSelection.aidl +++ b/core/java/android/view/textclassifier/TextSelection.aidl @@ -17,4 +17,4 @@ package android.view.textclassifier; parcelable TextSelection; -parcelable TextSelection.Options;
\ No newline at end of file +parcelable TextSelection.Request;
\ No newline at end of file diff --git a/core/java/android/view/textclassifier/TextSelection.java b/core/java/android/view/textclassifier/TextSelection.java index 1c93be75a3a1..939e71760dcd 100644 --- a/core/java/android/view/textclassifier/TextSelection.java +++ b/core/java/android/view/textclassifier/TextSelection.java @@ -25,6 +25,7 @@ import android.os.Parcel; import android.os.Parcelable; import android.util.ArrayMap; import android.view.textclassifier.TextClassifier.EntityType; +import android.view.textclassifier.TextClassifier.Utils; import com.android.internal.util.Preconditions; @@ -38,16 +39,15 @@ public final class TextSelection implements Parcelable { private final int mStartIndex; private final int mEndIndex; - @NonNull private final EntityConfidence mEntityConfidence; - @NonNull private final String mSignature; + private final EntityConfidence mEntityConfidence; + @Nullable private final String mId; private TextSelection( - int startIndex, int endIndex, @NonNull Map<String, Float> entityConfidence, - @NonNull String signature) { + int startIndex, int endIndex, Map<String, Float> entityConfidence, String id) { mStartIndex = startIndex; mEndIndex = endIndex; mEntityConfidence = new EntityConfidence(entityConfidence); - mSignature = signature; + mId = id; } /** @@ -80,7 +80,8 @@ public final class TextSelection implements Parcelable { * @see #getEntityCount() for the number of entities available. */ @NonNull - public @EntityType String getEntity(int index) { + @EntityType + public String getEntity(int index) { return mEntityConfidence.getEntities().get(index); } @@ -95,21 +96,19 @@ public final class TextSelection implements Parcelable { } /** - * Returns the signature for this object. - * The TextClassifier that generates this object may use it as a way to internally identify - * this object. + * Returns the id, if one exists, for this object. */ - @NonNull - public String getSignature() { - return mSignature; + @Nullable + public String getId() { + return mId; } @Override public String toString() { return String.format( Locale.US, - "TextSelection {startIndex=%d, endIndex=%d, entities=%s, signature=%s}", - mStartIndex, mEndIndex, mEntityConfidence, mSignature); + "TextSelection {id=%s, startIndex=%d, endIndex=%d, entities=%s}", + mId, mStartIndex, mEndIndex, mEntityConfidence); } /** @@ -119,8 +118,8 @@ public final class TextSelection implements Parcelable { private final int mStartIndex; private final int mEndIndex; - @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>(); - @NonNull private String mSignature = ""; + private final Map<String, Float> mEntityConfidence = new ArrayMap<>(); + @Nullable private String mId; /** * Creates a builder used to build {@link TextSelection} objects. @@ -142,78 +141,86 @@ public final class TextSelection implements Parcelable { * 0 implies the entity does not exist for the classified text. * Values greater than 1 are clamped to 1. */ + @NonNull public Builder setEntityType( @NonNull @EntityType String type, @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { + Preconditions.checkNotNull(type); mEntityConfidence.put(type, confidenceScore); return this; } /** - * Sets a signature for the TextSelection object. - * - * The TextClassifier that generates the TextSelection object may use it as a way to - * internally identify the TextSelection object. + * Sets an id for the TextSelection object. */ - public Builder setSignature(@NonNull String signature) { - mSignature = Preconditions.checkNotNull(signature); + @NonNull + public Builder setId(@NonNull String id) { + mId = Preconditions.checkNotNull(id); return this; } /** * Builds and returns {@link TextSelection} object. */ + @NonNull public TextSelection build() { return new TextSelection( - mStartIndex, mEndIndex, mEntityConfidence, mSignature); + mStartIndex, mEndIndex, mEntityConfidence, mId); } } /** - * Optional input parameters for generating TextSelection. + * A request object for generating TextSelection. */ - public static final class Options implements Parcelable { - - private @Nullable LocaleList mDefaultLocales; - private boolean mDarkLaunchAllowed; + public static final class Request implements Parcelable { - public Options() {} + private final CharSequence mText; + private final int mStartIndex; + private final int mEndIndex; + @Nullable private final LocaleList mDefaultLocales; + private final boolean mDarkLaunchAllowed; + + private Request( + CharSequence text, + int startIndex, + int endIndex, + LocaleList defaultLocales, + boolean darkLaunchAllowed) { + mText = text; + mStartIndex = startIndex; + mEndIndex = endIndex; + mDefaultLocales = defaultLocales; + mDarkLaunchAllowed = darkLaunchAllowed; + } /** - * @param defaultLocales ordered list of locale preferences that may be used to disambiguate - * the provided text. If no locale preferences exist, set this to null or an empty - * locale list. + * Returns the text providing context for the selected text (which is specified by the + * sub sequence starting at startIndex and ending at endIndex). */ - public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { - mDefaultLocales = defaultLocales; - return this; + @NonNull + public CharSequence getText() { + return mText; } /** - * @return ordered list of locale preferences that can be used to disambiguate - * the provided text. + * Returns start index of the selected part of text. */ - @Nullable - public LocaleList getDefaultLocales() { - return mDefaultLocales; + @IntRange(from = 0) + public int getStartIndex() { + return mStartIndex; } /** - * @param allowed whether or not the TextClassifier should return selection suggestions - * when "dark launched". When a TextClassifier is dark launched, it can suggest - * selection changes that should not be used to actually change the user's selection. - * Instead, the suggested selection is logged, compared with the user's selection - * interaction, and used to generate quality metrics for the TextClassifier. - * - * @hide + * Returns end index of the selected part of text. */ - public void setDarkLaunchAllowed(boolean allowed) { - mDarkLaunchAllowed = allowed; + @IntRange(from = 0) + public int getEndIndex() { + return mEndIndex; } /** - * Returns true if the TextClassifier should return selection suggestions when - * "dark launched". Otherwise, returns false. + * Returns true if the TextClassifier should return selection suggestions when "dark + * launched". Otherwise, returns false. * * @hide */ @@ -221,6 +228,83 @@ public final class TextSelection implements Parcelable { return mDarkLaunchAllowed; } + /** + * @return ordered list of locale preferences that can be used to disambiguate the + * provided text. + */ + @Nullable + public LocaleList getDefaultLocales() { + return mDefaultLocales; + } + + /** + * A builder for building TextSelection requests. + */ + public static final class Builder { + + private final CharSequence mText; + private final int mStartIndex; + private final int mEndIndex; + + @Nullable private LocaleList mDefaultLocales; + private boolean mDarkLaunchAllowed; + + /** + * @param text text providing context for the selected text (which is specified by the + * sub sequence starting at selectionStartIndex and ending at selectionEndIndex) + * @param startIndex start index of the selected part of text + * @param endIndex end index of the selected part of text + */ + public Builder( + @NonNull CharSequence text, + @IntRange(from = 0) int startIndex, + @IntRange(from = 0) int endIndex) { + Utils.checkArgument(text, startIndex, endIndex); + mText = text; + mStartIndex = startIndex; + mEndIndex = endIndex; + } + + /** + * @param defaultLocales ordered list of locale preferences that may be used to + * disambiguate the provided text. If no locale preferences exist, set this to null + * or an empty locale list. + * + * @return this builder. + */ + @NonNull + public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) { + mDefaultLocales = defaultLocales; + return this; + } + + /** + * @param allowed whether or not the TextClassifier should return selection suggestions + * when "dark launched". When a TextClassifier is dark launched, it can suggest + * selection changes that should not be used to actually change the user's + * selection. Instead, the suggested selection is logged, compared with the user's + * selection interaction, and used to generate quality metrics for the + * TextClassifier. Not parceled. + * + * @return this builder. + * @hide + */ + @NonNull + public Builder setDarkLaunchAllowed(boolean allowed) { + mDarkLaunchAllowed = allowed; + return this; + } + + /** + * Builds and returns the request object. + */ + @NonNull + public Request build() { + return new Request(mText, mStartIndex, mEndIndex, + mDefaultLocales, mDarkLaunchAllowed); + } + } + @Override public int describeContents() { return 0; @@ -228,31 +312,34 @@ public final class TextSelection implements Parcelable { @Override public void writeToParcel(Parcel dest, int flags) { + dest.writeString(mText.toString()); + dest.writeInt(mStartIndex); + dest.writeInt(mEndIndex); dest.writeInt(mDefaultLocales != null ? 1 : 0); if (mDefaultLocales != null) { mDefaultLocales.writeToParcel(dest, flags); } - dest.writeInt(mDarkLaunchAllowed ? 1 : 0); } - public static final Parcelable.Creator<Options> CREATOR = - new Parcelable.Creator<Options>() { + public static final Parcelable.Creator<Request> CREATOR = + new Parcelable.Creator<Request>() { @Override - public Options createFromParcel(Parcel in) { - return new Options(in); + public Request createFromParcel(Parcel in) { + return new Request(in); } @Override - public Options[] newArray(int size) { - return new Options[size]; + public Request[] newArray(int size) { + return new Request[size]; } }; - private Options(Parcel in) { - if (in.readInt() > 0) { - mDefaultLocales = LocaleList.CREATOR.createFromParcel(in); - } - mDarkLaunchAllowed = in.readInt() != 0; + private Request(Parcel in) { + mText = in.readString(); + mStartIndex = in.readInt(); + mEndIndex = in.readInt(); + mDefaultLocales = in.readInt() == 0 ? null : LocaleList.CREATOR.createFromParcel(in); + mDarkLaunchAllowed = false; } } @@ -266,7 +353,7 @@ public final class TextSelection implements Parcelable { dest.writeInt(mStartIndex); dest.writeInt(mEndIndex); mEntityConfidence.writeToParcel(dest, flags); - dest.writeString(mSignature); + dest.writeString(mId); } public static final Parcelable.Creator<TextSelection> CREATOR = @@ -286,6 +373,6 @@ public final class TextSelection implements Parcelable { mStartIndex = in.readInt(); mEndIndex = in.readInt(); mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in); - mSignature = in.readString(); + mId = in.readString(); } } diff --git a/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java b/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java index 157b3d82163b..f7d75cd89537 100644 --- a/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java +++ b/core/java/android/view/textclassifier/logging/SmartSelectionEventTracker.java @@ -473,7 +473,7 @@ public final class SmartSelectionEventTracker { final String entityType = classification.getEntityCount() > 0 ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = getVersionInfo(classification.getSignature()); + final String versionTag = getVersionInfo(classification.getId()); return new SelectionEvent( start, end, EventType.SELECTION_MODIFIED, entityType, versionTag); } @@ -489,7 +489,7 @@ public final class SmartSelectionEventTracker { */ public static SelectionEvent selectionModified( int start, int end, @NonNull TextSelection selection) { - final boolean smartSelection = getSourceClassifier(selection.getSignature()) + final boolean smartSelection = getSourceClassifier(selection.getId()) .equals(TextClassifier.DEFAULT_LOG_TAG); final int eventType; if (smartSelection) { @@ -503,7 +503,7 @@ public final class SmartSelectionEventTracker { final String entityType = selection.getEntityCount() > 0 ? selection.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = getVersionInfo(selection.getSignature()); + final String versionTag = getVersionInfo(selection.getId()); return new SelectionEvent(start, end, eventType, entityType, versionTag); } @@ -538,7 +538,7 @@ public final class SmartSelectionEventTracker { final String entityType = classification.getEntityCount() > 0 ? classification.getEntity(0) : TextClassifier.TYPE_UNKNOWN; - final String versionTag = getVersionInfo(classification.getSignature()); + final String versionTag = getVersionInfo(classification.getId()); return new SelectionEvent(start, end, actionType, entityType, versionTag); } diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java index 8b49ccbaafdf..b3327a70d579 100644 --- a/core/java/android/widget/SelectionActionModeHelper.java +++ b/core/java/android/widget/SelectionActionModeHelper.java @@ -499,7 +499,8 @@ public final class SelectionActionModeHelper { mOriginalEnd = mSelectionEnd = selectionEnd; mAllowReset = false; maybeInvalidateLogger(); - mLogger.logSelectionStarted(text, selectionStart, + mLogger.logSelectionStarted(mTextView.getTextClassificationSession(), + text, selectionStart, isLink ? SelectionEvent.INVOCATION_LINK : SelectionEvent.INVOCATION_MANUAL); } @@ -633,6 +634,7 @@ public final class SelectionActionModeHelper { mSelectionStart, mSelectionEnd, SelectionEvent.ACTION_ABANDON, null /* classification */); mSelectionStart = mSelectionEnd = -1; + mTextView.getTextClassificationSession().destroy(); mIsPending = false; } } @@ -661,16 +663,16 @@ 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; + + @Nullable private TextClassifier mClassificationSession; private int mStartIndex; private String mText; SelectionMetricsLogger(TextView textView) { Preconditions.checkNotNull(textView); - mTextClassificationSession = textView::getTextClassificationSession; mLogger = textView.getTextClassifier().getLogger( new Logger.Config(textView.getContext(), getWidetType(textView), null)); mEditTextLogger = textView.isTextEditable(); @@ -689,6 +691,7 @@ public final class SelectionActionModeHelper { } public void logSelectionStarted( + TextClassifier classificationSession, CharSequence text, int index, @InvocationMethod int invocationMethod) { try { @@ -701,7 +704,8 @@ public final class SelectionActionModeHelper { mStartIndex = index; mLogger.logSelectionStartedEvent(invocationMethod, 0); // TODO: Remove the above legacy logging. - mTextClassificationSession.get().onSelectionEvent( + mClassificationSession = classificationSession; + mClassificationSession.onSelectionEvent( SelectionEvent.createSelectionStartedEvent(invocationMethod, 0)); } catch (Exception e) { // Avoid crashes due to logging. @@ -719,23 +723,29 @@ public final class SelectionActionModeHelper { mLogger.logSelectionModifiedEvent( wordIndices[0], wordIndices[1], selection); // TODO: Remove the above legacy logging. - mTextClassificationSession.get().onSelectionEvent( - SelectionEvent.createSelectionModifiedEvent( - wordIndices[0], wordIndices[1], selection)); + if (mClassificationSession != null) { + mClassificationSession.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)); + if (mClassificationSession != null) { + mClassificationSession.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])); + if (mClassificationSession != null) { + mClassificationSession.onSelectionEvent( + SelectionEvent.createSelectionModifiedEvent( + wordIndices[0], wordIndices[1])); + } } } catch (Exception e) { // Avoid crashes due to logging. @@ -755,24 +765,24 @@ public final class SelectionActionModeHelper { 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)); + if (mClassificationSession != null) { + mClassificationSession.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)); + if (mClassificationSession != null) { + mClassificationSession.onSelectionEvent( + SelectionEvent.createSelectionActionEvent( + wordIndices[0], wordIndices[1], action)); + } } } catch (Exception e) { // Avoid crashes due to logging. Log.e(LOG_TAG, "" + e.getMessage(), e); - } finally { - if (SelectionEvent.isTerminal(action)) { - mTextClassificationSession.get().destroy(); - } } } @@ -926,9 +936,8 @@ public final class SelectionActionModeHelper { /** End index relative to mText. */ private int mSelectionEnd; - private final TextSelection.Options mSelectionOptions = new TextSelection.Options(); - private final TextClassification.Options mClassificationOptions = - new TextClassification.Options(); + @Nullable + private LocaleList mDefaultLocales; /** Trimmed text starting from mTrimStart in mText. */ private CharSequence mTrimmedText; @@ -966,9 +975,7 @@ public final class SelectionActionModeHelper { Preconditions.checkArgument(selectionEnd > selectionStart); mSelectionStart = selectionStart; mSelectionEnd = selectionEnd; - mClassificationOptions.setDefaultLocales(locales); - mSelectionOptions.setDefaultLocales(locales) - .setDarkLaunchAllowed(true); + mDefaultLocales = locales; } @WorkerThread @@ -983,13 +990,16 @@ public final class SelectionActionModeHelper { trimText(); final TextSelection selection; if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) { - selection = mTextClassifier.get().suggestSelection( - mTrimmedText, mRelativeStart, mRelativeEnd, mSelectionOptions); + final TextSelection.Request request = new TextSelection.Request.Builder( + mTrimmedText, mRelativeStart, mRelativeEnd) + .setDefaultLocales(mDefaultLocales) + .setDarkLaunchAllowed(true) + .build(); + selection = mTextClassifier.get().suggestSelection(request); } else { // Use old APIs. selection = mTextClassifier.get().suggestSelection( - mTrimmedText, mRelativeStart, mRelativeEnd, - mSelectionOptions.getDefaultLocales()); + mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales); } // Do not classify new selection boundaries if TextClassifier should be dark launched. if (!mDarkLaunchEnabled) { @@ -1024,25 +1034,26 @@ public final class SelectionActionModeHelper { if (!Objects.equals(mText, mLastClassificationText) || mSelectionStart != mLastClassificationSelectionStart || mSelectionEnd != mLastClassificationSelectionEnd - || !Objects.equals( - mClassificationOptions.getDefaultLocales(), - mLastClassificationLocales)) { + || !Objects.equals(mDefaultLocales, mLastClassificationLocales)) { mLastClassificationText = mText; mLastClassificationSelectionStart = mSelectionStart; mLastClassificationSelectionEnd = mSelectionEnd; - mLastClassificationLocales = mClassificationOptions.getDefaultLocales(); + mLastClassificationLocales = mDefaultLocales; trimText(); final TextClassification classification; if (mContext.getApplicationInfo().targetSdkVersion > Build.VERSION_CODES.O_MR1) { - classification = mTextClassifier.get().classifyText( - mTrimmedText, mRelativeStart, mRelativeEnd, mClassificationOptions); + final TextClassification.Request request = + new TextClassification.Request.Builder( + mTrimmedText, mRelativeStart, mRelativeEnd) + .setDefaultLocales(mDefaultLocales) + .build(); + classification = mTextClassifier.get().classifyText(request); } else { // Use old APIs. classification = mTextClassifier.get().classifyText( - mTrimmedText, mRelativeStart, mRelativeEnd, - mClassificationOptions.getDefaultLocales()); + mTrimmedText, mRelativeStart, mRelativeEnd, mDefaultLocales); } mLastClassificationResult = new SelectionResult( mSelectionStart, mSelectionEnd, classification, selection); diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 8bf497e0e90d..11db6b650583 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -36,6 +36,7 @@ import android.annotation.StringRes; import android.annotation.StyleRes; import android.annotation.XmlRes; import android.app.Activity; +import android.app.PendingIntent; import android.app.assist.AssistStructure; import android.content.ClipData; import android.content.ClipDescription; @@ -11541,6 +11542,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener /** * Returns a session-aware text classifier. + * This method creates one if none already exists or the current one is destroyed. */ @NonNull TextClassifier getTextClassificationSession() { @@ -11623,15 +11625,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int start = spanned.getSpanStart(clickedSpan); final int end = spanned.getSpanEnd(clickedSpan); if (start >= 0 && end <= mText.length() && start < end) { - final TextClassification.Options options = new TextClassification.Options() - .setDefaultLocales(getTextLocales()); + final TextClassification.Request request = new TextClassification.Request.Builder( + mText, start, end) + .setDefaultLocales(getTextLocales()) + .build(); final Supplier<TextClassification> supplier = () -> - getTextClassifier().classifyText(mText, start, end, options); + getTextClassifier().classifyText(request); final Consumer<TextClassification> consumer = classification -> { if (classification != null) { - final Intent intent = classification.getIntent(); - if (intent != null) { - TextClassification.fireIntent(mContext, intent); + if (!classification.getActions().isEmpty()) { + try { + classification.getActions().get(0).getActionIntent().send(); + } catch (PendingIntent.CanceledException e) { + Log.e(LOG_TAG, "Error sending PendingIntent", e); + } } else { Log.d(LOG_TAG, "No link action to perform"); } |
