From 0be540b227760a8218689432a3511ad40563ce6e Mon Sep 17 00:00:00 2001 From: Tony Mak Date: Fri, 9 Nov 2018 16:58:35 +0000 Subject: [PATCH] Add detectLanguage and suggestConversationActions to TCS BUG: 111406942 BUG: 111437455 Test: atest frameworks/base/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java Change-Id: Iee9c970ebbec6590906907d67be3dd4021c1b4b2 --- Android.bp | 2 + api/system-current.txt | 2 + .../IConversationActionsCallback.aidl | 28 ++ .../ITextClassifierService.aidl | 14 + .../textclassifier/ITextLanguageCallback.aidl | 28 ++ .../textclassifier/TextClassifierService.java | 117 ++++- .../textclassifier/ConversationActions.aidl | 20 + .../textclassifier/SystemTextClassifier.java | 101 +++- .../view/textclassifier/TextLanguage.aidl | 20 + .../TextClassificationManagerTest.java | 392 --------------- .../textclassifier/TextClassifierTest.java | 454 ++++++++++++++++++ .../TextClassificationManagerService.java | 48 ++ 12 files changed, 814 insertions(+), 412 deletions(-) create mode 100644 core/java/android/service/textclassifier/IConversationActionsCallback.aidl create mode 100644 core/java/android/service/textclassifier/ITextLanguageCallback.aidl create mode 100644 core/java/android/view/textclassifier/ConversationActions.aidl create mode 100644 core/java/android/view/textclassifier/TextLanguage.aidl create mode 100644 core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java diff --git a/Android.bp b/Android.bp index 1012bb811ace0..4cb330a1f30b1 100644 --- a/Android.bp +++ b/Android.bp @@ -334,8 +334,10 @@ java_defaults { "core/java/android/service/chooser/IChooserTargetResult.aidl", "core/java/android/service/resolver/IResolverRankerService.aidl", "core/java/android/service/resolver/IResolverRankerResult.aidl", + "core/java/android/service/textclassifier/IConversationActionsCallback.aidl", "core/java/android/service/textclassifier/ITextClassificationCallback.aidl", "core/java/android/service/textclassifier/ITextClassifierService.aidl", + "core/java/android/service/textclassifier/ITextLanguageCallback.aidl", "core/java/android/service/textclassifier/ITextLinksCallback.aidl", "core/java/android/service/textclassifier/ITextSelectionCallback.aidl", "core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl", diff --git a/api/system-current.txt b/api/system-current.txt index d7265c753e590..4b237b50cfa6d 100644 --- a/api/system-current.txt +++ b/api/system-current.txt @@ -5171,8 +5171,10 @@ package android.service.textclassifier { method public abstract void onClassifyText(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.TextClassification.Request, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback); method public void onCreateTextClassificationSession(android.view.textclassifier.TextClassificationContext, android.view.textclassifier.TextClassificationSessionId); 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 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); field public static final java.lang.String SERVICE_INTERFACE = "android.service.textclassifier.TextClassifierService"; } diff --git a/core/java/android/service/textclassifier/IConversationActionsCallback.aidl b/core/java/android/service/textclassifier/IConversationActionsCallback.aidl new file mode 100644 index 0000000000000..c35d4246e924c --- /dev/null +++ b/core/java/android/service/textclassifier/IConversationActionsCallback.aidl @@ -0,0 +1,28 @@ +/* + * 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.service.textclassifier; + +import android.view.textclassifier.ConversationActions; + +/** + * Callback for a ConversationActions request. + * @hide + */ +oneway interface IConversationActionsCallback { + void onSuccess(in ConversationActions conversationActions); + void onFailure(); +} \ No newline at end of file diff --git a/core/java/android/service/textclassifier/ITextClassifierService.aidl b/core/java/android/service/textclassifier/ITextClassifierService.aidl index 7ac72c7c8e83a..254a710a2c408 100644 --- a/core/java/android/service/textclassifier/ITextClassifierService.aidl +++ b/core/java/android/service/textclassifier/ITextClassifierService.aidl @@ -16,14 +16,18 @@ package android.service.textclassifier; +import android.service.textclassifier.IConversationActionsCallback; import android.service.textclassifier.ITextClassificationCallback; +import android.service.textclassifier.ITextLanguageCallback; import android.service.textclassifier.ITextLinksCallback; import android.service.textclassifier.ITextSelectionCallback; +import android.view.textclassifier.ConversationActions; 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.TextLanguage; import android.view.textclassifier.TextSelection; /** @@ -58,4 +62,14 @@ oneway interface ITextClassifierService { void onDestroyTextClassificationSession( in TextClassificationSessionId sessionId); + + void onDetectLanguage( + in TextClassificationSessionId sessionId, + in TextLanguage.Request request, + in ITextLanguageCallback callback); + + void onSuggestConversationActions( + in TextClassificationSessionId sessionId, + in ConversationActions.Request request, + in IConversationActionsCallback callback); } diff --git a/core/java/android/service/textclassifier/ITextLanguageCallback.aidl b/core/java/android/service/textclassifier/ITextLanguageCallback.aidl new file mode 100644 index 0000000000000..263d99afca5c7 --- /dev/null +++ b/core/java/android/service/textclassifier/ITextLanguageCallback.aidl @@ -0,0 +1,28 @@ +/* + * 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.service.textclassifier; + +import android.view.textclassifier.TextLanguage; + +/** + * Callback for a TextLanguage request. + * @hide + */ +oneway interface ITextLanguageCallback { + void onSuccess(in TextLanguage textLanguage); + void onFailure(); +} \ No newline at end of file diff --git a/core/java/android/service/textclassifier/TextClassifierService.java b/core/java/android/service/textclassifier/TextClassifierService.java index 7f1082d4dcc6b..d7359f1aa5ddf 100644 --- a/core/java/android/service/textclassifier/TextClassifierService.java +++ b/core/java/android/service/textclassifier/TextClassifierService.java @@ -32,12 +32,14 @@ import android.os.IBinder; import android.os.RemoteException; import android.text.TextUtils; import android.util.Slog; +import android.view.textclassifier.ConversationActions; 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.TextLanguage; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; @@ -92,8 +94,7 @@ public abstract class TextClassifierService extends Service { @Override public void onSuggestSelection( TextClassificationSessionId sessionId, - TextSelection.Request request, ITextSelectionCallback callback) - throws RemoteException { + TextSelection.Request request, ITextSelectionCallback callback) { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); TextClassifierService.this.onSuggestSelection( @@ -125,8 +126,7 @@ public abstract class TextClassifierService extends Service { @Override public void onClassifyText( TextClassificationSessionId sessionId, - TextClassification.Request request, ITextClassificationCallback callback) - throws RemoteException { + TextClassification.Request request, ITextClassificationCallback callback) { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); TextClassifierService.this.onClassifyText( @@ -156,8 +156,7 @@ public abstract class TextClassifierService extends Service { @Override public void onGenerateLinks( TextClassificationSessionId sessionId, - TextLinks.Request request, ITextLinksCallback callback) - throws RemoteException { + TextLinks.Request request, ITextLinksCallback callback) { Preconditions.checkNotNull(request); Preconditions.checkNotNull(callback); TextClassifierService.this.onGenerateLinks( @@ -188,16 +187,81 @@ public abstract class TextClassifierService extends Service { @Override public void onSelectionEvent( TextClassificationSessionId sessionId, - SelectionEvent event) throws RemoteException { + SelectionEvent event) { Preconditions.checkNotNull(event); TextClassifierService.this.onSelectionEvent(sessionId, event); } + /** {@inheritDoc} */ + @Override + public void onDetectLanguage( + TextClassificationSessionId sessionId, + TextLanguage.Request request, + ITextLanguageCallback callback) { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(callback); + TextClassifierService.this.onDetectLanguage( + sessionId, + request, + mCancellationSignal, + new Callback() { + @Override + public void onSuccess(TextLanguage result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + Slog.d(LOG_TAG, "Error calling callback"); + } + } + + @Override + public void onFailure(CharSequence error) { + try { + callback.onFailure(); + } catch (RemoteException e) { + Slog.d(LOG_TAG, "Error calling callback"); + } + }; + }); + } + + /** {@inheritDoc} */ + @Override + public void onSuggestConversationActions( + TextClassificationSessionId sessionId, + ConversationActions.Request request, + IConversationActionsCallback callback) { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(callback); + TextClassifierService.this.onSuggestConversationActions( + sessionId, + request, + mCancellationSignal, + new Callback() { + @Override + public void onSuccess(ConversationActions result) { + try { + callback.onSuccess(result); + } catch (RemoteException e) { + Slog.d(LOG_TAG, "Error calling callback"); + } + } + + @Override + public void onFailure(CharSequence error) { + try { + callback.onFailure(); + } catch (RemoteException e) { + Slog.d(LOG_TAG, "Error calling callback"); + } + } + }); + } + /** {@inheritDoc} */ @Override public void onCreateTextClassificationSession( - TextClassificationContext context, TextClassificationSessionId sessionId) - throws RemoteException { + TextClassificationContext context, TextClassificationSessionId sessionId) { Preconditions.checkNotNull(context); Preconditions.checkNotNull(sessionId); TextClassifierService.this.onCreateTextClassificationSession(context, sessionId); @@ -205,8 +269,7 @@ public abstract class TextClassifierService extends Service { /** {@inheritDoc} */ @Override - public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) - throws RemoteException { + public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) { TextClassifierService.this.onDestroyTextClassificationSession(sessionId); } }; @@ -265,6 +328,38 @@ public abstract class TextClassifierService extends Service { @NonNull CancellationSignal cancellationSignal, @NonNull Callback callback); + /** + * Detects and returns the language of the give text. + * + * @param sessionId the session id + * @param request the language detection request + * @param cancellationSignal object to watch for canceling the current operation + * @param callback the callback to return the result to + */ + public void onDetectLanguage( + @Nullable TextClassificationSessionId sessionId, + @NonNull TextLanguage.Request request, + @NonNull CancellationSignal cancellationSignal, + @NonNull Callback callback) { + callback.onSuccess(getLocalTextClassifier().detectLanguage(request)); + } + + /** + * Suggests and returns a list of actions according to the given conversation. + * + * @param sessionId the session id + * @param request the conversation actions request + * @param cancellationSignal object to watch for canceling the current operation + * @param callback the callback to return the result to + */ + public void onSuggestConversationActions( + @Nullable TextClassificationSessionId sessionId, + @NonNull ConversationActions.Request request, + @NonNull CancellationSignal cancellationSignal, + @NonNull Callback callback) { + callback.onSuccess(getLocalTextClassifier().suggestConversationActions(request)); + } + /** * Writes the selection event. * This is called when a selection event occurs. e.g. user changed selection; or smart selection diff --git a/core/java/android/view/textclassifier/ConversationActions.aidl b/core/java/android/view/textclassifier/ConversationActions.aidl new file mode 100644 index 0000000000000..fece939455e0a --- /dev/null +++ b/core/java/android/view/textclassifier/ConversationActions.aidl @@ -0,0 +1,20 @@ +/* + * 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 ConversationActions; +parcelable ConversationActions.Request; \ 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 16eb5afd978f9..f8fce62c84d25 100644 --- a/core/java/android/view/textclassifier/SystemTextClassifier.java +++ b/core/java/android/view/textclassifier/SystemTextClassifier.java @@ -23,8 +23,10 @@ import android.content.Context; import android.os.Looper; import android.os.RemoteException; import android.os.ServiceManager; +import android.service.textclassifier.IConversationActionsCallback; import android.service.textclassifier.ITextClassificationCallback; import android.service.textclassifier.ITextClassifierService; +import android.service.textclassifier.ITextLanguageCallback; import android.service.textclassifier.ITextLinksCallback; import android.service.textclassifier.ITextSelectionCallback; @@ -76,7 +78,7 @@ public final class SystemTextClassifier implements TextClassifier { if (selection != null) { return selection; } - } catch (RemoteException | InterruptedException e) { + } catch (RemoteException e) { Log.e(LOG_TAG, "Error suggesting selection for text. Using fallback.", e); } return mFallback.suggestSelection(request); @@ -97,7 +99,7 @@ public final class SystemTextClassifier implements TextClassifier { if (classification != null) { return classification; } - } catch (RemoteException | InterruptedException e) { + } catch (RemoteException e) { Log.e(LOG_TAG, "Error classifying text. Using fallback.", e); } return mFallback.classifyText(request); @@ -124,7 +126,7 @@ public final class SystemTextClassifier implements TextClassifier { if (links != null) { return links; } - } catch (RemoteException | InterruptedException e) { + } catch (RemoteException e) { Log.e(LOG_TAG, "Error generating links. Using fallback.", e); } return mFallback.generateLinks(request); @@ -142,6 +144,42 @@ public final class SystemTextClassifier implements TextClassifier { } } + @Override + public TextLanguage detectLanguage(TextLanguage.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); + + try { + final TextLanguageCallback callback = new TextLanguageCallback(); + mManagerService.onDetectLanguage(mSessionId, request, callback); + final TextLanguage textLanguage = callback.mReceiver.get(); + if (textLanguage != null) { + return textLanguage; + } + } catch (RemoteException e) { + Log.e(LOG_TAG, "Error detecting language.", e); + } + return mFallback.detectLanguage(request); + } + + @Override + public ConversationActions suggestConversationActions(ConversationActions.Request request) { + Preconditions.checkNotNull(request); + Utils.checkMainThread(); + + try { + final ConversationActionsCallback callback = new ConversationActionsCallback(); + mManagerService.onSuggestConversationActions(mSessionId, request, callback); + final ConversationActions conversationActions = callback.mReceiver.get(); + if (conversationActions != null) { + return conversationActions; + } + } catch (RemoteException e) { + Log.e(LOG_TAG, "Error reporting selection event.", e); + } + return mFallback.suggestConversationActions(request); + } + /** * @inheritDoc */ @@ -193,7 +231,7 @@ public final class SystemTextClassifier implements TextClassifier { private static final class TextSelectionCallback extends ITextSelectionCallback.Stub { - final ResponseReceiver mReceiver = new ResponseReceiver<>(); + final ResponseReceiver mReceiver = new ResponseReceiver<>("textselection"); @Override public void onSuccess(TextSelection selection) { @@ -208,7 +246,8 @@ public final class SystemTextClassifier implements TextClassifier { private static final class TextClassificationCallback extends ITextClassificationCallback.Stub { - final ResponseReceiver mReceiver = new ResponseReceiver<>(); + final ResponseReceiver mReceiver = + new ResponseReceiver<>("textclassification"); @Override public void onSuccess(TextClassification classification) { @@ -223,7 +262,7 @@ public final class SystemTextClassifier implements TextClassifier { private static final class TextLinksCallback extends ITextLinksCallback.Stub { - final ResponseReceiver mReceiver = new ResponseReceiver<>(); + final ResponseReceiver mReceiver = new ResponseReceiver<>("textlinks"); @Override public void onSuccess(TextLinks links) { @@ -236,12 +275,48 @@ public final class SystemTextClassifier implements TextClassifier { } } + private static final class TextLanguageCallback extends ITextLanguageCallback.Stub { + + final ResponseReceiver mReceiver = new ResponseReceiver<>("textlanguage"); + + @Override + public void onSuccess(TextLanguage textLanguage) { + mReceiver.onSuccess(textLanguage); + } + + @Override + public void onFailure() { + mReceiver.onFailure(); + } + } + + private static final class ConversationActionsCallback + extends IConversationActionsCallback.Stub { + + final ResponseReceiver mReceiver = + new ResponseReceiver<>("conversationaction"); + + @Override + public void onSuccess(ConversationActions conversationActions) { + mReceiver.onSuccess(conversationActions); + } + + @Override + public void onFailure() { + mReceiver.onFailure(); + } + } + private static final class ResponseReceiver { private final CountDownLatch mLatch = new CountDownLatch(1); - + private final String mName; private T mResponse; + private ResponseReceiver(String name) { + mName = name; + } + public void onSuccess(T response) { mResponse = response; mLatch.countDown(); @@ -253,13 +328,21 @@ public final class SystemTextClassifier implements TextClassifier { } @Nullable - public T get() throws InterruptedException { + public T get() { // If this is running on the main thread, do not block for a response. // The response will unfortunately be null and the TextClassifier should depend on its // fallback. // NOTE that TextClassifier calls should preferably always be called on a worker thread. if (Looper.myLooper() != Looper.getMainLooper()) { - mLatch.await(2, TimeUnit.SECONDS); + try { + boolean success = mLatch.await(2, TimeUnit.SECONDS); + if (!success) { + Log.w(LOG_TAG, "Timeout in ResponseReceiver.get(): " + mName); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + Log.e(LOG_TAG, "Interrupted during ResponseReceiver.get(): " + mName, e); + } } return mResponse; } diff --git a/core/java/android/view/textclassifier/TextLanguage.aidl b/core/java/android/view/textclassifier/TextLanguage.aidl new file mode 100644 index 0000000000000..54e3cf9fbdb8e --- /dev/null +++ b/core/java/android/view/textclassifier/TextLanguage.aidl @@ -0,0 +1,20 @@ +/* + * 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 TextLanguage; +parcelable TextLanguage.Request; \ No newline at end of file diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java index 3a33d5798c32b..46aa5b484c5fc 100644 --- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationManagerTest.java @@ -16,12 +16,9 @@ package android.view.textclassifier; -import static org.hamcrest.CoreMatchers.not; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNotSame; -import static org.junit.Assert.assertThat; import static org.junit.Assert.assertTrue; import static org.mockito.Matchers.argThat; import static org.mockito.Mockito.any; @@ -41,315 +38,24 @@ import android.support.test.InstrumentationRegistry; import android.support.test.filters.SmallTest; import android.support.test.runner.AndroidJUnit4; -import org.hamcrest.BaseMatcher; -import org.hamcrest.Description; -import org.hamcrest.Matcher; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatcher; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; - @SmallTest @RunWith(AndroidJUnit4.class) public class TextClassificationManagerTest { private static final LocaleList LOCALES = LocaleList.forLanguageTags("en-US"); - private static final String NO_TYPE = null; private Context mContext; private TextClassificationManager mTcm; - private TextClassifier mClassifier; @Before public void setup() { mContext = InstrumentationRegistry.getTargetContext(); mTcm = mContext.getSystemService(TextClassificationManager.class); - // Test with the local textClassifier only. (We only bundle "en" model by default). - // It's hard to reliably test the results of the device's TextClassifierServiceImpl here. - mClassifier = mTcm.getTextClassifier(TextClassifier.LOCAL); - } - - @Test - public void testSmartSelection() { - if (isTextClassifierDisabled()) return; - - String text = "Contact me at droid@android.com"; - String selected = "droid"; - String suggested = "droid@android.com"; - int startIndex = text.indexOf(selected); - int endIndex = startIndex + selected.length(); - int smartStartIndex = text.indexOf(suggested); - int smartEndIndex = smartStartIndex + suggested.length(); - TextSelection.Request request = new TextSelection.Request.Builder( - text, startIndex, endIndex) - .setDefaultLocales(LOCALES) - .build(); - - TextSelection selection = mClassifier.suggestSelection(request); - assertThat(selection, - isTextSelection(smartStartIndex, smartEndIndex, TextClassifier.TYPE_EMAIL)); - } - - @Test - public void testSmartSelection_url() { - if (isTextClassifierDisabled()) return; - - String text = "Visit http://www.android.com for more information"; - String selected = "http"; - String suggested = "http://www.android.com"; - int startIndex = text.indexOf(selected); - int endIndex = startIndex + selected.length(); - int smartStartIndex = text.indexOf(suggested); - int smartEndIndex = smartStartIndex + suggested.length(); - TextSelection.Request request = new TextSelection.Request.Builder( - text, startIndex, endIndex) - .setDefaultLocales(LOCALES) - .build(); - - TextSelection selection = mClassifier.suggestSelection(request); - assertThat(selection, - isTextSelection(smartStartIndex, smartEndIndex, TextClassifier.TYPE_URL)); - } - - @Test - public void testSmartSelection_withEmoji() { - if (isTextClassifierDisabled()) return; - - String text = "\uD83D\uDE02 Hello."; - String selected = "Hello"; - int startIndex = text.indexOf(selected); - int endIndex = startIndex + selected.length(); - TextSelection.Request request = new TextSelection.Request.Builder( - text, startIndex, endIndex) - .setDefaultLocales(LOCALES) - .build(); - - TextSelection selection = mClassifier.suggestSelection(request); - assertThat(selection, - isTextSelection(startIndex, endIndex, NO_TYPE)); - } - - @Test - public void testClassifyText() { - if (isTextClassifierDisabled()) return; - - String text = "Contact me at droid@android.com"; - String classifiedText = "droid@android.com"; - int startIndex = text.indexOf(classifiedText); - int endIndex = startIndex + classifiedText.length(); - TextClassification.Request request = new TextClassification.Request.Builder( - text, startIndex, endIndex) - .setDefaultLocales(LOCALES) - .build(); - - TextClassification classification = mClassifier.classifyText(request); - assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_EMAIL)); - } - - @Test - public void testTextClassifyText_url() { - if (isTextClassifierDisabled()) return; - - String text = "Visit www.android.com for more information"; - String classifiedText = "www.android.com"; - int startIndex = text.indexOf(classifiedText); - int endIndex = startIndex + classifiedText.length(); - TextClassification.Request request = new TextClassification.Request.Builder( - text, startIndex, endIndex) - .setDefaultLocales(LOCALES) - .build(); - - TextClassification classification = mClassifier.classifyText(request); - assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_URL)); - } - - @Test - public void testTextClassifyText_address() { - if (isTextClassifierDisabled()) return; - - String text = "Brandschenkestrasse 110, Zürich, Switzerland"; - TextClassification.Request request = new TextClassification.Request.Builder( - text, 0, text.length()) - .setDefaultLocales(LOCALES) - .build(); - - TextClassification classification = mClassifier.classifyText(request); - assertThat(classification, isTextClassification(text, TextClassifier.TYPE_ADDRESS)); - } - - @Test - public void testTextClassifyText_url_inCaps() { - if (isTextClassifierDisabled()) return; - - String text = "Visit HTTP://ANDROID.COM for more information"; - String classifiedText = "HTTP://ANDROID.COM"; - int startIndex = text.indexOf(classifiedText); - int endIndex = startIndex + classifiedText.length(); - TextClassification.Request request = new TextClassification.Request.Builder( - text, startIndex, endIndex) - .setDefaultLocales(LOCALES) - .build(); - - TextClassification classification = mClassifier.classifyText(request); - assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_URL)); - } - - @Test - public void testTextClassifyText_date() { - if (isTextClassifierDisabled()) return; - - String text = "Let's meet on January 9, 2018."; - String classifiedText = "January 9, 2018"; - int startIndex = text.indexOf(classifiedText); - int endIndex = startIndex + classifiedText.length(); - TextClassification.Request request = new TextClassification.Request.Builder( - text, startIndex, endIndex) - .setDefaultLocales(LOCALES) - .build(); - - TextClassification classification = mClassifier.classifyText(request); - assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_DATE)); - } - - @Test - public void testTextClassifyText_datetime() { - if (isTextClassifierDisabled()) return; - - String text = "Let's meet 2018/01/01 10:30:20."; - String classifiedText = "2018/01/01 10:30:20"; - int startIndex = text.indexOf(classifiedText); - int endIndex = startIndex + classifiedText.length(); - TextClassification.Request request = new TextClassification.Request.Builder( - text, startIndex, endIndex) - .setDefaultLocales(LOCALES) - .build(); - - TextClassification classification = mClassifier.classifyText(request); - assertThat(classification, - isTextClassification(classifiedText, TextClassifier.TYPE_DATE_TIME)); - } - - @Test - public void testGenerateLinks_phone() { - if (isTextClassifierDisabled()) return; - String text = "The number is +12122537077. See you tonight!"; - TextLinks.Request request = new TextLinks.Request.Builder(text).build(); - assertThat(mClassifier.generateLinks(request), - isTextLinksContaining(text, "+12122537077", TextClassifier.TYPE_PHONE)); - } - - @Test - public void testGenerateLinks_exclude() { - if (isTextClassifierDisabled()) return; - String text = "You want apple@banana.com. See you tonight!"; - List hints = Collections.EMPTY_LIST; - List included = Collections.EMPTY_LIST; - List excluded = Arrays.asList(TextClassifier.TYPE_EMAIL); - TextLinks.Request request = new TextLinks.Request.Builder(text) - .setEntityConfig(TextClassifier.EntityConfig.create(hints, included, excluded)) - .setDefaultLocales(LOCALES) - .build(); - assertThat(mClassifier.generateLinks(request), - not(isTextLinksContaining(text, "apple@banana.com", TextClassifier.TYPE_EMAIL))); - } - - @Test - public void testGenerateLinks_explicit_address() { - if (isTextClassifierDisabled()) return; - String text = "The address is 1600 Amphitheater Parkway, Mountain View, CA. See you!"; - List explicit = Arrays.asList(TextClassifier.TYPE_ADDRESS); - TextLinks.Request request = new TextLinks.Request.Builder(text) - .setEntityConfig(TextClassifier.EntityConfig.createWithExplicitEntityList(explicit)) - .setDefaultLocales(LOCALES) - .build(); - assertThat(mClassifier.generateLinks(request), - isTextLinksContaining(text, "1600 Amphitheater Parkway, Mountain View, CA", - TextClassifier.TYPE_ADDRESS)); - } - - @Test - public void testGenerateLinks_exclude_override() { - if (isTextClassifierDisabled()) return; - String text = "You want apple@banana.com. See you tonight!"; - List hints = Collections.EMPTY_LIST; - List included = Arrays.asList(TextClassifier.TYPE_EMAIL); - List excluded = Arrays.asList(TextClassifier.TYPE_EMAIL); - TextLinks.Request request = new TextLinks.Request.Builder(text) - .setEntityConfig(TextClassifier.EntityConfig.create(hints, included, excluded)) - .setDefaultLocales(LOCALES) - .build(); - assertThat(mClassifier.generateLinks(request), - not(isTextLinksContaining(text, "apple@banana.com", TextClassifier.TYPE_EMAIL))); - } - - @Test - public void testGenerateLinks_maxLength() { - if (isTextClassifierDisabled()) return; - char[] manySpaces = new char[mClassifier.getMaxGenerateLinksTextLength()]; - Arrays.fill(manySpaces, ' '); - TextLinks.Request request = new TextLinks.Request.Builder(new String(manySpaces)).build(); - TextLinks links = mClassifier.generateLinks(request); - assertTrue(links.getLinks().isEmpty()); - } - - @Test(expected = IllegalArgumentException.class) - public void testGenerateLinks_tooLong() { - if (isTextClassifierDisabled()) { - throw new IllegalArgumentException("pass if disabled"); - } - char[] manySpaces = new char[mClassifier.getMaxGenerateLinksTextLength() + 1]; - Arrays.fill(manySpaces, ' '); - TextLinks.Request request = new TextLinks.Request.Builder(new String(manySpaces)).build(); - mClassifier.generateLinks(request); - } - - @Test - public void testDetectLanguage() { - if (isTextClassifierDisabled()) return; - String text = "This is English text"; - TextLanguage.Request request = new TextLanguage.Request.Builder(text).build(); - TextLanguage textLanguage = mClassifier.detectLanguage(request); - assertThat(textLanguage, isTextLanguage("en")); - } - - @Test - public void testDetectLanguage_japanese() { - if (isTextClassifierDisabled()) return; - String text = "これは日本語のテキストです"; - TextLanguage.Request request = new TextLanguage.Request.Builder(text).build(); - TextLanguage textLanguage = mClassifier.detectLanguage(request); - assertThat(textLanguage, isTextLanguage("ja")); - } - - @Test - public void testSuggestConversationActions_textReplyOnly_maxThree() { - if (isTextClassifierDisabled()) return; - ConversationActions.Message message = - new ConversationActions.Message.Builder().setText("Hello").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)) - .setMaxSuggestions(1) - .setTypeConfig(typeConfig) - .build(); - - ConversationActions conversationActions = mClassifier.suggestConversationActions(request); - 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); - } } @Test @@ -411,102 +117,4 @@ public class TextClassificationManagerTest { assertFalse(result.getActions().isEmpty()); assertNotSame(result, fallbackResult); } - - private boolean isTextClassifierDisabled() { - return mClassifier == TextClassifier.NO_OP; - } - - private static Matcher isTextSelection( - final int startIndex, final int endIndex, final String type) { - return new BaseMatcher() { - @Override - public boolean matches(Object o) { - if (o instanceof TextSelection) { - TextSelection selection = (TextSelection) o; - return startIndex == selection.getSelectionStartIndex() - && endIndex == selection.getSelectionEndIndex() - && typeMatches(selection, type); - } - return false; - } - - private boolean typeMatches(TextSelection selection, String type) { - return type == null - || (selection.getEntityCount() > 0 - && type.trim().equalsIgnoreCase(selection.getEntity(0))); - } - - @Override - public void describeTo(Description description) { - description.appendValue( - String.format("%d, %d, %s", startIndex, endIndex, type)); - } - }; - } - - private static Matcher isTextLinksContaining( - final String text, final String substring, final String type) { - return new BaseMatcher() { - - @Override - public void describeTo(Description description) { - description.appendText("text=").appendValue(text) - .appendText(", substring=").appendValue(substring) - .appendText(", type=").appendValue(type); - } - - @Override - public boolean matches(Object o) { - if (o instanceof TextLinks) { - for (TextLinks.TextLink link : ((TextLinks) o).getLinks()) { - if (text.subSequence(link.getStart(), link.getEnd()).equals(substring)) { - return type.equals(link.getEntity(0)); - } - } - } - return false; - } - }; - } - - private static Matcher isTextClassification( - final String text, final String type) { - return new BaseMatcher() { - @Override - public boolean matches(Object o) { - if (o instanceof TextClassification) { - TextClassification result = (TextClassification) o; - return text.equals(result.getText()) - && result.getEntityCount() > 0 - && type.equals(result.getEntity(0)); - } - return false; - } - - @Override - public void describeTo(Description description) { - description.appendText("text=").appendValue(text) - .appendText(", type=").appendValue(type); - } - }; - } - - private static Matcher isTextLanguage(final String languageTag) { - return new BaseMatcher() { - @Override - public boolean matches(Object o) { - if (o instanceof TextLanguage) { - TextLanguage result = (TextLanguage) o; - return result.getLocaleHypothesisCount() > 0 - && languageTag.equals(result.getLocale(0).toLanguageTag()); - } - return false; - } - - @Override - public void describeTo(Description description) { - description.appendText("locale=").appendValue(languageTag); - } - }; - } } diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java new file mode 100644 index 0000000000000..06ba15e8e3399 --- /dev/null +++ b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java @@ -0,0 +1,454 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package android.view.textclassifier; + +import 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; + +import android.content.Context; +import android.os.LocaleList; +import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SmallTest; + +import org.hamcrest.BaseMatcher; +import org.hamcrest.Description; +import org.hamcrest.Matcher; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * Testing {@link TextClassifierTest} APIs on local and system textclassifier. + *

+ * Tests are skipped if such a textclassifier does not exist. + */ +@SmallTest +@RunWith(Parameterized.class) +public class TextClassifierTest { + private static final String LOCAL = "local"; + private static final String SYSTEM = "system"; + + @Parameterized.Parameters(name = "{0}") + public static Iterable textClassifierTypes() { + return Arrays.asList(LOCAL, SYSTEM); + } + + @Parameterized.Parameter + public String mTextClassifierType; + + private static final LocaleList LOCALES = LocaleList.forLanguageTags("en-US"); + private static final String NO_TYPE = null; + + private Context mContext; + private TextClassificationManager mTcm; + private TextClassifier mClassifier; + + @Before + public void setup() { + mContext = InstrumentationRegistry.getTargetContext(); + mTcm = mContext.getSystemService(TextClassificationManager.class); + mClassifier = mTcm.getTextClassifier( + mTextClassifierType.equals(LOCAL) ? TextClassifier.LOCAL : TextClassifier.SYSTEM); + } + + @Test + public void testSmartSelection() { + if (isTextClassifierDisabled()) return; + + String text = "Contact me at droid@android.com"; + String selected = "droid"; + String suggested = "droid@android.com"; + int startIndex = text.indexOf(selected); + int endIndex = startIndex + selected.length(); + int smartStartIndex = text.indexOf(suggested); + int smartEndIndex = smartStartIndex + suggested.length(); + TextSelection.Request request = new TextSelection.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(LOCALES) + .build(); + + TextSelection selection = mClassifier.suggestSelection(request); + assertThat(selection, + isTextSelection(smartStartIndex, smartEndIndex, TextClassifier.TYPE_EMAIL)); + } + + @Test + public void testSmartSelection_url() { + if (isTextClassifierDisabled()) return; + + String text = "Visit http://www.android.com for more information"; + String selected = "http"; + String suggested = "http://www.android.com"; + int startIndex = text.indexOf(selected); + int endIndex = startIndex + selected.length(); + int smartStartIndex = text.indexOf(suggested); + int smartEndIndex = smartStartIndex + suggested.length(); + TextSelection.Request request = new TextSelection.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(LOCALES) + .build(); + + TextSelection selection = mClassifier.suggestSelection(request); + assertThat(selection, + isTextSelection(smartStartIndex, smartEndIndex, TextClassifier.TYPE_URL)); + } + + @Test + public void testSmartSelection_withEmoji() { + if (isTextClassifierDisabled()) return; + + String text = "\uD83D\uDE02 Hello."; + String selected = "Hello"; + int startIndex = text.indexOf(selected); + int endIndex = startIndex + selected.length(); + TextSelection.Request request = new TextSelection.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(LOCALES) + .build(); + + TextSelection selection = mClassifier.suggestSelection(request); + assertThat(selection, + isTextSelection(startIndex, endIndex, NO_TYPE)); + } + + @Test + public void testClassifyText() { + if (isTextClassifierDisabled()) return; + + String text = "Contact me at droid@android.com"; + String classifiedText = "droid@android.com"; + int startIndex = text.indexOf(classifiedText); + int endIndex = startIndex + classifiedText.length(); + TextClassification.Request request = new TextClassification.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(LOCALES) + .build(); + + TextClassification classification = mClassifier.classifyText(request); + assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_EMAIL)); + } + + @Test + public void testTextClassifyText_url() { + if (isTextClassifierDisabled()) return; + + String text = "Visit www.android.com for more information"; + String classifiedText = "www.android.com"; + int startIndex = text.indexOf(classifiedText); + int endIndex = startIndex + classifiedText.length(); + TextClassification.Request request = new TextClassification.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(LOCALES) + .build(); + + TextClassification classification = mClassifier.classifyText(request); + assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_URL)); + } + + @Test + public void testTextClassifyText_address() { + if (isTextClassifierDisabled()) return; + + String text = "Brandschenkestrasse 110, Zürich, Switzerland"; + TextClassification.Request request = new TextClassification.Request.Builder( + text, 0, text.length()) + .setDefaultLocales(LOCALES) + .build(); + + TextClassification classification = mClassifier.classifyText(request); + assertThat(classification, isTextClassification(text, TextClassifier.TYPE_ADDRESS)); + } + + @Test + public void testTextClassifyText_url_inCaps() { + if (isTextClassifierDisabled()) return; + + String text = "Visit HTTP://ANDROID.COM for more information"; + String classifiedText = "HTTP://ANDROID.COM"; + int startIndex = text.indexOf(classifiedText); + int endIndex = startIndex + classifiedText.length(); + TextClassification.Request request = new TextClassification.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(LOCALES) + .build(); + + TextClassification classification = mClassifier.classifyText(request); + assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_URL)); + } + + @Test + public void testTextClassifyText_date() { + if (isTextClassifierDisabled()) return; + + String text = "Let's meet on January 9, 2018."; + String classifiedText = "January 9, 2018"; + int startIndex = text.indexOf(classifiedText); + int endIndex = startIndex + classifiedText.length(); + TextClassification.Request request = new TextClassification.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(LOCALES) + .build(); + + TextClassification classification = mClassifier.classifyText(request); + assertThat(classification, isTextClassification(classifiedText, TextClassifier.TYPE_DATE)); + } + + @Test + public void testTextClassifyText_datetime() { + if (isTextClassifierDisabled()) return; + + String text = "Let's meet 2018/01/01 10:30:20."; + String classifiedText = "2018/01/01 10:30:20"; + int startIndex = text.indexOf(classifiedText); + int endIndex = startIndex + classifiedText.length(); + TextClassification.Request request = new TextClassification.Request.Builder( + text, startIndex, endIndex) + .setDefaultLocales(LOCALES) + .build(); + + TextClassification classification = mClassifier.classifyText(request); + assertThat(classification, + isTextClassification(classifiedText, TextClassifier.TYPE_DATE_TIME)); + } + + @Test + public void testGenerateLinks_phone() { + if (isTextClassifierDisabled()) return; + String text = "The number is +12122537077. See you tonight!"; + TextLinks.Request request = new TextLinks.Request.Builder(text).build(); + assertThat(mClassifier.generateLinks(request), + isTextLinksContaining(text, "+12122537077", TextClassifier.TYPE_PHONE)); + } + + @Test + public void testGenerateLinks_exclude() { + if (isTextClassifierDisabled()) return; + String text = "You want apple@banana.com. See you tonight!"; + List hints = Collections.EMPTY_LIST; + List included = Collections.EMPTY_LIST; + List excluded = Arrays.asList(TextClassifier.TYPE_EMAIL); + TextLinks.Request request = new TextLinks.Request.Builder(text) + .setEntityConfig(TextClassifier.EntityConfig.create(hints, included, excluded)) + .setDefaultLocales(LOCALES) + .build(); + assertThat(mClassifier.generateLinks(request), + not(isTextLinksContaining(text, "apple@banana.com", TextClassifier.TYPE_EMAIL))); + } + + @Test + public void testGenerateLinks_explicit_address() { + if (isTextClassifierDisabled()) return; + String text = "The address is 1600 Amphitheater Parkway, Mountain View, CA. See you!"; + List explicit = Arrays.asList(TextClassifier.TYPE_ADDRESS); + TextLinks.Request request = new TextLinks.Request.Builder(text) + .setEntityConfig(TextClassifier.EntityConfig.createWithExplicitEntityList(explicit)) + .setDefaultLocales(LOCALES) + .build(); + assertThat(mClassifier.generateLinks(request), + isTextLinksContaining(text, "1600 Amphitheater Parkway, Mountain View, CA", + TextClassifier.TYPE_ADDRESS)); + } + + @Test + public void testGenerateLinks_exclude_override() { + if (isTextClassifierDisabled()) return; + String text = "You want apple@banana.com. See you tonight!"; + List hints = Collections.EMPTY_LIST; + List included = Arrays.asList(TextClassifier.TYPE_EMAIL); + List excluded = Arrays.asList(TextClassifier.TYPE_EMAIL); + TextLinks.Request request = new TextLinks.Request.Builder(text) + .setEntityConfig(TextClassifier.EntityConfig.create(hints, included, excluded)) + .setDefaultLocales(LOCALES) + .build(); + assertThat(mClassifier.generateLinks(request), + not(isTextLinksContaining(text, "apple@banana.com", TextClassifier.TYPE_EMAIL))); + } + + @Test + public void testGenerateLinks_maxLength() { + if (isTextClassifierDisabled()) return; + char[] manySpaces = new char[mClassifier.getMaxGenerateLinksTextLength()]; + Arrays.fill(manySpaces, ' '); + TextLinks.Request request = new TextLinks.Request.Builder(new String(manySpaces)).build(); + TextLinks links = mClassifier.generateLinks(request); + assertTrue(links.getLinks().isEmpty()); + } + + @Test(expected = IllegalArgumentException.class) + public void testGenerateLinks_tooLong() { + if (isTextClassifierDisabled()) { + throw new IllegalArgumentException("pass if disabled"); + } + char[] manySpaces = new char[mClassifier.getMaxGenerateLinksTextLength() + 1]; + Arrays.fill(manySpaces, ' '); + TextLinks.Request request = new TextLinks.Request.Builder(new String(manySpaces)).build(); + mClassifier.generateLinks(request); + } + + @Test + public void testDetectLanguage() { + if (isTextClassifierDisabled()) return; + String text = "This is English text"; + TextLanguage.Request request = new TextLanguage.Request.Builder(text).build(); + TextLanguage textLanguage = mClassifier.detectLanguage(request); + assertThat(textLanguage, isTextLanguage("en")); + } + + @Test + public void testDetectLanguage_japanese() { + if (isTextClassifierDisabled()) return; + String text = "これは日本語のテキストです"; + TextLanguage.Request request = new TextLanguage.Request.Builder(text).build(); + TextLanguage textLanguage = mClassifier.detectLanguage(request); + assertThat(textLanguage, isTextLanguage("ja")); + } + + @Test + public void testSuggestConversationActions_textReplyOnly_maxThree() { + if (isTextClassifierDisabled()) return; + ConversationActions.Message message = + new ConversationActions.Message.Builder().setText("Hello").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)) + .setMaxSuggestions(3) + .setTypeConfig(typeConfig) + .build(); + + ConversationActions conversationActions = mClassifier.suggestConversationActions(request); + assertTrue(conversationActions.getConversationActions().size() > 0); + assertTrue(conversationActions.getConversationActions().size() <= 3); + for (ConversationActions.ConversationAction conversationAction : + conversationActions.getConversationActions()) { + assertEquals(conversationAction.getType(), ConversationActions.TYPE_TEXT_REPLY); + assertNotNull(conversationAction.getTextReply()); + assertTrue(conversationAction.getConfidenceScore() > 0); + assertTrue(conversationAction.getConfidenceScore() <= 1); + } + } + + + private boolean isTextClassifierDisabled() { + return mClassifier == null || mClassifier == TextClassifier.NO_OP; + } + + private static Matcher isTextSelection( + final int startIndex, final int endIndex, final String type) { + return new BaseMatcher() { + @Override + public boolean matches(Object o) { + if (o instanceof TextSelection) { + TextSelection selection = (TextSelection) o; + return startIndex == selection.getSelectionStartIndex() + && endIndex == selection.getSelectionEndIndex() + && typeMatches(selection, type); + } + return false; + } + + private boolean typeMatches(TextSelection selection, String type) { + return type == null + || (selection.getEntityCount() > 0 + && type.trim().equalsIgnoreCase(selection.getEntity(0))); + } + + @Override + public void describeTo(Description description) { + description.appendValue( + String.format("%d, %d, %s", startIndex, endIndex, type)); + } + }; + } + + private static Matcher isTextLinksContaining( + final String text, final String substring, final String type) { + return new BaseMatcher() { + + @Override + public void describeTo(Description description) { + description.appendText("text=").appendValue(text) + .appendText(", substring=").appendValue(substring) + .appendText(", type=").appendValue(type); + } + + @Override + public boolean matches(Object o) { + if (o instanceof TextLinks) { + for (TextLinks.TextLink link : ((TextLinks) o).getLinks()) { + if (text.subSequence(link.getStart(), link.getEnd()).equals(substring)) { + return type.equals(link.getEntity(0)); + } + } + } + return false; + } + }; + } + + private static Matcher isTextClassification( + final String text, final String type) { + return new BaseMatcher() { + @Override + public boolean matches(Object o) { + if (o instanceof TextClassification) { + TextClassification result = (TextClassification) o; + return text.equals(result.getText()) + && result.getEntityCount() > 0 + && type.equals(result.getEntity(0)); + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("text=").appendValue(text) + .appendText(", type=").appendValue(type); + } + }; + } + + private static Matcher isTextLanguage(final String languageTag) { + return new BaseMatcher() { + @Override + public boolean matches(Object o) { + if (o instanceof TextLanguage) { + TextLanguage result = (TextLanguage) o; + return result.getLocaleHypothesisCount() > 0 + && languageTag.equals(result.getLocale(0).toLanguageTag()); + } + return false; + } + + @Override + public void describeTo(Description description) { + description.appendText("locale=").appendValue(languageTag); + } + }; + } +} diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java index 5ce8145fa00e7..8d27d1e043a7a 100644 --- a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java +++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java @@ -28,18 +28,22 @@ import android.os.Binder; import android.os.IBinder; import android.os.RemoteException; import android.os.UserHandle; +import android.service.textclassifier.IConversationActionsCallback; import android.service.textclassifier.ITextClassificationCallback; import android.service.textclassifier.ITextClassifierService; +import android.service.textclassifier.ITextLanguageCallback; import android.service.textclassifier.ITextLinksCallback; import android.service.textclassifier.ITextSelectionCallback; import android.service.textclassifier.TextClassifierService; import android.util.Slog; import android.util.SparseArray; +import android.view.textclassifier.ConversationActions; 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.TextLanguage; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; @@ -209,6 +213,50 @@ public final class TextClassificationManagerService extends ITextClassifierServi } } + @Override + public void onDetectLanguage( + TextClassificationSessionId sessionId, + TextLanguage.Request request, + ITextLanguageCallback callback) throws RemoteException { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(callback); + + synchronized (mLock) { + UserState userState = getCallingUserStateLocked(); + if (!userState.bindLocked()) { + callback.onFailure(); + } else if (userState.isBoundLocked()) { + userState.mService.onDetectLanguage(sessionId, request, callback); + } else { + userState.mPendingRequests.add(new PendingRequest( + () -> onDetectLanguage(sessionId, request, callback), + callback::onFailure, callback.asBinder(), this, userState)); + } + } + } + + @Override + public void onSuggestConversationActions( + TextClassificationSessionId sessionId, + ConversationActions.Request request, + IConversationActionsCallback callback) throws RemoteException { + Preconditions.checkNotNull(request); + Preconditions.checkNotNull(callback); + + synchronized (mLock) { + UserState userState = getCallingUserStateLocked(); + if (!userState.bindLocked()) { + callback.onFailure(); + } else if (userState.isBoundLocked()) { + userState.mService.onSuggestConversationActions(sessionId, request, callback); + } else { + userState.mPendingRequests.add(new PendingRequest( + () -> onSuggestConversationActions(sessionId, request, callback), + callback::onFailure, callback.asBinder(), this, userState)); + } + } + } + @Override public void onCreateTextClassificationSession( TextClassificationContext classificationContext, TextClassificationSessionId sessionId)