/* * 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 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 sessionId the session id * @param request the text classification request * @param cancellationSignal object to watch for canceling the current operation * @param callback the callback to return the result to */ @MainThread public abstract void onClassifyText( @Nullable TextClassificationSessionId sessionId, @NonNull TextClassification.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback callback); /** * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with * links information. * * @param sessionId the session id * @param request the text classification request * @param cancellationSignal object to watch for canceling the current operation * @param callback the callback to return the result to */ @MainThread public abstract void onGenerateLinks( @Nullable TextClassificationSessionId sessionId, @NonNull TextLinks.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback callback); /** * Detects and returns the language of the give text. * * @param sessionId the session id * @param request the language detection request * @param cancellationSignal object to watch for canceling the current operation * @param callback the callback to return the result to */ @MainThread public void onDetectLanguage( @Nullable TextClassificationSessionId sessionId, @NonNull TextLanguage.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback callback) { mSingleThreadExecutor.submit(() -> callback.onSuccess(getLocalTextClassifier().detectLanguage(request))); } /** * Suggests and returns a list of actions according to the given conversation. * * @param sessionId the session id * @param request the conversation actions request * @param cancellationSignal object to watch for canceling the current operation * @param callback the callback to return the result to */ @MainThread public void onSuggestConversationActions( @Nullable TextClassificationSessionId sessionId, @NonNull ConversationActions.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback callback) { mSingleThreadExecutor.submit(() -> callback.onSuccess(getLocalTextClassifier().suggestConversationActions(request))); } /** * Writes the selection event. * This is called when a selection event occurs. e.g. user changed selection; or smart selection * happened. * *

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 T getResponse(Bundle bundle) { return bundle.getParcelable(KEY_RESULT); } /** @hide **/ public static void putResponse(Bundle bundle, T response) { bundle.putParcelable(KEY_RESULT, response); } /** * Callbacks for TextClassifierService results. * * @param the type of the result */ public interface Callback { /** * Returns the result. */ void onSuccess(T result); /** * Signals a failure. */ void onFailure(@NonNull CharSequence error); } /** * Returns the component name of the textclassifier service from the given package. * Otherwise, returns null. * * @param context * @param packageName the package to look for. * @param resolveFlags the flags that are used by PackageManager to resolve the component name. * @hide */ @Nullable public static ComponentName getServiceComponentName( Context context, String packageName, int resolveFlags) { final Intent intent = new Intent(SERVICE_INTERFACE).setPackage(packageName); final ResolveInfo ri = context.getPackageManager().resolveService(intent, resolveFlags); if ((ri == null) || (ri.serviceInfo == null)) { Slog.w(LOG_TAG, String.format("Package or service not found in package %s for user %d", packageName, context.getUserId())); return null; } final ServiceInfo si = ri.serviceInfo; final String permission = si.permission; if (Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE.equals(permission)) { return si.getComponentName(); } Slog.w(LOG_TAG, String.format( "Service %s should require %s permission. Found %s permission", si.getComponentName(), Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE, si.permission)); return null; } /** * Forwards the callback result to a wrapped binder callback. */ private static final class ProxyCallback implements Callback { private ITextClassifierCallback mTextClassifierCallback; private ProxyCallback(ITextClassifierCallback textClassifierCallback) { mTextClassifierCallback = Objects.requireNonNull(textClassifierCallback); } @Override public void onSuccess(T result) { try { Bundle bundle = new Bundle(1); bundle.putParcelable(KEY_RESULT, result); mTextClassifierCallback.onSuccess(bundle); } catch (RemoteException e) { Slog.d(LOG_TAG, "Error calling callback"); } } @Override public void onFailure(CharSequence error) { try { Slog.w(LOG_TAG, "Request fail: " + error); mTextClassifierCallback.onFailure(); } catch (RemoteException e) { Slog.d(LOG_TAG, "Error calling callback"); } } } }