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