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.content.pm.ParceledListSlice; 27 import android.os.Binder; 28 import android.os.Bundle; 29 import android.os.Handler; 30 import android.os.IRemoteCallback; 31 import android.os.Looper; 32 import android.os.RemoteException; 33 import android.os.SynchronousResultReceiver; 34 import android.util.ArrayMap; 35 import android.util.ArraySet; 36 import android.util.IntArray; 37 import android.util.Log; 38 import android.util.Pair; 39 40 import com.android.internal.annotations.GuardedBy; 41 import com.android.internal.util.SyncResultReceiver; 42 43 import java.security.SecureRandom; 44 import java.util.ArrayList; 45 import java.util.Collections; 46 import java.util.Map; 47 import java.util.Objects; 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 // TODO(b/158778794): make the session ids truly globally unique across processes 96 private static final SecureRandom ID_GENERATOR = new SecureRandom(); 97 private final Object mLock = new Object(); 98 99 @NonNull 100 private final Context mContext; 101 102 private final ITranslationManager mService; 103 104 @NonNull 105 @GuardedBy("mLock") 106 private final IntArray mTranslatorIds = new IntArray(); 107 108 @NonNull 109 private final Handler mHandler; 110 111 private static final AtomicInteger sAvailableRequestId = new AtomicInteger(1); 112 113 /** 114 * @hide 115 */ TranslationManager(@onNull Context context, ITranslationManager service)116 public TranslationManager(@NonNull Context context, ITranslationManager service) { 117 mContext = Objects.requireNonNull(context, "context cannot be null"); 118 mService = service; 119 120 mHandler = Handler.createAsync(Looper.getMainLooper()); 121 } 122 123 /** 124 * Creates an on-device Translator for natural language translation. 125 * 126 * <p>In Android 12, this method provided the same cached Translator object when given the 127 * same TranslationContext object. Do not use a Translator destroyed elsewhere as this will 128 * cause an exception on Android 12. 129 * 130 * <p>In later versions, this method never returns a cached Translator. 131 * 132 * @param translationContext {@link TranslationContext} containing the specs for creating the 133 * Translator. 134 * @param executor Executor to run callback operations 135 * @param callback {@link Consumer} to receive the translator. A {@code null} value is returned 136 * if the service could not create the translator. 137 */ createOnDeviceTranslator(@onNull TranslationContext translationContext, @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Translator> callback)138 public void createOnDeviceTranslator(@NonNull TranslationContext translationContext, 139 @NonNull @CallbackExecutor Executor executor, @NonNull Consumer<Translator> callback) { 140 Objects.requireNonNull(translationContext, "translationContext cannot be null"); 141 Objects.requireNonNull(executor, "executor cannot be null"); 142 Objects.requireNonNull(callback, "callback cannot be null"); 143 144 synchronized (mLock) { 145 int translatorId; 146 do { 147 translatorId = Math.abs(ID_GENERATOR.nextInt()); 148 } while (translatorId == 0 || mTranslatorIds.indexOf(translatorId) >= 0); 149 final int tId = translatorId; 150 151 new Translator(mContext, translationContext, tId, this, mHandler, mService, 152 translator -> { 153 if (translator == null) { 154 Binder.withCleanCallingIdentity( 155 () -> executor.execute(() -> callback.accept(null))); 156 return; 157 } 158 159 synchronized (mLock) { 160 mTranslatorIds.add(tId); 161 } 162 Binder.withCleanCallingIdentity( 163 () -> executor.execute(() -> callback.accept(translator))); 164 }); 165 } 166 } 167 168 /** 169 * Creates an on-device Translator for natural language translation. 170 * 171 * <p><strong>NOTE: </strong>Call on a worker thread. 172 * 173 * @removed use {@link #createOnDeviceTranslator(TranslationContext, Executor, Consumer)} 174 * instead. 175 * 176 * @param translationContext {@link TranslationContext} containing the specs for creating the 177 * Translator. 178 */ 179 @Deprecated 180 @Nullable 181 @WorkerThread createOnDeviceTranslator(@onNull TranslationContext translationContext)182 public Translator createOnDeviceTranslator(@NonNull TranslationContext translationContext) { 183 Objects.requireNonNull(translationContext, "translationContext cannot be null"); 184 185 synchronized (mLock) { 186 int translatorId; 187 do { 188 translatorId = Math.abs(ID_GENERATOR.nextInt()); 189 } while (translatorId == 0 || mTranslatorIds.indexOf(translatorId) >= 0); 190 191 final Translator newTranslator = new Translator(mContext, translationContext, 192 translatorId, this, mHandler, mService); 193 // Start the Translator session and wait for the result 194 newTranslator.start(); 195 try { 196 if (!newTranslator.isSessionCreated()) { 197 return null; 198 } 199 mTranslatorIds.add(translatorId); 200 return newTranslator; 201 } catch (Translator.ServiceBinderReceiver.TimeoutException e) { 202 // TODO(b/176464808): maybe make SyncResultReceiver.TimeoutException constructor 203 // public and use it. 204 Log.e(TAG, "Timed out getting create session: " + e); 205 return null; 206 } 207 } 208 } 209 210 /** @removed Use {@link #createOnDeviceTranslator(TranslationContext)} */ 211 @Deprecated 212 @Nullable 213 @WorkerThread createTranslator(@onNull TranslationContext translationContext)214 public Translator createTranslator(@NonNull TranslationContext translationContext) { 215 return createOnDeviceTranslator(translationContext); 216 } 217 218 /** 219 * Returns a set of {@link TranslationCapability}s describing the capabilities for on-device 220 * {@link Translator}s. 221 * 222 * <p>These translation capabilities contains a source and target {@link TranslationSpec} 223 * representing the data expected for both ends of translation process. The capabilities 224 * provides the information and limitations for generating a {@link TranslationContext}. 225 * The context object can then be used by 226 * {@link #createOnDeviceTranslator(TranslationContext, Executor, Consumer)} to obtain a 227 * {@link Translator} for translations.</p> 228 * 229 * <p><strong>NOTE: </strong>Call on a worker thread. 230 * 231 * @param sourceFormat data format for the input data to be translated. 232 * @param targetFormat data format for the expected translated output data. 233 * @return A set of {@link TranslationCapability}s. 234 */ 235 @NonNull 236 @WorkerThread getOnDeviceTranslationCapabilities( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat)237 public Set<TranslationCapability> getOnDeviceTranslationCapabilities( 238 @TranslationSpec.DataFormat int sourceFormat, 239 @TranslationSpec.DataFormat int targetFormat) { 240 try { 241 final SynchronousResultReceiver receiver = new SynchronousResultReceiver(); 242 mService.onTranslationCapabilitiesRequest(sourceFormat, targetFormat, receiver, 243 mContext.getUserId()); 244 final SynchronousResultReceiver.Result result = 245 receiver.awaitResult(SYNC_CALLS_TIMEOUT_MS); 246 if (result.resultCode != STATUS_SYNC_CALL_SUCCESS) { 247 return Collections.emptySet(); 248 } 249 ParceledListSlice<TranslationCapability> listSlice = 250 result.bundle.getParcelable(EXTRA_CAPABILITIES); 251 ArraySet<TranslationCapability> capabilities = 252 new ArraySet<>(listSlice == null ? null : listSlice.getList()); 253 return capabilities; 254 } catch (RemoteException e) { 255 throw e.rethrowFromSystemServer(); 256 } catch (TimeoutException e) { 257 Log.e(TAG, "Timed out getting supported translation capabilities: " + e); 258 return Collections.emptySet(); 259 } 260 } 261 262 /** @removed Use {@link #getOnDeviceTranslationCapabilities(int, int)} */ 263 @Deprecated 264 @NonNull 265 @WorkerThread getTranslationCapabilities( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat)266 public Set<TranslationCapability> getTranslationCapabilities( 267 @TranslationSpec.DataFormat int sourceFormat, 268 @TranslationSpec.DataFormat int targetFormat) { 269 return getOnDeviceTranslationCapabilities(sourceFormat, targetFormat); 270 } 271 272 /** 273 * Adds a {@link TranslationCapability} Consumer to listen for updates on states of on-device 274 * {@link TranslationCapability}s. 275 * 276 * @param capabilityListener a {@link TranslationCapability} Consumer to receive the updated 277 * {@link TranslationCapability} from the on-device translation service. 278 */ addOnDeviceTranslationCapabilityUpdateListener( @onNull @allbackExecutor Executor executor, @NonNull Consumer<TranslationCapability> capabilityListener)279 public void addOnDeviceTranslationCapabilityUpdateListener( 280 @NonNull @CallbackExecutor Executor executor, 281 @NonNull Consumer<TranslationCapability> capabilityListener) { 282 Objects.requireNonNull(executor, "executor should not be null"); 283 Objects.requireNonNull(capabilityListener, "capability listener should not be null"); 284 285 synchronized (mLock) { 286 if (mCapabilityCallbacks.containsKey(capabilityListener)) { 287 Log.w(TAG, "addOnDeviceTranslationCapabilityUpdateListener: the listener for " 288 + capabilityListener + " already registered; ignoring."); 289 return; 290 } 291 final IRemoteCallback remoteCallback = new TranslationCapabilityRemoteCallback(executor, 292 capabilityListener); 293 try { 294 mService.registerTranslationCapabilityCallback(remoteCallback, 295 mContext.getUserId()); 296 } catch (RemoteException e) { 297 throw e.rethrowFromSystemServer(); 298 } 299 mCapabilityCallbacks.put(capabilityListener, remoteCallback); 300 } 301 } 302 303 304 /** 305 * @removed Use {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener( 306 * java.util.concurrent.Executor, java.util.function.Consumer)} 307 */ 308 @Deprecated addOnDeviceTranslationCapabilityUpdateListener( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent)309 public void addOnDeviceTranslationCapabilityUpdateListener( 310 @TranslationSpec.DataFormat int sourceFormat, 311 @TranslationSpec.DataFormat int targetFormat, 312 @NonNull PendingIntent pendingIntent) { 313 Objects.requireNonNull(pendingIntent, "pending intent should not be null"); 314 315 synchronized (mLock) { 316 final Pair<Integer, Integer> formatPair = new Pair<>(sourceFormat, targetFormat); 317 mTranslationCapabilityUpdateListeners.computeIfAbsent(formatPair, 318 (formats) -> new ArrayList<>()).add(pendingIntent); 319 } 320 } 321 322 /** 323 * @removed Use {@link TranslationManager#addOnDeviceTranslationCapabilityUpdateListener( 324 * java.util.concurrent.Executor, java.util.function.Consumer)} 325 */ 326 @Deprecated addTranslationCapabilityUpdateListener( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent)327 public void addTranslationCapabilityUpdateListener( 328 @TranslationSpec.DataFormat int sourceFormat, 329 @TranslationSpec.DataFormat int targetFormat, 330 @NonNull PendingIntent pendingIntent) { 331 addOnDeviceTranslationCapabilityUpdateListener(sourceFormat, targetFormat, pendingIntent); 332 } 333 334 /** 335 * Removes a {@link TranslationCapability} Consumer to listen for updates on states of 336 * on-device {@link TranslationCapability}s. 337 * 338 * @param capabilityListener the {@link TranslationCapability} Consumer to unregister 339 */ removeOnDeviceTranslationCapabilityUpdateListener( @onNull Consumer<TranslationCapability> capabilityListener)340 public void removeOnDeviceTranslationCapabilityUpdateListener( 341 @NonNull Consumer<TranslationCapability> capabilityListener) { 342 Objects.requireNonNull(capabilityListener, "capability callback should not be null"); 343 344 synchronized (mLock) { 345 final IRemoteCallback remoteCallback = mCapabilityCallbacks.get(capabilityListener); 346 if (remoteCallback == null) { 347 Log.w(TAG, "removeOnDeviceTranslationCapabilityUpdateListener: the capability " 348 + "listener not found; ignoring."); 349 return; 350 } 351 try { 352 mService.unregisterTranslationCapabilityCallback(remoteCallback, 353 mContext.getUserId()); 354 } catch (RemoteException e) { 355 throw e.rethrowFromSystemServer(); 356 } 357 mCapabilityCallbacks.remove(capabilityListener); 358 } 359 } 360 361 /** 362 * @removed Use {@link #removeOnDeviceTranslationCapabilityUpdateListener( 363 * java.util.function.Consumer)}. 364 */ 365 @Deprecated removeOnDeviceTranslationCapabilityUpdateListener( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent)366 public void removeOnDeviceTranslationCapabilityUpdateListener( 367 @TranslationSpec.DataFormat int sourceFormat, 368 @TranslationSpec.DataFormat int targetFormat, 369 @NonNull PendingIntent pendingIntent) { 370 Objects.requireNonNull(pendingIntent, "pending intent should not be null"); 371 372 synchronized (mLock) { 373 final Pair<Integer, Integer> formatPair = new Pair<>(sourceFormat, targetFormat); 374 if (mTranslationCapabilityUpdateListeners.containsKey(formatPair)) { 375 final ArrayList<PendingIntent> intents = 376 mTranslationCapabilityUpdateListeners.get(formatPair); 377 if (intents.contains(pendingIntent)) { 378 intents.remove(pendingIntent); 379 } else { 380 Log.w(TAG, "pending intent=" + pendingIntent + " does not exist in " 381 + "mTranslationCapabilityUpdateListeners"); 382 } 383 } else { 384 Log.w(TAG, "format pair=" + formatPair + " does not exist in " 385 + "mTranslationCapabilityUpdateListeners"); 386 } 387 } 388 } 389 390 /** 391 * @removed Use {@link #removeOnDeviceTranslationCapabilityUpdateListener( 392 * java.util.function.Consumer)}. 393 */ 394 @Deprecated removeTranslationCapabilityUpdateListener( @ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int targetFormat, @NonNull PendingIntent pendingIntent)395 public void removeTranslationCapabilityUpdateListener( 396 @TranslationSpec.DataFormat int sourceFormat, 397 @TranslationSpec.DataFormat int targetFormat, 398 @NonNull PendingIntent pendingIntent) { 399 removeOnDeviceTranslationCapabilityUpdateListener( 400 sourceFormat, targetFormat, pendingIntent); 401 } 402 403 /** 404 * Returns an immutable PendingIntent which can be used to launch an activity to view/edit 405 * on-device translation settings. 406 * 407 * @return An immutable PendingIntent or {@code null} if one of reason met: 408 * <ul> 409 * <li>Device manufacturer (OEM) does not provide TranslationService.</li> 410 * <li>The TranslationService doesn't provide the Settings.</li> 411 * </ul> 412 **/ 413 @Nullable getOnDeviceTranslationSettingsActivityIntent()414 public PendingIntent getOnDeviceTranslationSettingsActivityIntent() { 415 final SyncResultReceiver resultReceiver = new SyncResultReceiver(SYNC_CALLS_TIMEOUT_MS); 416 try { 417 mService.getServiceSettingsActivity(resultReceiver, mContext.getUserId()); 418 } catch (RemoteException e) { 419 throw e.rethrowFromSystemServer(); 420 } 421 try { 422 return resultReceiver.getParcelableResult(); 423 } catch (SyncResultReceiver.TimeoutException e) { 424 Log.e(TAG, "Fail to get translation service settings activity."); 425 return null; 426 } 427 } 428 429 /** @removed Use {@link #getOnDeviceTranslationSettingsActivityIntent()} */ 430 @Deprecated 431 @Nullable getTranslationSettingsActivityIntent()432 public PendingIntent getTranslationSettingsActivityIntent() { 433 return getOnDeviceTranslationSettingsActivityIntent(); 434 } 435 removeTranslator(int id)436 void removeTranslator(int id) { 437 synchronized (mLock) { 438 int index = mTranslatorIds.indexOf(id); 439 if (index >= 0) { 440 mTranslatorIds.remove(index); 441 } 442 } 443 } 444 getAvailableRequestId()445 AtomicInteger getAvailableRequestId() { 446 synchronized (mLock) { 447 return sAvailableRequestId; 448 } 449 } 450 451 private static class TranslationCapabilityRemoteCallback extends 452 IRemoteCallback.Stub { 453 private final Executor mExecutor; 454 private final Consumer<TranslationCapability> mListener; 455 TranslationCapabilityRemoteCallback(Executor executor, Consumer<TranslationCapability> listener)456 TranslationCapabilityRemoteCallback(Executor executor, 457 Consumer<TranslationCapability> listener) { 458 mExecutor = executor; 459 mListener = listener; 460 } 461 462 @Override sendResult(Bundle bundle)463 public void sendResult(Bundle bundle) { 464 Binder.withCleanCallingIdentity( 465 () -> mExecutor.execute(() -> onTranslationCapabilityUpdate(bundle))); 466 } 467 onTranslationCapabilityUpdate(Bundle bundle)468 private void onTranslationCapabilityUpdate(Bundle bundle) { 469 TranslationCapability capability = 470 (TranslationCapability) bundle.getParcelable(EXTRA_CAPABILITIES); 471 mListener.accept(capability); 472 } 473 } 474 } 475