/* * 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.IntDef; import android.annotation.MainThread; 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.ResolveInfo; import android.content.pm.ServiceInfo; import android.os.Bundle; import android.os.CancellationSignal; import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.Parcelable; import android.os.RemoteException; import android.text.TextUtils; import android.util.Slog; import android.view.textclassifier.ConversationActions; import android.view.textclassifier.SelectionEvent; import android.view.textclassifier.TextClassification; import android.view.textclassifier.TextClassificationContext; import android.view.textclassifier.TextClassificationManager; import android.view.textclassifier.TextClassificationSessionId; import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextClassifierEvent; import android.view.textclassifier.TextLanguage; import android.view.textclassifier.TextLinks; import android.view.textclassifier.TextSelection; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Objects; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * Abstract base class for the TextClassifier service. * *
A TextClassifier service provides text classification related features for the system. * The system's default TextClassifierService provider is configured in * {@code config_defaultTextClassifierPackage}. If this config has no value, a * {@link android.view.textclassifier.TextClassifierImpl} is loaded in the calling app's process. * *
See: {@link TextClassifier}. * See: {@link TextClassificationManager}. * *
Include the following in the manifest: * *
* {@literal ** ** }* **
From {@link android.os.Build.VERSION_CODES#Q} onward, all callbacks are called on the main * thread. Prior to Q, there is no guarantee on what thread the callback will happen. You should * make sure the callbacks are executed in your desired thread by using a executor, a handler or * something else along the line. * * @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. */ public static final String SERVICE_INTERFACE = "android.service.textclassifier.TextClassifierService"; /** @hide **/ public static final int CONNECTED = 0; /** @hide **/ public static final int DISCONNECTED = 1; /** @hide */ @IntDef(value = { CONNECTED, DISCONNECTED }) @Retention(RetentionPolicy.SOURCE) public @interface ConnectionState{} /** @hide **/ private static final String KEY_RESULT = "key_result"; private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper(), null, true); private final ExecutorService mSingleThreadExecutor = Executors.newSingleThreadExecutor(); private final ITextClassifierService.Stub mBinder = new ITextClassifierService.Stub() { // TODO(b/72533911): Implement cancellation signal @NonNull private final CancellationSignal mCancellationSignal = new CancellationSignal(); @Override public void onSuggestSelection( TextClassificationSessionId sessionId, TextSelection.Request request, ITextClassifierCallback callback) { Objects.requireNonNull(request); Objects.requireNonNull(callback); mMainThreadHandler.post(() -> TextClassifierService.this.onSuggestSelection( sessionId, request, mCancellationSignal, new ProxyCallback<>(callback))); } @Override public void onClassifyText( TextClassificationSessionId sessionId, TextClassification.Request request, ITextClassifierCallback callback) { Objects.requireNonNull(request); Objects.requireNonNull(callback); mMainThreadHandler.post(() -> TextClassifierService.this.onClassifyText( sessionId, request, mCancellationSignal, new ProxyCallback<>(callback))); } @Override public void onGenerateLinks( TextClassificationSessionId sessionId, TextLinks.Request request, ITextClassifierCallback callback) { Objects.requireNonNull(request); Objects.requireNonNull(callback); mMainThreadHandler.post(() -> TextClassifierService.this.onGenerateLinks( sessionId, request, mCancellationSignal, new ProxyCallback<>(callback))); } @Override public void onSelectionEvent( TextClassificationSessionId sessionId, SelectionEvent event) { Objects.requireNonNull(event); mMainThreadHandler.post( () -> TextClassifierService.this.onSelectionEvent(sessionId, event)); } @Override public void onTextClassifierEvent( TextClassificationSessionId sessionId, TextClassifierEvent event) { Objects.requireNonNull(event); mMainThreadHandler.post( () -> TextClassifierService.this.onTextClassifierEvent(sessionId, event)); } @Override public void onDetectLanguage( TextClassificationSessionId sessionId, TextLanguage.Request request, ITextClassifierCallback callback) { Objects.requireNonNull(request); Objects.requireNonNull(callback); mMainThreadHandler.post(() -> TextClassifierService.this.onDetectLanguage( sessionId, request, mCancellationSignal, new ProxyCallback<>(callback))); } @Override public void onSuggestConversationActions( TextClassificationSessionId sessionId, ConversationActions.Request request, ITextClassifierCallback callback) { Objects.requireNonNull(request); Objects.requireNonNull(callback); mMainThreadHandler.post(() -> TextClassifierService.this.onSuggestConversationActions( sessionId, request, mCancellationSignal, new ProxyCallback<>(callback))); } @Override public void onCreateTextClassificationSession( TextClassificationContext context, TextClassificationSessionId sessionId) { Objects.requireNonNull(context); Objects.requireNonNull(sessionId); mMainThreadHandler.post( () -> TextClassifierService.this.onCreateTextClassificationSession( context, sessionId)); } @Override public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) { mMainThreadHandler.post( () -> TextClassifierService.this.onDestroyTextClassificationSession(sessionId)); } @Override public void onConnectedStateChanged(@ConnectionState int connected) { mMainThreadHandler.post(connected == CONNECTED ? TextClassifierService.this::onConnected : TextClassifierService.this::onDisconnected); } }; @Nullable @Override public final IBinder onBind(@NonNull Intent intent) { if (SERVICE_INTERFACE.equals(intent.getAction())) { return mBinder; } return null; } @Override public boolean onUnbind(@NonNull Intent intent) { onDisconnected(); return super.onUnbind(intent); } /** * Called when the Android system connects to service. */ public void onConnected() { } /** * Called when the Android system disconnects from the service. * *
At this point this service may no longer be an active {@link TextClassifierService}.
*/
public void onDisconnected() {
}
/**
* 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 sessionId the session id
* @param request the text selection request
* @param cancellationSignal object to watch for canceling the current operation
* @param callback the callback to return the result to
*/
@MainThread
public abstract void onSuggestSelection(
@Nullable TextClassificationSessionId sessionId,
@NonNull TextSelection.Request request,
@NonNull CancellationSignal cancellationSignal,
@NonNull Callback The default implementation ignores the event.
*
* @param sessionId the session id
* @param event the selection event
* @deprecated
* Use {@link #onTextClassifierEvent(TextClassificationSessionId, TextClassifierEvent)}
* instead
*/
@Deprecated
@MainThread
public void onSelectionEvent(
@Nullable TextClassificationSessionId sessionId, @NonNull SelectionEvent event) {}
/**
* Writes the TextClassifier event.
* This is called when a TextClassifier event occurs. e.g. user changed selection,
* smart selection happened, or a link was clicked.
*
* The default implementation ignores the event.
*
* @param sessionId the session id
* @param event the TextClassifier event
*/
@MainThread
public void onTextClassifierEvent(
@Nullable TextClassificationSessionId sessionId, @NonNull TextClassifierEvent event) {}
/**
* Creates a new text classification session for the specified context.
*
* @param context the text classification context
* @param sessionId the session's Id
*/
@MainThread
public void onCreateTextClassificationSession(
@NonNull TextClassificationContext context,
@NonNull TextClassificationSessionId sessionId) {}
/**
* Destroys the text classification session identified by the specified sessionId.
*
* @param sessionId the id of the session to destroy
*/
@MainThread
public void onDestroyTextClassificationSession(
@NonNull TextClassificationSessionId sessionId) {}
/**
* Returns a TextClassifier that runs in this service's process.
* If the local TextClassifier is disabled, this returns {@link TextClassifier#NO_OP}.
*
* @deprecated Use {@link #getDefaultTextClassifierImplementation(Context)} instead.
*/
@Deprecated
public final TextClassifier getLocalTextClassifier() {
return TextClassifier.NO_OP;
}
/**
* Returns the platform's default TextClassifier implementation.
*
* @throws RuntimeException if the TextClassifier from
* PackageManager#getDefaultTextClassifierPackageName() calls
* this method.
*/
@NonNull
public static TextClassifier getDefaultTextClassifierImplementation(@NonNull Context context) {
final String defaultTextClassifierPackageName =
context.getPackageManager().getDefaultTextClassifierPackageName();
if (TextUtils.isEmpty(defaultTextClassifierPackageName)) {
return TextClassifier.NO_OP;
}
if (defaultTextClassifierPackageName.equals(context.getPackageName())) {
throw new RuntimeException(
"The default text classifier itself should not call the"
+ "getDefaultTextClassifierImplementation() method.");
}
final TextClassificationManager tcm =
context.getSystemService(TextClassificationManager.class);
return tcm.getTextClassifier(TextClassifier.DEFAULT_SYSTEM);
}
/** @hide **/
public static