• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2018 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.service.textclassifier;
18 
19 import android.Manifest;
20 import android.annotation.IntDef;
21 import android.annotation.MainThread;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.annotation.SystemApi;
25 import android.annotation.TestApi;
26 import android.app.Service;
27 import android.content.ComponentName;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.pm.ResolveInfo;
31 import android.content.pm.ServiceInfo;
32 import android.os.Bundle;
33 import android.os.CancellationSignal;
34 import android.os.Handler;
35 import android.os.IBinder;
36 import android.os.Looper;
37 import android.os.Parcelable;
38 import android.os.RemoteException;
39 import android.text.TextUtils;
40 import android.util.Slog;
41 import android.view.textclassifier.ConversationActions;
42 import android.view.textclassifier.SelectionEvent;
43 import android.view.textclassifier.TextClassification;
44 import android.view.textclassifier.TextClassificationContext;
45 import android.view.textclassifier.TextClassificationManager;
46 import android.view.textclassifier.TextClassificationSessionId;
47 import android.view.textclassifier.TextClassifier;
48 import android.view.textclassifier.TextClassifierEvent;
49 import android.view.textclassifier.TextLanguage;
50 import android.view.textclassifier.TextLinks;
51 import android.view.textclassifier.TextSelection;
52 
53 import com.android.internal.util.Preconditions;
54 
55 import java.lang.annotation.Retention;
56 import java.lang.annotation.RetentionPolicy;
57 import java.util.concurrent.ExecutorService;
58 import java.util.concurrent.Executors;
59 
60 /**
61  * Abstract base class for the TextClassifier service.
62  *
63  * <p>A TextClassifier service provides text classification related features for the system.
64  * The system's default TextClassifierService provider is configured in
65  * {@code config_defaultTextClassifierPackage}. If this config has no value, a
66  * {@link android.view.textclassifier.TextClassifierImpl} is loaded in the calling app's process.
67  *
68  * <p>See: {@link TextClassifier}.
69  * See: {@link TextClassificationManager}.
70  *
71  * <p>Include the following in the manifest:
72  *
73  * <pre>
74  * {@literal
75  * <service android:name=".YourTextClassifierService"
76  *          android:permission="android.permission.BIND_TEXTCLASSIFIER_SERVICE">
77  *     <intent-filter>
78  *         <action android:name="android.service.textclassifier.TextClassifierService" />
79  *     </intent-filter>
80  * </service>}</pre>
81  *
82  * <p>From {@link android.os.Build.VERSION_CODES#Q} onward, all callbacks are called on the main
83  * thread. Prior to Q, there is no guarantee on what thread the callback will happen. You should
84  * make sure the callbacks are executed in your desired thread by using a executor, a handler or
85  * something else along the line.
86  *
87  * @see TextClassifier
88  * @hide
89  */
90 @SystemApi
91 @TestApi
92 public abstract class TextClassifierService extends Service {
93 
94     private static final String LOG_TAG = "TextClassifierService";
95 
96     /**
97      * The {@link Intent} that must be declared as handled by the service.
98      * To be supported, the service must also require the
99      * {@link android.Manifest.permission#BIND_TEXTCLASSIFIER_SERVICE} permission so
100      * that other applications can not abuse it.
101      */
102     public static final String SERVICE_INTERFACE =
103             "android.service.textclassifier.TextClassifierService";
104 
105     /** @hide **/
106     public static final int CONNECTED = 0;
107     /** @hide **/
108     public static final int DISCONNECTED = 1;
109     /** @hide */
110     @IntDef(value = {
111             CONNECTED,
112             DISCONNECTED
113     })
114     @Retention(RetentionPolicy.SOURCE)
115     public @interface ConnectionState{}
116 
117     /** @hide **/
118     private static final String KEY_RESULT = "key_result";
119 
120     private final Handler mMainThreadHandler = new Handler(Looper.getMainLooper(), null, true);
121     private final ExecutorService mSingleThreadExecutor = Executors.newSingleThreadExecutor();
122 
123     private final ITextClassifierService.Stub mBinder = new ITextClassifierService.Stub() {
124 
125         // TODO(b/72533911): Implement cancellation signal
126         @NonNull private final CancellationSignal mCancellationSignal = new CancellationSignal();
127 
128         @Override
129         public void onSuggestSelection(
130                 TextClassificationSessionId sessionId,
131                 TextSelection.Request request, ITextClassifierCallback callback) {
132             Preconditions.checkNotNull(request);
133             Preconditions.checkNotNull(callback);
134             mMainThreadHandler.post(() -> TextClassifierService.this.onSuggestSelection(
135                     sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
136 
137         }
138 
139         @Override
140         public void onClassifyText(
141                 TextClassificationSessionId sessionId,
142                 TextClassification.Request request, ITextClassifierCallback callback) {
143             Preconditions.checkNotNull(request);
144             Preconditions.checkNotNull(callback);
145             mMainThreadHandler.post(() -> TextClassifierService.this.onClassifyText(
146                     sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
147         }
148 
149         @Override
150         public void onGenerateLinks(
151                 TextClassificationSessionId sessionId,
152                 TextLinks.Request request, ITextClassifierCallback callback) {
153             Preconditions.checkNotNull(request);
154             Preconditions.checkNotNull(callback);
155             mMainThreadHandler.post(() -> TextClassifierService.this.onGenerateLinks(
156                     sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
157         }
158 
159         @Override
160         public void onSelectionEvent(
161                 TextClassificationSessionId sessionId,
162                 SelectionEvent event) {
163             Preconditions.checkNotNull(event);
164             mMainThreadHandler.post(
165                     () -> TextClassifierService.this.onSelectionEvent(sessionId, event));
166         }
167 
168         @Override
169         public void onTextClassifierEvent(
170                 TextClassificationSessionId sessionId,
171                 TextClassifierEvent event) {
172             Preconditions.checkNotNull(event);
173             mMainThreadHandler.post(
174                     () -> TextClassifierService.this.onTextClassifierEvent(sessionId, event));
175         }
176 
177         @Override
178         public void onDetectLanguage(
179                 TextClassificationSessionId sessionId,
180                 TextLanguage.Request request,
181                 ITextClassifierCallback callback) {
182             Preconditions.checkNotNull(request);
183             Preconditions.checkNotNull(callback);
184             mMainThreadHandler.post(() -> TextClassifierService.this.onDetectLanguage(
185                     sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
186         }
187 
188         @Override
189         public void onSuggestConversationActions(
190                 TextClassificationSessionId sessionId,
191                 ConversationActions.Request request,
192                 ITextClassifierCallback callback) {
193             Preconditions.checkNotNull(request);
194             Preconditions.checkNotNull(callback);
195             mMainThreadHandler.post(() -> TextClassifierService.this.onSuggestConversationActions(
196                     sessionId, request, mCancellationSignal, new ProxyCallback<>(callback)));
197         }
198 
199         @Override
200         public void onCreateTextClassificationSession(
201                 TextClassificationContext context, TextClassificationSessionId sessionId) {
202             Preconditions.checkNotNull(context);
203             Preconditions.checkNotNull(sessionId);
204             mMainThreadHandler.post(
205                     () -> TextClassifierService.this.onCreateTextClassificationSession(
206                             context, sessionId));
207         }
208 
209         @Override
210         public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId) {
211             mMainThreadHandler.post(
212                     () -> TextClassifierService.this.onDestroyTextClassificationSession(sessionId));
213         }
214 
215         @Override
216         public void onConnectedStateChanged(@ConnectionState int connected) {
217             mMainThreadHandler.post(connected == CONNECTED ? TextClassifierService.this::onConnected
218                     : TextClassifierService.this::onDisconnected);
219         }
220     };
221 
222     @Nullable
223     @Override
onBind(@onNull Intent intent)224     public final IBinder onBind(@NonNull Intent intent) {
225         if (SERVICE_INTERFACE.equals(intent.getAction())) {
226             return mBinder;
227         }
228         return null;
229     }
230 
231     @Override
onUnbind(@onNull Intent intent)232     public boolean onUnbind(@NonNull Intent intent) {
233         onDisconnected();
234         return super.onUnbind(intent);
235     }
236 
237     /**
238      * Called when the Android system connects to service.
239      */
onConnected()240     public void onConnected() {
241     }
242 
243     /**
244      * Called when the Android system disconnects from the service.
245      *
246      * <p> At this point this service may no longer be an active {@link TextClassifierService}.
247      */
onDisconnected()248     public void onDisconnected() {
249     }
250 
251     /**
252      * Returns suggested text selection start and end indices, recognized entity types, and their
253      * associated confidence scores. The entity types are ordered from highest to lowest scoring.
254      *
255      * @param sessionId the session id
256      * @param request the text selection request
257      * @param cancellationSignal object to watch for canceling the current operation
258      * @param callback the callback to return the result to
259      */
260     @MainThread
onSuggestSelection( @ullable TextClassificationSessionId sessionId, @NonNull TextSelection.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextSelection> callback)261     public abstract void onSuggestSelection(
262             @Nullable TextClassificationSessionId sessionId,
263             @NonNull TextSelection.Request request,
264             @NonNull CancellationSignal cancellationSignal,
265             @NonNull Callback<TextSelection> callback);
266 
267     /**
268      * Classifies the specified text and returns a {@link TextClassification} object that can be
269      * used to generate a widget for handling the classified text.
270      *
271      * @param sessionId the session id
272      * @param request the text classification request
273      * @param cancellationSignal object to watch for canceling the current operation
274      * @param callback the callback to return the result to
275      */
276     @MainThread
onClassifyText( @ullable TextClassificationSessionId sessionId, @NonNull TextClassification.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextClassification> callback)277     public abstract void onClassifyText(
278             @Nullable TextClassificationSessionId sessionId,
279             @NonNull TextClassification.Request request,
280             @NonNull CancellationSignal cancellationSignal,
281             @NonNull Callback<TextClassification> callback);
282 
283     /**
284      * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
285      * links information.
286      *
287      * @param sessionId the session id
288      * @param request the text classification request
289      * @param cancellationSignal object to watch for canceling the current operation
290      * @param callback the callback to return the result to
291      */
292     @MainThread
onGenerateLinks( @ullable TextClassificationSessionId sessionId, @NonNull TextLinks.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextLinks> callback)293     public abstract void onGenerateLinks(
294             @Nullable TextClassificationSessionId sessionId,
295             @NonNull TextLinks.Request request,
296             @NonNull CancellationSignal cancellationSignal,
297             @NonNull Callback<TextLinks> callback);
298 
299     /**
300      * Detects and returns the language of the give text.
301      *
302      * @param sessionId the session id
303      * @param request the language detection request
304      * @param cancellationSignal object to watch for canceling the current operation
305      * @param callback the callback to return the result to
306      */
307     @MainThread
onDetectLanguage( @ullable TextClassificationSessionId sessionId, @NonNull TextLanguage.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextLanguage> callback)308     public void onDetectLanguage(
309             @Nullable TextClassificationSessionId sessionId,
310             @NonNull TextLanguage.Request request,
311             @NonNull CancellationSignal cancellationSignal,
312             @NonNull Callback<TextLanguage> callback) {
313         mSingleThreadExecutor.submit(() ->
314                 callback.onSuccess(getLocalTextClassifier().detectLanguage(request)));
315     }
316 
317     /**
318      * Suggests and returns a list of actions according to the given conversation.
319      *
320      * @param sessionId the session id
321      * @param request the conversation actions request
322      * @param cancellationSignal object to watch for canceling the current operation
323      * @param callback the callback to return the result to
324      */
325     @MainThread
onSuggestConversationActions( @ullable TextClassificationSessionId sessionId, @NonNull ConversationActions.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<ConversationActions> callback)326     public void onSuggestConversationActions(
327             @Nullable TextClassificationSessionId sessionId,
328             @NonNull ConversationActions.Request request,
329             @NonNull CancellationSignal cancellationSignal,
330             @NonNull Callback<ConversationActions> callback) {
331         mSingleThreadExecutor.submit(() ->
332                 callback.onSuccess(getLocalTextClassifier().suggestConversationActions(request)));
333     }
334 
335     /**
336      * Writes the selection event.
337      * This is called when a selection event occurs. e.g. user changed selection; or smart selection
338      * happened.
339      *
340      * <p>The default implementation ignores the event.
341      *
342      * @param sessionId the session id
343      * @param event the selection event
344      * @deprecated
345      *      Use {@link #onTextClassifierEvent(TextClassificationSessionId, TextClassifierEvent)}
346      *      instead
347      */
348     @Deprecated
349     @MainThread
onSelectionEvent( @ullable TextClassificationSessionId sessionId, @NonNull SelectionEvent event)350     public void onSelectionEvent(
351             @Nullable TextClassificationSessionId sessionId, @NonNull SelectionEvent event) {}
352 
353     /**
354      * Writes the TextClassifier event.
355      * This is called when a TextClassifier event occurs. e.g. user changed selection,
356      * smart selection happened, or a link was clicked.
357      *
358      * <p>The default implementation ignores the event.
359      *
360      * @param sessionId the session id
361      * @param event the TextClassifier event
362      */
363     @MainThread
onTextClassifierEvent( @ullable TextClassificationSessionId sessionId, @NonNull TextClassifierEvent event)364     public void onTextClassifierEvent(
365             @Nullable TextClassificationSessionId sessionId, @NonNull TextClassifierEvent event) {}
366 
367     /**
368      * Creates a new text classification session for the specified context.
369      *
370      * @param context the text classification context
371      * @param sessionId the session's Id
372      */
373     @MainThread
onCreateTextClassificationSession( @onNull TextClassificationContext context, @NonNull TextClassificationSessionId sessionId)374     public void onCreateTextClassificationSession(
375             @NonNull TextClassificationContext context,
376             @NonNull TextClassificationSessionId sessionId) {}
377 
378     /**
379      * Destroys the text classification session identified by the specified sessionId.
380      *
381      * @param sessionId the id of the session to destroy
382      */
383     @MainThread
onDestroyTextClassificationSession( @onNull TextClassificationSessionId sessionId)384     public void onDestroyTextClassificationSession(
385             @NonNull TextClassificationSessionId sessionId) {}
386 
387     /**
388      * Returns a TextClassifier that runs in this service's process.
389      * If the local TextClassifier is disabled, this returns {@link TextClassifier#NO_OP}.
390      *
391      * @deprecated Use {@link #getDefaultTextClassifierImplementation(Context)} instead.
392      */
393     @Deprecated
getLocalTextClassifier()394     public final TextClassifier getLocalTextClassifier() {
395         return TextClassifier.NO_OP;
396     }
397 
398     /**
399      * Returns the platform's default TextClassifier implementation.
400      *
401      * @throws RuntimeException if the TextClassifier from
402      *                          PackageManager#getDefaultTextClassifierPackageName() calls
403      *                          this method.
404      */
405     @NonNull
getDefaultTextClassifierImplementation(@onNull Context context)406     public static TextClassifier getDefaultTextClassifierImplementation(@NonNull Context context) {
407         final String defaultTextClassifierPackageName =
408                 context.getPackageManager().getDefaultTextClassifierPackageName();
409         if (TextUtils.isEmpty(defaultTextClassifierPackageName)) {
410             return TextClassifier.NO_OP;
411         }
412         if (defaultTextClassifierPackageName.equals(context.getPackageName())) {
413             throw new RuntimeException(
414                     "The default text classifier itself should not call the"
415                             + "getDefaultTextClassifierImplementation() method.");
416         }
417         final TextClassificationManager tcm =
418                 context.getSystemService(TextClassificationManager.class);
419         return tcm.getTextClassifier(TextClassifier.DEFAULT_SYSTEM);
420     }
421 
422     /** @hide **/
getResponse(Bundle bundle)423     public static <T extends Parcelable> T getResponse(Bundle bundle) {
424         return bundle.getParcelable(KEY_RESULT);
425     }
426 
427     /** @hide **/
putResponse(Bundle bundle, T response)428     public static <T extends Parcelable> void putResponse(Bundle bundle, T response) {
429         bundle.putParcelable(KEY_RESULT, response);
430     }
431 
432     /**
433      * Callbacks for TextClassifierService results.
434      *
435      * @param <T> the type of the result
436      */
437     public interface Callback<T> {
438         /**
439          * Returns the result.
440          */
onSuccess(T result)441         void onSuccess(T result);
442 
443         /**
444          * Signals a failure.
445          */
onFailure(@onNull CharSequence error)446         void onFailure(@NonNull CharSequence error);
447     }
448 
449     /**
450      * Returns the component name of the textclassifier service from the given package.
451      * Otherwise, returns null.
452      *
453      * @param context
454      * @param packageName  the package to look for.
455      * @param resolveFlags the flags that are used by PackageManager to resolve the component name.
456      * @hide
457      */
458     @Nullable
getServiceComponentName( Context context, String packageName, int resolveFlags)459     public static ComponentName getServiceComponentName(
460             Context context, String packageName, int resolveFlags) {
461         final Intent intent = new Intent(SERVICE_INTERFACE).setPackage(packageName);
462 
463         final ResolveInfo ri = context.getPackageManager().resolveService(intent, resolveFlags);
464 
465         if ((ri == null) || (ri.serviceInfo == null)) {
466             Slog.w(LOG_TAG, String.format("Package or service not found in package %s for user %d",
467                     packageName, context.getUserId()));
468             return null;
469         }
470 
471         final ServiceInfo si = ri.serviceInfo;
472 
473         final String permission = si.permission;
474         if (Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE.equals(permission)) {
475             return si.getComponentName();
476         }
477         Slog.w(LOG_TAG, String.format(
478                 "Service %s should require %s permission. Found %s permission",
479                 si.getComponentName(),
480                 Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE,
481                 si.permission));
482         return null;
483     }
484 
485     /**
486      * Forwards the callback result to a wrapped binder callback.
487      */
488     private static final class ProxyCallback<T extends Parcelable> implements Callback<T> {
489         private ITextClassifierCallback mTextClassifierCallback;
490 
ProxyCallback(ITextClassifierCallback textClassifierCallback)491         private ProxyCallback(ITextClassifierCallback textClassifierCallback) {
492             mTextClassifierCallback = Preconditions.checkNotNull(textClassifierCallback);
493         }
494 
495         @Override
onSuccess(T result)496         public void onSuccess(T result) {
497             try {
498                 Bundle bundle = new Bundle(1);
499                 bundle.putParcelable(KEY_RESULT, result);
500                 mTextClassifierCallback.onSuccess(bundle);
501             } catch (RemoteException e) {
502                 Slog.d(LOG_TAG, "Error calling callback");
503             }
504         }
505 
506         @Override
onFailure(CharSequence error)507         public void onFailure(CharSequence error) {
508             try {
509                 Slog.w(LOG_TAG, "Request fail: " + error);
510                 mTextClassifierCallback.onFailure();
511             } catch (RemoteException e) {
512                 Slog.d(LOG_TAG, "Error calling callback");
513             }
514         }
515     }
516 }
517