| /* |
| * 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(); |
| } |
| } |