/* * Copyright (C) 2020 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package android.view.translation; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; import android.annotation.RequiresPermission; import android.annotation.SystemApi; import android.app.assist.ActivityId; import android.content.ComponentName; import android.content.Context; import android.icu.util.ULocale; import android.os.Binder; import android.os.Bundle; import android.os.IRemoteCallback; import android.os.RemoteException; import android.util.ArrayMap; import android.util.Log; import android.view.View; import android.view.autofill.AutofillId; import android.widget.TextView; import com.android.internal.annotations.GuardedBy; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; /** *
The {@link UiTranslationManager} class provides ways for apps to use the ui translation * function in framework. * *
The UI translation provides ways for apps to support inline translation for the views. For * example the system supports text translation for {@link TextView}. To support UI translation for * your views, you should override the following methods to provide the content to be translated * and deal with the translated result. Here is an example for {@link TextView}-like views: * *
* public class MyTextView extends View {
* public MyTextView(...) {
* // implements how to show the translated result in your View in
* // ViewTranslationCallback and set it by setViewTranslationCallback()
* setViewTranslationCallback(new MyViewTranslationCallback());
* }
*
* public void onCreateViewTranslationRequest(int[] supportedFormats,
* Consumer requestsCollector) {
* // collect the information that needs to be translated
* ViewTranslationRequest.Builder requestBuilder =
* new ViewTranslationRequest.Builder(getAutofillId());
* requestBuilder.setValue(ViewTranslationRequest.ID_TEXT,
* TranslationRequestValue.forText(etText()));
* requestsCollector.accept(requestBuilder.build());
* }
*
* public void onProvideContentCaptureStructure(
* ViewStructure structure, int flags) {
* // set ViewTranslationResponse
* super.onViewTranslationResponse(response);
* }
* }
*
*
* If your view provides its own virtual hierarchy (for example, if it's a browser that draws the
* HTML using {@link android.graphics.Canvas} or native libraries in a different render process),
* you must override {@link View#onCreateVirtualViewTranslationRequests(long[], int[], Consumer)} to
* provide the content to be translated and implement
* {@link View#onVirtualViewTranslationResponses(android.util.LongSparseArray)} for the translated
* result. You also need to implement {@link android.view.translation.ViewTranslationCallback} to
* handle the translated information show or hide in your {@link View}.
*/
public final class UiTranslationManager {
private static final String TAG = "UiTranslationManager";
/**
* The tag which uses for enabling debug log dump. To enable it, we can use command "adb shell
* setprop log.tag.UiTranslation DEBUG".
*
* @hide
*/
public static final String LOG_TAG = "UiTranslation";
/**
* The state the caller requests to enable UI translation.
*
* @hide
*/
public static final int STATE_UI_TRANSLATION_STARTED = 0;
/**
* The state caller requests to pause UI translation. It will switch back to the original text.
*
* @hide
*/
public static final int STATE_UI_TRANSLATION_PAUSED = 1;
/**
* The state caller requests to resume the paused UI translation. It will show the translated
* text again if the text had been translated.
*
* @hide
*/
public static final int STATE_UI_TRANSLATION_RESUMED = 2;
/**
* The state caller requests to disable UI translation when it no longer needs translation.
*
* @hide
*/
public static final int STATE_UI_TRANSLATION_FINISHED = 3;
/** @hide */
@IntDef(prefix = {"STATE__TRANSLATION"}, value = {
STATE_UI_TRANSLATION_STARTED,
STATE_UI_TRANSLATION_PAUSED,
STATE_UI_TRANSLATION_RESUMED,
STATE_UI_TRANSLATION_FINISHED
})
@Retention(RetentionPolicy.SOURCE)
public @interface UiTranslationState {
}
// Keys for the data transmitted in the internal UI Translation state callback.
/** @hide */
public static final String EXTRA_STATE = "state";
/** @hide */
public static final String EXTRA_SOURCE_LOCALE = "source_locale";
/** @hide */
public static final String EXTRA_TARGET_LOCALE = "target_locale";
/** @hide */
public static final String EXTRA_PACKAGE_NAME = "package_name";
@NonNull
private final Context mContext;
private final ITranslationManager mService;
/**
* @hide
*/
public UiTranslationManager(@NonNull Context context, ITranslationManager service) {
mContext = Objects.requireNonNull(context);
mService = service;
}
/**
* @removed Use {@link #startTranslation(TranslationSpec, TranslationSpec, List, ActivityId,
* UiTranslationSpec)} instead.
* @hide
*/
@RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION)
@Deprecated
@SystemApi
public void startTranslation(@NonNull TranslationSpec sourceSpec,
@NonNull TranslationSpec targetSpec, @NonNull List
* The application whose UI is being translated can use this to customize the UI Translation
* behavior in ways that aren't made easy by methods like
* {@link View#onCreateViewTranslationRequest(int[], Consumer)}.
*
* Input methods can use this to offer complementary features to UI Translation; for example,
* enabling outgoing message translation when the system is translating incoming messages in a
* communication app.
*
* Starting from {@link android.os.Build.VERSION_CODES#TIRAMISU}, if Activities are already
* being translated when a callback is registered, methods on the callback will be invoked for
* each translated activity, depending on the state of translation:
*
*
*
* @param callback the callback to register for receiving the state change
* notifications
*/
public void registerUiTranslationStateCallback(
@NonNull @CallbackExecutor Executor executor,
@NonNull UiTranslationStateCallback callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
synchronized (mCallbacks) {
if (mCallbacks.containsKey(callback)) {
Log.w(TAG, "registerUiTranslationStateCallback: callback already registered;"
+ " ignoring.");
return;
}
final IRemoteCallback remoteCallback =
new UiTranslationStateRemoteCallback(executor, callback);
try {
mService.registerUiTranslationStateCallback(remoteCallback, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
mCallbacks.put(callback, remoteCallback);
}
}
/**
* Unregister {@code callback}.
*
* @see #registerUiTranslationStateCallback(Executor, UiTranslationStateCallback)
*/
public void unregisterUiTranslationStateCallback(@NonNull UiTranslationStateCallback callback) {
Objects.requireNonNull(callback);
synchronized (mCallbacks) {
final IRemoteCallback remoteCallback = mCallbacks.get(callback);
if (remoteCallback == null) {
Log.w(TAG, "unregisterUiTranslationStateCallback: callback not found; ignoring.");
return;
}
try {
mService.unregisterUiTranslationStateCallback(remoteCallback, mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
mCallbacks.remove(callback);
}
}
/**
* Notify apps the translation is finished because {@link #finishTranslation(ActivityId)} is
* called or Activity is destroyed.
*
* @param activityDestroyed if the ui translation is finished because of activity destroyed.
* @param activityId the identifier for the Activity which needs ui translation
* @param componentName the ui translated Activity componentName.
* @hide
*/
public void onTranslationFinished(boolean activityDestroyed, ActivityId activityId,
ComponentName componentName) {
try {
mService.onTranslationFinished(activityDestroyed, activityId.getToken(), componentName,
mContext.getUserId());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
@NonNull
@GuardedBy("mCallbacks")
private final Map