• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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