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