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