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