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