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.IntRange;
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.CancellationSignal;
32 import android.os.IBinder;
33 import android.os.RemoteException;
34 import android.text.TextUtils;
35 import android.util.Slog;
36 import android.view.textclassifier.SelectionEvent;
37 import android.view.textclassifier.TextClassification;
38 import android.view.textclassifier.TextClassificationContext;
39 import android.view.textclassifier.TextClassificationManager;
40 import android.view.textclassifier.TextClassificationSessionId;
41 import android.view.textclassifier.TextClassifier;
42 import android.view.textclassifier.TextLinks;
43 import android.view.textclassifier.TextSelection;
44 
45 import com.android.internal.util.Preconditions;
46 
47 /**
48  * Abstract base class for the TextClassifier service.
49  *
50  * <p>A TextClassifier service provides text classification related features for the system.
51  * The system's default TextClassifierService is configured in
52  * {@code config_defaultTextClassifierService}. If this config has no value, a
53  * {@link android.view.textclassifier.TextClassifierImpl} is loaded in the calling app's process.
54  *
55  * <p>See: {@link TextClassifier}.
56  * See: {@link TextClassificationManager}.
57  *
58  * <p>Include the following in the manifest:
59  *
60  * <pre>
61  * {@literal
62  * <service android:name=".YourTextClassifierService"
63  *          android:permission="android.permission.BIND_TEXTCLASSIFIER_SERVICE">
64  *     <intent-filter>
65  *         <action android:name="android.service.textclassifier.TextClassifierService" />
66  *     </intent-filter>
67  * </service>}</pre>
68  *
69  * @see TextClassifier
70  * @hide
71  */
72 @SystemApi
73 public abstract class TextClassifierService extends Service {
74 
75     private static final String LOG_TAG = "TextClassifierService";
76 
77     /**
78      * The {@link Intent} that must be declared as handled by the service.
79      * To be supported, the service must also require the
80      * {@link android.Manifest.permission#BIND_TEXTCLASSIFIER_SERVICE} permission so
81      * that other applications can not abuse it.
82      */
83     @SystemApi
84     public static final String SERVICE_INTERFACE =
85             "android.service.textclassifier.TextClassifierService";
86 
87     private final ITextClassifierService.Stub mBinder = new ITextClassifierService.Stub() {
88 
89         // TODO(b/72533911): Implement cancellation signal
90         @NonNull private final CancellationSignal mCancellationSignal = new CancellationSignal();
91 
92         /** {@inheritDoc} */
93         @Override
94         public void onSuggestSelection(
95                 TextClassificationSessionId sessionId,
96                 TextSelection.Request request, ITextSelectionCallback callback)
97                 throws RemoteException {
98             Preconditions.checkNotNull(request);
99             Preconditions.checkNotNull(callback);
100             TextClassifierService.this.onSuggestSelection(
101                     request.getText(), request.getStartIndex(), request.getEndIndex(),
102                     TextSelection.Options.from(sessionId, request), mCancellationSignal,
103                     new Callback<TextSelection>() {
104                         @Override
105                         public void onSuccess(TextSelection result) {
106                             try {
107                                 callback.onSuccess(result);
108                             } catch (RemoteException e) {
109                                 Slog.d(LOG_TAG, "Error calling callback");
110                             }
111                         }
112 
113                         @Override
114                         public void onFailure(CharSequence error) {
115                             try {
116                                 if (callback.asBinder().isBinderAlive()) {
117                                     callback.onFailure();
118                                 }
119                             } catch (RemoteException e) {
120                                 Slog.d(LOG_TAG, "Error calling callback");
121                             }
122                         }
123                     });
124         }
125 
126         /** {@inheritDoc} */
127         @Override
128         public void onClassifyText(
129                 TextClassificationSessionId sessionId,
130                 TextClassification.Request request, ITextClassificationCallback callback)
131                 throws RemoteException {
132             Preconditions.checkNotNull(request);
133             Preconditions.checkNotNull(callback);
134             TextClassifierService.this.onClassifyText(
135                     request.getText(), request.getStartIndex(), request.getEndIndex(),
136                     TextClassification.Options.from(sessionId, request), mCancellationSignal,
137                     new Callback<TextClassification>() {
138                         @Override
139                         public void onSuccess(TextClassification result) {
140                             try {
141                                 callback.onSuccess(result);
142                             } catch (RemoteException e) {
143                                 Slog.d(LOG_TAG, "Error calling callback");
144                             }
145                         }
146 
147                         @Override
148                         public void onFailure(CharSequence error) {
149                             try {
150                                 callback.onFailure();
151                             } catch (RemoteException e) {
152                                 Slog.d(LOG_TAG, "Error calling callback");
153                             }
154                         }
155                     });
156         }
157 
158         /** {@inheritDoc} */
159         @Override
160         public void onGenerateLinks(
161                 TextClassificationSessionId sessionId,
162                 TextLinks.Request request, ITextLinksCallback callback)
163                 throws RemoteException {
164             Preconditions.checkNotNull(request);
165             Preconditions.checkNotNull(callback);
166             TextClassifierService.this.onGenerateLinks(
167                     request.getText(), TextLinks.Options.from(sessionId, request),
168                     mCancellationSignal,
169                     new Callback<TextLinks>() {
170                         @Override
171                         public void onSuccess(TextLinks result) {
172                             try {
173                                 callback.onSuccess(result);
174                             } catch (RemoteException e) {
175                                 Slog.d(LOG_TAG, "Error calling callback");
176                             }
177                         }
178 
179                         @Override
180                         public void onFailure(CharSequence error) {
181                             try {
182                                 callback.onFailure();
183                             } catch (RemoteException e) {
184                                 Slog.d(LOG_TAG, "Error calling callback");
185                             }
186                         }
187                     });
188         }
189 
190         /** {@inheritDoc} */
191         @Override
192         public void onSelectionEvent(
193                 TextClassificationSessionId sessionId,
194                 SelectionEvent event) throws RemoteException {
195             Preconditions.checkNotNull(event);
196             TextClassifierService.this.onSelectionEvent(sessionId, event);
197         }
198 
199         /** {@inheritDoc} */
200         @Override
201         public void onCreateTextClassificationSession(
202                 TextClassificationContext context, TextClassificationSessionId sessionId)
203                 throws RemoteException {
204             Preconditions.checkNotNull(context);
205             Preconditions.checkNotNull(sessionId);
206             TextClassifierService.this.onCreateTextClassificationSession(context, sessionId);
207         }
208 
209         /** {@inheritDoc} */
210         @Override
211         public void onDestroyTextClassificationSession(TextClassificationSessionId sessionId)
212                 throws RemoteException {
213             TextClassifierService.this.onDestroyTextClassificationSession(sessionId);
214         }
215     };
216 
217     @Nullable
218     @Override
onBind(Intent intent)219     public final IBinder onBind(Intent intent) {
220         if (SERVICE_INTERFACE.equals(intent.getAction())) {
221             return mBinder;
222         }
223         return null;
224     }
225 
226     /**
227      * Returns suggested text selection start and end indices, recognized entity types, and their
228      * associated confidence scores. The entity types are ordered from highest to lowest scoring.
229      *
230      * @param sessionId the session id
231      * @param request the text selection request
232      * @param cancellationSignal object to watch for canceling the current operation
233      * @param callback the callback to return the result to
234      */
onSuggestSelection( @ullable TextClassificationSessionId sessionId, @NonNull TextSelection.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextSelection> callback)235     public abstract void onSuggestSelection(
236             @Nullable TextClassificationSessionId sessionId,
237             @NonNull TextSelection.Request request,
238             @NonNull CancellationSignal cancellationSignal,
239             @NonNull Callback<TextSelection> callback);
240 
241     // TODO: Remove once apps can build against the latest sdk.
242     /** @hide */
onSuggestSelection( @onNull CharSequence text, @IntRange(from = 0) int selectionStartIndex, @IntRange(from = 0) int selectionEndIndex, @Nullable TextSelection.Options options, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextSelection> callback)243     public void onSuggestSelection(
244             @NonNull CharSequence text,
245             @IntRange(from = 0) int selectionStartIndex,
246             @IntRange(from = 0) int selectionEndIndex,
247             @Nullable TextSelection.Options options,
248             @NonNull CancellationSignal cancellationSignal,
249             @NonNull Callback<TextSelection> callback) {
250         final TextClassificationSessionId sessionId = options.getSessionId();
251         final TextSelection.Request request = options.getRequest() != null
252                 ? options.getRequest()
253                 : new TextSelection.Request.Builder(
254                         text, selectionStartIndex, selectionEndIndex)
255                         .setDefaultLocales(options.getDefaultLocales())
256                         .build();
257         onSuggestSelection(sessionId, request, cancellationSignal, callback);
258     }
259 
260     /**
261      * Classifies the specified text and returns a {@link TextClassification} object that can be
262      * used to generate a widget for handling the classified text.
263      *
264      * @param sessionId the session id
265      * @param request the text classification request
266      * @param cancellationSignal object to watch for canceling the current operation
267      * @param callback the callback to return the result to
268      */
onClassifyText( @ullable TextClassificationSessionId sessionId, @NonNull TextClassification.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextClassification> callback)269     public abstract void onClassifyText(
270             @Nullable TextClassificationSessionId sessionId,
271             @NonNull TextClassification.Request request,
272             @NonNull CancellationSignal cancellationSignal,
273             @NonNull Callback<TextClassification> callback);
274 
275     // TODO: Remove once apps can build against the latest sdk.
276     /** @hide */
onClassifyText( @onNull CharSequence text, @IntRange(from = 0) int startIndex, @IntRange(from = 0) int endIndex, @Nullable TextClassification.Options options, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextClassification> callback)277     public void onClassifyText(
278             @NonNull CharSequence text,
279             @IntRange(from = 0) int startIndex,
280             @IntRange(from = 0) int endIndex,
281             @Nullable TextClassification.Options options,
282             @NonNull CancellationSignal cancellationSignal,
283             @NonNull Callback<TextClassification> callback) {
284         final TextClassificationSessionId sessionId = options.getSessionId();
285         final TextClassification.Request request = options.getRequest() != null
286                 ? options.getRequest()
287                 : new TextClassification.Request.Builder(
288                         text, startIndex, endIndex)
289                         .setDefaultLocales(options.getDefaultLocales())
290                         .setReferenceTime(options.getReferenceTime())
291                         .build();
292         onClassifyText(sessionId, request, cancellationSignal, callback);
293     }
294 
295     /**
296      * Generates and returns a {@link TextLinks} that may be applied to the text to annotate it with
297      * links information.
298      *
299      * @param sessionId the session id
300      * @param request the text classification request
301      * @param cancellationSignal object to watch for canceling the current operation
302      * @param callback the callback to return the result to
303      */
onGenerateLinks( @ullable TextClassificationSessionId sessionId, @NonNull TextLinks.Request request, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextLinks> callback)304     public abstract void onGenerateLinks(
305             @Nullable TextClassificationSessionId sessionId,
306             @NonNull TextLinks.Request request,
307             @NonNull CancellationSignal cancellationSignal,
308             @NonNull Callback<TextLinks> callback);
309 
310     // TODO: Remove once apps can build against the latest sdk.
311     /** @hide */
onGenerateLinks( @onNull CharSequence text, @Nullable TextLinks.Options options, @NonNull CancellationSignal cancellationSignal, @NonNull Callback<TextLinks> callback)312     public void onGenerateLinks(
313             @NonNull CharSequence text,
314             @Nullable TextLinks.Options options,
315             @NonNull CancellationSignal cancellationSignal,
316             @NonNull Callback<TextLinks> callback) {
317         final TextClassificationSessionId sessionId = options.getSessionId();
318         final TextLinks.Request request = options.getRequest() != null
319                 ? options.getRequest()
320                 : new TextLinks.Request.Builder(text)
321                         .setDefaultLocales(options.getDefaultLocales())
322                         .setEntityConfig(options.getEntityConfig())
323                         .build();
324         onGenerateLinks(sessionId, request, cancellationSignal, callback);
325     }
326 
327     /**
328      * Writes the selection event.
329      * This is called when a selection event occurs. e.g. user changed selection; or smart selection
330      * happened.
331      *
332      * <p>The default implementation ignores the event.
333      *
334      * @param sessionId the session id
335      * @param event the selection event
336      */
onSelectionEvent( @ullable TextClassificationSessionId sessionId, @NonNull SelectionEvent event)337     public void onSelectionEvent(
338             @Nullable TextClassificationSessionId sessionId, @NonNull SelectionEvent event) {}
339 
340     /**
341      * Creates a new text classification session for the specified context.
342      *
343      * @param context the text classification context
344      * @param sessionId the session's Id
345      */
onCreateTextClassificationSession( @onNull TextClassificationContext context, @NonNull TextClassificationSessionId sessionId)346     public void onCreateTextClassificationSession(
347             @NonNull TextClassificationContext context,
348             @NonNull TextClassificationSessionId sessionId) {}
349 
350     /**
351      * Destroys the text classification session identified by the specified sessionId.
352      *
353      * @param sessionId the id of the session to destroy
354      */
onDestroyTextClassificationSession( @onNull TextClassificationSessionId sessionId)355     public void onDestroyTextClassificationSession(
356             @NonNull TextClassificationSessionId sessionId) {}
357 
358     /**
359      * Returns a TextClassifier that runs in this service's process.
360      * If the local TextClassifier is disabled, this returns {@link TextClassifier#NO_OP}.
361      */
getLocalTextClassifier()362     public final TextClassifier getLocalTextClassifier() {
363         final TextClassificationManager tcm = getSystemService(TextClassificationManager.class);
364         if (tcm != null) {
365             return tcm.getTextClassifier(TextClassifier.LOCAL);
366         }
367         return TextClassifier.NO_OP;
368     }
369 
370     /**
371      * Callbacks for TextClassifierService results.
372      *
373      * @param <T> the type of the result
374      * @hide
375      */
376     @SystemApi
377     public interface Callback<T> {
378         /**
379          * Returns the result.
380          */
onSuccess(T result)381         void onSuccess(T result);
382 
383         /**
384          * Signals a failure.
385          */
onFailure(CharSequence error)386         void onFailure(CharSequence error);
387     }
388 
389     /**
390      * Returns the component name of the system default textclassifier service if it can be found
391      * on the system. Otherwise, returns null.
392      * @hide
393      */
394     @Nullable
getServiceComponentName(Context context)395     public static ComponentName getServiceComponentName(Context context) {
396         final String packageName = context.getPackageManager().getSystemTextClassifierPackageName();
397         if (TextUtils.isEmpty(packageName)) {
398             Slog.d(LOG_TAG, "No configured system TextClassifierService");
399             return null;
400         }
401 
402         final Intent intent = new Intent(SERVICE_INTERFACE).setPackage(packageName);
403 
404         final ResolveInfo ri = context.getPackageManager().resolveService(intent,
405                 PackageManager.MATCH_SYSTEM_ONLY);
406 
407         if ((ri == null) || (ri.serviceInfo == null)) {
408             Slog.w(LOG_TAG, String.format("Package or service not found in package %s",
409                     packageName));
410             return null;
411         }
412         final ServiceInfo si = ri.serviceInfo;
413 
414         final String permission = si.permission;
415         if (Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE.equals(permission)) {
416             return si.getComponentName();
417         }
418         Slog.w(LOG_TAG, String.format(
419                 "Service %s should require %s permission. Found %s permission",
420                 si.getComponentName(),
421                 Manifest.permission.BIND_TEXTCLASSIFIER_SERVICE,
422                 si.permission));
423         return null;
424     }
425 }
426