• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2021 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 static android.view.translation.Helper.ANIMATION_DURATION_MILLIS;
20 import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_FINISHED;
21 import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_PAUSED;
22 import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_RESUMED;
23 import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_STARTED;
24 
25 import android.annotation.NonNull;
26 import android.annotation.WorkerThread;
27 import android.app.Activity;
28 import android.app.assist.ActivityId;
29 import android.content.Context;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.HandlerThread;
33 import android.os.Process;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 import android.util.Dumpable;
37 import android.util.IntArray;
38 import android.util.Log;
39 import android.util.LongSparseArray;
40 import android.util.Pair;
41 import android.util.SparseArray;
42 import android.util.SparseIntArray;
43 import android.view.View;
44 import android.view.ViewGroup;
45 import android.view.ViewRootImpl;
46 import android.view.WindowManagerGlobal;
47 import android.view.autofill.AutofillId;
48 import android.view.translation.UiTranslationManager.UiTranslationState;
49 import android.widget.TextView;
50 import android.widget.TextViewTranslationCallback;
51 
52 import com.android.internal.util.function.pooled.PooledLambda;
53 
54 import java.io.PrintWriter;
55 import java.lang.ref.WeakReference;
56 import java.util.ArrayList;
57 import java.util.List;
58 import java.util.Map;
59 import java.util.function.BiConsumer;
60 
61 /**
62  * A controller to manage the ui translation requests for the {@link Activity}.
63  *
64  * @hide
65  */
66 public class UiTranslationController implements Dumpable {
67 
68     public static final boolean DEBUG = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
69 
70     /** @hide */
71     public static final String DUMPABLE_NAME = "UiTranslationController";
72 
73     private static final String TAG = "UiTranslationController";
74 
75     @NonNull
76     private final Activity mActivity;
77     @NonNull
78     private final Context mContext;
79     @NonNull
80     private final Object mLock = new Object();
81 
82     // Each Translator is distinguished by sourceSpec and desSepc.
83     @NonNull
84     private final ArrayMap<Pair<TranslationSpec, TranslationSpec>, Translator> mTranslators;
85     @NonNull
86     private final ArrayMap<AutofillId, WeakReference<View>> mViews;
87     /**
88      * Views for which {@link UiTranslationSpec#shouldPadContentForCompat()} is true.
89      */
90     @NonNull
91     private final ArraySet<AutofillId> mViewsToPadContent;
92     @NonNull
93     private final HandlerThread mWorkerThread;
94     @NonNull
95     private final Handler mWorkerHandler;
96     private int mCurrentState;
97     @NonNull
98     private ArraySet<AutofillId> mLastRequestAutofillIds;
99 
UiTranslationController(Activity activity, Context context)100     public UiTranslationController(Activity activity, Context context) {
101         mActivity = activity;
102         mContext = context;
103         mViews = new ArrayMap<>();
104         mTranslators = new ArrayMap<>();
105         mViewsToPadContent = new ArraySet<>();
106 
107         mWorkerThread =
108                 new HandlerThread("UiTranslationController_" + mActivity.getComponentName(),
109                         Process.THREAD_PRIORITY_FOREGROUND);
110         mWorkerThread.start();
111         mWorkerHandler = mWorkerThread.getThreadHandler();
112         activity.addDumpable(this);
113     }
114 
115     /**
116      * Update the Ui translation state.
117      */
updateUiTranslationState(@iTranslationState int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, List<AutofillId> views, UiTranslationSpec uiTranslationSpec)118     public void updateUiTranslationState(@UiTranslationState int state, TranslationSpec sourceSpec,
119             TranslationSpec targetSpec, List<AutofillId> views,
120             UiTranslationSpec uiTranslationSpec) {
121         if (mActivity.isDestroyed()) {
122             Log.i(TAG, "Cannot update " + stateToString(state) + " for destroyed " + mActivity);
123             return;
124         }
125         Log.i(TAG, "updateUiTranslationState state: " + stateToString(state)
126                 + (DEBUG ? (", views: " + views + ", spec: " + uiTranslationSpec) : ""));
127         synchronized (mLock) {
128             mCurrentState = state;
129             if (views != null) {
130                 setLastRequestAutofillIdsLocked(views);
131             }
132         }
133         switch (state) {
134             case STATE_UI_TRANSLATION_STARTED:
135                 if (uiTranslationSpec != null && uiTranslationSpec.shouldPadContentForCompat()) {
136                     synchronized (mLock) {
137                         mViewsToPadContent.addAll(views);
138                         // TODO: Cleanup disappeared views from mViews and mViewsToPadContent at
139                         //  some appropriate place.
140                     }
141                 }
142                 final Pair<TranslationSpec, TranslationSpec> specs =
143                         new Pair<>(sourceSpec, targetSpec);
144                 if (!mTranslators.containsKey(specs)) {
145                     mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
146                             UiTranslationController::createTranslatorAndStart,
147                             UiTranslationController.this, sourceSpec, targetSpec, views));
148                 } else {
149                     onUiTranslationStarted(mTranslators.get(specs), views);
150                 }
151                 break;
152             case STATE_UI_TRANSLATION_PAUSED:
153                 runForEachView((view, callback) -> callback.onHideTranslation(view));
154                 break;
155             case STATE_UI_TRANSLATION_RESUMED:
156                 runForEachView((view, callback) -> callback.onShowTranslation(view));
157                 break;
158             case STATE_UI_TRANSLATION_FINISHED:
159                 destroyTranslators();
160                 runForEachView((view, callback) -> {
161                     view.clearTranslationState();
162                 });
163                 notifyTranslationFinished(/* activityDestroyed= */ false);
164                 synchronized (mLock) {
165                     mViews.clear();
166                 }
167                 break;
168             default:
169                 Log.w(TAG, "onAutoTranslationStateChange(): unknown state: " + state);
170         }
171     }
172 
173     /**
174      * Called when the Activity is destroyed.
175      */
onActivityDestroyed()176     public void onActivityDestroyed() {
177         synchronized (mLock) {
178             Log.i(TAG, "onActivityDestroyed(): mCurrentState is " + stateToString(mCurrentState));
179             if (mCurrentState != STATE_UI_TRANSLATION_FINISHED) {
180                 notifyTranslationFinished(/* activityDestroyed= */ true);
181             }
182             mViews.clear();
183             destroyTranslators();
184             mWorkerThread.quitSafely();
185         }
186     }
187 
notifyTranslationFinished(boolean activityDestroyed)188     private void notifyTranslationFinished(boolean activityDestroyed) {
189         UiTranslationManager manager = mContext.getSystemService(UiTranslationManager.class);
190         if (manager != null) {
191             manager.onTranslationFinished(activityDestroyed,
192                     new ActivityId(mActivity.getTaskId(), mActivity.getShareableActivityToken()),
193                     mActivity.getComponentName());
194         }
195     }
196 
setLastRequestAutofillIdsLocked(List<AutofillId> views)197     private void setLastRequestAutofillIdsLocked(List<AutofillId> views) {
198         if (mLastRequestAutofillIds == null) {
199             mLastRequestAutofillIds = new ArraySet<>();
200         }
201         if (mLastRequestAutofillIds.size() > 0) {
202             mLastRequestAutofillIds.clear();
203         }
204         mLastRequestAutofillIds.addAll(views);
205     }
206 
207     @Override
getDumpableName()208     public String getDumpableName() {
209         return DUMPABLE_NAME;
210     }
211 
212     @Override
dump(PrintWriter pw, String[] args)213     public void dump(PrintWriter pw, String[] args) {
214         String outerPrefix = "";
215         pw.print(outerPrefix); pw.println("UiTranslationController:");
216         final String pfx = outerPrefix + "  ";
217         pw.print(pfx); pw.print("activity: "); pw.print(mActivity);
218         pw.print(pfx); pw.print("resumed: "); pw.println(mActivity.isResumed());
219         pw.print(pfx); pw.print("current state: "); pw.println(mCurrentState);
220         final int translatorSize = mTranslators.size();
221         pw.print(outerPrefix); pw.print("number translator: "); pw.println(translatorSize);
222         for (int i = 0; i < translatorSize; i++) {
223             pw.print(outerPrefix); pw.print("#"); pw.println(i);
224             final Translator translator = mTranslators.valueAt(i);
225             translator.dump(outerPrefix, pw);
226             pw.println();
227         }
228         synchronized (mLock) {
229             final int viewSize = mViews.size();
230             pw.print(outerPrefix); pw.print("number views: "); pw.println(viewSize);
231             for (int i = 0; i < viewSize; i++) {
232                 pw.print(outerPrefix); pw.print("#"); pw.println(i);
233                 final AutofillId autofillId = mViews.keyAt(i);
234                 final View view = mViews.valueAt(i).get();
235                 pw.print(pfx); pw.print("autofillId: "); pw.println(autofillId);
236                 pw.print(pfx); pw.print("view:"); pw.println(view);
237             }
238             pw.print(outerPrefix); pw.print("padded views: "); pw.println(mViewsToPadContent);
239         }
240         if (DEBUG) {
241             dumpViewByTraversal(outerPrefix, pw);
242         }
243     }
244 
dumpViewByTraversal(String outerPrefix, PrintWriter pw)245     private void dumpViewByTraversal(String outerPrefix, PrintWriter pw) {
246         final ArrayList<ViewRootImpl> roots =
247                 WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
248         pw.print(outerPrefix); pw.println("Dump views:");
249         for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
250             final View rootView = roots.get(rootNum).getView();
251             if (rootView instanceof ViewGroup) {
252                 dumpChildren((ViewGroup) rootView, outerPrefix, pw);
253             } else {
254                 dumpViewInfo(rootView, outerPrefix, pw);
255             }
256         }
257     }
258 
dumpChildren(ViewGroup viewGroup, String outerPrefix, PrintWriter pw)259     private void dumpChildren(ViewGroup viewGroup, String outerPrefix, PrintWriter pw) {
260         final int childCount = viewGroup.getChildCount();
261         for (int i = 0; i < childCount; ++i) {
262             final View child = viewGroup.getChildAt(i);
263             if (child instanceof ViewGroup) {
264                 pw.print(outerPrefix); pw.println("Children: ");
265                 pw.print(outerPrefix); pw.print(outerPrefix); pw.println(child);
266                 dumpChildren((ViewGroup) child, outerPrefix, pw);
267             } else {
268                 pw.print(outerPrefix); pw.println("End Children: ");
269                 pw.print(outerPrefix); pw.print(outerPrefix); pw.print(child);
270                 dumpViewInfo(child, outerPrefix, pw);
271             }
272         }
273     }
274 
dumpViewInfo(View view, String outerPrefix, PrintWriter pw)275     private void dumpViewInfo(View view, String outerPrefix, PrintWriter pw) {
276         final AutofillId autofillId = view.getAutofillId();
277         pw.print(outerPrefix); pw.print("autofillId: "); pw.print(autofillId);
278         // TODO: print TranslationTransformation
279         boolean isContainsView = false;
280         boolean isRequestedView = false;
281         synchronized (mLock) {
282             if (mLastRequestAutofillIds.contains(autofillId)) {
283                 isRequestedView = true;
284             }
285             final WeakReference<View> viewRef = mViews.get(autofillId);
286             if (viewRef != null && viewRef.get() != null) {
287                 isContainsView = true;
288             }
289         }
290         pw.print(outerPrefix); pw.print("isContainsView: "); pw.print(isContainsView);
291         pw.print(outerPrefix); pw.print("isRequestedView: "); pw.println(isRequestedView);
292     }
293 
294     /**
295      * The method is used by {@link Translator}, it will be called when the translation is done. The
296      * translation result can be get from here.
297      */
onTranslationCompleted(TranslationResponse response)298     public void onTranslationCompleted(TranslationResponse response) {
299         if (response == null || response.getTranslationStatus()
300                 != TranslationResponse.TRANSLATION_STATUS_SUCCESS) {
301             Log.w(TAG, "Fail result from TranslationService, status=" + (response == null
302                     ? "null"
303                     : response.getTranslationStatus()));
304             return;
305         }
306         final SparseArray<ViewTranslationResponse> translatedResult =
307                 response.getViewTranslationResponses();
308         final SparseArray<ViewTranslationResponse> viewsResult = new SparseArray<>();
309         final SparseArray<LongSparseArray<ViewTranslationResponse>> virtualViewsResult =
310                 new SparseArray<>();
311         final IntArray viewIds = new IntArray(1);
312         for (int i = 0; i < translatedResult.size(); i++) {
313             final ViewTranslationResponse result = translatedResult.valueAt(i);
314             final AutofillId autofillId = result.getAutofillId();
315             if (viewIds.indexOf(autofillId.getViewId()) < 0) {
316                 viewIds.add(autofillId.getViewId());
317             }
318             if (autofillId.isNonVirtual()) {
319                 viewsResult.put(translatedResult.keyAt(i), result);
320             } else {
321                 final boolean isVirtualViewAdded =
322                         virtualViewsResult.indexOfKey(autofillId.getViewId()) >= 0;
323                 final LongSparseArray<ViewTranslationResponse> childIds =
324                         isVirtualViewAdded ? virtualViewsResult.get(autofillId.getViewId())
325                                 : new LongSparseArray<>();
326                 childIds.put(autofillId.getVirtualChildLongId(), result);
327                 if (!isVirtualViewAdded) {
328                     virtualViewsResult.put(autofillId.getViewId(), childIds);
329                 }
330             }
331         }
332         // Traverse tree and get views by the responsed AutofillId
333         findViewsTraversalByAutofillIds(viewIds);
334 
335         if (viewsResult.size() > 0) {
336             onTranslationCompleted(viewsResult);
337         }
338         if (virtualViewsResult.size() > 0) {
339             onVirtualViewTranslationCompleted(virtualViewsResult);
340         }
341     }
342 
343     /**
344      * The method is used to handle the translation result for the vertual views.
345      */
onVirtualViewTranslationCompleted( SparseArray<LongSparseArray<ViewTranslationResponse>> translatedResult)346     private void onVirtualViewTranslationCompleted(
347             SparseArray<LongSparseArray<ViewTranslationResponse>> translatedResult) {
348         if (mActivity.isDestroyed()) {
349             Log.v(TAG, "onTranslationCompleted:" + mActivity + "is destroyed.");
350             return;
351         }
352         synchronized (mLock) {
353             if (mCurrentState == STATE_UI_TRANSLATION_FINISHED) {
354                 Log.w(TAG, "onTranslationCompleted: the translation state is finished now. "
355                         + "Skip to show the translated text.");
356                 return;
357             }
358             for (int i = 0; i < translatedResult.size(); i++) {
359                 final AutofillId autofillId = new AutofillId(translatedResult.keyAt(i));
360                 final WeakReference<View> viewRef = mViews.get(autofillId);
361                 if (viewRef == null) {
362                     continue;
363                 }
364                 final View view = viewRef.get();
365                 if (view == null) {
366                     Log.w(TAG, "onTranslationCompleted: the view for autofill id " + autofillId
367                             + " may be gone.");
368                     continue;
369                 }
370                 final LongSparseArray<ViewTranslationResponse> virtualChildResponse =
371                         translatedResult.valueAt(i);
372                 if (DEBUG) {
373                     Log.v(TAG, "onVirtualViewTranslationCompleted: received response for "
374                             + "AutofillId " + autofillId);
375                 }
376                 view.onVirtualViewTranslationResponses(virtualChildResponse);
377                 if (mCurrentState == STATE_UI_TRANSLATION_PAUSED) {
378                     return;
379                 }
380                 mActivity.runOnUiThread(() -> {
381                     if (view.getViewTranslationCallback() == null) {
382                         if (DEBUG) {
383                             Log.d(TAG, view + " doesn't support showing translation because of "
384                                     + "null ViewTranslationCallback.");
385                         }
386                         return;
387                     }
388                     if (view.getViewTranslationCallback() != null) {
389                         view.getViewTranslationCallback().onShowTranslation(view);
390                     }
391                 });
392             }
393         }
394     }
395 
396     /**
397      * The method is used to handle the translation result for non-vertual views.
398      */
onTranslationCompleted(SparseArray<ViewTranslationResponse> translatedResult)399     private void onTranslationCompleted(SparseArray<ViewTranslationResponse> translatedResult) {
400         if (mActivity.isDestroyed()) {
401             Log.v(TAG, "onTranslationCompleted:" + mActivity + "is destroyed.");
402             return;
403         }
404         final int resultCount = translatedResult.size();
405         if (DEBUG) {
406             Log.v(TAG, "onTranslationCompleted: receive " + resultCount + " responses.");
407         }
408         synchronized (mLock) {
409             if (mCurrentState == STATE_UI_TRANSLATION_FINISHED) {
410                 Log.w(TAG, "onTranslationCompleted: the translation state is finished now. "
411                         + "Skip to show the translated text.");
412                 return;
413             }
414             for (int i = 0; i < resultCount; i++) {
415                 final ViewTranslationResponse response = translatedResult.valueAt(i);
416                 if (DEBUG) {
417                     Log.v(TAG, "onTranslationCompleted: "
418                             + sanitizedViewTranslationResponse(response));
419                 }
420                 final AutofillId autofillId = response.getAutofillId();
421                 if (autofillId == null) {
422                     Log.w(TAG, "No AutofillId is set in ViewTranslationResponse");
423                     continue;
424                 }
425                 final WeakReference<View> viewRef = mViews.get(autofillId);
426                 if (viewRef == null) {
427                     continue;
428                 }
429                 final View view = viewRef.get();
430                 if (view == null) {
431                     Log.w(TAG, "onTranslationCompleted: the view for autofill id " + autofillId
432                             + " may be gone.");
433                     continue;
434                 }
435                 int currentState;
436                 currentState = mCurrentState;
437                 mActivity.runOnUiThread(() -> {
438                     ViewTranslationCallback callback = view.getViewTranslationCallback();
439                     if (view.getViewTranslationResponse() != null
440                             && view.getViewTranslationResponse().equals(response)) {
441                         if (callback instanceof TextViewTranslationCallback) {
442                             TextViewTranslationCallback textViewCallback =
443                                     (TextViewTranslationCallback) callback;
444                             if (textViewCallback.isShowingTranslation()
445                                     || textViewCallback.isAnimationRunning()) {
446                                 if (DEBUG) {
447                                     Log.d(TAG, "Duplicate ViewTranslationResponse for " + autofillId
448                                             + ". Ignoring.");
449                                 }
450                                 return;
451                             }
452                         }
453                     }
454                     if (callback == null) {
455                         if (view instanceof TextView) {
456                             // developer doesn't provide their override, we set the default TextView
457                             // implementation.
458                             callback = new TextViewTranslationCallback();
459                             view.setViewTranslationCallback(callback);
460                         } else {
461                             if (DEBUG) {
462                                 Log.d(TAG, view + " doesn't support showing translation because of "
463                                         + "null ViewTranslationCallback.");
464                             }
465                             return;
466                         }
467                     }
468                     callback.setAnimationDurationMillis(ANIMATION_DURATION_MILLIS);
469                     if (mViewsToPadContent.contains(autofillId)) {
470                         callback.enableContentPadding();
471                     }
472                     view.onViewTranslationResponse(response);
473                     if (currentState == STATE_UI_TRANSLATION_PAUSED) {
474                         return;
475                     }
476                     callback.onShowTranslation(view);
477                 });
478             }
479         }
480     }
481 
482     /**
483      * Creates a Translator for the given source and target translation specs and start the ui
484      * translation when the Translator is created successfully.
485      */
486     @WorkerThread
createTranslatorAndStart(TranslationSpec sourceSpec, TranslationSpec targetSpec, List<AutofillId> views)487     private void createTranslatorAndStart(TranslationSpec sourceSpec, TranslationSpec targetSpec,
488             List<AutofillId> views) {
489         // Create Translator
490         final Translator translator = createTranslatorIfNeeded(sourceSpec, targetSpec);
491         if (translator == null) {
492             Log.w(TAG, "Can not create Translator for sourceSpec:" + sourceSpec + " targetSpec:"
493                     + targetSpec);
494             return;
495         }
496         onUiTranslationStarted(translator, views);
497     }
498 
499     @WorkerThread
sendTranslationRequest(Translator translator, List<ViewTranslationRequest> requests)500     private void sendTranslationRequest(Translator translator,
501             List<ViewTranslationRequest> requests) {
502         if (requests.size() == 0) {
503             Log.w(TAG, "No ViewTranslationRequest was collected.");
504             return;
505         }
506         final TranslationRequest request = new TranslationRequest.Builder()
507                 .setViewTranslationRequests(requests)
508                 .build();
509         if (DEBUG) {
510             StringBuilder msg = new StringBuilder("sendTranslationRequest:{requests=[");
511             for (ViewTranslationRequest viewRequest: requests) {
512                 msg.append("{request=")
513                         .append(sanitizedViewTranslationRequest(viewRequest))
514                         .append("}, ");
515             }
516             Log.d(TAG, "sendTranslationRequest: " + msg.toString());
517         }
518         translator.requestUiTranslate(request, (r) -> r.run(), this::onTranslationCompleted);
519     }
520 
521     /**
522      * Called when there is an ui translation request comes to request view translation.
523      */
onUiTranslationStarted(Translator translator, List<AutofillId> views)524     private void onUiTranslationStarted(Translator translator, List<AutofillId> views) {
525         synchronized (mLock) {
526             // Filter the request views' AutofillId
527             SparseIntArray virtualViewChildCount = getRequestVirtualViewChildCount(views);
528             Map<AutofillId, long[]> viewIds = new ArrayMap<>();
529             Map<AutofillId, Integer> unusedIndices = null;
530             for (int i = 0; i < views.size(); i++) {
531                 AutofillId autofillId = views.get(i);
532                 if (autofillId.isNonVirtual()) {
533                     viewIds.put(autofillId, null);
534                 } else {
535                     if (unusedIndices == null) {
536                         unusedIndices = new ArrayMap<>();
537                     }
538                     // The virtual id get from content capture is long, see getVirtualChildLongId()
539                     // e.g. 1001, 1001:2, 1002:1 -> 1001, <1,2>; 1002, <1>
540                     AutofillId virtualViewAutofillId = new AutofillId(autofillId.getViewId());
541                     long[] childs;
542                     int end = 0;
543                     if (viewIds.containsKey(virtualViewAutofillId)) {
544                         childs = viewIds.get(virtualViewAutofillId);
545                         end = unusedIndices.get(virtualViewAutofillId);
546                     } else {
547                         int childCount = virtualViewChildCount.get(autofillId.getViewId());
548                         childs = new long[childCount];
549                         viewIds.put(virtualViewAutofillId, childs);
550                     }
551                     unusedIndices.put(virtualViewAutofillId, end + 1);
552                     childs[end] = autofillId.getVirtualChildLongId();
553                 }
554             }
555             ArrayList<ViewTranslationRequest> requests = new ArrayList<>();
556             int[] supportedFormats = getSupportedFormatsLocked();
557             ArrayList<ViewRootImpl> roots =
558                     WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
559             TranslationCapability capability =
560                     getTranslationCapability(translator.getTranslationContext());
561             mActivity.runOnUiThread(() -> {
562                 // traverse the hierarchy to collect ViewTranslationRequests
563                 for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
564                     View rootView = roots.get(rootNum).getView();
565                     rootView.dispatchCreateViewTranslationRequest(viewIds, supportedFormats,
566                             capability, requests);
567                 }
568                 mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
569                         UiTranslationController::sendTranslationRequest,
570                         UiTranslationController.this, translator, requests));
571             });
572         }
573     }
574 
getRequestVirtualViewChildCount(List<AutofillId> views)575     private SparseIntArray getRequestVirtualViewChildCount(List<AutofillId> views) {
576         SparseIntArray virtualViewCount = new SparseIntArray();
577         for (int i = 0; i < views.size(); i++) {
578             AutofillId autofillId = views.get(i);
579             if (!autofillId.isNonVirtual()) {
580                 int virtualViewId = autofillId.getViewId();
581                 if (virtualViewCount.indexOfKey(virtualViewId) < 0) {
582                     virtualViewCount.put(virtualViewId, 1);
583                 } else {
584                     virtualViewCount.put(virtualViewId, (virtualViewCount.get(virtualViewId) + 1));
585                 }
586             }
587         }
588         return virtualViewCount;
589     }
590 
getSupportedFormatsLocked()591     private int[] getSupportedFormatsLocked() {
592         // We only support text now
593         return new int[] {TranslationSpec.DATA_FORMAT_TEXT};
594     }
595 
getTranslationCapability(TranslationContext translationContext)596     private TranslationCapability getTranslationCapability(TranslationContext translationContext) {
597         // We only support text to text capability now, we will query real status from service when
598         // we support more translation capabilities.
599         return new TranslationCapability(TranslationCapability.STATE_ON_DEVICE,
600                 translationContext.getSourceSpec(),
601                 translationContext.getTargetSpec(), /* uiTranslationEnabled= */ true,
602                 /* supportedTranslationFlags= */ 0);
603     }
604 
findViewsTraversalByAutofillIds(IntArray sourceViewIds)605     private void findViewsTraversalByAutofillIds(IntArray sourceViewIds) {
606         final ArrayList<ViewRootImpl> roots =
607                 WindowManagerGlobal.getInstance().getRootViews(mActivity.getActivityToken());
608         for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
609             final View rootView = roots.get(rootNum).getView();
610             if (rootView instanceof ViewGroup) {
611                 findViewsTraversalByAutofillIds((ViewGroup) rootView, sourceViewIds);
612             }
613             addViewIfNeeded(sourceViewIds, rootView);
614         }
615     }
616 
findViewsTraversalByAutofillIds(ViewGroup viewGroup, IntArray sourceViewIds)617     private void findViewsTraversalByAutofillIds(ViewGroup viewGroup,
618             IntArray sourceViewIds) {
619         final int childCount = viewGroup.getChildCount();
620         for (int i = 0; i < childCount; ++i) {
621             final View child = viewGroup.getChildAt(i);
622             if (child instanceof ViewGroup) {
623                 findViewsTraversalByAutofillIds((ViewGroup) child, sourceViewIds);
624             }
625             addViewIfNeeded(sourceViewIds, child);
626         }
627     }
628 
addViewIfNeeded(IntArray sourceViewIds, View view)629     private void addViewIfNeeded(IntArray sourceViewIds, View view) {
630         final AutofillId autofillId = view.getAutofillId();
631         if (autofillId != null && (sourceViewIds.indexOf(autofillId.getViewId()) >= 0)
632                 && !mViews.containsKey(autofillId)) {
633             mViews.put(autofillId, new WeakReference<>(view));
634         }
635     }
636 
runForEachView(BiConsumer<View, ViewTranslationCallback> action)637     private void runForEachView(BiConsumer<View, ViewTranslationCallback> action) {
638         synchronized (mLock) {
639             final ArrayMap<AutofillId, WeakReference<View>> views = new ArrayMap<>(mViews);
640             if (views.size() == 0) {
641                 Log.w(TAG, "No views can be excuted for runForEachView.");
642             }
643             mActivity.runOnUiThread(() -> {
644                 final int viewCounts = views.size();
645                 for (int i = 0; i < viewCounts; i++) {
646                     final View view = views.valueAt(i).get();
647                     if (DEBUG) {
648                         Log.d(TAG, "runForEachView for autofillId = " + (view != null
649                                 ? view.getAutofillId() : " null"));
650                     }
651                     if (view == null || view.getViewTranslationCallback() == null) {
652                         if (DEBUG) {
653                             Log.d(TAG, "View was gone or ViewTranslationCallback for autofillId "
654                                     + "= " + views.keyAt(i));
655                         }
656                         continue;
657                     }
658                     action.accept(view, view.getViewTranslationCallback());
659                 }
660             });
661         }
662     }
663 
createTranslatorIfNeeded( TranslationSpec sourceSpec, TranslationSpec targetSpec)664     private Translator createTranslatorIfNeeded(
665             TranslationSpec sourceSpec, TranslationSpec targetSpec) {
666         final TranslationManager tm = mContext.getSystemService(TranslationManager.class);
667         if (tm == null) {
668             Log.e(TAG, "Can not find TranslationManager when trying to create translator.");
669             return null;
670         }
671         final TranslationContext translationContext =
672                 new TranslationContext.Builder(sourceSpec, targetSpec)
673                         .setActivityId(
674                                 new ActivityId(
675                                         mActivity.getTaskId(),
676                                         mActivity.getShareableActivityToken()))
677                         .build();
678         final Translator translator = tm.createTranslator(translationContext);
679         if (translator != null) {
680             final Pair<TranslationSpec, TranslationSpec> specs = new Pair<>(sourceSpec, targetSpec);
681             mTranslators.put(specs, translator);
682         }
683         return translator;
684     }
685 
destroyTranslators()686     private void destroyTranslators() {
687         synchronized (mLock) {
688             final int count = mTranslators.size();
689             for (int i = 0; i < count; i++) {
690                 Translator translator = mTranslators.valueAt(i);
691                 translator.destroy();
692             }
693             mTranslators.clear();
694         }
695     }
696 
697     /**
698      * Returns a string representation of the state.
699      */
stateToString(@iTranslationState int state)700     public static String stateToString(@UiTranslationState int state) {
701         switch (state) {
702             case STATE_UI_TRANSLATION_STARTED:
703                 return "UI_TRANSLATION_STARTED";
704             case STATE_UI_TRANSLATION_PAUSED:
705                 return "UI_TRANSLATION_PAUSED";
706             case STATE_UI_TRANSLATION_RESUMED:
707                 return "UI_TRANSLATION_RESUMED";
708             case STATE_UI_TRANSLATION_FINISHED:
709                 return "UI_TRANSLATION_FINISHED";
710             default:
711                 return "Unknown state (" + state + ")";
712         }
713     }
714 
715     /**
716      * Returns a sanitized string representation of {@link ViewTranslationRequest};
717      */
sanitizedViewTranslationRequest(@onNull ViewTranslationRequest request)718     private static String sanitizedViewTranslationRequest(@NonNull ViewTranslationRequest request) {
719         StringBuilder msg = new StringBuilder("ViewTranslationRequest:{values=[");
720         for (String key: request.getKeys()) {
721             final TranslationRequestValue value = request.getValue(key);
722             msg.append("{text=").append(value.getText() == null
723                     ? "null"
724                     : "string[" + value.getText().length() + "]}, ");
725         }
726         return msg.toString();
727     }
728 
729     /**
730      * Returns a sanitized string representation of {@link ViewTranslationResponse};
731      */
sanitizedViewTranslationResponse( @onNull ViewTranslationResponse response)732     private static String sanitizedViewTranslationResponse(
733             @NonNull ViewTranslationResponse response) {
734         StringBuilder msg = new StringBuilder("ViewTranslationResponse:{values=[");
735         for (String key: response.getKeys()) {
736             final TranslationResponseValue value = response.getValue(key);
737             msg.append("{status=").append(value.getStatusCode()).append(", ");
738             msg.append("text=").append(value.getText() == null
739                     ? "null"
740                     : "string[" + value.getText().length() + "], ");
741             final Bundle definitions =
742                     (Bundle) value.getExtras().get(TranslationResponseValue.EXTRA_DEFINITIONS);
743             if (definitions != null) {
744                 msg.append("definitions={");
745                 for (String partOfSpeech : definitions.keySet()) {
746                     msg.append(partOfSpeech).append(":[");
747                     for (CharSequence definition : definitions.getCharSequenceArray(partOfSpeech)) {
748                         msg.append(definition == null
749                                 ? "null, "
750                                 : "string[" + definition.length() + "], ");
751                     }
752                     msg.append("], ");
753                 }
754                 msg.append("}");
755             }
756             msg.append("transliteration=").append(value.getTransliteration() == null
757                     ? "null"
758                     : "string[" + value.getTransliteration().length() + "]}, ");
759         }
760         return msg.toString();
761     }
762 }
763