Home | History | Annotate | Download | only in textclassifier
      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
    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      */
    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 */
    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      */
    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 */
    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      */
    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 */
    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      */
    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      */
    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      */
    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      */
    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          */
    381         void onSuccess(T result);
    382 
    383         /**
    384          * Signals a failure.
    385          */
    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
    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