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