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