summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Android.bp4
-rw-r--r--api/current.txt10
-rw-r--r--api/system-current.txt19
-rw-r--r--core/java/android/service/textclassifier/ITextClassificationCallback.aidl28
-rw-r--r--core/java/android/service/textclassifier/ITextClassifierService.aidl47
-rw-r--r--core/java/android/service/textclassifier/ITextLinksCallback.aidl28
-rw-r--r--core/java/android/service/textclassifier/ITextSelectionCallback.aidl28
-rw-r--r--core/java/android/service/textclassifier/TextClassifierService.java290
-rw-r--r--core/java/android/view/textclassifier/SystemTextClassifier.java197
-rw-r--r--core/java/android/view/textclassifier/TextClassification.aidl20
-rw-r--r--core/java/android/view/textclassifier/TextClassification.java118
-rw-r--r--core/java/android/view/textclassifier/TextClassificationManager.java48
-rw-r--r--core/java/android/view/textclassifier/TextClassifier.java52
-rw-r--r--core/java/android/view/textclassifier/TextClassifierImpl.java45
-rw-r--r--core/java/android/view/textclassifier/TextLinks.aidl20
-rw-r--r--core/java/android/view/textclassifier/TextSelection.aidl20
-rw-r--r--core/java/android/view/textclassifier/TextSelection.java82
-rw-r--r--core/java/android/widget/SelectionActionModeHelper.java1
-rw-r--r--core/res/AndroidManifest.xml8
-rw-r--r--core/res/res/values/config.xml9
-rw-r--r--core/res/res/values/symbols.xml1
-rw-r--r--core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java10
-rw-r--r--core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java10
-rw-r--r--services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java395
-rw-r--r--services/java/com/android/server/SystemServer.java9
25 files changed, 1321 insertions, 178 deletions
diff --git a/Android.bp b/Android.bp
index 763d242a12f9..ad306cfad014 100644
--- a/Android.bp
+++ b/Android.bp
@@ -319,6 +319,10 @@ java_library {
"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/ITextClassificationCallback.aidl",
+ "core/java/android/service/textclassifier/ITextClassifierService.aidl",
+ "core/java/android/service/textclassifier/ITextLinksCallback.aidl",
+ "core/java/android/service/textclassifier/ITextSelectionCallback.aidl",
"core/java/android/view/accessibility/IAccessibilityInteractionConnection.aidl",
"core/java/android/view/accessibility/IAccessibilityInteractionConnectionCallback.aidl",
"core/java/android/view/accessibility/IAccessibilityManager.aidl",
diff --git a/api/current.txt b/api/current.txt
index 292ef5275aa4..56c0dc433672 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -49983,7 +49983,8 @@ package android.view.inputmethod {
package android.view.textclassifier {
- public final class TextClassification {
+ public final class TextClassification implements android.os.Parcelable {
+ method public int describeContents();
method public float getConfidenceScore(java.lang.String);
method public java.lang.String getEntity(int);
method public int getEntityCount();
@@ -49997,6 +49998,8 @@ package android.view.textclassifier {
method public java.lang.CharSequence getSecondaryLabel(int);
method public java.lang.String getSignature();
method public java.lang.String getText();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextClassification> CREATOR;
}
public static final class TextClassification.Builder {
@@ -50102,13 +50105,16 @@ package android.view.textclassifier {
field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextLinks.TextLink> CREATOR;
}
- public final class TextSelection {
+ public final class TextSelection implements android.os.Parcelable {
+ method public int describeContents();
method public float getConfidenceScore(java.lang.String);
method public java.lang.String getEntity(int);
method public int getEntityCount();
method public int getSelectionEndIndex();
method public int getSelectionStartIndex();
method public java.lang.String getSignature();
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.view.textclassifier.TextSelection> CREATOR;
}
public static final class TextSelection.Builder {
diff --git a/api/system-current.txt b/api/system-current.txt
index 663ad112280a..52612d52220b 100644
--- a/api/system-current.txt
+++ b/api/system-current.txt
@@ -29,6 +29,7 @@ package android {
field public static final java.lang.String BIND_RESOLVER_RANKER_SERVICE = "android.permission.BIND_RESOLVER_RANKER_SERVICE";
field public static final java.lang.String BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE = "android.permission.BIND_RUNTIME_PERMISSION_PRESENTER_SERVICE";
field public static final java.lang.String BIND_SETTINGS_SUGGESTIONS_SERVICE = "android.permission.BIND_SETTINGS_SUGGESTIONS_SERVICE";
+ field public static final java.lang.String BIND_TEXTCLASSIFIER_SERVICE = "android.permission.BIND_TEXTCLASSIFIER_SERVICE";
field public static final java.lang.String BIND_TRUST_AGENT = "android.permission.BIND_TRUST_AGENT";
field public static final java.lang.String BIND_TV_REMOTE_SERVICE = "android.permission.BIND_TV_REMOTE_SERVICE";
field public static final java.lang.String BLUETOOTH_PRIVILEGED = "android.permission.BLUETOOTH_PRIVILEGED";
@@ -4438,6 +4439,24 @@ package android.service.settings.suggestions {
}
+package android.service.textclassifier {
+
+ public abstract class TextClassifierService extends android.app.Service {
+ ctor public TextClassifierService();
+ method public final android.os.IBinder onBind(android.content.Intent);
+ method public abstract void onClassifyText(java.lang.CharSequence, int, int, android.view.textclassifier.TextClassification.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextClassification>);
+ method public abstract void onGenerateLinks(java.lang.CharSequence, android.view.textclassifier.TextLinks.Options, android.os.CancellationSignal, android.service.textclassifier.TextClassifierService.Callback<android.view.textclassifier.TextLinks>);
+ method public abstract void onSuggestSelection(java.lang.CharSequence, int, int, android.view.textclassifier.TextSelection.Options, 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";
+ }
+
+ public static abstract interface TextClassifierService.Callback<T> {
+ method public abstract void onFailure(java.lang.CharSequence);
+ method public abstract void onSuccess(T);
+ }
+
+}
+
package android.service.trust {
public class TrustAgentService extends android.app.Service {
diff --git a/core/java/android/service/textclassifier/ITextClassificationCallback.aidl b/core/java/android/service/textclassifier/ITextClassificationCallback.aidl
new file mode 100644
index 000000000000..10bfe6324509
--- /dev/null
+++ b/core/java/android/service/textclassifier/ITextClassificationCallback.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.TextClassification;
+
+/**
+ * Callback for a TextClassification request.
+ * @hide
+ */
+oneway interface ITextClassificationCallback {
+ void onSuccess(in TextClassification classification);
+ void onFailure();
+}
diff --git a/core/java/android/service/textclassifier/ITextClassifierService.aidl b/core/java/android/service/textclassifier/ITextClassifierService.aidl
new file mode 100644
index 000000000000..d2ffe345ae38
--- /dev/null
+++ b/core/java/android/service/textclassifier/ITextClassifierService.aidl
@@ -0,0 +1,47 @@
+/*
+ * 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.service.textclassifier.ITextClassificationCallback;
+import android.service.textclassifier.ITextLinksCallback;
+import android.service.textclassifier.ITextSelectionCallback;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+
+/**
+ * TextClassifierService binder interface.
+ * See TextClassifier for interface documentation.
+ * {@hide}
+ */
+oneway interface ITextClassifierService {
+
+ void onSuggestSelection(
+ in CharSequence text, int selectionStartIndex, int selectionEndIndex,
+ in TextSelection.Options options,
+ in ITextSelectionCallback c);
+
+ void onClassifyText(
+ in CharSequence text, int startIndex, int endIndex,
+ in TextClassification.Options options,
+ in ITextClassificationCallback c);
+
+ void onGenerateLinks(
+ in CharSequence text,
+ in TextLinks.Options options,
+ in ITextLinksCallback c);
+}
diff --git a/core/java/android/service/textclassifier/ITextLinksCallback.aidl b/core/java/android/service/textclassifier/ITextLinksCallback.aidl
new file mode 100644
index 000000000000..a9e0dde56773
--- /dev/null
+++ b/core/java/android/service/textclassifier/ITextLinksCallback.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.TextLinks;
+
+/**
+ * Callback for a TextLinks request.
+ * @hide
+ */
+oneway interface ITextLinksCallback {
+ void onSuccess(in TextLinks links);
+ void onFailure();
+} \ No newline at end of file
diff --git a/core/java/android/service/textclassifier/ITextSelectionCallback.aidl b/core/java/android/service/textclassifier/ITextSelectionCallback.aidl
new file mode 100644
index 000000000000..1b4c4d10d427
--- /dev/null
+++ b/core/java/android/service/textclassifier/ITextSelectionCallback.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.TextSelection;
+
+/**
+ * Callback for a TextSelection request.
+ * @hide
+ */
+oneway interface ITextSelectionCallback {
+ void onSuccess(in TextSelection selection);
+ 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
new file mode 100644
index 000000000000..6c8c8bc36127
--- /dev/null
+++ b/core/java/android/service/textclassifier/TextClassifierService.java
@@ -0,0 +1,290 @@
+/*
+ * 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.Manifest;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.SystemApi;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ServiceInfo;
+import android.os.CancellationSignal;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Slog;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+
+import com.android.internal.R;
+
+/**
+ * Abstract base class for the TextClassifier service.
+ *
+ * <p>A TextClassifier service provides text classification related features for the system.
+ * The system's default TextClassifierService is configured in
+ * {@code config_defaultTextClassifierService}. If this config has no value, a
+ * {@link android.view.textclassifier.TextClassifierImpl} is loaded in the calling app's process.
+ *
+ * <p>See: {@link TextClassifier}.
+ * See: {@link android.view.textclassifier.TextClassificationManager}.
+ *
+ * <p>Include the following in the manifest:
+ *
+ * <pre>
+ * {@literal
+ * <service android:name=".YourTextClassifierService"
+ * android:permission="android.permission.BIND_TEXTCLASSIFIER_SERVICE">
+ * <intent-filter>
+ * <action android:name="android.service.textclassifier.TextClassifierService" />
+ * </intent-filter>
+ * </service>}</pre>
+ *
+ * @see TextClassifier
+ * @hide
+ */
+@SystemApi
+public abstract class TextClassifierService extends Service {
+
+ private static final String LOG_TAG = "TextClassifierService";
+
+ /**
+ * The {@link Intent} that must be declared as handled by the service.
+ * To be supported, the service must also require the
+ * {@link android.Manifest.permission#BIND_TEXTCLASSIFIER_SERVICE} permission so
+ * that other applications can not abuse it.
+ */
+ @SystemApi
+ public static final String SERVICE_INTERFACE =
+ "android.service.textclassifier.TextClassifierService";
+
+ private final ITextClassifierService.Stub mBinder = new ITextClassifierService.Stub() {
+
+ // TODO(b/72533911): Implement cancellation signal
+ @NonNull private final CancellationSignal mCancellationSignal = new CancellationSignal();
+
+ /** {@inheritDoc} */
+ @Override
+ public void onSuggestSelection(
+ CharSequence text, int selectionStartIndex, int selectionEndIndex,
+ TextSelection.Options options, ITextSelectionCallback callback)
+ throws RemoteException {
+ TextClassifierService.this.onSuggestSelection(
+ text, selectionStartIndex, selectionEndIndex, options, mCancellationSignal,
+ new Callback<TextSelection>() {
+ @Override
+ public void onSuccess(TextSelection result) {
+ try {
+ callback.onSuccess(result);
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+
+ @Override
+ public void onFailure(CharSequence error) {
+ try {
+ if (callback.asBinder().isBinderAlive()) {
+ callback.onFailure();
+ }
+ } catch (RemoteException e) {
+ Slog.d(LOG_TAG, "Error calling callback");
+ }
+ }
+ });
+ }
+
+ /** {@inheritDoc} */
+ @Override
+ public void onClassifyText(
+ CharSequence text, int startIndex, int endIndex,
+ TextClassification.Options options, ITextClassificationCallback callback)
+ throws RemoteException {
+ TextClassifierService.this.onClassifyText(
+ text, startIndex, endIndex, options, mCancellationSignal,
+ new Callback<TextClassification>() {
+ @Override
+ public void onSuccess(TextClassification 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 onGenerateLinks(
+ CharSequence text, TextLinks.Options options, ITextLinksCallback callback)
+ throws RemoteException {
+ TextClassifierService.this.onGenerateLinks(
+ text, options, mCancellationSignal,
+ new Callback<TextLinks>() {
+ @Override
+ public void onSuccess(TextLinks 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");
+ }
+ }
+ });
+ }
+ };
+
+ @Nullable
+ @Override
+ public final IBinder onBind(Intent intent) {
+ if (SERVICE_INTERFACE.equals(intent.getAction())) {
+ return mBinder;
+ }
+ return null;
+ }
+
+ /**
+ * Returns suggested text selection start and end indices, recognized entity types, and their
+ * associated confidence scores. The entity types are ordered from highest to lowest scoring.
+ *
+ * @param text text providing context for the selected text (which is specified
+ * by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
+ * @param selectionStartIndex start index of the selected part of text
+ * @param selectionEndIndex end index of the selected part of text
+ * @param options optional input parameters
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ public abstract void onSuggestSelection(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int selectionStartIndex,
+ @IntRange(from = 0) int selectionEndIndex,
+ @Nullable TextSelection.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextSelection> callback);
+
+ /**
+ * Classifies the specified text and returns a {@link TextClassification} object that can be
+ * used to generate a widget for handling the classified text.
+ *
+ * @param text text providing context for the text to classify (which is specified
+ * by the sub sequence starting at startIndex and ending at endIndex)
+ * @param startIndex start index of the text to classify
+ * @param endIndex end index of the text to classify
+ * @param options optional input parameters
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ public abstract void onClassifyText(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex,
+ @Nullable TextClassification.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextClassification> callback);
+
+ /**
+ * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
+ * links information.
+ *
+ * @param text the text to generate annotations for
+ * @param options configuration for link generation
+ * @param cancellationSignal object to watch for canceling the current operation
+ * @param callback the callback to return the result to
+ */
+ public abstract void onGenerateLinks(
+ @NonNull CharSequence text,
+ @Nullable TextLinks.Options options,
+ @NonNull CancellationSignal cancellationSignal,
+ @NonNull Callback<TextLinks> callback);
+
+ /**
+ * Callbacks for TextClassifierService results.
+ *
+ * @param <T> the type of the result
+ * @hide
+ */
+ @SystemApi
+ public interface Callback<T> {
+ /**
+ * Returns the result.
+ */
+ void onSuccess(T result);
+
+ /**
+ * Signals a failure.
+ */
+ void onFailure(CharSequence error);
+ }
+
+ /**
+ * Returns the component name of the system default textclassifier service if it can be found
+ * on the system. Otherwise, returns null.
+ * @hide
+ */
+ @Nullable
+ public static ComponentName getServiceComponentName(Context context) {
+ final String str = context.getString(R.string.config_defaultTextClassifierService);
+ if (!TextUtils.isEmpty(str)) {
+ try {
+ final ComponentName componentName = ComponentName.unflattenFromString(str);
+ final Intent intent = new Intent(SERVICE_INTERFACE).setComponent(componentName);
+ final ServiceInfo si = context.getPackageManager()
+ .getServiceInfo(intent.getComponent(), 0);
+ final String permission = si == null ? null : si.permission;
+ if (Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE.equals(permission)) {
+ return componentName;
+ }
+ Slog.w(LOG_TAG, String.format(
+ "Service %s should require %s permission. Found %s permission",
+ intent.getComponent().flattenToString(),
+ Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE,
+ si.permission));
+ } catch (PackageManager.NameNotFoundException e) {
+ Slog.w(LOG_TAG, String.format("Service %s not found", str));
+ }
+ } else {
+ Slog.d(LOG_TAG, "No configured system TextClassifierService");
+ }
+ return null;
+ }
+}
diff --git a/core/java/android/view/textclassifier/SystemTextClassifier.java b/core/java/android/view/textclassifier/SystemTextClassifier.java
new file mode 100644
index 000000000000..af55dcd0ed72
--- /dev/null
+++ b/core/java/android/view/textclassifier/SystemTextClassifier.java
@@ -0,0 +1,197 @@
+/*
+ * Copyright (C) 2018 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.view.textclassifier;
+
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.os.Looper;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.service.textclassifier.ITextClassificationCallback;
+import android.service.textclassifier.ITextClassifierService;
+import android.service.textclassifier.ITextLinksCallback;
+import android.service.textclassifier.ITextSelectionCallback;
+
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Proxy to the system's default TextClassifier.
+ */
+final class SystemTextClassifier implements TextClassifier {
+
+ private static final String LOG_TAG = "SystemTextClassifier";
+
+ private final ITextClassifierService mManagerService;
+ private final TextClassifier mFallback;
+
+ SystemTextClassifier(Context context) throws ServiceManager.ServiceNotFoundException {
+ mManagerService = ITextClassifierService.Stub.asInterface(
+ ServiceManager.getServiceOrThrow(Context.TEXT_CLASSIFICATION_SERVICE));
+ mFallback = new TextClassifierImpl(context);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @WorkerThread
+ public TextSelection suggestSelection(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int selectionStartIndex,
+ @IntRange(from = 0) int selectionEndIndex,
+ @Nullable TextSelection.Options options) {
+ Utils.validate(text, selectionStartIndex, selectionEndIndex, false /* allowInMainThread */);
+ try {
+ final TextSelectionCallback callback = new TextSelectionCallback();
+ mManagerService.onSuggestSelection(
+ text, selectionStartIndex, selectionEndIndex, options, callback);
+ final TextSelection selection = callback.mReceiver.get();
+ if (selection != null) {
+ return selection;
+ }
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ } catch (InterruptedException e) {
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ return mFallback.suggestSelection(text, selectionStartIndex, selectionEndIndex, options);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @WorkerThread
+ public TextClassification classifyText(
+ @NonNull CharSequence text,
+ @IntRange(from = 0) int startIndex,
+ @IntRange(from = 0) int endIndex,
+ @Nullable TextClassification.Options options) {
+ Utils.validate(text, startIndex, endIndex, false /* allowInMainThread */);
+ try {
+ final TextClassificationCallback callback = new TextClassificationCallback();
+ mManagerService.onClassifyText(text, startIndex, endIndex, options, callback);
+ final TextClassification classification = callback.mReceiver.get();
+ if (classification != null) {
+ return classification;
+ }
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ } catch (InterruptedException e) {
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ return mFallback.classifyText(text, startIndex, endIndex, options);
+ }
+
+ /**
+ * @inheritDoc
+ */
+ @WorkerThread
+ public TextLinks generateLinks(
+ @NonNull CharSequence text, @Nullable TextLinks.Options options) {
+ Utils.validate(text, false /* allowInMainThread */);
+ try {
+ final TextLinksCallback callback = new TextLinksCallback();
+ mManagerService.onGenerateLinks(text, options, callback);
+ final TextLinks links = callback.mReceiver.get();
+ if (links != null) {
+ return links;
+ }
+ } catch (RemoteException e) {
+ e.rethrowAsRuntimeException();
+ } catch (InterruptedException e) {
+ Log.d(LOG_TAG, e.getMessage());
+ }
+ return mFallback.generateLinks(text, options);
+ }
+
+ private static final class TextSelectionCallback extends ITextSelectionCallback.Stub {
+
+ final ResponseReceiver<TextSelection> mReceiver = new ResponseReceiver<>();
+
+ @Override
+ public void onSuccess(TextSelection selection) {
+ mReceiver.onSuccess(selection);
+ }
+
+ @Override
+ public void onFailure() {
+ mReceiver.onFailure();
+ }
+ }
+
+ private static final class TextClassificationCallback extends ITextClassificationCallback.Stub {
+
+ final ResponseReceiver<TextClassification> mReceiver = new ResponseReceiver<>();
+
+ @Override
+ public void onSuccess(TextClassification classification) {
+ mReceiver.onSuccess(classification);
+ }
+
+ @Override
+ public void onFailure() {
+ mReceiver.onFailure();
+ }
+ }
+
+ private static final class TextLinksCallback extends ITextLinksCallback.Stub {
+
+ final ResponseReceiver<TextLinks> mReceiver = new ResponseReceiver<>();
+
+ @Override
+ public void onSuccess(TextLinks links) {
+ mReceiver.onSuccess(links);
+ }
+
+ @Override
+ public void onFailure() {
+ mReceiver.onFailure();
+ }
+ }
+
+ private static final class ResponseReceiver<T> {
+
+ private final CountDownLatch mLatch = new CountDownLatch(1);
+
+ private T mResponse;
+
+ public void onSuccess(T response) {
+ mResponse = response;
+ mLatch.countDown();
+ }
+
+ public void onFailure() {
+ Log.e(LOG_TAG, "Request failed.", null);
+ mLatch.countDown();
+ }
+
+ @Nullable
+ public T get() throws InterruptedException {
+ // 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);
+ }
+ return mResponse;
+ }
+ }
+}
diff --git a/core/java/android/view/textclassifier/TextClassification.aidl b/core/java/android/view/textclassifier/TextClassification.aidl
new file mode 100644
index 000000000000..9fefe5d4176a
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextClassification.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 TextClassification;
+parcelable TextClassification.Options; \ No newline at end of file
diff --git a/core/java/android/view/textclassifier/TextClassification.java b/core/java/android/view/textclassifier/TextClassification.java
index 54e93d5afd88..a6a2a945e774 100644
--- a/core/java/android/view/textclassifier/TextClassification.java
+++ b/core/java/android/view/textclassifier/TextClassification.java
@@ -97,7 +97,7 @@ import java.util.Map;
* });
* }</pre>
*/
-public final class TextClassification {
+public final class TextClassification implements Parcelable {
/**
* @hide
@@ -310,42 +310,6 @@ public final class TextClassification {
mSignature);
}
- /** Helper for parceling via #ParcelableWrapper. */
- private void writeToParcel(Parcel dest, int flags) {
- dest.writeString(mText);
- final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
- dest.writeInt(primaryIconBitmap != null ? 1 : 0);
- if (primaryIconBitmap != null) {
- primaryIconBitmap.writeToParcel(dest, flags);
- }
- dest.writeString(mPrimaryLabel);
- dest.writeInt(mPrimaryIntent != null ? 1 : 0);
- if (mPrimaryIntent != null) {
- mPrimaryIntent.writeToParcel(dest, flags);
- }
- // mPrimaryOnClickListener is not parcelable.
- dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
- dest.writeStringList(mSecondaryLabels);
- dest.writeTypedList(mSecondaryIntents);
- mEntityConfidence.writeToParcel(dest, flags);
- dest.writeString(mSignature);
- }
-
- /** Helper for unparceling via #ParcelableWrapper. */
- private TextClassification(Parcel in) {
- mText = in.readString();
- mPrimaryIcon = in.readInt() == 0
- ? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
- mPrimaryLabel = in.readString();
- mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
- mPrimaryOnClickListener = null; // not parcelable
- mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
- mSecondaryLabels = in.createStringArrayList();
- mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
- mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
- mSignature = in.readString();
- }
-
/**
* Creates an OnClickListener that starts an activity with the specified intent.
*
@@ -675,46 +639,56 @@ public final class TextClassification {
}
}
- /**
- * Parcelable wrapper for TextClassification objects.
- * @hide
- */
- public static final class ParcelableWrapper implements Parcelable {
-
- @NonNull private TextClassification mTextClassification;
-
- public ParcelableWrapper(@NonNull TextClassification textClassification) {
- Preconditions.checkNotNull(textClassification);
- mTextClassification = textClassification;
- }
-
- @NonNull
- public TextClassification getTextClassification() {
- return mTextClassification;
- }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
- @Override
- public int describeContents() {
- return 0;
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeString(mText);
+ final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE);
+ dest.writeInt(primaryIconBitmap != null ? 1 : 0);
+ if (primaryIconBitmap != null) {
+ primaryIconBitmap.writeToParcel(dest, flags);
}
-
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- mTextClassification.writeToParcel(dest, flags);
+ dest.writeString(mPrimaryLabel);
+ dest.writeInt(mPrimaryIntent != null ? 1 : 0);
+ if (mPrimaryIntent != null) {
+ mPrimaryIntent.writeToParcel(dest, flags);
}
+ // mPrimaryOnClickListener is not parcelable.
+ dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE));
+ dest.writeStringList(mSecondaryLabels);
+ dest.writeTypedList(mSecondaryIntents);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
- public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
- new Parcelable.Creator<ParcelableWrapper>() {
- @Override
- public ParcelableWrapper createFromParcel(Parcel in) {
- return new ParcelableWrapper(new TextClassification(in));
- }
+ public static final Parcelable.Creator<TextClassification> CREATOR =
+ new Parcelable.Creator<TextClassification>() {
+ @Override
+ public TextClassification createFromParcel(Parcel in) {
+ return new TextClassification(in);
+ }
- @Override
- public ParcelableWrapper[] newArray(int size) {
- return new ParcelableWrapper[size];
- }
- };
+ @Override
+ public TextClassification[] newArray(int size) {
+ return new TextClassification[size];
+ }
+ };
+ private TextClassification(Parcel in) {
+ mText = in.readString();
+ mPrimaryIcon = in.readInt() == 0
+ ? null : new BitmapDrawable(null, Bitmap.CREATOR.createFromParcel(in));
+ mPrimaryLabel = in.readString();
+ mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in);
+ mPrimaryOnClickListener = null; // not parcelable
+ mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR));
+ mSecondaryLabels = in.createStringArrayList();
+ mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR);
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
}
}
diff --git a/core/java/android/view/textclassifier/TextClassificationManager.java b/core/java/android/view/textclassifier/TextClassificationManager.java
index d7b07761a653..300aef2d172e 100644
--- a/core/java/android/view/textclassifier/TextClassificationManager.java
+++ b/core/java/android/view/textclassifier/TextClassificationManager.java
@@ -19,6 +19,8 @@ package android.view.textclassifier;
import android.annotation.Nullable;
import android.annotation.SystemService;
import android.content.Context;
+import android.os.ServiceManager;
+import android.service.textclassifier.TextClassifierService;
import com.android.internal.util.Preconditions;
@@ -28,10 +30,16 @@ import com.android.internal.util.Preconditions;
@SystemService(Context.TEXT_CLASSIFICATION_SERVICE)
public final class TextClassificationManager {
- private final Object mTextClassifierLock = new Object();
+ // TODO: Make this a configurable flag.
+ private static final boolean SYSTEM_TEXT_CLASSIFIER_ENABLED = true;
+
+ private static final String LOG_TAG = "TextClassificationManager";
+
+ private final Object mLock = new Object();
private final Context mContext;
private TextClassifier mTextClassifier;
+ private TextClassifier mSystemTextClassifier;
/** @hide */
public TextClassificationManager(Context context) {
@@ -39,12 +47,39 @@ public final class TextClassificationManager {
}
/**
+ * Returns the system's default TextClassifier.
+ * @hide
+ */
+ // TODO: Unhide when this is ready.
+ public TextClassifier getSystemDefaultTextClassifier() {
+ synchronized (mLock) {
+ if (mSystemTextClassifier == null && isSystemTextClassifierEnabled()) {
+ try {
+ Log.d(LOG_TAG, "Initialized SystemTextClassifier");
+ mSystemTextClassifier = new SystemTextClassifier(mContext);
+ } catch (ServiceManager.ServiceNotFoundException e) {
+ Log.e(LOG_TAG, "Could not initialize SystemTextClassifier", e);
+ }
+ }
+ if (mSystemTextClassifier == null) {
+ Log.d(LOG_TAG, "Using an in-process TextClassifier as the system default");
+ mSystemTextClassifier = new TextClassifierImpl(mContext);
+ }
+ }
+ return mSystemTextClassifier;
+ }
+
+ /**
* Returns the text classifier.
*/
public TextClassifier getTextClassifier() {
- synchronized (mTextClassifierLock) {
+ synchronized (mLock) {
if (mTextClassifier == null) {
- mTextClassifier = new TextClassifierImpl(mContext);
+ if (isSystemTextClassifierEnabled()) {
+ mTextClassifier = getSystemDefaultTextClassifier();
+ } else {
+ mTextClassifier = new TextClassifierImpl(mContext);
+ }
}
return mTextClassifier;
}
@@ -56,8 +91,13 @@ public final class TextClassificationManager {
* Set to {@link TextClassifier#NO_OP} to disable text classifier features.
*/
public void setTextClassifier(@Nullable TextClassifier textClassifier) {
- synchronized (mTextClassifierLock) {
+ synchronized (mLock) {
mTextClassifier = textClassifier;
}
}
+
+ private boolean isSystemTextClassifierEnabled() {
+ return SYSTEM_TEXT_CLASSIFIER_ENABLED
+ && TextClassifierService.getServiceComponentName(mContext) != null;
+ }
}
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index 04ab4474a40c..5dd9ac62fbbd 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -23,9 +23,11 @@ import android.annotation.Nullable;
import android.annotation.StringDef;
import android.annotation.WorkerThread;
import android.os.LocaleList;
+import android.os.Looper;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.ArraySet;
+import android.util.Slog;
import com.android.internal.util.Preconditions;
@@ -39,8 +41,8 @@ import java.util.List;
/**
* Interface for providing text classification related features.
*
- * <p>Unless otherwise stated, methods of this interface are blocking operations.
- * Avoid calling them on the UI thread.
+ * <p><strong>NOTE: </strong>Unless otherwise stated, methods of this interface are blocking
+ * operations. Call on a worker thread.
*/
public interface TextClassifier {
@@ -107,6 +109,8 @@ public interface TextClassifier {
* Returns suggested text selection start and end indices, recognized entity types, and their
* associated confidence scores. The entity types are ordered from highest to lowest scoring.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* @param text text providing context for the selected text (which is specified
* by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
* @param selectionStartIndex start index of the selected part of text
@@ -125,7 +129,7 @@ public interface TextClassifier {
@IntRange(from = 0) int selectionStartIndex,
@IntRange(from = 0) int selectionEndIndex,
@Nullable TextSelection.Options options) {
- Utils.validateInput(text, selectionStartIndex, selectionEndIndex);
+ Utils.validate(text, selectionStartIndex, selectionEndIndex, false);
return new TextSelection.Builder(selectionStartIndex, selectionEndIndex).build();
}
@@ -137,6 +141,8 @@ public interface TextClassifier {
* {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method
* calls this method, a stack overflow error will happen.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* @param text text providing context for the selected text (which is specified
* by the sub sequence starting at selectionStartIndex and ending at selectionEndIndex)
* @param selectionStartIndex start index of the selected part of text
@@ -161,6 +167,8 @@ public interface TextClassifier {
* See {@link #suggestSelection(CharSequence, int, int)} or
* {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #suggestSelection(CharSequence, int, int, TextSelection.Options)}. If that method
* calls this method, a stack overflow error will happen.
@@ -182,6 +190,8 @@ public interface TextClassifier {
* Classifies the specified text and returns a {@link TextClassification} object that can be
* used to generate a widget for handling the classified text.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* @param text text providing context for the text to classify (which is specified
* by the sub sequence starting at startIndex and ending at endIndex)
* @param startIndex start index of the text to classify
@@ -200,7 +210,7 @@ public interface TextClassifier {
@IntRange(from = 0) int startIndex,
@IntRange(from = 0) int endIndex,
@Nullable TextClassification.Options options) {
- Utils.validateInput(text, startIndex, endIndex);
+ Utils.validate(text, startIndex, endIndex, false);
return TextClassification.EMPTY;
}
@@ -208,6 +218,8 @@ public interface TextClassifier {
* Classifies the specified text and returns a {@link TextClassification} object that can be
* used to generate a widget for handling the classified text.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method
* calls this method, a stack overflow error will happen.
@@ -235,6 +247,8 @@ public interface TextClassifier {
* See {@link #classifyText(CharSequence, int, int, TextClassification.Options)} or
* {@link #classifyText(CharSequence, int, int)}.
*
+ * <p><strong>NOTE: </strong>Call on a worker thread.
+ *
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #classifyText(CharSequence, int, int, TextClassification.Options)}. If that method
* calls this method, a stack overflow error will happen.
@@ -253,10 +267,10 @@ public interface TextClassifier {
}
/**
- * Returns a {@link TextLinks} that may be applied to the text to annotate it with links
- * information.
+ * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
+ * links information.
*
- * If no options are supplied, default values will be used, determined by the TextClassifier.
+ * <p><strong>NOTE: </strong>Call on a worker thread.
*
* @param text the text to generate annotations for
* @param options configuration for link generation
@@ -268,13 +282,15 @@ public interface TextClassifier {
@WorkerThread
default TextLinks generateLinks(
@NonNull CharSequence text, @Nullable TextLinks.Options options) {
- Utils.validateInput(text);
+ Utils.validate(text, false);
return new TextLinks.Builder(text.toString()).build();
}
/**
- * Returns a {@link TextLinks} that may be applied to the text to annotate it with links
- * information.
+ * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
+ * links information.
+ *
+ * <p><strong>NOTE: </strong>Call on a worker thread.
*
* <p><b>NOTE:</b> Do not implement. The default implementation of this method calls
* {@link #generateLinks(CharSequence, TextLinks.Options)}. If that method calls this method,
@@ -296,6 +312,7 @@ public interface TextClassifier {
*
* @see #ENTITY_PRESET_ALL
* @see #ENTITY_PRESET_NONE
+ * @see #ENTITY_PRESET_BASE
*/
default Collection<String> getEntitiesForPreset(@EntityPreset int entityPreset) {
return Collections.EMPTY_LIST;
@@ -424,19 +441,28 @@ public interface TextClassifier {
* endIndex is greater than text.length() or is not greater than startIndex;
* options is null
*/
- static void validateInput(
- @NonNull CharSequence text, int startIndex, int endIndex) {
+ public static void validate(
+ @NonNull CharSequence text, int startIndex, int endIndex,
+ boolean allowInMainThread) {
Preconditions.checkArgument(text != null);
Preconditions.checkArgument(startIndex >= 0);
Preconditions.checkArgument(endIndex <= text.length());
Preconditions.checkArgument(endIndex > startIndex);
+ checkMainThread(allowInMainThread);
}
/**
* @throws IllegalArgumentException if text is null or options is null
*/
- static void validateInput(@NonNull CharSequence text) {
+ public static void validate(@NonNull CharSequence text, boolean allowInMainThread) {
Preconditions.checkArgument(text != null);
+ checkMainThread(allowInMainThread);
+ }
+
+ private static void checkMainThread(boolean allowInMainThread) {
+ if (!allowInMainThread && Looper.myLooper() == Looper.getMainLooper()) {
+ Slog.w(DEFAULT_LOG_TAG, "TextClassifier called on main thread");
+ }
}
}
}
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 6a44fb38ee13..8f285bdf42d6 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -66,7 +66,7 @@ import java.util.regex.Pattern;
*
* @hide
*/
-final class TextClassifierImpl implements TextClassifier {
+public final class TextClassifierImpl implements TextClassifier {
private static final String LOG_TAG = DEFAULT_LOG_TAG;
private static final String MODEL_DIR = "/etc/textclassifier/";
@@ -90,30 +90,33 @@ final class TextClassifierImpl implements TextClassifier {
TextClassifier.TYPE_URL));
private final Context mContext;
+ private final TextClassifier mFallback;
private final MetricsLogger mMetricsLogger = new MetricsLogger();
- private final Object mSmartSelectionLock = new Object();
- @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+ private final Object mLock = new Object();
+ @GuardedBy("mLock") // Do not access outside this lock.
private Map<Locale, String> mModelFilePaths;
- @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+ @GuardedBy("mLock") // Do not access outside this lock.
private Locale mLocale;
- @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+ @GuardedBy("mLock") // Do not access outside this lock.
private int mVersion;
- @GuardedBy("mSmartSelectionLock") // Do not access outside this lock.
+ @GuardedBy("mLock") // Do not access outside this lock.
private SmartSelection mSmartSelection;
private TextClassifierConstants mSettings;
- TextClassifierImpl(Context context) {
+ public TextClassifierImpl(Context context) {
mContext = Preconditions.checkNotNull(context);
+ mFallback = TextClassifier.NO_OP;
}
+ /** @inheritDoc */
@Override
public TextSelection suggestSelection(
@NonNull CharSequence text, int selectionStartIndex, int selectionEndIndex,
@Nullable TextSelection.Options options) {
- Utils.validateInput(text, selectionStartIndex, selectionEndIndex);
+ Utils.validate(text, selectionStartIndex, selectionEndIndex, false /* allowInMainThread */);
try {
if (text.length() > 0) {
final LocaleList locales = (options == null) ? null : options.getDefaultLocales();
@@ -159,15 +162,16 @@ final class TextClassifierImpl implements TextClassifier {
t);
}
// Getting here means something went wrong, return a NO_OP result.
- return TextClassifier.NO_OP.suggestSelection(
+ return mFallback.suggestSelection(
text, selectionStartIndex, selectionEndIndex, options);
}
+ /** @inheritDoc */
@Override
public TextClassification classifyText(
@NonNull CharSequence text, int startIndex, int endIndex,
@Nullable TextClassification.Options options) {
- Utils.validateInput(text, startIndex, endIndex);
+ Utils.validate(text, startIndex, endIndex, false /* allowInMainThread */);
try {
if (text.length() > 0) {
final String string = text.toString();
@@ -186,13 +190,14 @@ final class TextClassifierImpl implements TextClassifier {
Log.e(LOG_TAG, "Error getting text classification info.", t);
}
// Getting here means something went wrong, return a NO_OP result.
- return TextClassifier.NO_OP.classifyText(text, startIndex, endIndex, options);
+ return mFallback.classifyText(text, startIndex, endIndex, options);
}
+ /** @inheritDoc */
@Override
public TextLinks generateLinks(
@NonNull CharSequence text, @Nullable TextLinks.Options options) {
- Utils.validateInput(text);
+ Utils.validate(text, false /* allowInMainThread */);
final String textString = text.toString();
final TextLinks.Builder builder = new TextLinks.Builder(textString);
@@ -223,7 +228,7 @@ final class TextClassifierImpl implements TextClassifier {
// Avoid throwing from this method. Log the error.
Log.e(LOG_TAG, "Error getting links info.", t);
}
- return builder.build();
+ return mFallback.generateLinks(text, options);
}
@Override
@@ -240,6 +245,7 @@ final class TextClassifierImpl implements TextClassifier {
}
}
+ /** @hide */
@Override
public void logEvent(String source, String event) {
if (LOG_TAG.equals(source)) {
@@ -247,6 +253,7 @@ final class TextClassifierImpl implements TextClassifier {
}
}
+ /** @hide */
@Override
public TextClassifierConstants getSettings() {
if (mSettings == null) {
@@ -257,7 +264,7 @@ final class TextClassifierImpl implements TextClassifier {
}
private SmartSelection getSmartSelection(LocaleList localeList) throws FileNotFoundException {
- synchronized (mSmartSelectionLock) {
+ synchronized (mLock) {
localeList = localeList == null ? LocaleList.getEmptyLocaleList() : localeList;
final Locale locale = findBestSupportedLocaleLocked(localeList);
if (locale == null) {
@@ -277,7 +284,7 @@ final class TextClassifierImpl implements TextClassifier {
}
private String getSignature(String text, int start, int end) {
- synchronized (mSmartSelectionLock) {
+ synchronized (mLock) {
final String versionInfo = (mLocale != null)
? String.format(Locale.US, "%s_v%d", mLocale.toLanguageTag(), mVersion)
: "";
@@ -286,7 +293,7 @@ final class TextClassifierImpl implements TextClassifier {
}
}
- @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+ @GuardedBy("mLock") // Do not call outside this lock.
private ParcelFileDescriptor getFdLocked(Locale locale) throws FileNotFoundException {
ParcelFileDescriptor updateFd;
int updateVersion = -1;
@@ -353,7 +360,7 @@ final class TextClassifierImpl implements TextClassifier {
}
}
- @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+ @GuardedBy("mLock") // Do not call outside this lock.
private void destroySmartSelectionIfExistsLocked() {
if (mSmartSelection != null) {
mSmartSelection.close();
@@ -361,7 +368,7 @@ final class TextClassifierImpl implements TextClassifier {
}
}
- @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+ @GuardedBy("mLock") // Do not call outside this lock.
@Nullable
private Locale findBestSupportedLocaleLocked(LocaleList localeList) {
// Specified localeList takes priority over the system default, so it is listed first.
@@ -379,7 +386,7 @@ final class TextClassifierImpl implements TextClassifier {
return Locale.lookup(languageRangeList, supportedLocales);
}
- @GuardedBy("mSmartSelectionLock") // Do not call outside this lock.
+ @GuardedBy("mLock") // Do not call outside this lock.
private Map<Locale, String> getFactoryModelFilePathsLocked() {
if (mModelFilePaths == null) {
final Map<Locale, String> modelFilePaths = new HashMap<>();
diff --git a/core/java/android/view/textclassifier/TextLinks.aidl b/core/java/android/view/textclassifier/TextLinks.aidl
new file mode 100644
index 000000000000..1bbb79845528
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextLinks.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 TextLinks;
+parcelable TextLinks.Options; \ No newline at end of file
diff --git a/core/java/android/view/textclassifier/TextSelection.aidl b/core/java/android/view/textclassifier/TextSelection.aidl
new file mode 100644
index 000000000000..dab1aefa3532
--- /dev/null
+++ b/core/java/android/view/textclassifier/TextSelection.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 TextSelection;
+parcelable TextSelection.Options; \ No newline at end of file
diff --git a/core/java/android/view/textclassifier/TextSelection.java b/core/java/android/view/textclassifier/TextSelection.java
index 774d42db67a0..1c93be75a3a1 100644
--- a/core/java/android/view/textclassifier/TextSelection.java
+++ b/core/java/android/view/textclassifier/TextSelection.java
@@ -34,7 +34,7 @@ import java.util.Map;
/**
* Information about where text selection should be.
*/
-public final class TextSelection {
+public final class TextSelection implements Parcelable {
private final int mStartIndex;
private final int mEndIndex;
@@ -112,22 +112,6 @@ public final class TextSelection {
mStartIndex, mEndIndex, mEntityConfidence, mSignature);
}
- /** Helper for parceling via #ParcelableWrapper. */
- private void writeToParcel(Parcel dest, int flags) {
- dest.writeInt(mStartIndex);
- dest.writeInt(mEndIndex);
- mEntityConfidence.writeToParcel(dest, flags);
- dest.writeString(mSignature);
- }
-
- /** Helper for unparceling via #ParcelableWrapper. */
- private TextSelection(Parcel in) {
- mStartIndex = in.readInt();
- mEndIndex = in.readInt();
- mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
- mSignature = in.readString();
- }
-
/**
* Builder used to build {@link TextSelection} objects.
*/
@@ -272,46 +256,36 @@ public final class TextSelection {
}
}
- /**
- * Parcelable wrapper for TextSelection objects.
- * @hide
- */
- public static final class ParcelableWrapper implements Parcelable {
-
- @NonNull private TextSelection mTextSelection;
-
- public ParcelableWrapper(@NonNull TextSelection textSelection) {
- Preconditions.checkNotNull(textSelection);
- mTextSelection = textSelection;
- }
-
- @NonNull
- public TextSelection getTextSelection() {
- return mTextSelection;
- }
-
- @Override
- public int describeContents() {
- return 0;
- }
+ @Override
+ public int describeContents() {
+ return 0;
+ }
- @Override
- public void writeToParcel(Parcel dest, int flags) {
- mTextSelection.writeToParcel(dest, flags);
- }
+ @Override
+ public void writeToParcel(Parcel dest, int flags) {
+ dest.writeInt(mStartIndex);
+ dest.writeInt(mEndIndex);
+ mEntityConfidence.writeToParcel(dest, flags);
+ dest.writeString(mSignature);
+ }
- public static final Parcelable.Creator<ParcelableWrapper> CREATOR =
- new Parcelable.Creator<ParcelableWrapper>() {
- @Override
- public ParcelableWrapper createFromParcel(Parcel in) {
- return new ParcelableWrapper(new TextSelection(in));
- }
+ public static final Parcelable.Creator<TextSelection> CREATOR =
+ new Parcelable.Creator<TextSelection>() {
+ @Override
+ public TextSelection createFromParcel(Parcel in) {
+ return new TextSelection(in);
+ }
- @Override
- public ParcelableWrapper[] newArray(int size) {
- return new ParcelableWrapper[size];
- }
- };
+ @Override
+ public TextSelection[] newArray(int size) {
+ return new TextSelection[size];
+ }
+ };
+ private TextSelection(Parcel in) {
+ mStartIndex = in.readInt();
+ mEndIndex = in.readInt();
+ mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
+ mSignature = in.readString();
}
}
diff --git a/core/java/android/widget/SelectionActionModeHelper.java b/core/java/android/widget/SelectionActionModeHelper.java
index 3bfa520cd942..3f5584e6d3fb 100644
--- a/core/java/android/widget/SelectionActionModeHelper.java
+++ b/core/java/android/widget/SelectionActionModeHelper.java
@@ -65,6 +65,7 @@ public final class SelectionActionModeHelper {
private static final String LOG_TAG = "SelectActionModeHelper";
+ // TODO: Make this a configurable flag.
private static final boolean SMART_SELECT_ANIMATION_ENABLED = true;
private final Editor mEditor;
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 2779cd6846d4..a2581fdd3033 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -2730,6 +2730,14 @@
<permission android:name="android.permission.BIND_AUTOFILL_FIELD_CLASSIFICATION_SERVICE"
android:protectionLevel="signature" />
+ <!-- Must be required by a android.service.textclassifier.TextClassifierService,
+ to ensure that only the system can bind to it.
+ @SystemApi @hide This is not a third-party API (intended for OEMs and system apps).
+ <p>Protection level: signature
+ -->
+ <permission android:name="android.permission.BIND_TEXTCLASSIFIER_SERVICE"
+ android:protectionLevel="signature" />
+
<!-- Must be required by hotword enrollment application,
to ensure that only the system can interact with it.
@hide <p>Not for use by third-party applications.</p> -->
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index 38f890a1e95e..0215e2156e0f 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -3126,6 +3126,15 @@
-->
<string name="config_defaultAutofillService" translatable="false"></string>
+ <!-- The component name, flattened to a string, for the default system textclassifier service.
+ This service must be trusted, as it can be activated without explicit consent of the user.
+ (e.g. com.android.textclassifier/.TextClassifierServiceImpl).
+ If no textclassifier service with the specified name exists on the device (or if this is
+ set to empty string), a default textclassifier will be loaded in the calling app's process.
+ See android.view.textclassifier.TextClassificationManager.
+ -->
+ <string name="config_defaultTextClassifierService" translatable="false"></string>
+
<!-- Whether the device uses the default focus highlight when focus state isn't specified. -->
<bool name="config_useDefaultFocusHighlight">true</bool>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e8ab0be78b36..ab2bad505bd3 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3102,6 +3102,7 @@
<java-symbol type="string" name="notification_channel_usb" />
<java-symbol type="string" name="notification_channel_heavy_weight_app" />
<java-symbol type="string" name="config_defaultAutofillService" />
+ <java-symbol type="string" name="config_defaultTextClassifierService" />
<java-symbol type="string" name="notification_channel_foreground_service" />
<java-symbol type="string" name="foreground_service_app_in_background" />
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
index 8a81743c8154..cf41eb89ba20 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationTest.java
@@ -86,15 +86,11 @@ public class TextClassificationTest {
.setSignature(signature)
.build();
- // Parcel and unparcel using ParcelableWrapper.
- final TextClassification.ParcelableWrapper parcelableReference = new TextClassification
- .ParcelableWrapper(reference);
+ // Parcel and unparcel
final Parcel parcel = Parcel.obtain();
- parcelableReference.writeToParcel(parcel, parcelableReference.describeContents());
+ reference.writeToParcel(parcel, reference.describeContents());
parcel.setDataPosition(0);
- final TextClassification result =
- TextClassification.ParcelableWrapper.CREATOR.createFromParcel(
- parcel).getTextClassification();
+ final TextClassification result = TextClassification.CREATOR.createFromParcel(parcel);
assertEquals(text, result.getText());
assertEquals(signature, result.getSignature());
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java b/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java
index e9202361c84a..a6ea0211fbc0 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextSelectionTest.java
@@ -45,15 +45,11 @@ public class TextSelectionTest {
.setSignature(signature)
.build();
- // Parcel and unparcel using ParcelableWrapper.
- final TextSelection.ParcelableWrapper parcelableReference = new TextSelection
- .ParcelableWrapper(reference);
+ // Parcel and unparcel
final Parcel parcel = Parcel.obtain();
- parcelableReference.writeToParcel(parcel, parcelableReference.describeContents());
+ reference.writeToParcel(parcel, reference.describeContents());
parcel.setDataPosition(0);
- final TextSelection result =
- TextSelection.ParcelableWrapper.CREATOR.createFromParcel(
- parcel).getTextSelection();
+ final TextSelection result = TextSelection.CREATOR.createFromParcel(parcel);
assertEquals(startIndex, result.getSelectionStartIndex());
assertEquals(endIndex, result.getSelectionEndIndex());
diff --git a/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
new file mode 100644
index 000000000000..853c7eb51b84
--- /dev/null
+++ b/services/core/java/com/android/server/textclassifier/TextClassificationManagerService.java
@@ -0,0 +1,395 @@
+/*
+ * 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 com.android.server.textclassifier;
+
+import android.annotation.NonNull;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.os.Binder;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.util.Slog;
+import android.service.textclassifier.ITextClassifierService;
+import android.service.textclassifier.ITextClassificationCallback;
+import android.service.textclassifier.ITextLinksCallback;
+import android.service.textclassifier.ITextSelectionCallback;
+import android.service.textclassifier.TextClassifierService;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextLinks;
+import android.view.textclassifier.TextSelection;
+
+import com.android.internal.annotations.GuardedBy;
+import com.android.internal.util.Preconditions;
+import com.android.server.SystemService;
+
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.Callable;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * A manager for TextClassifier services.
+ * Apps bind to the TextClassificationManagerService for text classification. This service
+ * reroutes calls to it to a {@link TextClassifierService} that it manages.
+ */
+public final class TextClassificationManagerService extends ITextClassifierService.Stub {
+
+ private static final String LOG_TAG = "TextClassificationManagerService";
+
+ // How long after the last interaction with the service we would unbind
+ private static final long TIMEOUT_IDLE_BIND_MILLIS = TimeUnit.MINUTES.toMillis(1);
+
+ public static final class Lifecycle extends SystemService {
+
+ private final TextClassificationManagerService mManagerService;
+
+ public Lifecycle(Context context) {
+ super(context);
+ mManagerService = new TextClassificationManagerService(context);
+ }
+
+ @Override
+ public void onStart() {
+ try {
+ publishBinderService(Context.TEXT_CLASSIFICATION_SERVICE, mManagerService);
+ } catch (Throwable t) {
+ // Starting this service is not critical to the running of this device and should
+ // therefore not crash the device. If it fails, log the error and continue.
+ Slog.e(LOG_TAG, "Could not start the TextClassificationManagerService.", t);
+ }
+ }
+ }
+
+ private final Context mContext;
+ private final Handler mHandler;
+ private final Intent mServiceIntent;
+ private final ServiceConnection mConnection;
+ private final Runnable mUnbind;
+ private final Object mLock;
+ @GuardedBy("mLock")
+ private final Queue<PendingRequest> mPendingRequests;
+
+ @GuardedBy("mLock")
+ private ITextClassifierService mService;
+ @GuardedBy("mLock")
+ private boolean mBinding;
+
+ private TextClassificationManagerService(Context context) {
+ mContext = Preconditions.checkNotNull(context);
+ mHandler = new Handler();
+ mServiceIntent = new Intent(TextClassifierService.SERVICE_INTERFACE)
+ .setComponent(TextClassifierService.getServiceComponentName(mContext));
+ mConnection = new ServiceConnection() {
+ @Override
+ public void onServiceConnected(ComponentName name, IBinder service) {
+ synchronized (mLock) {
+ mService = ITextClassifierService.Stub.asInterface(service);
+ setBindingLocked(false);
+ handlePendingRequestsLocked();
+ }
+ }
+
+ @Override
+ public void onServiceDisconnected(ComponentName name) {
+ cleanupService();
+ }
+
+ @Override
+ public void onBindingDied(ComponentName name) {
+ cleanupService();
+ }
+
+ @Override
+ public void onNullBinding(ComponentName name) {
+ cleanupService();
+ }
+
+ private void cleanupService() {
+ synchronized (mLock) {
+ mService = null;
+ setBindingLocked(false);
+ handlePendingRequestsLocked();
+ }
+ }
+ };
+ mPendingRequests = new LinkedList<>();
+ mUnbind = this::unbind;
+ mLock = new Object();
+ }
+
+ @Override
+ public void onSuggestSelection(
+ CharSequence text, int selectionStartIndex, int selectionEndIndex,
+ TextSelection.Options options, ITextSelectionCallback callback)
+ throws RemoteException {
+ // TODO(b/72481438): All remote calls need to take userId.
+ validateInput(text, selectionStartIndex, selectionEndIndex, callback);
+
+ if (!bind()) {
+ callback.onFailure();
+ return;
+ }
+
+ synchronized (mLock) {
+ if (isBoundLocked()) {
+ mService.onSuggestSelection(
+ text, selectionStartIndex, selectionEndIndex, options, callback);
+ scheduleUnbindLocked();
+ } else {
+ final Callable<Void> request = () -> {
+ onSuggestSelection(
+ text, selectionStartIndex, selectionEndIndex,
+ options, callback);
+ return null;
+ };
+ final Callable<Void> onServiceFailure = () -> {
+ callback.onFailure();
+ return null;
+ };
+ enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
+ }
+ }
+ }
+
+ @Override
+ public void onClassifyText(
+ CharSequence text, int startIndex, int endIndex,
+ TextClassification.Options options, ITextClassificationCallback callback)
+ throws RemoteException {
+ validateInput(text, startIndex, endIndex, callback);
+
+ if (!bind()) {
+ callback.onFailure();
+ return;
+ }
+
+ synchronized (mLock) {
+ if (isBoundLocked()) {
+ mService.onClassifyText(text, startIndex, endIndex, options, callback);
+ scheduleUnbindLocked();
+ } else {
+ final Callable<Void> request = () -> {
+ onClassifyText(text, startIndex, endIndex, options, callback);
+ return null;
+ };
+ final Callable<Void> onServiceFailure = () -> {
+ callback.onFailure();
+ return null;
+ };
+ enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
+ }
+ }
+ }
+
+ @Override
+ public void onGenerateLinks(
+ CharSequence text, TextLinks.Options options, ITextLinksCallback callback)
+ throws RemoteException {
+ validateInput(text, callback);
+
+ if (!bind()) {
+ callback.onFailure();
+ return;
+ }
+
+ synchronized (mLock) {
+ if (isBoundLocked()) {
+ mService.onGenerateLinks(text, options, callback);
+ scheduleUnbindLocked();
+ } else {
+ final Callable<Void> request = () -> {
+ onGenerateLinks(text, options, callback);
+ return null;
+ };
+ final Callable<Void> onServiceFailure = () -> {
+ callback.onFailure();
+ return null;
+ };
+ enqueueRequestLocked(request, onServiceFailure, callback.asBinder());
+ }
+ }
+ }
+
+ /**
+ * @return true if the service is bound or in the process of being bound.
+ * Returns false otherwise.
+ */
+ private boolean bind() {
+ synchronized (mLock) {
+ if (isBoundLocked() || isBindingLocked()) {
+ return true;
+ }
+
+ // TODO: Handle bind timeout.
+ final boolean willBind;
+ final long identity = Binder.clearCallingIdentity();
+ try {
+ Slog.d(LOG_TAG, "Binding to " + mServiceIntent.getComponent());
+ willBind = mContext.bindServiceAsUser(
+ mServiceIntent, mConnection,
+ Context.BIND_AUTO_CREATE | Context.BIND_FOREGROUND_SERVICE,
+ Binder.getCallingUserHandle());
+ setBindingLocked(willBind);
+ } finally {
+ Binder.restoreCallingIdentity(identity);
+ }
+ return willBind;
+ }
+ }
+
+ @GuardedBy("mLock")
+ private boolean isBoundLocked() {
+ return mService != null;
+ }
+
+ @GuardedBy("mLock")
+ private boolean isBindingLocked() {
+ return mBinding;
+ }
+
+ @GuardedBy("mLock")
+ private void setBindingLocked(boolean binding) {
+ mBinding = binding;
+ }
+
+ private void unbind() {
+ synchronized (mLock) {
+ if (!isBoundLocked()) {
+ return;
+ }
+
+ Slog.d(LOG_TAG, "Unbinding from " + mServiceIntent.getComponent());
+ mContext.unbindService(mConnection);
+
+ synchronized (mLock) {
+ mService = null;
+ }
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void scheduleUnbindLocked() {
+ mHandler.removeCallbacks(mUnbind);
+ mHandler.postDelayed(mUnbind, TIMEOUT_IDLE_BIND_MILLIS);
+ }
+
+ @GuardedBy("mLock")
+ private void enqueueRequestLocked(
+ Callable<Void> request, Callable<Void> onServiceFailure, IBinder binder) {
+ mPendingRequests.add(new PendingRequest(request, onServiceFailure, binder));
+ }
+
+ @GuardedBy("mLock")
+ private void handlePendingRequestsLocked() {
+ // TODO(b/72481146): Implement PendingRequest similar to that in RemoteFillService.
+ final PendingRequest[] pendingRequests =
+ mPendingRequests.toArray(new PendingRequest[mPendingRequests.size()]);
+ for (PendingRequest pendingRequest : pendingRequests) {
+ if (isBoundLocked()) {
+ pendingRequest.executeLocked();
+ } else {
+ pendingRequest.notifyServiceFailureLocked();
+ }
+ }
+ }
+
+ private final class PendingRequest implements IBinder.DeathRecipient {
+
+ private final Callable<Void> mRequest;
+ private final Callable<Void> mOnServiceFailure;
+ private final IBinder mBinder;
+
+ /**
+ * Initializes a new pending request.
+ *
+ * @param request action to perform when the service is bound
+ * @param onServiceFailure action to perform when the service dies or disconnects
+ * @param binder binder to the process that made this pending request
+ */
+ PendingRequest(
+ @NonNull Callable<Void> request, @NonNull Callable<Void> onServiceFailure,
+ @NonNull IBinder binder) {
+ mRequest = Preconditions.checkNotNull(request);
+ mOnServiceFailure = Preconditions.checkNotNull(onServiceFailure);
+ mBinder = Preconditions.checkNotNull(binder);
+ try {
+ mBinder.linkToDeath(this, 0);
+ } catch (RemoteException e) {
+ e.printStackTrace();
+ }
+ }
+
+ @GuardedBy("mLock")
+ void executeLocked() {
+ removeLocked();
+ try {
+ mRequest.call();
+ } catch (Exception e) {
+ Slog.d(LOG_TAG, "Error handling pending request: " + e.getMessage());
+ }
+ }
+
+ @GuardedBy("mLock")
+ void notifyServiceFailureLocked() {
+ removeLocked();
+ try {
+ mOnServiceFailure.call();
+ } catch (Exception e) {
+ Slog.d(LOG_TAG, "Error notifying callback of service failure: "
+ + e.getMessage());
+ }
+ }
+
+ @Override
+ public void binderDied() {
+ synchronized (mLock) {
+ // No need to handle this pending request anymore. Remove.
+ removeLocked();
+ }
+ }
+
+ @GuardedBy("mLock")
+ private void removeLocked() {
+ mPendingRequests.remove(this);
+ mBinder.unlinkToDeath(this, 0);
+ }
+ }
+
+ private static void validateInput(
+ CharSequence text, int startIndex, int endIndex, Object callback)
+ throws RemoteException {
+ try {
+ TextClassifier.Utils.validate(text, startIndex, endIndex, true /* allowInMainThread */);
+ Preconditions.checkNotNull(callback);
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new RemoteException(e.getMessage());
+ }
+ }
+
+ private static void validateInput(CharSequence text, Object callback) throws RemoteException {
+ try {
+ TextClassifier.Utils.validate(text, true /* allowInMainThread */);
+ Preconditions.checkNotNull(callback);
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new RemoteException(e.getMessage());
+ }
+ }
+}
diff --git a/services/java/com/android/server/SystemServer.java b/services/java/com/android/server/SystemServer.java
index f95c6f042fed..210fd473ccd4 100644
--- a/services/java/com/android/server/SystemServer.java
+++ b/services/java/com/android/server/SystemServer.java
@@ -111,6 +111,7 @@ import com.android.server.stats.StatsCompanionService;
import com.android.server.statusbar.StatusBarManagerService;
import com.android.server.storage.DeviceStorageMonitorService;
import com.android.server.telecom.TelecomLoaderService;
+import com.android.server.textclassifier.TextClassificationManagerService;
import com.android.server.trust.TrustManagerService;
import com.android.server.tv.TvInputManagerService;
import com.android.server.tv.TvRemoteService;
@@ -733,6 +734,8 @@ public final class SystemServer {
false);
boolean disableTextServices = SystemProperties.getBoolean("config.disable_textservices",
false);
+ boolean disableSystemTextClassifier = SystemProperties.getBoolean(
+ "config.disable_systemtextclassifier", false);
boolean disableConsumerIr = SystemProperties.getBoolean("config.disable_consumerir", false);
boolean disableVrManager = SystemProperties.getBoolean("config.disable_vrmanager", false);
boolean disableCameraService = SystemProperties.getBoolean("config.disable_cameraservice",
@@ -1066,6 +1069,12 @@ public final class SystemServer {
traceEnd();
}
+ if (!disableSystemTextClassifier) {
+ traceBeginAndSlog("StartTextClassificationManagerService");
+ mSystemServiceManager.startService(TextClassificationManagerService.Lifecycle.class);
+ traceEnd();
+ }
+
traceBeginAndSlog("StartNetworkScoreService");
try {
networkScore = new NetworkScoreService(context);