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