1 /* 2 * Copyright (C) 2020 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.view.translation; 18 19 import android.annotation.CallbackExecutor; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SystemService; 23 import android.annotation.WorkerThread; 24 import android.app.PendingIntent; 25 import android.content.Context; 26 import android.os.Binder; 27 import android.os.Bundle; 28 import android.os.Handler; 29 import android.os.IRemoteCallback; 30 import android.os.Looper; 31 import android.os.Parcelable; 32 import android.os.RemoteException; 33 import android.os.SynchronousResultReceiver; 34 import android.util.ArrayMap; 35 import android.util.ArraySet; 36 import android.util.Log; 37 import android.util.Pair; 38 import android.util.SparseArray; 39 40 import com.android.internal.annotations.GuardedBy; 41 import com.android.internal.util.SyncResultReceiver; 42 43 import java.util.ArrayList; 44 import java.util.Collections; 45 import java.util.Map; 46 import java.util.Objects; 47 import java.util.Random; 48 import java.util.Set; 49 import java.util.concurrent.Executor; 50 import java.util.concurrent.TimeoutException; 51 import java.util.concurrent.atomic.AtomicInteger; 52 import java.util.function.Consumer; 53 54 /** 55 * The {@link TranslationManager} class provides ways for apps to integrate and use the 56 * translation framework. 57 * 58 * <p>The TranslationManager manages {@link Translator}s and help bridge client calls to 59 * the server {@link android.service.translation.TranslationService} </p> 60 */ 61 @SystemService(Context.TRANSLATION_MANAGER_SERVICE) 62 public final class TranslationManager { 63 64 private static final String TAG = "TranslationManager"; 65 66 /** 67 * Timeout for calls to system_server, default 1 minute. 68 */ 69 static final int SYNC_CALLS_TIMEOUT_MS = 60_000; 70 /** 71 * The result code from result receiver success. 72 * @hide 73 */ 74 public static final int STATUS_SYNC_CALL_SUCCESS = 1; 75 /** 76 * The result code from result receiver fail. 77 * @hide 78 */ 79 public static final int STATUS_SYNC_CALL_FAIL = 2; 80 81 /** 82 * Name of the extra used to pass the translation capabilities. 83 * @hide 84 */ 85 public static final String EXTRA_CAPABILITIES = "translation_capabilities"; 86 87 @GuardedBy("mLock") 88 private final ArrayMap<Pair<Integer, Integer>, ArrayList<PendingIntent>> 89 mTranslationCapabilityUpdateListeners = new ArrayMap<>(); 90 91 @GuardedBy("mLock") 92 private final Map<Consumer<TranslationCapability>, IRemoteCallback> mCapabilityCallbacks = 93 new ArrayMap<>(); 94 95 private static final Random ID_GENERATOR = new Random(); 96 private final Object mLock = new Object(); 97 98 @NonNull 99 private final Context mContext; 100 101 private final ITranslationManager mService; 102 103 @NonNull 104 @GuardedBy("mLock") 105 private final SparseArray<Translator> mTranslators = new SparseArray<>(); 106 107 @NonNull 108 @GuardedBy("mLock") 109 private final ArrayMap<TranslationContext, Integer> mTranslatorIds = 110 new ArrayMap<>(); 111 112 @NonNull 113 private final Handler mHandler; 114 115 private static final AtomicInteger sAvailableRequestId = new AtomicInteger(1); 116 117 /** 118 * @hide 119 */ TranslationManager(@onNull Context context, ITranslationManager service)120 public TranslationManager(@NonNull Context context, ITranslationManager service) { 121 mContext = Objects.requireNonNull(context, "context cannot be null"); 122 mService = service; 123 124 mHandler = Handler.createAsync(Looper.getMainLooper()); 125 } 126 127 /** 128 * Creates an on-device Translator for natural language translation. 129 * 130 * @param translationContext {@link TranslationContext} containing the specs for creating the 131 * Translator. 132 * @param executor Executor to run callback operations 133 * @param callback {@link Consumer} to receive the translator. A {@code null} value is returned 134 * if the service could not create the translator. 135 */ createOnDeviceTranslator(@onNull TranslationContext translationContext, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Translator> callback)136 public void createOnDeviceTranslator(@NonNull TranslationContext translationContext, 137 @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Translator> callback) { 138 Objects.requireNonNull(translationContext, "translationContext cannot be null"); 139 Objects.requireNonNull(executor, "executor cannot be null"); 140 Objects.requireNonNull(callback, "callback cannot be null"); 141 142 synchronized (mLock) { 143 // TODO(b/176464808): Disallow multiple Translator now, it will throw 144 // IllegalStateException. Need to discuss if we can allow multiple Translators. 145 if (mTranslatorIds.containsKey(translationContext)) { 146 executor.execute(() -> callback.accept( 147 mTranslators.get(mTranslatorIds.get(translationContext)))); 148 return; 149 } 150 151 int translatorId; 152 do { 153 translatorId = Math.abs(ID_GENERATOR.nextInt()); 154 } while (translatorId == 0 || mTranslators.indexOfKey(translatorId) >= 0); 155 final int tId = translatorId; 156 157 new Translator(mContext, translationContext, translatorId, this, mHandler, mService, 158 new Consumer<Translator>() { 159 @Override 160 public void accept(Translator translator) { 161 if (translator == null) { 162 final long token = Binder.clearCallingIdentity(); 163 try { 164 executor.execute(() -> callback.accept(null)); 165 } finally { 166 Binder.restoreCallingIdentity(token); 167 } 168 return; 169 } 170 171 synchronized (mLock) { 172 mTranslators.put(tId, translator); 173 mTranslatorIds.put(translationContext, tId); 174 } 175 final long token = Binder.clearCallingIdentity(); 176 try { 177 executor.execute(() -> callback.accept(translator)); 178 } finally { 179 Binder.restoreCallingIdentity(token); 180 } 181 } 182 }); 183 } 184 } 185 186 /** 187 * Creates an on-device Translator for natural language translation. 188 * 189 * <p><strong>NOTE: </strong>Call on a worker thread. 190 * 191 * @removed use {@link #createOnDeviceTranslator(TranslationContext, Executor, Consumer)} 192 * instead. 193 * 194 * @param translationContext {@link TranslationContext} containing the specs for creating the 195 * Translator. 196 */ 197 @Deprecated 198 @Nullable 199 @WorkerThread createOnDeviceTranslator(@onNull TranslationContext translationContext)200 public Translator createOnDeviceTranslator(@NonNull TranslationContext translationContext) { 201 Objects.requireNonNull(translationContext, "translationContext cannot be null"); 202 203 synchronized (mLock) { 204 // TODO(b/176464808): Disallow multiple Translator now, it will throw 205 // IllegalStateException. Need to discuss if we can allow multiple Translators. 206 if (mTranslatorIds.containsKey(translationContext)) { 207 return mTranslators.get(mTranslatorIds.get(translationContext)); 208 } 209 210 int translatorId; 211 do { 212 translatorId = Math.abs(ID_GENERATOR.nextInt()); 213 } while (translatorId == 0 || mTranslators.indexOfKey(translatorId) >= 0); 214 215 final Translator newTranslator = new Translator(mContext, translationContext, 216 translatorId, this, mHandler, mService); 217 // Start the Translator session and wait for the result 218 newTranslator.start(); 219 try { 220 if (!newTranslator.isSessionCreated()) { 221 return null; 222 } 223 mTranslators.put(translatorId, newTranslator); 224 mTranslatorIds.put(translationContext, translatorId); 225 return newTranslator; 226 } catch (Translator.ServiceBinderReceiver.TimeoutException e) { 227 // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor 228 // public and use it. 229 Log.e(TAG, "Timed out getting create session: " + e); 230 return null; 231 } 232 } 233 } 234 235 /** @removed Use {@link #createOnDeviceTranslator(TranslationContext)} */ 236 @Deprecated 237 @Nullable 238 @WorkerThread createTranslator(@onNull TranslationContext translationContext)239 public Translator createTranslator(@NonNull TranslationContext translationContext) { 240 return createOnDeviceTranslator(translationContext); 241 } 242 243 /** 244 * Returns a set of {@link TranslationCapability}s describing the capabilities for on-device 245 * {@link Translator}s. 246 * 247 * <p>These translation capabilities contains a source and target {@link TranslationSpec} 248 * representing the data expected for both ends of translation process. The capabilities 249 * provides the information and limitations for generating a {@link TranslationContext}. 250 * The context object can then be used by 251 * {@link #createOnDeviceTranslator(TranslationContext, Executor, Consumer)} to obtain a 252 * {@link Translator} for translations.</p> 253 * 254 * <p><strong>NOTE: </strong>Call on a worker thread. 255 * 256 * @param sourceFormat data format for the input data to be translated. 257 * @param targetFormat data format for the expected translated output data. 258 * @return A set of {@link TranslationCapability}s. 259 */ 260 @NonNull 261 @WorkerThread getOnDeviceTranslationCapabilities( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat)262 public Set<TranslationCapability> getOnDeviceTranslationCapabilities( 263 @TranslationSpec.DataFormat int sourceFormat, 264 @TranslationSpec.DataFormat int targetFormat) { 265 try { 266 final SynchronousResultReceiver receiver = new SynchronousResultReceiver(); 267 mService.onTranslationCapabilitiesRequest(sourceFormat, targetFormat, receiver, 268 mContext.getUserId()); 269 final SynchronousResultReceiver.Result result = 270 receiver.awaitResult(SYNC_CALLS_TIMEOUT_MS); 271 if (result.resultCode != STATUS_SYNC_CALL_SUCCESS) { 272 return Collections.emptySet(); 273 } 274 Parcelable[] parcelables = result.bundle.getParcelableArray(EXTRA_CAPABILITIES); 275 ArraySet<TranslationCapability> capabilities = new ArraySet(); 276 for (Parcelable obj : parcelables) { 277 if (obj instanceof TranslationCapability) { 278 capabilities.add((TranslationCapability) obj); 279 } 280 } 281 return capabilities; 282 } catch (RemoteException e) { 283 throw e.rethrowFromSystemServer(); 284 } catch (TimeoutException e) { 285 Log.e(TAG, "Timed out getting supported translation capabilities: " + e); 286 return Collections.emptySet(); 287 } 288 } 289 290 /** @removed Use {@link #getOnDeviceTranslationCapabilities(int, int)} */ 291 @Deprecated 292 @NonNull 293 @WorkerThread getTranslationCapabilities( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat)294 public Set<TranslationCapability> getTranslationCapabilities( 295 @TranslationSpec.DataFormat int sourceFormat, 296 @TranslationSpec.DataFormat int targetFormat) { 297 return getOnDeviceTranslationCapabilities(sourceFormat, targetFormat); 298 } 299 300 /** 301 * Adds a {@link TranslationCapability} Consumer to listen for updates on states of on-device 302 * {@link TranslationCapability}s. 303 * 304 * @param capabilityListener a {@link TranslationCapability} Consumer to receive the updated 305 * {@link TranslationCapability} from the on-device translation service. 306 */ addOnDeviceTranslationCapabilityUpdateListener( @onNull @allbackExecutor Executor executor, @NonNull Consumer<TranslationCapability> capabilityListener)307 public void addOnDeviceTranslationCapabilityUpdateListener( 308 @NonNull @CallbackExecutor Executor executor, 309 @NonNull Consumer<TranslationCapability> capabilityListener) { 310 Objects.requireNonNull(executor, "executor should not be null"); 311 Objects.requireNonNull(capabilityListener, "capability listener should not be null"); 312 313 synchronized (mLock) { 314 if (mCapabilityCallbacks.containsKey(capabilityListener)) { 315 Log.w(TAG, "addOnDeviceTranslationCapabilityUpdateListener: the listener for " 316 + capabilityListener + " already registered; ignoring."); 317 return; 318 } 319 final IRemoteCallback remoteCallback = new TranslationCapabilityRemoteCallback(executor, 320 capabilityListener); 321 try { 322 mService.registerTranslationCapabilityCallback(remoteCallback, 323 mContext.getUserId()); 324 } catch (RemoteException e) { 325 throw e.rethrowFromSystemServer(); 326 } 327 mCapabilityCallbacks.put(capabilityListener, remoteCallback); 328 } 329 } 330 331 332 /** 333 * @removed Use {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener( 334 * java.util.concurrent.Executor, java.util.function.Consumer)} 335 */ 336 @Deprecated addOnDeviceTranslationCapabilityUpdateListener( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent)337 public void addOnDeviceTranslationCapabilityUpdateListener( 338 @TranslationSpec.DataFormat int sourceFormat, 339 @TranslationSpec.DataFormat int targetFormat, 340 @NonNull PendingIntent pendingIntent) { 341 Objects.requireNonNull(pendingIntent, "pending intent should not be null"); 342 343 synchronized (mLock) { 344 final Pair<Integer, Integer> formatPair = new Pair<>(sourceFormat, targetFormat); 345 mTranslationCapabilityUpdateListeners.computeIfAbsent(formatPair, 346 (formats) -> new ArrayList<>()).add(pendingIntent); 347 } 348 } 349 350 /** 351 * @removed Use {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener( 352 * java.util.concurrent.Executor, java.util.function.Consumer)} 353 */ 354 @Deprecated addTranslationCapabilityUpdateListener( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent)355 public void addTranslationCapabilityUpdateListener( 356 @TranslationSpec.DataFormat int sourceFormat, 357 @TranslationSpec.DataFormat int targetFormat, 358 @NonNull PendingIntent pendingIntent) { 359 addOnDeviceTranslationCapabilityUpdateListener(sourceFormat, targetFormat, pendingIntent); 360 } 361 362 /** 363 * Removes a {@link TranslationCapability} Consumer to listen for updates on states of 364 * on-device {@link TranslationCapability}s. 365 * 366 * @param capabilityListener the {@link TranslationCapability} Consumer to unregister 367 */ removeOnDeviceTranslationCapabilityUpdateListener( @onNull Consumer<TranslationCapability> capabilityListener)368 public void removeOnDeviceTranslationCapabilityUpdateListener( 369 @NonNull Consumer<TranslationCapability> capabilityListener) { 370 Objects.requireNonNull(capabilityListener, "capability callback should not be null"); 371 372 synchronized (mLock) { 373 final IRemoteCallback remoteCallback = mCapabilityCallbacks.get(capabilityListener); 374 if (remoteCallback == null) { 375 Log.w(TAG, "removeOnDeviceTranslationCapabilityUpdateListener: the capability " 376 + "listener not found; ignoring."); 377 return; 378 } 379 try { 380 mService.unregisterTranslationCapabilityCallback(remoteCallback, 381 mContext.getUserId()); 382 } catch (RemoteException e) { 383 throw e.rethrowFromSystemServer(); 384 } 385 mCapabilityCallbacks.remove(capabilityListener); 386 } 387 } 388 389 /** 390 * @removed Use {@link #removeOnDeviceTranslationCapabilityUpdateListener( 391 * java.util.function.Consumer)}. 392 */ 393 @Deprecated removeOnDeviceTranslationCapabilityUpdateListener( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent)394 public void removeOnDeviceTranslationCapabilityUpdateListener( 395 @TranslationSpec.DataFormat int sourceFormat, 396 @TranslationSpec.DataFormat int targetFormat, 397 @NonNull PendingIntent pendingIntent) { 398 Objects.requireNonNull(pendingIntent, "pending intent should not be null"); 399 400 synchronized (mLock) { 401 final Pair<Integer, Integer> formatPair = new Pair<>(sourceFormat, targetFormat); 402 if (mTranslationCapabilityUpdateListeners.containsKey(formatPair)) { 403 final ArrayList<PendingIntent> intents = 404 mTranslationCapabilityUpdateListeners.get(formatPair); 405 if (intents.contains(pendingIntent)) { 406 intents.remove(pendingIntent); 407 } else { 408 Log.w(TAG, "pending intent=" + pendingIntent + " does not exist in " 409 + "mTranslationCapabilityUpdateListeners"); 410 } 411 } else { 412 Log.w(TAG, "format pair=" + formatPair + " does not exist in " 413 + "mTranslationCapabilityUpdateListeners"); 414 } 415 } 416 } 417 418 /** 419 * @removed Use {@link #removeOnDeviceTranslationCapabilityUpdateListener( 420 * java.util.function.Consumer)}. 421 */ 422 @Deprecated removeTranslationCapabilityUpdateListener( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent)423 public void removeTranslationCapabilityUpdateListener( 424 @TranslationSpec.DataFormat int sourceFormat, 425 @TranslationSpec.DataFormat int targetFormat, 426 @NonNull PendingIntent pendingIntent) { 427 removeOnDeviceTranslationCapabilityUpdateListener( 428 sourceFormat, targetFormat, pendingIntent); 429 } 430 431 /** 432 * Returns an immutable PendingIntent which can be used to launch an activity to view/edit 433 * on-device translation settings. 434 * 435 * @return An immutable PendingIntent or {@code null} if one of reason met: 436 * <ul> 437 * <li>Device manufacturer (OEM) does not provide TranslationService.</li> 438 * <li>The TranslationService doesn't provide the Settings.</li> 439 * </ul> 440 **/ 441 @Nullable getOnDeviceTranslationSettingsActivityIntent()442 public PendingIntent getOnDeviceTranslationSettingsActivityIntent() { 443 final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); 444 try { 445 mService.getServiceSettingsActivity(resultReceiver, mContext.getUserId()); 446 } catch (RemoteException e) { 447 throw e.rethrowFromSystemServer(); 448 } 449 try { 450 return resultReceiver.getParcelableResult(); 451 } catch (SyncResultReceiver.TimeoutException e) { 452 Log.e(TAG, "Fail to get translation service settings activity."); 453 return null; 454 } 455 } 456 457 /** @removed Use {@link #getOnDeviceTranslationSettingsActivityIntent()} */ 458 @Deprecated 459 @Nullable getTranslationSettingsActivityIntent()460 public PendingIntent getTranslationSettingsActivityIntent() { 461 return getOnDeviceTranslationSettingsActivityIntent(); 462 } 463 removeTranslator(int id)464 void removeTranslator(int id) { 465 synchronized (mLock) { 466 mTranslators.remove(id); 467 for (int i = 0; i < mTranslatorIds.size(); i++) { 468 if (mTranslatorIds.valueAt(i) == id) { 469 mTranslatorIds.removeAt(i); 470 break; 471 } 472 } 473 } 474 } 475 getAvailableRequestId()476 AtomicInteger getAvailableRequestId() { 477 synchronized (mLock) { 478 return sAvailableRequestId; 479 } 480 } 481 482 private static class TranslationCapabilityRemoteCallback extends 483 IRemoteCallback.Stub { 484 private final Executor mExecutor; 485 private final Consumer<TranslationCapability> mListener; 486 TranslationCapabilityRemoteCallback(Executor executor, Consumer<TranslationCapability> listener)487 TranslationCapabilityRemoteCallback(Executor executor, 488 Consumer<TranslationCapability> listener) { 489 mExecutor = executor; 490 mListener = listener; 491 } 492 493 @Override sendResult(Bundle bundle)494 public void sendResult(Bundle bundle) { 495 Binder.withCleanCallingIdentity( 496 () -> mExecutor.execute(() -> onTranslationCapabilityUpdate(bundle))); 497 } 498 onTranslationCapabilityUpdate(Bundle bundle)499 private void onTranslationCapabilityUpdate(Bundle bundle) { 500 TranslationCapability capability = 501 (TranslationCapability) bundle.getParcelable(EXTRA_CAPABILITIES); 502 mListener.accept(capability); 503 } 504 } 505 } 506