767 lines
34 KiB
Java
767 lines
34 KiB
Java
/*
|
|
* Copyright (C) 2021 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 static android.view.translation.Helper.ANIMATION_DURATION_MILLIS;
|
|
import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_FINISHED;
|
|
import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_PAUSED;
|
|
import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_RESUMED;
|
|
import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_STARTED;
|
|
|
|
import android.annotation.NonNull;
|
|
import android.annotation.WorkerThread;
|
|
import android.app.Activity;
|
|
import android.app.assist.ActivityId;
|
|
import android.content.Context;
|
|
import android.os.Bundle;
|
|
import android.os.Handler;
|
|
import android.os.HandlerThread;
|
|
import android.os.Process;
|
|
import android.util.ArrayMap;
|
|
import android.util.ArraySet;
|
|
import android.util.Dumpable;
|
|
import android.util.IntArray;
|
|
import android.util.Log;
|
|
import android.util.LongSparseArray;
|
|
import android.util.Pair;
|
|
import android.util.SparseArray;
|
|
import android.util.SparseIntArray;
|
|
import android.view.View;
|
|
import android.view.ViewGroup;
|
|
import android.view.ViewRootImpl;
|
|
import android.view.WindowManagerGlobal;
|
|
import android.view.autofill.AutofillId;
|
|
import android.view.translation.UiTranslationManager.UiTranslationState;
|
|
import android.widget.TextView;
|
|
import android.widget.TextViewTranslationCallback;
|
|
|
|
import com.android.internal.util.function.pooled.PooledLambda;
|
|
|
|
import java.io.PrintWriter;
|
|
import java.lang.ref.WeakReference;
|
|
import java.util.ArrayList;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.function.BiConsumer;
|
|
|
|
/**
|
|
* A controller to manage the ui translation requests for the {@link Activity}.
|
|
*
|
|
* @hide
|
|
*/
|
|
public class UiTranslationController implements Dumpable {
|
|
|
|
public static final boolean DEBUG = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
|
|
|
|
/** @hide */
|
|
public static final String DUMPABLE_NAME = "UiTranslationController";
|
|
|
|
private static final String TAG = "UiTranslationController";
|
|
|
|
@NonNull
|
|
private final Activity mActivity;
|
|
@NonNull
|
|
private final Context mContext;
|
|
@NonNull
|
|
private final Object mLock = new Object();
|
|
|
|
// Each Translator is distinguished by sourceSpec and desSepc.
|
|
@NonNull
|
|
private final ArrayMap<Pair<TranslationSpec, TranslationSpec>, Translator> mTranslators;
|
|
@NonNull
|
|
private final ArrayMap<AutofillId, WeakReference<View>> mViews;
|
|
/**
|
|
* Views for which {@link UiTranslationSpec#shouldPadContentForCompat()} is true.
|
|
*/
|
|
@NonNull
|
|
private final ArraySet<AutofillId> mViewsToPadContent;
|
|
@NonNull
|
|
private final HandlerThread mWorkerThread;
|
|
@NonNull
|
|
private final Handler mWorkerHandler;
|
|
private int mCurrentState;
|
|
@NonNull
|
|
private ArraySet<AutofillId> mLastRequestAutofillIds;
|
|
|
|
public UiTranslationController(Activity activity, Context context) {
|
|
mActivity = activity;
|
|
mContext = context;
|
|
mViews = new ArrayMap<>();
|
|
mTranslators = new ArrayMap<>();
|
|
mViewsToPadContent = new ArraySet<>();
|
|
|
|
mWorkerThread =
|
|
new HandlerThread("UiTranslationController_" + mActivity.getComponentName(),
|
|
Process.THREAD_PRIORITY_FOREGROUND);
|
|
mWorkerThread.start();
|
|
mWorkerHandler = mWorkerThread.getThreadHandler();
|
|
activity.addDumpable(this);
|
|
}
|
|
|
|
/**
|
|
* Update the Ui translation state.
|
|
*/
|
|
public void updateUiTranslationState(@UiTranslationState int state, TranslationSpec sourceSpec,
|
|
TranslationSpec targetSpec, List<AutofillId> views,
|
|
UiTranslationSpec uiTranslationSpec) {
|
|
if (mActivity.isDestroyed()) {
|
|
Log.i(TAG, "Cannot update " + stateToString(state) + " for destroyed " + mActivity);
|
|
return;
|
|
}
|
|
boolean isLoggable = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
|
|
Log.i(TAG, "updateUiTranslationState state: " + stateToString(state)
|
|
+ (isLoggable ? (", views: " + views + ", spec: " + uiTranslationSpec) : ""));
|
|
synchronized (mLock) {
|
|
mCurrentState = state;
|
|
if (views != null) {
|
|
setLastRequestAutofillIdsLocked(views);
|
|
}
|
|
}
|
|
switch (state) {
|
|
case STATE_UI_TRANSLATION_STARTED:
|
|
if (uiTranslationSpec != null && uiTranslationSpec.shouldPadContentForCompat()) {
|
|
synchronized (mLock) {
|
|
mViewsToPadContent.addAll(views);
|
|
// TODO: Cleanup disappeared views from mViews and mViewsToPadContent at
|
|
// some appropriate place.
|
|
}
|
|
}
|
|
final Pair<TranslationSpec, TranslationSpec> specs =
|
|
new Pair<>(sourceSpec, targetSpec);
|
|
if (!mTranslators.containsKey(specs)) {
|
|
mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
|
|
UiTranslationController::createTranslatorAndStart,
|
|
UiTranslationController.this, sourceSpec, targetSpec, views));
|
|
} else {
|
|
onUiTranslationStarted(mTranslators.get(specs), views);
|
|
}
|
|
break;
|
|
case STATE_UI_TRANSLATION_PAUSED:
|
|
runForEachView((view, callback) -> callback.onHideTranslation(view));
|
|
break;
|
|
case STATE_UI_TRANSLATION_RESUMED:
|
|
runForEachView((view, callback) -> callback.onShowTranslation(view));
|
|
break;
|
|
case STATE_UI_TRANSLATION_FINISHED:
|
|
destroyTranslators();
|
|
runForEachView((view, callback) -> {
|
|
view.clearTranslationState();
|
|
});
|
|
notifyTranslationFinished(/* activityDestroyed= */ false);
|
|
synchronized (mLock) {
|
|
mViews.clear();
|
|
}
|
|
break;
|
|
default:
|
|
Log.w(TAG, "onAutoTranslationStateChange(): unknown state: " + state);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Called when the Activity is destroyed.
|
|
*/
|
|
public void onActivityDestroyed() {
|
|
synchronized (mLock) {
|
|
Log.i(TAG, "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState));
|
|
if (mCurrentState != STATE_UI_TRANSLATION_FINISHED) {
|
|
notifyTranslationFinished(/* activityDestroyed= */ true);
|
|
}
|
|
mViews.clear();
|
|
destroyTranslators();
|
|
mWorkerThread.quitSafely();
|
|
}
|
|
}
|
|
|
|
private void notifyTranslationFinished(boolean activityDestroyed) {
|
|
UiTranslationManager manager = mContext.getSystemService(UiTranslationManager.class);
|
|
if (manager != null) {
|
|
manager.onTranslationFinished(activityDestroyed,
|
|
new ActivityId(mActivity.getTaskId(), mActivity.getShareableActivityToken()),
|
|
mActivity.getComponentName());
|
|
}
|
|
}
|
|
|
|
private void setLastRequestAutofillIdsLocked(List<AutofillId> views) {
|
|
if (mLastRequestAutofillIds == null) {
|
|
mLastRequestAutofillIds = new ArraySet<>();
|
|
}
|
|
if (mLastRequestAutofillIds.size() > 0) {
|
|
mLastRequestAutofillIds.clear();
|
|
}
|
|
mLastRequestAutofillIds.addAll(views);
|
|
}
|
|
|
|
@Override
|
|
public String getDumpableName() {
|
|
return DUMPABLE_NAME;
|
|
}
|
|
|
|
@Override
|
|
public void dump(PrintWriter pw, String[] args) {
|
|
String outerPrefix = "";
|
|
pw.print(outerPrefix); pw.println("UiTranslationController:");
|
|
final String pfx = outerPrefix + " ";
|
|
pw.print(pfx); pw.print("activity: "); pw.print(mActivity);
|
|
pw.print(pfx); pw.print("resumed: "); pw.println(mActivity.isResumed());
|
|
pw.print(pfx); pw.print("current state: "); pw.println(mCurrentState);
|
|
final int translatorSize = mTranslators.size();
|
|
pw.print(outerPrefix); pw.print("number translator: "); pw.println(translatorSize);
|
|
for (int i = 0; i < translatorSize; i++) {
|
|
pw.print(outerPrefix); pw.print("#"); pw.println(i);
|
|
final Translator translator = mTranslators.valueAt(i);
|
|
translator.dump(outerPrefix, pw);
|
|
pw.println();
|
|
}
|
|
synchronized (mLock) {
|
|
final int viewSize = mViews.size();
|
|
pw.print(outerPrefix); pw.print("number views: "); pw.println(viewSize);
|
|
for (int i = 0; i < viewSize; i++) {
|
|
pw.print(outerPrefix); pw.print("#"); pw.println(i);
|
|
final AutofillId autofillId = mViews.keyAt(i);
|
|
final View view = mViews.valueAt(i).get();
|
|
pw.print(pfx); pw.print("autofillId: "); pw.println(autofillId);
|
|
pw.print(pfx); pw.print("view:"); pw.println(view);
|
|
}
|
|
pw.print(outerPrefix); pw.print("padded views: "); pw.println(mViewsToPadContent);
|
|
}
|
|
if (Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG)) {
|
|
dumpViewByTraversal(outerPrefix, pw);
|
|
}
|
|
}
|
|
|
|
private void dumpViewByTraversal(String outerPrefix, PrintWriter pw) {
|
|
final ArrayList<ViewRootImpl> roots =
|
|
WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
|
|
pw.print(outerPrefix); pw.println("Dump views:");
|
|
for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
|
|
final View rootView = roots.get(rootNum).getView();
|
|
if (rootView instanceof ViewGroup) {
|
|
dumpChildren((ViewGroup) rootView, outerPrefix, pw);
|
|
} else {
|
|
dumpViewInfo(rootView, outerPrefix, pw);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void dumpChildren(ViewGroup viewGroup, String outerPrefix, PrintWriter pw) {
|
|
final int childCount = viewGroup.getChildCount();
|
|
for (int i = 0; i < childCount; ++i) {
|
|
final View child = viewGroup.getChildAt(i);
|
|
if (child instanceof ViewGroup) {
|
|
pw.print(outerPrefix); pw.println("Children: ");
|
|
pw.print(outerPrefix); pw.print(outerPrefix); pw.println(child);
|
|
dumpChildren((ViewGroup) child, outerPrefix, pw);
|
|
} else {
|
|
pw.print(outerPrefix); pw.println("End Children: ");
|
|
pw.print(outerPrefix); pw.print(outerPrefix); pw.print(child);
|
|
dumpViewInfo(child, outerPrefix, pw);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void dumpViewInfo(View view, String outerPrefix, PrintWriter pw) {
|
|
final AutofillId autofillId = view.getAutofillId();
|
|
pw.print(outerPrefix); pw.print("autofillId: "); pw.print(autofillId);
|
|
// TODO: print TranslationTransformation
|
|
boolean isContainsView = false;
|
|
boolean isRequestedView = false;
|
|
synchronized (mLock) {
|
|
if (mLastRequestAutofillIds.contains(autofillId)) {
|
|
isRequestedView = true;
|
|
}
|
|
final WeakReference<View> viewRef = mViews.get(autofillId);
|
|
if (viewRef != null && viewRef.get() != null) {
|
|
isContainsView = true;
|
|
}
|
|
}
|
|
pw.print(outerPrefix); pw.print("isContainsView: "); pw.print(isContainsView);
|
|
pw.print(outerPrefix); pw.print("isRequestedView: "); pw.println(isRequestedView);
|
|
}
|
|
|
|
/**
|
|
* The method is used by {@link Translator}, it will be called when the translation is done. The
|
|
* translation result can be get from here.
|
|
*/
|
|
public void onTranslationCompleted(TranslationResponse response) {
|
|
if (response == null || response.getTranslationStatus()
|
|
!= TranslationResponse.TRANSLATION_STATUS_SUCCESS) {
|
|
Log.w(TAG, "Fail result from TranslationService, status=" + (response == null
|
|
? "null"
|
|
: response.getTranslationStatus()));
|
|
return;
|
|
}
|
|
final SparseArray<ViewTranslationResponse> translatedResult =
|
|
response.getViewTranslationResponses();
|
|
final SparseArray<ViewTranslationResponse> viewsResult = new SparseArray<>();
|
|
final SparseArray<LongSparseArray<ViewTranslationResponse>> virtualViewsResult =
|
|
new SparseArray<>();
|
|
final IntArray viewIds = new IntArray(1);
|
|
for (int i = 0; i < translatedResult.size(); i++) {
|
|
final ViewTranslationResponse result = translatedResult.valueAt(i);
|
|
final AutofillId autofillId = result.getAutofillId();
|
|
if (viewIds.indexOf(autofillId.getViewId()) < 0) {
|
|
viewIds.add(autofillId.getViewId());
|
|
}
|
|
if (autofillId.isNonVirtual()) {
|
|
viewsResult.put(translatedResult.keyAt(i), result);
|
|
} else {
|
|
final boolean isVirtualViewAdded =
|
|
virtualViewsResult.indexOfKey(autofillId.getViewId()) >= 0;
|
|
final LongSparseArray<ViewTranslationResponse> childIds =
|
|
isVirtualViewAdded ? virtualViewsResult.get(autofillId.getViewId())
|
|
: new LongSparseArray<>();
|
|
childIds.put(autofillId.getVirtualChildLongId(), result);
|
|
if (!isVirtualViewAdded) {
|
|
virtualViewsResult.put(autofillId.getViewId(), childIds);
|
|
}
|
|
}
|
|
}
|
|
// Traverse tree and get views by the responsed AutofillId
|
|
findViewsTraversalByAutofillIds(viewIds);
|
|
|
|
if (viewsResult.size() > 0) {
|
|
onTranslationCompleted(viewsResult);
|
|
}
|
|
if (virtualViewsResult.size() > 0) {
|
|
onVirtualViewTranslationCompleted(virtualViewsResult);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The method is used to handle the translation result for the vertual views.
|
|
*/
|
|
private void onVirtualViewTranslationCompleted(
|
|
SparseArray<LongSparseArray<ViewTranslationResponse>> translatedResult) {
|
|
boolean isLoggable = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
|
|
if (mActivity.isDestroyed()) {
|
|
Log.v(TAG, "onTranslationCompleted:" + mActivity + "is destroyed.");
|
|
return;
|
|
}
|
|
synchronized (mLock) {
|
|
if (mCurrentState == STATE_UI_TRANSLATION_FINISHED) {
|
|
Log.w(TAG, "onTranslationCompleted: the translation state is finished now. "
|
|
+ "Skip to show the translated text.");
|
|
return;
|
|
}
|
|
for (int i = 0; i < translatedResult.size(); i++) {
|
|
final AutofillId autofillId = new AutofillId(translatedResult.keyAt(i));
|
|
final WeakReference<View> viewRef = mViews.get(autofillId);
|
|
if (viewRef == null) {
|
|
continue;
|
|
}
|
|
final View view = viewRef.get();
|
|
if (view == null) {
|
|
Log.w(TAG, "onTranslationCompleted: the view for autofill id " + autofillId
|
|
+ " may be gone.");
|
|
continue;
|
|
}
|
|
final LongSparseArray<ViewTranslationResponse> virtualChildResponse =
|
|
translatedResult.valueAt(i);
|
|
if (isLoggable) {
|
|
Log.v(TAG, "onVirtualViewTranslationCompleted: received response for "
|
|
+ "AutofillId " + autofillId);
|
|
}
|
|
view.onVirtualViewTranslationResponses(virtualChildResponse);
|
|
if (mCurrentState == STATE_UI_TRANSLATION_PAUSED) {
|
|
return;
|
|
}
|
|
mActivity.runOnUiThread(() -> {
|
|
if (view.getViewTranslationCallback() == null) {
|
|
if (isLoggable) {
|
|
Log.d(TAG, view + " doesn't support showing translation because of "
|
|
+ "null ViewTranslationCallback.");
|
|
}
|
|
return;
|
|
}
|
|
if (view.getViewTranslationCallback() != null) {
|
|
view.getViewTranslationCallback().onShowTranslation(view);
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The method is used to handle the translation result for non-vertual views.
|
|
*/
|
|
private void onTranslationCompleted(SparseArray<ViewTranslationResponse> translatedResult) {
|
|
boolean isLoggable = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
|
|
if (mActivity.isDestroyed()) {
|
|
Log.v(TAG, "onTranslationCompleted:" + mActivity + "is destroyed.");
|
|
return;
|
|
}
|
|
final int resultCount = translatedResult.size();
|
|
if (isLoggable) {
|
|
Log.v(TAG, "onTranslationCompleted: receive " + resultCount + " responses.");
|
|
}
|
|
synchronized (mLock) {
|
|
if (mCurrentState == STATE_UI_TRANSLATION_FINISHED) {
|
|
Log.w(TAG, "onTranslationCompleted: the translation state is finished now. "
|
|
+ "Skip to show the translated text.");
|
|
return;
|
|
}
|
|
for (int i = 0; i < resultCount; i++) {
|
|
final ViewTranslationResponse response = translatedResult.valueAt(i);
|
|
if (isLoggable) {
|
|
Log.v(TAG, "onTranslationCompleted: "
|
|
+ sanitizedViewTranslationResponse(response));
|
|
}
|
|
final AutofillId autofillId = response.getAutofillId();
|
|
if (autofillId == null) {
|
|
Log.w(TAG, "No AutofillId is set in ViewTranslationResponse");
|
|
continue;
|
|
}
|
|
final WeakReference<View> viewRef = mViews.get(autofillId);
|
|
if (viewRef == null) {
|
|
continue;
|
|
}
|
|
final View view = viewRef.get();
|
|
if (view == null) {
|
|
Log.w(TAG, "onTranslationCompleted: the view for autofill id " + autofillId
|
|
+ " may be gone.");
|
|
continue;
|
|
}
|
|
int currentState;
|
|
currentState = mCurrentState;
|
|
mActivity.runOnUiThread(() -> {
|
|
ViewTranslationCallback callback = view.getViewTranslationCallback();
|
|
if (view.getViewTranslationResponse() != null
|
|
&& view.getViewTranslationResponse().equals(response)) {
|
|
if (callback instanceof TextViewTranslationCallback) {
|
|
TextViewTranslationCallback textViewCallback =
|
|
(TextViewTranslationCallback) callback;
|
|
if (textViewCallback.isShowingTranslation()
|
|
|| textViewCallback.isAnimationRunning()) {
|
|
if (isLoggable) {
|
|
Log.d(TAG, "Duplicate ViewTranslationResponse for " + autofillId
|
|
+ ". Ignoring.");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (callback == null) {
|
|
if (view instanceof TextView) {
|
|
// developer doesn't provide their override, we set the default TextView
|
|
// implementation.
|
|
callback = new TextViewTranslationCallback();
|
|
view.setViewTranslationCallback(callback);
|
|
} else {
|
|
if (isLoggable) {
|
|
Log.d(TAG, view + " doesn't support showing translation because of "
|
|
+ "null ViewTranslationCallback.");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
callback.setAnimationDurationMillis(ANIMATION_DURATION_MILLIS);
|
|
if (mViewsToPadContent.contains(autofillId)) {
|
|
callback.enableContentPadding();
|
|
}
|
|
view.onViewTranslationResponse(response);
|
|
if (currentState == STATE_UI_TRANSLATION_PAUSED) {
|
|
return;
|
|
}
|
|
callback.onShowTranslation(view);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates a Translator for the given source and target translation specs and start the ui
|
|
* translation when the Translator is created successfully.
|
|
*/
|
|
@WorkerThread
|
|
private void createTranslatorAndStart(TranslationSpec sourceSpec, TranslationSpec targetSpec,
|
|
List<AutofillId> views) {
|
|
// Create Translator
|
|
final Translator translator = createTranslatorIfNeeded(sourceSpec, targetSpec);
|
|
if (translator == null) {
|
|
Log.w(TAG, "Can not create Translator for sourceSpec:" + sourceSpec + " targetSpec:"
|
|
+ targetSpec);
|
|
return;
|
|
}
|
|
onUiTranslationStarted(translator, views);
|
|
}
|
|
|
|
@WorkerThread
|
|
private void sendTranslationRequest(Translator translator,
|
|
List<ViewTranslationRequest> requests) {
|
|
if (requests.size() == 0) {
|
|
Log.w(TAG, "No ViewTranslationRequest was collected.");
|
|
return;
|
|
}
|
|
final TranslationRequest request = new TranslationRequest.Builder()
|
|
.setViewTranslationRequests(requests)
|
|
.build();
|
|
if (Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG)) {
|
|
StringBuilder msg = new StringBuilder("sendTranslationRequest:{requests=[");
|
|
for (ViewTranslationRequest viewRequest: requests) {
|
|
msg.append("{request=")
|
|
.append(sanitizedViewTranslationRequest(viewRequest))
|
|
.append("}, ");
|
|
}
|
|
Log.d(TAG, "sendTranslationRequest: " + msg.toString());
|
|
}
|
|
translator.requestUiTranslate(request, (r) -> r.run(), this::onTranslationCompleted);
|
|
}
|
|
|
|
/**
|
|
* Called when there is an ui translation request comes to request view translation.
|
|
*/
|
|
private void onUiTranslationStarted(Translator translator, List<AutofillId> views) {
|
|
synchronized (mLock) {
|
|
// Filter the request views' AutofillId
|
|
SparseIntArray virtualViewChildCount = getRequestVirtualViewChildCount(views);
|
|
Map<AutofillId, long[]> viewIds = new ArrayMap<>();
|
|
Map<AutofillId, Integer> unusedIndices = null;
|
|
for (int i = 0; i < views.size(); i++) {
|
|
AutofillId autofillId = views.get(i);
|
|
if (autofillId.isNonVirtual()) {
|
|
viewIds.put(autofillId, null);
|
|
} else {
|
|
if (unusedIndices == null) {
|
|
unusedIndices = new ArrayMap<>();
|
|
}
|
|
// The virtual id get from content capture is long, see getVirtualChildLongId()
|
|
// e.g. 1001, 1001:2, 1002:1 -> 1001, <1,2>; 1002, <1>
|
|
AutofillId virtualViewAutofillId = new AutofillId(autofillId.getViewId());
|
|
long[] childs;
|
|
int end = 0;
|
|
if (viewIds.containsKey(virtualViewAutofillId)) {
|
|
childs = viewIds.get(virtualViewAutofillId);
|
|
end = unusedIndices.get(virtualViewAutofillId);
|
|
} else {
|
|
int childCount = virtualViewChildCount.get(autofillId.getViewId());
|
|
childs = new long[childCount];
|
|
viewIds.put(virtualViewAutofillId, childs);
|
|
}
|
|
unusedIndices.put(virtualViewAutofillId, end + 1);
|
|
childs[end] = autofillId.getVirtualChildLongId();
|
|
}
|
|
}
|
|
ArrayList<ViewTranslationRequest> requests = new ArrayList<>();
|
|
int[] supportedFormats = getSupportedFormatsLocked();
|
|
ArrayList<ViewRootImpl> roots =
|
|
WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
|
|
TranslationCapability capability =
|
|
getTranslationCapability(translator.getTranslationContext());
|
|
mActivity.runOnUiThread(() -> {
|
|
// traverse the hierarchy to collect ViewTranslationRequests
|
|
for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
|
|
View rootView = roots.get(rootNum).getView();
|
|
rootView.dispatchCreateViewTranslationRequest(viewIds, supportedFormats,
|
|
capability, requests);
|
|
}
|
|
mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
|
|
UiTranslationController::sendTranslationRequest,
|
|
UiTranslationController.this, translator, requests));
|
|
});
|
|
}
|
|
}
|
|
|
|
private SparseIntArray getRequestVirtualViewChildCount(List<AutofillId> views) {
|
|
SparseIntArray virtualViewCount = new SparseIntArray();
|
|
for (int i = 0; i < views.size(); i++) {
|
|
AutofillId autofillId = views.get(i);
|
|
if (!autofillId.isNonVirtual()) {
|
|
int virtualViewId = autofillId.getViewId();
|
|
if (virtualViewCount.indexOfKey(virtualViewId) < 0) {
|
|
virtualViewCount.put(virtualViewId, 1);
|
|
} else {
|
|
virtualViewCount.put(virtualViewId, (virtualViewCount.get(virtualViewId) + 1));
|
|
}
|
|
}
|
|
}
|
|
return virtualViewCount;
|
|
}
|
|
|
|
private int[] getSupportedFormatsLocked() {
|
|
// We only support text now
|
|
return new int[] {TranslationSpec.DATA_FORMAT_TEXT};
|
|
}
|
|
|
|
private TranslationCapability getTranslationCapability(TranslationContext translationContext) {
|
|
// We only support text to text capability now, we will query real status from service when
|
|
// we support more translation capabilities.
|
|
return new TranslationCapability(TranslationCapability.STATE_ON_DEVICE,
|
|
translationContext.getSourceSpec(),
|
|
translationContext.getTargetSpec(), /* uiTranslationEnabled= */ true,
|
|
/* supportedTranslationFlags= */ 0);
|
|
}
|
|
|
|
private void findViewsTraversalByAutofillIds(IntArray sourceViewIds) {
|
|
final ArrayList<ViewRootImpl> roots =
|
|
WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
|
|
for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
|
|
final View rootView = roots.get(rootNum).getView();
|
|
if (rootView instanceof ViewGroup) {
|
|
findViewsTraversalByAutofillIds((ViewGroup) rootView, sourceViewIds);
|
|
}
|
|
addViewIfNeeded(sourceViewIds, rootView);
|
|
}
|
|
}
|
|
|
|
private void findViewsTraversalByAutofillIds(ViewGroup viewGroup,
|
|
IntArray sourceViewIds) {
|
|
final int childCount = viewGroup.getChildCount();
|
|
for (int i = 0; i < childCount; ++i) {
|
|
final View child = viewGroup.getChildAt(i);
|
|
if (child instanceof ViewGroup) {
|
|
findViewsTraversalByAutofillIds((ViewGroup) child, sourceViewIds);
|
|
}
|
|
addViewIfNeeded(sourceViewIds, child);
|
|
}
|
|
}
|
|
|
|
private void addViewIfNeeded(IntArray sourceViewIds, View view) {
|
|
final AutofillId autofillId = view.getAutofillId();
|
|
if (autofillId != null && (sourceViewIds.indexOf(autofillId.getViewId()) >= 0)
|
|
&& !mViews.containsKey(autofillId)) {
|
|
mViews.put(autofillId, new WeakReference<>(view));
|
|
}
|
|
}
|
|
|
|
private void runForEachView(BiConsumer<View, ViewTranslationCallback> action) {
|
|
synchronized (mLock) {
|
|
boolean isLoggable = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
|
|
final ArrayMap<AutofillId, WeakReference<View>> views = new ArrayMap<>(mViews);
|
|
if (views.size() == 0) {
|
|
Log.w(TAG, "No views can be excuted for runForEachView.");
|
|
}
|
|
mActivity.runOnUiThread(() -> {
|
|
final int viewCounts = views.size();
|
|
for (int i = 0; i < viewCounts; i++) {
|
|
final View view = views.valueAt(i).get();
|
|
if (isLoggable) {
|
|
Log.d(TAG, "runForEachView for autofillId = " + (view != null
|
|
? view.getAutofillId() : " null"));
|
|
}
|
|
if (view == null || view.getViewTranslationCallback() == null) {
|
|
if (isLoggable) {
|
|
Log.d(TAG, "View was gone or ViewTranslationCallback for autofillId "
|
|
+ "= " + views.keyAt(i));
|
|
}
|
|
continue;
|
|
}
|
|
action.accept(view, view.getViewTranslationCallback());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
private Translator createTranslatorIfNeeded(
|
|
TranslationSpec sourceSpec, TranslationSpec targetSpec) {
|
|
final TranslationManager tm = mContext.getSystemService(TranslationManager.class);
|
|
if (tm == null) {
|
|
Log.e(TAG, "Can not find TranslationManager when trying to create translator.");
|
|
return null;
|
|
}
|
|
final TranslationContext translationContext =
|
|
new TranslationContext.Builder(sourceSpec, targetSpec)
|
|
.setActivityId(
|
|
new ActivityId(
|
|
mActivity.getTaskId(),
|
|
mActivity.getShareableActivityToken()))
|
|
.build();
|
|
final Translator translator = tm.createTranslator(translationContext);
|
|
if (translator != null) {
|
|
final Pair<TranslationSpec, TranslationSpec> specs = new Pair<>(sourceSpec, targetSpec);
|
|
mTranslators.put(specs, translator);
|
|
}
|
|
return translator;
|
|
}
|
|
|
|
private void destroyTranslators() {
|
|
synchronized (mLock) {
|
|
final int count = mTranslators.size();
|
|
for (int i = 0; i < count; i++) {
|
|
Translator translator = mTranslators.valueAt(i);
|
|
translator.destroy();
|
|
}
|
|
mTranslators.clear();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a string representation of the state.
|
|
*/
|
|
public static String stateToString(@UiTranslationState int state) {
|
|
switch (state) {
|
|
case STATE_UI_TRANSLATION_STARTED:
|
|
return "UI_TRANSLATION_STARTED";
|
|
case STATE_UI_TRANSLATION_PAUSED:
|
|
return "UI_TRANSLATION_PAUSED";
|
|
case STATE_UI_TRANSLATION_RESUMED:
|
|
return "UI_TRANSLATION_RESUMED";
|
|
case STATE_UI_TRANSLATION_FINISHED:
|
|
return "UI_TRANSLATION_FINISHED";
|
|
default:
|
|
return "Unknown state (" + state + ")";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns a sanitized string representation of {@link ViewTranslationRequest};
|
|
*/
|
|
private static String sanitizedViewTranslationRequest(@NonNull ViewTranslationRequest request) {
|
|
StringBuilder msg = new StringBuilder("ViewTranslationRequest:{values=[");
|
|
for (String key: request.getKeys()) {
|
|
final TranslationRequestValue value = request.getValue(key);
|
|
msg.append("{text=").append(value.getText() == null
|
|
? "null"
|
|
: "string[" + value.getText().length() + "]}, ");
|
|
}
|
|
return msg.toString();
|
|
}
|
|
|
|
/**
|
|
* Returns a sanitized string representation of {@link ViewTranslationResponse};
|
|
*/
|
|
private static String sanitizedViewTranslationResponse(
|
|
@NonNull ViewTranslationResponse response) {
|
|
StringBuilder msg = new StringBuilder("ViewTranslationResponse:{values=[");
|
|
for (String key: response.getKeys()) {
|
|
final TranslationResponseValue value = response.getValue(key);
|
|
msg.append("{status=").append(value.getStatusCode()).append(", ");
|
|
msg.append("text=").append(value.getText() == null
|
|
? "null"
|
|
: "string[" + value.getText().length() + "], ");
|
|
final Bundle definitions =
|
|
(Bundle) value.getExtras().get(TranslationResponseValue.EXTRA_DEFINITIONS);
|
|
if (definitions != null) {
|
|
msg.append("definitions={");
|
|
for (String partOfSpeech : definitions.keySet()) {
|
|
msg.append(partOfSpeech).append(":[");
|
|
for (CharSequence definition : definitions.getCharSequenceArray(partOfSpeech)) {
|
|
msg.append(definition == null
|
|
? "null, "
|
|
: "string[" + definition.length() + "], ");
|
|
}
|
|
msg.append("], ");
|
|
}
|
|
msg.append("}");
|
|
}
|
|
msg.append("transliteration=").append(value.getTransliteration() == null
|
|
? "null"
|
|
: "string[" + value.getTransliteration().length() + "]}, ");
|
|
}
|
|
return msg.toString();
|
|
}
|
|
}
|