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