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