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