diff --git a/api/current.txt b/api/current.txt index 40f2d67a9f1f0..189028b18c001 100644 --- a/api/current.txt +++ b/api/current.txt @@ -52606,9 +52606,10 @@ package android.view.inspector { package android.view.textclassifier { public final class ConversationActions implements android.os.Parcelable { - ctor public ConversationActions(java.util.List); + ctor public ConversationActions(java.util.List, java.lang.String); method public int describeContents(); method public java.util.List getConversationActions(); + method public java.lang.String getId(); method public void writeToParcel(android.os.Parcel, int); field public static final android.os.Parcelable.Creator CREATOR; field public static final java.lang.String HINT_FOR_IN_APP = "in_app"; @@ -52669,6 +52670,7 @@ package android.view.textclassifier { method public int describeContents(); method public java.lang.String getCallingPackageName(); method public java.util.List getConversation(); + method public java.lang.String getConversationId(); method public java.util.List getHints(); method public int getMaxSuggestions(); method public android.view.textclassifier.ConversationActions.TypeConfig getTypeConfig(); @@ -52679,6 +52681,7 @@ package android.view.textclassifier { public static final class ConversationActions.Request.Builder { ctor public ConversationActions.Request.Builder(java.util.List); method public android.view.textclassifier.ConversationActions.Request build(); + method public android.view.textclassifier.ConversationActions.Request.Builder setConversationId(java.lang.String); method public android.view.textclassifier.ConversationActions.Request.Builder setHints(java.util.List); method public android.view.textclassifier.ConversationActions.Request.Builder setMaxSuggestions(int); method public android.view.textclassifier.ConversationActions.Request.Builder setTypeConfig(android.view.textclassifier.ConversationActions.TypeConfig); @@ -52841,6 +52844,7 @@ package android.view.textclassifier { method public default int getMaxGenerateLinksTextLength(); method public default boolean isDestroyed(); method public default void onSelectionEvent(android.view.textclassifier.SelectionEvent); + method public default void onTextClassifierEvent(android.view.textclassifier.TextClassifierEvent); method public default android.view.textclassifier.ConversationActions suggestConversationActions(android.view.textclassifier.ConversationActions.Request); method public default android.view.textclassifier.TextSelection suggestSelection(android.view.textclassifier.TextSelection.Request); method public default android.view.textclassifier.TextSelection suggestSelection(java.lang.CharSequence, int, int, android.os.LocaleList); @@ -52862,6 +52866,7 @@ package android.view.textclassifier { field public static final java.lang.String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview"; field public static final java.lang.String WIDGET_TYPE_EDITTEXT = "edittext"; field public static final java.lang.String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview"; + field public static final java.lang.String WIDGET_TYPE_NOTIFICATION = "notification"; field public static final java.lang.String WIDGET_TYPE_TEXTVIEW = "textview"; field public static final java.lang.String WIDGET_TYPE_UNKNOWN = "unknown"; field public static final java.lang.String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview"; @@ -52879,6 +52884,68 @@ package android.view.textclassifier { field public static final android.os.Parcelable.Creator CREATOR; } + public final class TextClassifierEvent implements android.os.Parcelable { + method public int describeContents(); + method public int[] getActionIndices(); + method public java.lang.String getEntityType(); + method public int getEventCategory(); + method public android.view.textclassifier.TextClassificationContext getEventContext(); + method public int getEventIndex(); + method public long getEventTime(); + method public int getEventType(); + method public android.os.Bundle getExtras(); + method public java.lang.String getLanguage(); + method public int getRelativeSuggestedWordEndIndex(); + method public int getRelativeSuggestedWordStartIndex(); + method public int getRelativeWordEndIndex(); + method public int getRelativeWordStartIndex(); + method public java.lang.String getResultId(); + method public void writeToParcel(android.os.Parcel, int); + field public static final int CATEGORY_CONVERSATION_ACTIONS = 3; // 0x3 + field public static final int CATEGORY_LANGUAGE_DETECTION = 4; // 0x4 + field public static final int CATEGORY_LINKIFY = 2; // 0x2 + field public static final int CATEGORY_SELECTION = 1; // 0x1 + field public static final int CATEGORY_UNDEFINED = 0; // 0x0 + field public static final android.os.Parcelable.Creator CREATOR; + field public static final int TYPE_ACTIONS_SHOWN = 6; // 0x6 + field public static final int TYPE_AUTO_SELECTION = 5; // 0x5 + field public static final int TYPE_COPY_ACTION = 9; // 0x9 + field public static final int TYPE_CUT_ACTION = 11; // 0xb + field public static final int TYPE_LINK_CLICKED = 7; // 0x7 + field public static final int TYPE_MANUAL_REPLY = 19; // 0x13 + field public static final int TYPE_OTHER_ACTION = 16; // 0x10 + field public static final int TYPE_OVERTYPE = 8; // 0x8 + field public static final int TYPE_PASTE_ACTION = 10; // 0xa + field public static final int TYPE_SELECTION_DESTROYED = 15; // 0xf + field public static final int TYPE_SELECTION_DRAG = 14; // 0xe + field public static final int TYPE_SELECTION_MODIFIED = 2; // 0x2 + field public static final int TYPE_SELECTION_RESET = 18; // 0x12 + field public static final int TYPE_SELECTION_STARTED = 1; // 0x1 + field public static final int TYPE_SELECT_ALL = 17; // 0x11 + field public static final int TYPE_SHARE_ACTION = 12; // 0xc + field public static final int TYPE_SMART_ACTION = 13; // 0xd + field public static final int TYPE_SMART_SELECTION_MULTI = 4; // 0x4 + field public static final int TYPE_SMART_SELECTION_SINGLE = 3; // 0x3 + field public static final int TYPE_UNDEFINED = 0; // 0x0 + } + + public static final class TextClassifierEvent.Builder { + ctor public TextClassifierEvent.Builder(int, int); + method public android.view.textclassifier.TextClassifierEvent build(); + method public android.view.textclassifier.TextClassifierEvent.Builder setActionIndices(int...); + method public android.view.textclassifier.TextClassifierEvent.Builder setEntityType(java.lang.String); + method public android.view.textclassifier.TextClassifierEvent.Builder setEventContext(android.view.textclassifier.TextClassificationContext); + method public android.view.textclassifier.TextClassifierEvent.Builder setEventIndex(int); + method public android.view.textclassifier.TextClassifierEvent.Builder setEventTime(long); + method public android.view.textclassifier.TextClassifierEvent.Builder setExtras(android.os.Bundle); + method public android.view.textclassifier.TextClassifierEvent.Builder setLanguage(java.lang.String); + method public android.view.textclassifier.TextClassifierEvent.Builder setRelativeSuggestedWordEndIndex(int); + method public android.view.textclassifier.TextClassifierEvent.Builder setRelativeSuggestedWordStartIndex(int); + method public android.view.textclassifier.TextClassifierEvent.Builder setRelativeWordEndIndex(int); + method public android.view.textclassifier.TextClassifierEvent.Builder setRelativeWordStartIndex(int); + method public android.view.textclassifier.TextClassifierEvent.Builder setResultId(java.lang.String); + } + public final class TextLanguage implements android.os.Parcelable { method public int describeContents(); method public float getConfidenceScore(android.icu.util.ULocale); diff --git a/api/system-current.txt b/api/system-current.txt index 5a49f0bf33261..a2e5121a670bd 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5357,9 +5357,10 @@ package android.service.textclassifier { method public void onDestroyTextClassificationSession(android.view.textclassifier.TextClassificationSessionId); method public void onDetectLanguage(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.TextLanguage.Request, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback); method public abstract void onGenerateLinks(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.TextLinks.Request, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback); - method public void onSelectionEvent(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.SelectionEvent); + method public deprecated void onSelectionEvent(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.SelectionEvent); method public void onSuggestConversationActions(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.ConversationActions.Request, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback); method public abstract void onSuggestSelection(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.TextSelection.Request, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback); + method public void onTextClassifierEvent(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.TextClassifierEvent); field public static final java.lang.String SERVICE_INTERFACE = "android.service.textclassifier.TextClassifierService"; } diff --git a/core/java/android/service/textclassifier/ITextClassifierService.aidl b/core/java/android/service/textclassifier/ITextClassifierService.aidl index 254a710a2c408..794179415fc0b 100644 --- a/core/java/android/service/textclassifier/ITextClassifierService.aidl +++ b/core/java/android/service/textclassifier/ITextClassifierService.aidl @@ -26,6 +26,7 @@ import android.view.textclassifier.SelectionEvent; import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassificationContext; import android.view.textclassifier.TextClassificationSessionId; +import android.view.textclassifier.TextClassifierEvent; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextLanguage; import android.view.textclassifier.TextSelection; @@ -52,10 +53,15 @@ oneway interface ITextClassifierService { in TextLinks.Request request, in ITextLinksCallback callback); + // TODO: Remove void onSelectionEvent( in TextClassificationSessionId sessionId, in SelectionEvent event); + void onTextClassifierEvent( + in TextClassificationSessionId sessionId, + in TextClassifierEvent event); + void onCreateTextClassificationSession( in TextClassificationContext context, in TextClassificationSessionId sessionId); diff --git a/core/java/android/service/textclassifier/TextClassifierService.java b/core/java/android/service/textclassifier/TextClassifierService.java index 3b813c7df82b3..2221d6ec72961 100644 --- a/core/java/android/service/textclassifier/TextClassifierService.java +++ b/core/java/android/service/textclassifier/TextClassifierService.java @@ -39,6 +39,7 @@ import android.view.textclassifier.TextClassificationContext; import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassificationSessionId; import android.view.textclassifier.TextClassifier; +import android.view.textclassifier.TextClassifierEvent; import android.view.textclassifier.TextLanguage; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; @@ -191,6 +192,15 @@ public abstract class TextClassifierService extends Service { TextClassifierService.this.onSelectionEvent(sessionId, event); } + /** {@inheritDoc} */ + @Override + public void onTextClassifierEvent( + TextClassificationSessionId sessionId, + TextClassifierEvent event) { + Preconditions.checkNotNull(event); + TextClassifierService.this.onTextClassifierEvent(sessionId, event); + } + /** {@inheritDoc} */ @Override public void onDetectLanguage( @@ -368,10 +378,27 @@ public abstract class TextClassifierService extends Service { * * @param sessionId the session id * @param event the selection event + * @deprecated + * Use {@link #onTextClassifierEvent(TextClassificationSessionId, TextClassifierEvent)} + * instead */ + @Deprecated public void onSelectionEvent( @Nullable TextClassificationSessionId sessionId, @NonNull SelectionEvent event) {} + /** + * Writes the TextClassifier event. + * This is called when a TextClassifier event occurs. e.g. user changed selection, + * smart selection happened, or a link was clicked. + * + *

The default implementation ignores the event. + * + * @param sessionId the session id + * @param event the TextClassifier event + */ + public void onTextClassifierEvent( + @Nullable TextClassificationSessionId sessionId, @NonNull TextClassifierEvent event) {} + /** * Creates a new text classification session for the specified context. * diff --git a/core/java/android/view/textclassifier/ConversationActions.java b/core/java/android/view/textclassifier/ConversationActions.java index 04924c9a59c43..3f690f70d2751 100644 --- a/core/java/android/view/textclassifier/ConversationActions.java +++ b/core/java/android/view/textclassifier/ConversationActions.java @@ -139,17 +139,21 @@ public final class ConversationActions implements Parcelable { */ public static final String HINT_FOR_NOTIFICATION = "notification"; - private List mConversationActions; + private final List mConversationActions; + private final String mId; /** Constructs a {@link ConversationActions} object. */ - public ConversationActions(@NonNull List conversationActions) { + public ConversationActions( + @NonNull List conversationActions, @Nullable String id) { mConversationActions = Collections.unmodifiableList(Preconditions.checkNotNull(conversationActions)); + mId = id; } private ConversationActions(Parcel in) { mConversationActions = Collections.unmodifiableList(in.createTypedArrayList(ConversationAction.CREATOR)); + mId = in.readString(); } @Override @@ -160,14 +164,26 @@ public final class ConversationActions implements Parcelable { @Override public void writeToParcel(Parcel parcel, int flags) { parcel.writeTypedList(mConversationActions); + parcel.writeString(mId); } - /** Returns an immutable list of {@link ConversationAction} objects. */ + /** + * Returns an immutable list of {@link ConversationAction} objects, which are ordered from high + * confidence to low confidence. + */ @NonNull public List getConversationActions() { return mConversationActions; } + /** + * Returns the id, if one exists, for this object. + */ + @Nullable + public String getId() { + return mId; + } + /** Represents the action suggested by a {@link TextClassifier} on a given conversation. */ public static final class ConversationAction implements Parcelable { @@ -678,35 +694,37 @@ public final class ConversationActions implements Parcelable { private final List mHints; @Nullable private String mCallingPackageName; + @Nullable + private final String mConversationId; private Request( @NonNull List conversation, @NonNull TypeConfig typeConfig, int maxSuggestions, + String conversationId, @Nullable @Hint List hints) { mConversation = Preconditions.checkNotNull(conversation); mTypeConfig = Preconditions.checkNotNull(typeConfig); mMaxSuggestions = maxSuggestions; + mConversationId = conversationId; mHints = hints; } private static Request readFromParcel(Parcel in) { List conversation = new ArrayList<>(); in.readParcelableList(conversation, null); - TypeConfig typeConfig = in.readParcelable(null); - int maxSuggestions = in.readInt(); - + String conversationId = in.readString(); List hints = new ArrayList<>(); in.readStringList(hints); - String callingPackageName = in.readString(); Request request = new Request( conversation, typeConfig, maxSuggestions, + conversationId, hints); request.setCallingPackageName(callingPackageName); return request; @@ -717,6 +735,7 @@ public final class ConversationActions implements Parcelable { parcel.writeParcelableList(mConversation, flags); parcel.writeParcelable(mTypeConfig, flags); parcel.writeInt(mMaxSuggestions); + parcel.writeString(mConversationId); parcel.writeStringList(mHints); parcel.writeString(mCallingPackageName); } @@ -759,6 +778,16 @@ public final class ConversationActions implements Parcelable { return mMaxSuggestions; } + /** + * Return an unique identifier of the conversation that is generating actions for. This + * identifier is unique within the calling package only, so use it with + * {@link #getCallingPackageName()}. + */ + @Nullable + public String getConversationId() { + return mConversationId; + } + /** Returns an immutable list of hints */ @Nullable @Hint @@ -794,6 +823,8 @@ public final class ConversationActions implements Parcelable { private TypeConfig mTypeConfig; private int mMaxSuggestions; @Nullable + private String mConversationId; + @Nullable @Hint private List mHints; @@ -823,7 +854,8 @@ public final class ConversationActions implements Parcelable { return this; } - /** Sets the maximum number of suggestions you want. + /** + * Sets the maximum number of suggestions you want. *

* Value 0 means no restriction. */ @@ -833,6 +865,15 @@ public final class ConversationActions implements Parcelable { return this; } + /** + * Sets an unique identifier of the conversation that is generating actions for. + */ + @NonNull + public Builder setConversationId(@Nullable String conversationId) { + mConversationId = conversationId; + return this; + } + /** Builds the {@link Request} object. */ @NonNull public Request build() { @@ -840,6 +881,7 @@ public final class ConversationActions implements Parcelable { Collections.unmodifiableList(mConversation), mTypeConfig == null ? new TypeConfig.Builder().build() : mTypeConfig, mMaxSuggestions, + mConversationId, mHints == null ? Collections.emptyList() : Collections.unmodifiableList(mHints)); diff --git a/core/java/android/view/textclassifier/SystemTextClassifier.java b/core/java/android/view/textclassifier/SystemTextClassifier.java index c24489c8740b4..8b370f543cccf 100644 --- a/core/java/android/view/textclassifier/SystemTextClassifier.java +++ b/core/java/android/view/textclassifier/SystemTextClassifier.java @@ -146,6 +146,18 @@ public final class SystemTextClassifier implements TextClassifier { } } + @Override + public void onTextClassifierEvent(@NonNull TextClassifierEvent event) { + Preconditions.checkNotNull(event); + Utils.checkMainThread(); + + try { + mManagerService.onTextClassifierEvent(mSessionId, event); + } catch (RemoteException e) { + Log.e(LOG_TAG, "Error reporting textclassifier event.", e); + } + } + @Override public TextLanguage detectLanguage(TextLanguage.Request request) { Preconditions.checkNotNull(request); diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java index ea82bf3f38467..8709e09bbf558 100644 --- a/core/java/android/view/textclassifier/TextClassifier.java +++ b/core/java/android/view/textclassifier/TextClassifier.java @@ -127,32 +127,34 @@ public interface TextClassifier { @StringDef({WIDGET_TYPE_TEXTVIEW, WIDGET_TYPE_EDITTEXT, WIDGET_TYPE_UNSELECTABLE_TEXTVIEW, WIDGET_TYPE_WEBVIEW, WIDGET_TYPE_EDIT_WEBVIEW, WIDGET_TYPE_CUSTOM_TEXTVIEW, WIDGET_TYPE_CUSTOM_EDITTEXT, WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW, - WIDGET_TYPE_UNKNOWN}) + WIDGET_TYPE_NOTIFICATION, WIDGET_TYPE_UNKNOWN}) @interface WidgetType {} - /** The widget involved in the text classification session is a standard + /** The widget involved in the text classification context is a standard * {@link android.widget.TextView}. */ String WIDGET_TYPE_TEXTVIEW = "textview"; - /** The widget involved in the text classification session is a standard + /** The widget involved in the text classification context is a standard * {@link android.widget.EditText}. */ String WIDGET_TYPE_EDITTEXT = "edittext"; - /** The widget involved in the text classification session is a standard non-selectable + /** The widget involved in the text classification context is a standard non-selectable * {@link android.widget.TextView}. */ String WIDGET_TYPE_UNSELECTABLE_TEXTVIEW = "nosel-textview"; - /** The widget involved in the text classification session is a standard + /** The widget involved in the text classification context is a standard * {@link android.webkit.WebView}. */ String WIDGET_TYPE_WEBVIEW = "webview"; - /** The widget involved in the text classification session is a standard editable + /** The widget involved in the text classification context is a standard editable * {@link android.webkit.WebView}. */ String WIDGET_TYPE_EDIT_WEBVIEW = "edit-webview"; - /** The widget involved in the text classification session is a custom text widget. */ + /** The widget involved in the text classification context is a custom text widget. */ String WIDGET_TYPE_CUSTOM_TEXTVIEW = "customview"; - /** The widget involved in the text classification session is a custom editable text widget. */ + /** The widget involved in the text classification context is a custom editable text widget. */ String WIDGET_TYPE_CUSTOM_EDITTEXT = "customedit"; - /** The widget involved in the text classification session is a custom non-selectable text + /** The widget involved in the text classification context is a custom non-selectable text * widget. */ String WIDGET_TYPE_CUSTOM_UNSELECTABLE_TEXTVIEW = "nosel-customview"; - /** The widget involved in the text classification session is of an unknown/unspecified type. */ + /** The widget involved in the text classification context is a notification */ + String WIDGET_TYPE_NOTIFICATION = "notification"; + /** The widget involved in the text classification context is of an unknown/unspecified type. */ String WIDGET_TYPE_UNKNOWN = "unknown"; /** @@ -349,16 +351,30 @@ public interface TextClassifier { @NonNull ConversationActions.Request request) { Preconditions.checkNotNull(request); Utils.checkMainThread(); - return new ConversationActions(Collections.emptyList()); + return new ConversationActions(Collections.emptyList(), null); } /** + * NOTE: Use {@link #onTextClassifierEvent(TextClassifierEvent)} instead. + *

* Reports a selection event. * *

NOTE: If a TextClassifier has been destroyed, calls to this method should * throw an {@link IllegalStateException}. See {@link #isDestroyed()}. */ - default void onSelectionEvent(@NonNull SelectionEvent event) {} + default void onSelectionEvent(@NonNull SelectionEvent event) { + // TODO: Consider rerouting to onTextClassifierEvent() + } + + /** + * Reports a text classifier event. + *

+ * NOTE: Call on a worker thread. + * + * @throws IllegalStateException if this TextClassifier has been destroyed. + * @see #isDestroyed() + */ + default void onTextClassifierEvent(@NonNull TextClassifierEvent event) {} /** * Destroys this TextClassifier. diff --git a/core/java/android/view/textclassifier/TextClassifierEvent.aidl b/core/java/android/view/textclassifier/TextClassifierEvent.aidl new file mode 100644 index 0000000000000..86fa4f887de45 --- /dev/null +++ b/core/java/android/view/textclassifier/TextClassifierEvent.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 TextClassifierEvent; diff --git a/core/java/android/view/textclassifier/TextClassifierEvent.java b/core/java/android/view/textclassifier/TextClassifierEvent.java new file mode 100644 index 0000000000000..3bb9ee88dae91 --- /dev/null +++ b/core/java/android/view/textclassifier/TextClassifierEvent.java @@ -0,0 +1,501 @@ +/* + * 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.IntDef; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.os.Bundle; +import android.os.Parcel; +import android.os.Parcelable; + +import com.android.internal.util.Preconditions; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +/** + * A text classifier event. + */ +// TODO: Comprehensive javadoc. +public final class TextClassifierEvent implements Parcelable { + + public static final Creator CREATOR = new Creator() { + @Override + public TextClassifierEvent createFromParcel(Parcel in) { + return readFromParcel(in); + } + + @Override + public TextClassifierEvent[] newArray(int size) { + return new TextClassifierEvent[size]; + } + }; + + /** @hide **/ + @Retention(RetentionPolicy.SOURCE) + @IntDef({CATEGORY_UNDEFINED, CATEGORY_SELECTION, CATEGORY_LINKIFY, + CATEGORY_CONVERSATION_ACTIONS, CATEGORY_LANGUAGE_DETECTION}) + public @interface Category { + // For custom event categories, use range 1000+. + } + /** Undefined category */ + public static final int CATEGORY_UNDEFINED = 0; + /** Smart selection */ + public static final int CATEGORY_SELECTION = 1; + /** Linkify */ + public static final int CATEGORY_LINKIFY = 2; + /** Conversation actions */ + public static final int CATEGORY_CONVERSATION_ACTIONS = 3; + /** Language detection */ + public static final int CATEGORY_LANGUAGE_DETECTION = 4; + + /** @hide */ + @Retention(RetentionPolicy.SOURCE) + @IntDef({TYPE_UNDEFINED, TYPE_SELECTION_STARTED, TYPE_SELECTION_MODIFIED, + TYPE_SMART_SELECTION_SINGLE, TYPE_SMART_SELECTION_MULTI, TYPE_AUTO_SELECTION, + TYPE_ACTIONS_SHOWN, TYPE_LINK_CLICKED, TYPE_OVERTYPE, TYPE_COPY_ACTION, + TYPE_PASTE_ACTION, TYPE_CUT_ACTION, TYPE_SHARE_ACTION, TYPE_SMART_ACTION, + TYPE_SELECTION_DRAG, TYPE_SELECTION_DESTROYED, TYPE_OTHER_ACTION, TYPE_SELECT_ALL, + TYPE_SELECTION_RESET, TYPE_MANUAL_REPLY}) + public @interface Type { + // For custom event types, use range 1,000,000+. + } + /** User started a new selection. */ + public static final int TYPE_UNDEFINED = 0; + /** User started a new selection. */ + public static final int TYPE_SELECTION_STARTED = 1; + /** User modified an existing selection. */ + public static final int TYPE_SELECTION_MODIFIED = 2; + /** Smart selection triggered for a single token (word). */ + public static final int TYPE_SMART_SELECTION_SINGLE = 3; + /** Smart selection triggered spanning multiple tokens (words). */ + public static final int TYPE_SMART_SELECTION_MULTI = 4; + /** Something else other than user or the default TextClassifier triggered a selection. */ + public static final int TYPE_AUTO_SELECTION = 5; + /** Smart actions shown to the user. */ + public static final int TYPE_ACTIONS_SHOWN = 6; + /** User clicked a link. */ + public static final int TYPE_LINK_CLICKED = 7; + /** User typed over the selection. */ + public static final int TYPE_OVERTYPE = 8; + /** User clicked on Copy action. */ + public static final int TYPE_COPY_ACTION = 9; + /** User clicked on Paste action. */ + public static final int TYPE_PASTE_ACTION = 10; + /** User clicked on Cut action. */ + public static final int TYPE_CUT_ACTION = 11; + /** User clicked on Share action. */ + public static final int TYPE_SHARE_ACTION = 12; + /** User clicked on a Smart action. */ + public static final int TYPE_SMART_ACTION = 13; + /** User dragged+dropped the selection. */ + public static final int TYPE_SELECTION_DRAG = 14; + /** Selection is destroyed. */ + public static final int TYPE_SELECTION_DESTROYED = 15; + /** User clicked on a custom action. */ + public static final int TYPE_OTHER_ACTION = 16; + /** User clicked on Select All action */ + public static final int TYPE_SELECT_ALL = 17; + /** User reset the smart selection. */ + public static final int TYPE_SELECTION_RESET = 18; + /** User composed a reply. */ + public static final int TYPE_MANUAL_REPLY = 19; + + @Category private final int mEventCategory; + @Type private final int mEventType; + @Nullable private final String mEntityType; + @Nullable private final TextClassificationContext mEventContext; + @Nullable private final String mResultId; + private final int mEventIndex; + private final long mEventTime; + private final Bundle mExtras; + + // Smart selection. + private final int mRelativeWordStartIndex; + private final int mRelativeWordEndIndex; + private final int mRelativeSuggestedWordStartIndex; + private final int mRelativeSuggestedWordEndIndex; + + // Smart action. + private final int[] mActionIndices; + + // Language detection. + @Nullable private final String mLanguage; + + private TextClassifierEvent( + int eventCategory, + int eventType, + String entityType, + TextClassificationContext eventContext, + String resultId, + int eventIndex, + long eventTime, + Bundle extras, + int relativeWordStartIndex, + int relativeWordEndIndex, + int relativeSuggestedWordStartIndex, + int relativeSuggestedWordEndIndex, + int[] actionIndex, + String language) { + mEventCategory = eventCategory; + mEventType = eventType; + mEntityType = entityType; + mEventContext = eventContext; + mResultId = resultId; + mEventIndex = eventIndex; + mEventTime = eventTime; + mExtras = extras; + mRelativeWordStartIndex = relativeWordStartIndex; + mRelativeWordEndIndex = relativeWordEndIndex; + mRelativeSuggestedWordStartIndex = relativeSuggestedWordStartIndex; + mRelativeSuggestedWordEndIndex = relativeSuggestedWordEndIndex; + mActionIndices = actionIndex; + mLanguage = language; + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeInt(mEventCategory); + dest.writeInt(mEventType); + dest.writeString(mEntityType); + dest.writeParcelable(mEventContext, flags); + dest.writeString(mResultId); + dest.writeInt(mEventIndex); + dest.writeLong(mEventTime); + dest.writeBundle(mExtras); + dest.writeInt(mRelativeWordStartIndex); + dest.writeInt(mRelativeWordEndIndex); + dest.writeInt(mRelativeSuggestedWordStartIndex); + dest.writeInt(mRelativeSuggestedWordEndIndex); + dest.writeIntArray(mActionIndices); + dest.writeString(mLanguage); + } + + private static TextClassifierEvent readFromParcel(Parcel in) { + return new TextClassifierEvent( + /* eventCategory= */ in.readInt(), + /* eventType= */ in.readInt(), + /* entityType= */ in.readString(), + /* eventContext= */ in.readParcelable(null), + /* resultId= */ in.readString(), + /* eventIndex= */ in.readInt(), + /* eventTime= */ in.readLong(), + /* extras= */ in.readBundle(), + /* relativeWordStartIndex= */ in.readInt(), + /* relativeWordEndIndex= */ in.readInt(), + /* relativeSuggestedWordStartIndex= */ in.readInt(), + /* relativeSuggestedWordEndIndex= */ in.readInt(), + /* actionIndices= */ in.createIntArray(), + /* language= */ in.readString()); + } + + /** + * Returns the event category. e.g. {@link #CATEGORY_SELECTION}. + */ + @Category + public int getEventCategory() { + return mEventCategory; + } + + /** + * Returns the event type. e.g. {@link #TYPE_SELECTION_STARTED}. + */ + @Type + public int getEventType() { + return mEventType; + } + + /** + * Returns the entity type. e.g. {@link TextClassifier#TYPE_ADDRESS}. + */ + @Nullable + public String getEntityType() { + return mEntityType; + } + + /** + * Returns the event context. + */ + @Nullable + public TextClassificationContext getEventContext() { + return mEventContext; + } + + /** + * Returns the id of the text classifier result related to this event. + */ + @Nullable + public String getResultId() { + return mResultId; + } + + /** + * Returns the index of this event in the series of event it belongs to. + */ + public int getEventIndex() { + return mEventIndex; + } + + /** + * Returns the time this event occurred. This is the number of milliseconds since + * January 1, 1970, 00:00:00 GMT. 0 indicates not set. + */ + public long getEventTime() { + return mEventTime; + } + + /** + * Returns a bundle containing non-structured extra information about this event. + * + *

NOTE: Do not modify this bundle. + */ + @NonNull + public Bundle getExtras() { + return mExtras; + } + + /** + * For smart selection. Returns the relative word index of the start of the selection. + */ + public int getRelativeWordStartIndex() { + return mRelativeWordStartIndex; + } + + /** + * For smart selection. Returns the relative word (exclusive) index of the end of the selection. + */ + public int getRelativeWordEndIndex() { + return mRelativeWordEndIndex; + } + + /** + * For smart selection. Returns the relative word index of the start of the smart selection. + */ + public int getRelativeSuggestedWordStartIndex() { + return mRelativeSuggestedWordStartIndex; + } + + /** + * For smart selection. Returns the relative word (exclusive) index of the end of the + * smart selection. + */ + public int getRelativeSuggestedWordEndIndex() { + return mRelativeSuggestedWordEndIndex; + } + + /** + * Returns the indices of the actions relating to this event. + * Actions are usually returned by the text classifier in priority order with the most + * preferred action at index 0. This list gives an indication of the position of the actions + * that are being reported. + */ + @NonNull + public int[] getActionIndices() { + return mActionIndices; + } + + /** + * For language detection. Returns the language tag for the detected locale. + * @see java.util.Locale#forLanguageTag(String). + */ + @Nullable + public String getLanguage() { + return mLanguage; + } + + /** + * Builder to build a text classifier event. + */ + public static final class Builder { + + private final int mEventCategory; + private final int mEventType; + @Nullable private String mEntityType; + @Nullable private TextClassificationContext mEventContext; + @Nullable private String mResultId; + private int mEventIndex; + private long mEventTime; + @Nullable private Bundle mExtras; + private int mRelativeWordStartIndex; + private int mRelativeWordEndIndex; + private int mRelativeSuggestedWordStartIndex; + private int mRelativeSuggestedWordEndIndex; + private int[] mActionIndices = new int[0]; + @Nullable private String mLanguage; + + /** + * Creates a builder for building {@link TextClassifierEvent}s. + * + * @param eventCategory The event category. e.g. {@link #CATEGORY_SELECTION} + * @param eventType The event type. e.g. {@link #TYPE_SELECTION_STARTED} + */ + public Builder(@Category int eventCategory, @Type int eventType) { + mEventCategory = eventCategory; + mEventType = eventType; + } + + /** + * Sets the entity type. e.g. {@link TextClassifier#TYPE_ADDRESS}. + */ + @NonNull + public Builder setEntityType(@Nullable String entityType) { + mEntityType = entityType; + return this; + } + + /** + * Sets the event context. + */ + @NonNull + public Builder setEventContext(@Nullable TextClassificationContext eventContext) { + mEventContext = eventContext; + return this; + } + + /** + * Sets the id of the text classifier result related to this event. + */ + @NonNull + public Builder setResultId(@Nullable String resultId) { + mResultId = resultId; + return this; + } + + /** + * Sets the index of this events in the series of events it belongs to. + */ + @NonNull + public Builder setEventIndex(int eventIndex) { + mEventIndex = eventIndex; + return this; + } + + /** + * Sets the time this event occurred. This is the number of milliseconds since + * January 1, 1970, 00:00:00 GMT. 0 indicates not set. + */ + @NonNull + public Builder setEventTime(long eventTime) { + mEventTime = eventTime; + return this; + } + + /** + * Sets a bundle containing non-structured extra information about the event. + * + *

NOTE: Prefer to set only immutable values on the bundle otherwise, avoid + * updating the internals of this bundle as it may have unexpected consequences on the + * clients of the built event object. For similar reasons, avoid depending on mutable + * objects in this bundle. + */ + @NonNull + public Builder setExtras(@NonNull Bundle extras) { + mExtras = Preconditions.checkNotNull(extras); + return this; + } + + /** + * For smart selection. Sets the relative word index of the start of the selection. + */ + @NonNull + public Builder setRelativeWordStartIndex(int relativeWordStartIndex) { + mRelativeWordStartIndex = relativeWordStartIndex; + return this; + } + + /** + * For smart selection. Sets the relative word (exclusive) index of the end of the + * selection. + */ + @NonNull + public Builder setRelativeWordEndIndex(int relativeWordEndIndex) { + mRelativeWordEndIndex = relativeWordEndIndex; + return this; + } + + /** + * For smart selection. Sets the relative word index of the start of the smart selection. + */ + @NonNull + public Builder setRelativeSuggestedWordStartIndex(int relativeSuggestedWordStartIndex) { + mRelativeSuggestedWordStartIndex = relativeSuggestedWordStartIndex; + return this; + } + + /** + * For smart selection. Sets the relative word (exclusive) index of the end of the + * smart selection. + */ + @NonNull + public Builder setRelativeSuggestedWordEndIndex(int relativeSuggestedWordEndIndex) { + mRelativeSuggestedWordEndIndex = relativeSuggestedWordEndIndex; + return this; + } + + /** + * Sets the indices of the actions involved in this event. Actions are usually returned by + * the text classifier in priority order with the most preferred action at index 0. + * This index gives an indication of the position of the action that is being reported. + */ + @NonNull + public Builder setActionIndices(@NonNull int... actionIndices) { + mActionIndices = new int[actionIndices.length]; + System.arraycopy(actionIndices, 0, mActionIndices, 0, actionIndices.length); + return this; + } + + /** + * For language detection. Sets the language tag for the detected locale. + * @see java.util.Locale#forLanguageTag(String). + */ + @NonNull + public Builder setLanguage(@Nullable String language) { + mLanguage = language; + return this; + } + + /** + * Builds and returns a text classifier event. + */ + @NonNull + public TextClassifierEvent build() { + mExtras = mExtras == null ? Bundle.EMPTY : mExtras; + return new TextClassifierEvent( + mEventCategory, + mEventType, + mEntityType, + mEventContext, + mResultId, + mEventIndex, + mEventTime, + mExtras, + mRelativeWordStartIndex, + mRelativeWordEndIndex, + mRelativeSuggestedWordStartIndex, + mRelativeSuggestedWordEndIndex, + mActionIndices, + mLanguage); + } + // TODO: Add build(boolean validate). + } +} diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java index deda92646e712..9b0f9c6ee5e85 100644 --- a/core/java/android/view/textclassifier/TextClassifierImpl.java +++ b/core/java/android/view/textclassifier/TextClassifierImpl.java @@ -341,6 +341,11 @@ public final class TextClassifierImpl implements TextClassifier { } } + @Override + public void onTextClassifierEvent(@NonNull TextClassifierEvent event) { + // TODO: Implement. + } + /** @inheritDoc */ @Override public TextLanguage detectLanguage(@NonNull TextLanguage.Request request) { @@ -387,7 +392,10 @@ public final class TextClassifierImpl implements TextClassifier { Collection expectedTypes = resolveActionTypesFromRequest(request); List conversationActions = new ArrayList<>(); - int maxSuggestions = Math.min(request.getMaxSuggestions(), nativeSuggestions.length); + int maxSuggestions = nativeSuggestions.length; + if (request.getMaxSuggestions() > 0) { + maxSuggestions = Math.min(request.getMaxSuggestions(), nativeSuggestions.length); + } for (int i = 0; i < maxSuggestions; i++) { ActionsSuggestionsModel.ActionSuggestion nativeSuggestion = nativeSuggestions[i]; String actionType = nativeSuggestion.getActionType(); @@ -400,7 +408,7 @@ public final class TextClassifierImpl implements TextClassifier { .setConfidenceScore(nativeSuggestion.getScore()) .build()); } - return new ConversationActions(conversationActions); + return new ConversationActions(conversationActions, /*id*/ null); } catch (Throwable t) { // Avoid throwing from this method. Log the error. Log.e(LOG_TAG, "Error suggesting conversation actions.", t); diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java index 9b5c0347bdb69..81ec85eb2ea55 100644 --- a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java @@ -18,7 +18,6 @@ package android.view.textclassifier; import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; @@ -375,7 +374,7 @@ public class TextClassifierTest { ConversationActions.Message message = new ConversationActions.Message.Builder( ConversationActions.Message.PERSON_USER_REMOTE) - .setText("Hello") + .setText("Where are you?") .build(); ConversationActions.TypeConfig typeConfig = new ConversationActions.TypeConfig.Builder().includeTypesFromTextClassifier(false) @@ -384,19 +383,44 @@ public class TextClassifierTest { .build(); ConversationActions.Request request = new ConversationActions.Request.Builder(Collections.singletonList(message)) - .setMaxSuggestions(3) + .setMaxSuggestions(1) .setTypeConfig(typeConfig) .build(); ConversationActions conversationActions = mClassifier.suggestConversationActions(request); assertTrue(conversationActions.getConversationActions().size() > 0); - assertTrue(conversationActions.getConversationActions().size() <= 3); + assertTrue(conversationActions.getConversationActions().size() == 1); for (ConversationActions.ConversationAction conversationAction : conversationActions.getConversationActions()) { - assertEquals(conversationAction.getType(), ConversationActions.TYPE_TEXT_REPLY); - assertNotNull(conversationAction.getTextReply()); - assertTrue(conversationAction.getConfidenceScore() > 0); - assertTrue(conversationAction.getConfidenceScore() <= 1); + assertThat(conversationAction, + isConversationAction(ConversationActions.TYPE_TEXT_REPLY)); + } + } + + @Test + public void testSuggestConversationActions_textReplyOnly_noMax() { + if (isTextClassifierDisabled()) return; + ConversationActions.Message message = + new ConversationActions.Message.Builder( + ConversationActions.Message.PERSON_USER_REMOTE) + .setText("Where are you?") + .build(); + ConversationActions.TypeConfig typeConfig = + new ConversationActions.TypeConfig.Builder().includeTypesFromTextClassifier(false) + .setIncludedTypes( + Collections.singletonList(ConversationActions.TYPE_TEXT_REPLY)) + .build(); + ConversationActions.Request request = + new ConversationActions.Request.Builder(Collections.singletonList(message)) + .setTypeConfig(typeConfig) + .build(); + + ConversationActions conversationActions = mClassifier.suggestConversationActions(request); + assertTrue(conversationActions.getConversationActions().size() > 1); + for (ConversationActions.ConversationAction conversationAction : + conversationActions.getConversationActions()) { + assertThat(conversationAction, + isConversationAction(ConversationActions.TYPE_TEXT_REPLY)); } } @@ -498,4 +522,36 @@ public class TextClassifierTest { } }; } + + private static Matcher isConversationAction( + String actionType) { + return new BaseMatcher() { + @Override + public boolean matches(Object o) { + if (!(o instanceof ConversationActions.ConversationAction)) { + return false; + } + ConversationActions.ConversationAction conversationAction = + (ConversationActions.ConversationAction) o; + if (!actionType.equals(conversationAction.getType())) { + return false; + } + if (ConversationActions.TYPE_TEXT_REPLY.equals(actionType)) { + if (conversationAction.getTextReply() == null) { + return false; + } + } + if (conversationAction.getConfidenceScore() < 0 + || conversationAction.getConfidenceScore() > 1) { + return false; + } + return true; + } + + @Override + public void describeTo(Description description) { + description.appendText("actionType=").appendValue(actionType); + } + }; + } } diff --git a/packages/ExtServices/tests/src/android/ext/services/notification/SmartActionHelperTest.java b/packages/ExtServices/tests/src/android/ext/services/notification/SmartActionHelperTest.java index 60d31fca8ddbb..0352ebcec8b3f 100644 --- a/packages/ExtServices/tests/src/android/ext/services/notification/SmartActionHelperTest.java +++ b/packages/ExtServices/tests/src/android/ext/services/notification/SmartActionHelperTest.java @@ -75,7 +75,7 @@ public class SmartActionHelperTest { mContext.getSystemService(TextClassificationManager.class) .setTextClassifier(mTextClassifier); when(mTextClassifier.suggestConversationActions(any(ConversationActions.Request.class))) - .thenReturn(new ConversationActions(Collections.emptyList())); + .thenReturn(new ConversationActions(Collections.emptyList(), null)); when(mNotificationEntry.getSbn()).thenReturn(mStatusBarNotification); // The notification is eligible to have smart suggestions. diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java index 6b0419e0f7adf..2cab63aa680a1 100644 --- a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java +++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java @@ -42,6 +42,7 @@ import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassificationContext; import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassificationSessionId; +import android.view.textclassifier.TextClassifierEvent; import android.view.textclassifier.TextLanguage; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; @@ -214,6 +215,27 @@ public final class TextClassificationManagerService extends ITextClassifierServi } } } + @Override + public void onTextClassifierEvent( + TextClassificationSessionId sessionId, + TextClassifierEvent event) throws RemoteException { + Preconditions.checkNotNull(event); + final String packageName = event.getEventContext() == null + ? null + : event.getEventContext().getPackageName(); + validateInput(mContext, packageName); + + synchronized (mLock) { + UserState userState = getCallingUserStateLocked(); + if (userState.isBoundLocked()) { + userState.mService.onTextClassifierEvent(sessionId, event); + } else { + userState.mPendingRequests.add(new PendingRequest( + () -> onTextClassifierEvent(sessionId, event), + null /* onServiceFailure */, null /* binder */, this, userState)); + } + } + } @Override public void onDetectLanguage(