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
diff --git a/Android.bp b/Android.bp
index 1012bb81..4cb330a 100644
--- a/Android.bp
+++ b/Android.bp
@@ -334,8 +334,10 @@
         "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 d7265c7..4b237b5 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -5171,8 +5171,10 @@
     method public abstract void onClassifyText(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.TextClassification.Request, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextClassification>);
     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<android.view.textclassifier.TextLanguage>);
     method public abstract void onGenerateLinks(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.TextLinks.Request, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextLinks>);
     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<android.view.textclassifier.ConversationActions>);
     method public abstract void onSuggestSelection(android.view.textclassifier.TextClassificationSessionId, android.view.textclassifier.TextSelection.Request, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextSelection>);
     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 0000000..c35d424
--- /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 7ac72c7..254a710 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 @@
 
     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 0000000..263d99af
--- /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 7f1082d..d7359f1 100644
--- a/core/java/android/service/textclassifier/TextClassifierService.java
+++ b/core/java/android/service/textclassifier/TextClassifierService.java
@@ -32,12 +32,14 @@
 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 @@
         @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 @@
         @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 @@
         @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 @@
         @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<TextLanguage>() {
+                        @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<ConversationActions>() {
+                        @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 @@
 
         /** {@inheritDoc} */
         @Override
-        public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId)
-                throws RemoteException {
+        public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) {
             TextClassifierService.this.onDestroyTextClassificationSession(sessionId);
         }
     };
@@ -266,6 +329,38 @@
             @NonNull Callback<TextLinks> 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<TextLanguage> 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<ConversationActions> 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
      * happened.
diff --git a/core/java/android/view/textclassifier/ConversationActions.aidl b/core/java/android/view/textclassifier/ConversationActions.aidl
new file mode 100644
index 0000000..fece939
--- /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 16eb5af..f8fce62 100644
--- a/core/java/android/view/textclassifier/SystemTextClassifier.java
+++ b/core/java/android/view/textclassifier/SystemTextClassifier.java
@@ -23,8 +23,10 @@
 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 @@
             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 @@
             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 @@
             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 @@
         }
     }
 
+    @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 @@
 
     private static final class TextSelectionCallback extends ITextSelectionCallback.Stub {
 
-        final ResponseReceiver<TextSelection> mReceiver = new ResponseReceiver<>();
+        final ResponseReceiver<TextSelection> mReceiver = new ResponseReceiver<>("textselection");
 
         @Override
         public void onSuccess(TextSelection selection) {
@@ -208,7 +246,8 @@
 
     private static final class TextClassificationCallback extends ITextClassificationCallback.Stub {
 
-        final ResponseReceiver<TextClassification> mReceiver = new ResponseReceiver<>();
+        final ResponseReceiver<TextClassification> mReceiver =
+                new ResponseReceiver<>("textclassification");
 
         @Override
         public void onSuccess(TextClassification classification) {
@@ -223,7 +262,7 @@
 
     private static final class TextLinksCallback extends ITextLinksCallback.Stub {
 
-        final ResponseReceiver<TextLinks> mReceiver = new ResponseReceiver<>();
+        final ResponseReceiver<TextLinks> mReceiver = new ResponseReceiver<>("textlinks");
 
         @Override
         public void onSuccess(TextLinks links) {
@@ -236,12 +275,48 @@
         }
     }
 
+    private static final class TextLanguageCallback extends ITextLanguageCallback.Stub {
+
+        final ResponseReceiver<TextLanguage> 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<ConversationActions> mReceiver =
+                new ResponseReceiver<>("conversationaction");
+
+        @Override
+        public void onSuccess(ConversationActions conversationActions) {
+            mReceiver.onSuccess(conversationActions);
+        }
+
+        @Override
+        public void onFailure() {
+            mReceiver.onFailure();
+        }
+    }
+
     private static final class ResponseReceiver<T> {
 
         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 @@
         }
 
         @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 0000000..54e3cf9f
--- /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 3a33d57..46aa5b4 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.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<String> hints = Collections.EMPTY_LIST;
-        List<String> included = Collections.EMPTY_LIST;
-        List<String> 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<String> 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<String> hints = Collections.EMPTY_LIST;
-        List<String> included = Arrays.asList(TextClassifier.TYPE_EMAIL);
-        List<String> 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 @@
         assertFalse(result.getActions().isEmpty());
         assertNotSame(result, fallbackResult);
     }
-
-    private boolean isTextClassifierDisabled() {
-        return mClassifier == TextClassifier.NO_OP;
-    }
-
-    private static Matcher<TextSelection> isTextSelection(
-            final int startIndex, final int endIndex, final String type) {
-        return new BaseMatcher<TextSelection>() {
-            @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<TextLinks> isTextLinksContaining(
-            final String text, final String substring, final String type) {
-        return new BaseMatcher<TextLinks>() {
-
-            @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<TextClassification> isTextClassification(
-            final String text, final String type) {
-        return new BaseMatcher<TextClassification>() {
-            @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<TextLanguage> isTextLanguage(final String languageTag) {
-        return new BaseMatcher<TextLanguage>() {
-            @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 0000000..06ba15e
--- /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.
+ * <p>
+ * 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<Object> 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<String> hints = Collections.EMPTY_LIST;
+        List<String> included = Collections.EMPTY_LIST;
+        List<String> 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<String> 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<String> hints = Collections.EMPTY_LIST;
+        List<String> included = Arrays.asList(TextClassifier.TYPE_EMAIL);
+        List<String> 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<TextSelection> isTextSelection(
+            final int startIndex, final int endIndex, final String type) {
+        return new BaseMatcher<TextSelection>() {
+            @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<TextLinks> isTextLinksContaining(
+            final String text, final String substring, final String type) {
+        return new BaseMatcher<TextLinks>() {
+
+            @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<TextClassification> isTextClassification(
+            final String text, final String type) {
+        return new BaseMatcher<TextClassification>() {
+            @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<TextLanguage> isTextLanguage(final String languageTag) {
+        return new BaseMatcher<TextLanguage>() {
+            @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 5ce8145..8d27d1e 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.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;
 
@@ -210,6 +214,50 @@
     }
 
     @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)
             throws RemoteException {