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