• 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.IntDef;
21 import android.annotation.NonNull;
22 import android.annotation.RequiresPermission;
23 import android.annotation.SystemApi;
24 import android.app.assist.ActivityId;
25 import android.content.ComponentName;
26 import android.content.Context;
27 import android.icu.util.ULocale;
28 import android.os.Binder;
29 import android.os.Bundle;
30 import android.os.IRemoteCallback;
31 import android.os.RemoteException;
32 import android.util.ArrayMap;
33 import android.util.Log;
34 import android.view.View;
35 import android.view.autofill.AutofillId;
36 import android.widget.TextView;
37 
38 import com.android.internal.annotations.GuardedBy;
39 
40 import java.lang.annotation.Retention;
41 import java.lang.annotation.RetentionPolicy;
42 import java.util.List;
43 import java.util.Map;
44 import java.util.Objects;
45 import java.util.concurrent.Executor;
46 import java.util.function.Consumer;
47 
48 /**
49  * <p>The {@link UiTranslationManager} class provides ways for apps to use the ui translation
50  * function in framework.
51  *
52  * <p> The UI translation provides ways for apps to support inline translation for the views. For
53  * example the system supports text translation for {@link TextView}. To support UI translation for
54  * your views, you should override the following methods to provide the content to be translated
55  * and deal with the translated result. Here is an example for {@link TextView}-like views:
56  *
57  * <pre><code>
58  * public class MyTextView extends View {
59  *     public MyTextView(...) {
60  *         // implements how to show the translated result in your View in
61  *         // ViewTranslationCallback and set it by setViewTranslationCallback()
62  *         setViewTranslationCallback(new MyViewTranslationCallback());
63  *     }
64  *
65  *     public void onCreateViewTranslationRequest(int[] supportedFormats,
66  *             Consumer<ViewTranslationRequest> requestsCollector) {
67  *        // collect the information that needs to be translated
68  *        ViewTranslationRequest.Builder requestBuilder =
69  *                     new ViewTranslationRequest.Builder(getAutofillId());
70  *        requestBuilder.setValue(ViewTranslationRequest.ID_TEXT,
71  *                         TranslationRequestValue.forText(etText()));
72  *        requestsCollector.accept(requestBuilder.build());
73  *     }
74  *
75  *     public void onProvideContentCaptureStructure(
76  *             ViewStructure structure, int flags) {
77  *         // set ViewTranslationResponse
78  *         super.onViewTranslationResponse(response);
79  *     }
80  * }
81  * </code></pre>
82  *
83  * <p>If your view provides its own virtual hierarchy (for example, if it's a browser that draws the
84  * HTML using {@link android.graphics.Canvas} or native libraries in a different render process),
85  * you must override {@link View#onCreateVirtualViewTranslationRequests(long[], int[], Consumer)} to
86  * provide the content to be translated and implement
87  * {@link View#onVirtualViewTranslationResponses(android.util.LongSparseArray)} for the translated
88  * result. You also need to implement {@link android.view.translation.ViewTranslationCallback} to
89  * handle the translated information show or hide in your {@link View}.
90  */
91 public final class UiTranslationManager {
92 
93     private static final String TAG = "UiTranslationManager";
94 
95     /**
96      * The tag which uses for enabling debug log dump. To enable it, we can use command "adb shell
97      * setprop log.tag.UiTranslation DEBUG".
98      *
99      * @hide
100      */
101     public static final String LOG_TAG = "UiTranslation";
102 
103     /**
104      * The state the caller requests to enable UI translation.
105      *
106      * @hide
107      */
108     public static final int STATE_UI_TRANSLATION_STARTED = 0;
109     /**
110      * The state caller requests to pause UI translation. It will switch back to the original text.
111      *
112      * @hide
113      */
114     public static final int STATE_UI_TRANSLATION_PAUSED = 1;
115     /**
116      * The state caller requests to resume the paused UI translation. It will show the translated
117      * text again if the text had been translated.
118      *
119      * @hide
120      */
121     public static final int STATE_UI_TRANSLATION_RESUMED = 2;
122     /**
123      * The state caller requests to disable UI translation when it no longer needs translation.
124      *
125      * @hide
126      */
127     public static final int STATE_UI_TRANSLATION_FINISHED = 3;
128 
129     /** @hide */
130     @IntDef(prefix = {"STATE__TRANSLATION"}, value = {
131             STATE_UI_TRANSLATION_STARTED,
132             STATE_UI_TRANSLATION_PAUSED,
133             STATE_UI_TRANSLATION_RESUMED,
134             STATE_UI_TRANSLATION_FINISHED
135     })
136     @Retention(RetentionPolicy.SOURCE)
137     public @interface UiTranslationState {
138     }
139 
140     // Keys for the data transmitted in the internal UI Translation state callback.
141     /** @hide */
142     public static final String EXTRA_STATE = "state";
143     /** @hide */
144     public static final String EXTRA_SOURCE_LOCALE = "source_locale";
145     /** @hide */
146     public static final String EXTRA_TARGET_LOCALE = "target_locale";
147     /** @hide */
148     public static final String EXTRA_PACKAGE_NAME = "package_name";
149 
150     @NonNull
151     private final Context mContext;
152 
153     private final ITranslationManager mService;
154 
155     /**
156      * @hide
157      */
UiTranslationManager(@onNull Context context, ITranslationManager service)158     public UiTranslationManager(@NonNull Context context, ITranslationManager service) {
159         mContext = Objects.requireNonNull(context);
160         mService = service;
161     }
162 
163     /**
164      * @removed Use {@link #startTranslation(TranslationSpec, TranslationSpec, List, ActivityId,
165      * UiTranslationSpec)} instead.
166      * @hide
167      */
168     @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
169     @Deprecated
170     @SystemApi
startTranslation(@onNull TranslationSpec sourceSpec, @NonNull TranslationSpec targetSpec, @NonNull List<AutofillId> viewIds, @NonNull ActivityId activityId)171     public void startTranslation(@NonNull TranslationSpec sourceSpec,
172             @NonNull TranslationSpec targetSpec, @NonNull List<AutofillId> viewIds,
173             @NonNull ActivityId activityId) {
174         startTranslation(
175                 sourceSpec, targetSpec, viewIds, activityId,
176                 new UiTranslationSpec.Builder().setShouldPadContentForCompat(true).build());
177     }
178 
179     /**
180      * Request ui translation for a given Views.
181      *
182      * @param sourceSpec        {@link TranslationSpec} for the data to be translated.
183      * @param targetSpec        {@link TranslationSpec} for the translated data.
184      * @param viewIds           A list of the {@link View}'s {@link AutofillId} which needs to be
185      *                          translated
186      * @param activityId        the identifier for the Activity which needs ui translation
187      * @param uiTranslationSpec configuration for translation of the specified views
188      * @throws IllegalArgumentException if the no {@link View}'s {@link AutofillId} in the list
189      * @hide
190      */
191     @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
192     @SystemApi
startTranslation(@onNull TranslationSpec sourceSpec, @NonNull TranslationSpec targetSpec, @NonNull List<AutofillId> viewIds, @NonNull ActivityId activityId, @NonNull UiTranslationSpec uiTranslationSpec)193     public void startTranslation(@NonNull TranslationSpec sourceSpec,
194             @NonNull TranslationSpec targetSpec, @NonNull List<AutofillId> viewIds,
195             @NonNull ActivityId activityId, @NonNull UiTranslationSpec uiTranslationSpec) {
196         // TODO(b/177789967): Return result code or find a way to notify the status.
197         Objects.requireNonNull(sourceSpec);
198         Objects.requireNonNull(targetSpec);
199         Objects.requireNonNull(viewIds);
200         Objects.requireNonNull(activityId);
201         Objects.requireNonNull(activityId.getToken());
202         Objects.requireNonNull(uiTranslationSpec);
203         if (viewIds.size() == 0) {
204             throw new IllegalArgumentException("Invalid empty views: " + viewIds);
205         }
206         try {
207             mService.updateUiTranslationState(STATE_UI_TRANSLATION_STARTED, sourceSpec,
208                     targetSpec, viewIds, activityId.getToken(), activityId.getTaskId(),
209                     uiTranslationSpec,
210                     mContext.getUserId());
211         } catch (RemoteException e) {
212             throw e.rethrowFromSystemServer();
213         }
214     }
215 
216     /**
217      * Request to disable the ui translation. It will destroy all the {@link Translator}s and no
218      * longer to show the translated text.
219      *
220      * @param activityId the identifier for the Activity which needs ui translation
221      * @throws NullPointerException the activityId or
222      *                              {@link android.app.assist.ActivityId#getToken()} is {@code null}
223      * @hide
224      */
225     @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
226     @SystemApi
finishTranslation(@onNull ActivityId activityId)227     public void finishTranslation(@NonNull ActivityId activityId) {
228         try {
229             Objects.requireNonNull(activityId);
230             Objects.requireNonNull(activityId.getToken());
231             mService.updateUiTranslationState(STATE_UI_TRANSLATION_FINISHED,
232                     null /* sourceSpec */, null /* targetSpec */, null /* viewIds */,
233                     activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */,
234                     mContext.getUserId());
235         } catch (RemoteException e) {
236             throw e.rethrowFromSystemServer();
237         }
238     }
239 
240     /**
241      * Request to pause the current ui translation's {@link Translator} which will switch back to
242      * the original language.
243      *
244      * @param activityId the identifier for the Activity which needs ui translation
245      * @throws NullPointerException the activityId or
246      *                              {@link android.app.assist.ActivityId#getToken()} is {@code null}
247      * @hide
248      */
249     @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
250     @SystemApi
pauseTranslation(@onNull ActivityId activityId)251     public void pauseTranslation(@NonNull ActivityId activityId) {
252         try {
253             Objects.requireNonNull(activityId);
254             Objects.requireNonNull(activityId.getToken());
255             mService.updateUiTranslationState(STATE_UI_TRANSLATION_PAUSED,
256                     null /* sourceSpec */, null /* targetSpec */, null /* viewIds */,
257                     activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */,
258                     mContext.getUserId());
259         } catch (RemoteException e) {
260             throw e.rethrowFromSystemServer();
261         }
262     }
263 
264     /**
265      * Request to resume the paused ui translation's {@link Translator} which will switch to the
266      * translated language if the text had been translated.
267      *
268      * @param activityId the identifier for the Activity which needs ui translation
269      * @throws NullPointerException the activityId or
270      *                              {@link android.app.assist.ActivityId#getToken()} is {@code null}
271      * @hide
272      */
273     @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
274     @SystemApi
resumeTranslation(@onNull ActivityId activityId)275     public void resumeTranslation(@NonNull ActivityId activityId) {
276         try {
277             Objects.requireNonNull(activityId);
278             Objects.requireNonNull(activityId.getToken());
279             mService.updateUiTranslationState(STATE_UI_TRANSLATION_RESUMED,
280                     null /* sourceSpec */, null /* targetSpec */, null /* viewIds */,
281                     activityId.getToken(), activityId.getTaskId(), null /* uiTranslationSpec */,
282                     mContext.getUserId());
283         } catch (RemoteException e) {
284             throw e.rethrowFromSystemServer();
285         }
286     }
287 
288     /**
289      * Register for notifications of UI Translation state changes on the foreground Activity. This
290      * is available to the owning application itself and also the current input method.
291      * <p>
292      * The application whose UI is being translated can use this to customize the UI Translation
293      * behavior in ways that aren't made easy by methods like
294      * {@link View#onCreateViewTranslationRequest(int[], Consumer)}.
295      * <p>
296      * Input methods can use this to offer complementary features to UI Translation; for example,
297      * enabling outgoing message translation when the system is translating incoming messages in a
298      * communication app.
299      * <p>
300      * Starting from {@link android.os.Build.VERSION_CODES#TIRAMISU}, if Activities are already
301      * being translated when a callback is registered, methods on the callback will be invoked for
302      * each translated activity, depending on the state of translation:
303      * <ul>
304      *     <li>If translation is <em>not</em> paused,
305      *     {@link UiTranslationStateCallback#onStarted} will be invoked.</li>
306      *     <li>If translation <em>is</em> paused, {@link UiTranslationStateCallback#onStarted}
307      *     will first be invoked, followed by {@link UiTranslationStateCallback#onPaused}.</li>
308      * </ul>
309      *
310      * @param callback the callback to register for receiving the state change
311      *                 notifications
312      */
registerUiTranslationStateCallback( @onNull @allbackExecutor Executor executor, @NonNull UiTranslationStateCallback callback)313     public void registerUiTranslationStateCallback(
314             @NonNull @CallbackExecutor Executor executor,
315             @NonNull UiTranslationStateCallback callback) {
316         Objects.requireNonNull(executor);
317         Objects.requireNonNull(callback);
318         synchronized (mCallbacks) {
319             if (mCallbacks.containsKey(callback)) {
320                 Log.w(TAG, "registerUiTranslationStateCallback: callback already registered;"
321                         + " ignoring.");
322                 return;
323             }
324             final IRemoteCallback remoteCallback =
325                     new UiTranslationStateRemoteCallback(executor, callback);
326             try {
327                 mService.registerUiTranslationStateCallback(remoteCallback, mContext.getUserId());
328             } catch (RemoteException e) {
329                 throw e.rethrowFromSystemServer();
330             }
331             mCallbacks.put(callback, remoteCallback);
332         }
333     }
334 
335     /**
336      * Unregister {@code callback}.
337      *
338      * @see #registerUiTranslationStateCallback(Executor, UiTranslationStateCallback)
339      */
unregisterUiTranslationStateCallback(@onNull UiTranslationStateCallback callback)340     public void unregisterUiTranslationStateCallback(@NonNull UiTranslationStateCallback callback) {
341         Objects.requireNonNull(callback);
342 
343         synchronized (mCallbacks) {
344             final IRemoteCallback remoteCallback = mCallbacks.get(callback);
345             if (remoteCallback == null) {
346                 Log.w(TAG, "unregisterUiTranslationStateCallback: callback not found; ignoring.");
347                 return;
348             }
349             try {
350                 mService.unregisterUiTranslationStateCallback(remoteCallback, mContext.getUserId());
351             } catch (RemoteException e) {
352                 throw e.rethrowFromSystemServer();
353             }
354             mCallbacks.remove(callback);
355         }
356     }
357 
358     /**
359      * Notify apps the translation is finished because {@link #finishTranslation(ActivityId)} is
360      * called or Activity is destroyed.
361      *
362      * @param activityDestroyed if the ui translation is finished because of activity destroyed.
363      * @param activityId        the identifier for the Activity which needs ui translation
364      * @param componentName     the ui translated Activity componentName.
365      * @hide
366      */
onTranslationFinished(boolean activityDestroyed, ActivityId activityId, ComponentName componentName)367     public void onTranslationFinished(boolean activityDestroyed, ActivityId activityId,
368             ComponentName componentName) {
369         try {
370             mService.onTranslationFinished(activityDestroyed, activityId.getToken(), componentName,
371                     mContext.getUserId());
372         } catch (RemoteException e) {
373             throw e.rethrowFromSystemServer();
374         }
375     }
376 
377     @NonNull
378     @GuardedBy("mCallbacks")
379     private final Map<UiTranslationStateCallback, IRemoteCallback> mCallbacks = new ArrayMap<>();
380 
381     private static class UiTranslationStateRemoteCallback extends IRemoteCallback.Stub {
382         private final Executor mExecutor;
383         private final UiTranslationStateCallback mCallback;
384         private ULocale mSourceLocale;
385         private ULocale mTargetLocale;
386 
UiTranslationStateRemoteCallback(Executor executor, UiTranslationStateCallback callback)387         UiTranslationStateRemoteCallback(Executor executor,
388                 UiTranslationStateCallback callback) {
389             mExecutor = executor;
390             mCallback = callback;
391         }
392 
393         @Override
sendResult(Bundle bundle)394         public void sendResult(Bundle bundle) {
395             Binder.withCleanCallingIdentity(() -> mExecutor.execute(() -> onStateChange(bundle)));
396         }
397 
onStateChange(Bundle bundle)398         private void onStateChange(Bundle bundle) {
399             int state = bundle.getInt(EXTRA_STATE);
400             String packageName = bundle.getString(EXTRA_PACKAGE_NAME);
401             switch (state) {
402                 case STATE_UI_TRANSLATION_STARTED:
403                     mSourceLocale = bundle.getSerializable(EXTRA_SOURCE_LOCALE, ULocale.class);
404                     mTargetLocale = bundle.getSerializable(EXTRA_TARGET_LOCALE, ULocale.class);
405                     mCallback.onStarted(mSourceLocale, mTargetLocale, packageName);
406                     break;
407                 case STATE_UI_TRANSLATION_RESUMED:
408                     mCallback.onResumed(mSourceLocale, mTargetLocale, packageName);
409                     break;
410                 case STATE_UI_TRANSLATION_PAUSED:
411                     mCallback.onPaused(packageName);
412                     break;
413                 case STATE_UI_TRANSLATION_FINISHED:
414                     mCallback.onFinished(packageName);
415                     break;
416                 default:
417                     Log.wtf(TAG, "Unexpected translation state:" + state);
418             }
419         }
420     }
421 }
422