diff options
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); |