1 /* 2 * Copyright (C) 2020 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 com.android.server.translation; 18 19 import static android.view.translation.TranslationManager.EXTRA_CAPABILITIES; 20 import static android.view.translation.UiTranslationManager.EXTRA_PACKAGE_NAME; 21 import static android.view.translation.UiTranslationManager.EXTRA_SOURCE_LOCALE; 22 import static android.view.translation.UiTranslationManager.EXTRA_STATE; 23 import static android.view.translation.UiTranslationManager.EXTRA_TARGET_LOCALE; 24 import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_FINISHED; 25 import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_PAUSED; 26 import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_RESUMED; 27 import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_STARTED; 28 29 import android.annotation.NonNull; 30 import android.annotation.Nullable; 31 import android.annotation.SuppressLint; 32 import android.app.Activity; 33 import android.content.ComponentName; 34 import android.content.Context; 35 import android.content.pm.PackageManager; 36 import android.content.pm.ServiceInfo; 37 import android.os.Bundle; 38 import android.os.IBinder; 39 import android.os.IRemoteCallback; 40 import android.os.RemoteCallbackList; 41 import android.os.RemoteException; 42 import android.os.ResultReceiver; 43 import android.service.translation.TranslationServiceInfo; 44 import android.util.ArrayMap; 45 import android.util.ArraySet; 46 import android.util.Log; 47 import android.util.Slog; 48 import android.view.autofill.AutofillId; 49 import android.view.inputmethod.InputMethodInfo; 50 import android.view.translation.ITranslationServiceCallback; 51 import android.view.translation.TranslationCapability; 52 import android.view.translation.TranslationContext; 53 import android.view.translation.TranslationSpec; 54 import android.view.translation.UiTranslationController; 55 import android.view.translation.UiTranslationManager.UiTranslationState; 56 import android.view.translation.UiTranslationSpec; 57 58 import com.android.internal.annotations.GuardedBy; 59 import com.android.internal.os.IResultReceiver; 60 import com.android.internal.os.TransferPipe; 61 import com.android.server.LocalServices; 62 import com.android.server.infra.AbstractPerUserSystemService; 63 import com.android.server.inputmethod.InputMethodManagerInternal; 64 import com.android.server.wm.ActivityTaskManagerInternal; 65 import com.android.server.wm.ActivityTaskManagerInternal.ActivityTokens; 66 67 import java.io.FileDescriptor; 68 import java.io.IOException; 69 import java.io.PrintWriter; 70 import java.lang.ref.WeakReference; 71 import java.util.List; 72 73 final class TranslationManagerServiceImpl extends 74 AbstractPerUserSystemService<TranslationManagerServiceImpl, TranslationManagerService> 75 implements IBinder.DeathRecipient { 76 77 private static final String TAG = "TranslationManagerServiceImpl"; 78 @SuppressLint("IsLoggableTagLength") 79 private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); 80 81 @GuardedBy("mLock") 82 @Nullable 83 private RemoteTranslationService mRemoteTranslationService; 84 85 @GuardedBy("mLock") 86 @Nullable 87 private ServiceInfo mRemoteTranslationServiceInfo; 88 89 @GuardedBy("mLock") 90 private TranslationServiceInfo mTranslationServiceInfo; 91 92 @GuardedBy("mLock") 93 private WeakReference<ActivityTokens> mLastActivityTokens; 94 95 private final ActivityTaskManagerInternal mActivityTaskManagerInternal; 96 97 private final TranslationServiceRemoteCallback mRemoteServiceCallback = 98 new TranslationServiceRemoteCallback(); 99 private final RemoteCallbackList<IRemoteCallback> mTranslationCapabilityCallbacks = 100 new RemoteCallbackList<>(); 101 private final ArraySet<IBinder> mWaitingFinishedCallbackActivities = new ArraySet<>(); 102 103 /** 104 * Key is translated activity token, value is the specification and state for the translation. 105 */ 106 @GuardedBy("mLock") 107 private final ArrayMap<IBinder, ActiveTranslation> mActiveTranslations = new ArrayMap<>(); 108 TranslationManagerServiceImpl( @onNull TranslationManagerService master, @NonNull Object lock, int userId, boolean disabled)109 protected TranslationManagerServiceImpl( 110 @NonNull TranslationManagerService master, 111 @NonNull Object lock, int userId, boolean disabled) { 112 super(master, lock, userId); 113 updateRemoteServiceLocked(); 114 mActivityTaskManagerInternal = LocalServices.getService(ActivityTaskManagerInternal.class); 115 } 116 117 @GuardedBy("mLock") 118 @Override // from PerUserSystemService newServiceInfoLocked(@onNull ComponentName serviceComponent)119 protected ServiceInfo newServiceInfoLocked(@NonNull ComponentName serviceComponent) 120 throws PackageManager.NameNotFoundException { 121 mTranslationServiceInfo = new TranslationServiceInfo(getContext(), 122 serviceComponent, isTemporaryServiceSetLocked(), mUserId); 123 mRemoteTranslationServiceInfo = mTranslationServiceInfo.getServiceInfo(); 124 return mTranslationServiceInfo.getServiceInfo(); 125 } 126 127 @GuardedBy("mLock") 128 @Override // from PerUserSystemService updateLocked(boolean disabled)129 protected boolean updateLocked(boolean disabled) { 130 final boolean enabledChanged = super.updateLocked(disabled); 131 updateRemoteServiceLocked(); 132 return enabledChanged; 133 } 134 135 /** 136 * Updates the reference to the remote service. 137 */ 138 @GuardedBy("mLock") updateRemoteServiceLocked()139 private void updateRemoteServiceLocked() { 140 if (mRemoteTranslationService != null) { 141 if (mMaster.debug) Slog.d(TAG, "updateRemoteService(): destroying old remote service"); 142 mRemoteTranslationService.unbind(); 143 mRemoteTranslationService = null; 144 } 145 } 146 147 @GuardedBy("mLock") 148 @Nullable ensureRemoteServiceLocked()149 private RemoteTranslationService ensureRemoteServiceLocked() { 150 if (mRemoteTranslationService == null) { 151 final String serviceName = getComponentNameLocked(); 152 if (serviceName == null) { 153 if (mMaster.verbose) { 154 Slog.v(TAG, "ensureRemoteServiceLocked(): no service component name."); 155 } 156 return null; 157 } 158 final ComponentName serviceComponent = ComponentName.unflattenFromString(serviceName); 159 mRemoteTranslationService = new RemoteTranslationService(getContext(), serviceComponent, 160 mUserId, /* isInstantAllowed= */ false, mRemoteServiceCallback); 161 } 162 return mRemoteTranslationService; 163 } 164 165 @GuardedBy("mLock") onTranslationCapabilitiesRequestLocked(@ranslationSpec.DataFormat int sourceFormat, @TranslationSpec.DataFormat int destFormat, @NonNull ResultReceiver resultReceiver)166 void onTranslationCapabilitiesRequestLocked(@TranslationSpec.DataFormat int sourceFormat, 167 @TranslationSpec.DataFormat int destFormat, 168 @NonNull ResultReceiver resultReceiver) { 169 final RemoteTranslationService remoteService = ensureRemoteServiceLocked(); 170 if (remoteService != null) { 171 remoteService.onTranslationCapabilitiesRequest(sourceFormat, destFormat, 172 resultReceiver); 173 } 174 } 175 registerTranslationCapabilityCallback(IRemoteCallback callback, int sourceUid)176 public void registerTranslationCapabilityCallback(IRemoteCallback callback, int sourceUid) { 177 mTranslationCapabilityCallbacks.register(callback, sourceUid); 178 ensureRemoteServiceLocked(); 179 } 180 unregisterTranslationCapabilityCallback(IRemoteCallback callback)181 public void unregisterTranslationCapabilityCallback(IRemoteCallback callback) { 182 mTranslationCapabilityCallbacks.unregister(callback); 183 } 184 185 @GuardedBy("mLock") onSessionCreatedLocked(@onNull TranslationContext translationContext, int sessionId, IResultReceiver resultReceiver)186 void onSessionCreatedLocked(@NonNull TranslationContext translationContext, int sessionId, 187 IResultReceiver resultReceiver) { 188 final RemoteTranslationService remoteService = ensureRemoteServiceLocked(); 189 if (remoteService != null) { 190 remoteService.onSessionCreated(translationContext, sessionId, resultReceiver); 191 } 192 } 193 getAppUidByComponentName(Context context, ComponentName componentName, int userId)194 private int getAppUidByComponentName(Context context, ComponentName componentName, int userId) { 195 int translatedAppUid = -1; 196 try { 197 if (componentName != null) { 198 translatedAppUid = context.getPackageManager().getApplicationInfoAsUser( 199 componentName.getPackageName(), 0, userId).uid; 200 } 201 } catch (PackageManager.NameNotFoundException e) { 202 Slog.d(TAG, "Cannot find packageManager for" + componentName); 203 } 204 return translatedAppUid; 205 } 206 207 @GuardedBy("mLock") onTranslationFinishedLocked(boolean activityDestroyed, IBinder token, ComponentName componentName)208 public void onTranslationFinishedLocked(boolean activityDestroyed, IBinder token, 209 ComponentName componentName) { 210 final int translatedAppUid = 211 getAppUidByComponentName(getContext(), componentName, getUserId()); 212 final String packageName = componentName.getPackageName(); 213 // In the Activity destroyed case, we only call onTranslationFinished() in 214 // non-finishTranslation() state. If there is a finishTranslation() call by apps, we 215 // should remove the waiting callback to avoid invoking callbacks twice. 216 if (activityDestroyed || mWaitingFinishedCallbackActivities.contains(token)) { 217 invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, 218 /* sourceSpec= */ null, /* targetSpec= */ null, 219 packageName, translatedAppUid); 220 mWaitingFinishedCallbackActivities.remove(token); 221 mActiveTranslations.remove(token); 222 } 223 } 224 225 @GuardedBy("mLock") updateUiTranslationStateLocked(@iTranslationState int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, List<AutofillId> viewIds, IBinder token, int taskId, UiTranslationSpec uiTranslationSpec)226 public void updateUiTranslationStateLocked(@UiTranslationState int state, 227 TranslationSpec sourceSpec, TranslationSpec targetSpec, List<AutofillId> viewIds, 228 IBinder token, int taskId, UiTranslationSpec uiTranslationSpec) { 229 // If the app starts a new Activity in the same task then the finish or pause API 230 // is called, the operation doesn't work if we only check task top Activity. The top 231 // Activity is the new Activity, the original Activity is paused in the same task. 232 // To make sure the operation still work, we use the token to find the target Activity in 233 // this task, not the top Activity only. 234 // 235 // Note: getAttachedNonFinishingActivityForTask() takes the shareable activity token. We 236 // call this method so that we can get the regular activity token below. 237 ActivityTokens candidateActivityTokens = 238 mActivityTaskManagerInternal.getAttachedNonFinishingActivityForTask(taskId, token); 239 if (candidateActivityTokens == null) { 240 Slog.w(TAG, "Unknown activity or it was finished to query for update " 241 + "translation state for token=" + token + " taskId=" + taskId + " for " 242 + "state= " + state); 243 return; 244 } 245 mLastActivityTokens = new WeakReference<>(candidateActivityTokens); 246 if (state == STATE_UI_TRANSLATION_FINISHED) { 247 mWaitingFinishedCallbackActivities.add(token); 248 } 249 IBinder activityToken = candidateActivityTokens.getActivityToken(); 250 try { 251 candidateActivityTokens.getApplicationThread().updateUiTranslationState( 252 activityToken, state, sourceSpec, targetSpec, 253 viewIds, uiTranslationSpec); 254 } catch (RemoteException e) { 255 Slog.w(TAG, "Update UiTranslationState fail: " + e); 256 } 257 258 ComponentName componentName = mActivityTaskManagerInternal.getActivityName(activityToken); 259 int translatedAppUid = 260 getAppUidByComponentName(getContext(), componentName, getUserId()); 261 String packageName = componentName.getPackageName(); 262 263 invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, token, 264 translatedAppUid); 265 updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, token, 266 translatedAppUid); 267 } 268 269 @GuardedBy("mLock") updateActiveTranslationsLocked(int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken, int translatedAppUid)270 private void updateActiveTranslationsLocked(int state, TranslationSpec sourceSpec, 271 TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken, 272 int translatedAppUid) { 273 // We keep track of active translations and their state so that we can: 274 // 1. Trigger callbacks that are registered after translation has started. 275 // See registerUiTranslationStateCallbackLocked(). 276 // 2. NOT trigger callbacks when the state didn't change. 277 // See invokeCallbacksIfNecessaryLocked(). 278 ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken); 279 switch (state) { 280 case STATE_UI_TRANSLATION_STARTED: { 281 if (activeTranslation == null) { 282 try { 283 shareableActivityToken.linkToDeath(this, /* flags= */ 0); 284 } catch (RemoteException e) { 285 Slog.w(TAG, "Failed to call linkToDeath for translated app with uid=" 286 + translatedAppUid + "; activity is already dead", e); 287 288 // Apps with registered callbacks were just notified that translation 289 // started. We should let them know translation is finished too. 290 invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, sourceSpec, targetSpec, 291 packageName, translatedAppUid); 292 return; 293 } 294 mActiveTranslations.put(shareableActivityToken, 295 new ActiveTranslation(sourceSpec, targetSpec, translatedAppUid, 296 packageName)); 297 } 298 break; 299 } 300 301 case STATE_UI_TRANSLATION_PAUSED: { 302 if (activeTranslation != null) { 303 activeTranslation.isPaused = true; 304 } 305 break; 306 } 307 308 case STATE_UI_TRANSLATION_RESUMED: { 309 if (activeTranslation != null) { 310 activeTranslation.isPaused = false; 311 } 312 break; 313 } 314 315 case STATE_UI_TRANSLATION_FINISHED: { 316 if (activeTranslation != null) { 317 mActiveTranslations.remove(shareableActivityToken); 318 } 319 break; 320 } 321 } 322 323 if (DEBUG) { 324 Slog.d(TAG, 325 "Updating to translation state=" + state + " for app with uid=" 326 + translatedAppUid + " packageName=" + packageName); 327 } 328 } 329 330 @GuardedBy("mLock") invokeCallbacksIfNecessaryLocked(int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken, int translatedAppUid)331 private void invokeCallbacksIfNecessaryLocked(int state, TranslationSpec sourceSpec, 332 TranslationSpec targetSpec, String packageName, IBinder shareableActivityToken, 333 int translatedAppUid) { 334 boolean shouldInvokeCallbacks = true; 335 int stateForCallbackInvocation = state; 336 337 ActiveTranslation activeTranslation = mActiveTranslations.get(shareableActivityToken); 338 if (activeTranslation == null) { 339 if (state != STATE_UI_TRANSLATION_STARTED) { 340 shouldInvokeCallbacks = false; 341 Slog.w(TAG, 342 "Updating to translation state=" + state + " for app with uid=" 343 + translatedAppUid + " packageName=" + packageName 344 + " but no active translation was found for it"); 345 } 346 } else { 347 switch (state) { 348 case STATE_UI_TRANSLATION_STARTED: { 349 boolean specsAreIdentical = activeTranslation.sourceSpec.getLocale().equals( 350 sourceSpec.getLocale()) 351 && activeTranslation.targetSpec.getLocale().equals( 352 targetSpec.getLocale()); 353 if (specsAreIdentical) { 354 if (activeTranslation.isPaused) { 355 // Ideally UiTranslationManager.resumeTranslation() should be first 356 // used to resume translation, but for the purposes of invoking the 357 // callback, we want to call onResumed() instead of onStarted(). This 358 // way there can only be one call to onStarted() for the lifetime of 359 // a translated activity and this will simplify the number of states 360 // apps have to handle. 361 stateForCallbackInvocation = STATE_UI_TRANSLATION_RESUMED; 362 } else { 363 // Don't invoke callbacks if the state or specs didn't change. For a 364 // given activity, startTranslation() will be called every time there 365 // are new views to be translated, but we don't need to repeatedly 366 // notify apps about it. 367 shouldInvokeCallbacks = false; 368 } 369 } 370 break; 371 } 372 373 case STATE_UI_TRANSLATION_PAUSED: { 374 if (activeTranslation.isPaused) { 375 // Don't invoke callbacks if the state didn't change. 376 shouldInvokeCallbacks = false; 377 } 378 break; 379 } 380 381 case STATE_UI_TRANSLATION_RESUMED: { 382 if (!activeTranslation.isPaused) { 383 // Don't invoke callbacks if the state didn't change. Either 384 // resumeTranslation() was called consecutive times, or right after 385 // startTranslation(). The latter case shouldn't happen normally, so we 386 // don't want apps to have to handle that particular transition. 387 shouldInvokeCallbacks = false; 388 } 389 break; 390 } 391 392 case STATE_UI_TRANSLATION_FINISHED: { 393 // Note: Here finishTranslation() was called but we don't want to invoke 394 // onFinished() on the callbacks. They will be invoked when 395 // UiTranslationManager.onTranslationFinished() is called (see 396 // onTranslationFinishedLocked()). 397 shouldInvokeCallbacks = false; 398 break; 399 } 400 } 401 } 402 403 if (shouldInvokeCallbacks) { 404 invokeCallbacks(stateForCallbackInvocation, sourceSpec, targetSpec, packageName, 405 translatedAppUid); 406 } 407 } 408 409 @GuardedBy("mLock") dumpLocked(String prefix, FileDescriptor fd, PrintWriter pw)410 public void dumpLocked(String prefix, FileDescriptor fd, PrintWriter pw) { 411 if (mLastActivityTokens != null) { 412 ActivityTokens activityTokens = mLastActivityTokens.get(); 413 if (activityTokens == null) { 414 return; 415 } 416 try (TransferPipe tp = new TransferPipe()) { 417 activityTokens.getApplicationThread().dumpActivity(tp.getWriteFd(), 418 activityTokens.getActivityToken(), prefix, 419 new String[]{ 420 Activity.DUMP_ARG_DUMP_DUMPABLE, 421 UiTranslationController.DUMPABLE_NAME 422 }); 423 tp.go(fd); 424 } catch (IOException e) { 425 pw.println(prefix + "Failure while dumping the activity: " + e); 426 } catch (RemoteException e) { 427 pw.println(prefix + "Got a RemoteException while dumping the activity"); 428 } 429 } else { 430 pw.print(prefix); 431 pw.println("No requested UiTranslation Activity."); 432 } 433 final int waitingFinishCallbackSize = mWaitingFinishedCallbackActivities.size(); 434 if (waitingFinishCallbackSize > 0) { 435 pw.print(prefix); 436 pw.print("number waiting finish callback activities: "); 437 pw.println(waitingFinishCallbackSize); 438 for (IBinder activityToken : mWaitingFinishedCallbackActivities) { 439 pw.print(prefix); 440 pw.print("shareableActivityToken: "); 441 pw.println(activityToken); 442 } 443 } 444 } 445 invokeCallbacks( int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName, int translatedAppUid)446 private void invokeCallbacks( 447 int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName, 448 int translatedAppUid) { 449 Bundle result = createResultForCallback(state, sourceSpec, targetSpec, packageName); 450 int registeredCallbackCount = mCallbacks.getRegisteredCallbackCount(); 451 if (DEBUG) { 452 Slog.d(TAG, "Invoking " + registeredCallbackCount + " callbacks for translation state=" 453 + state + " for app with uid=" + translatedAppUid 454 + " packageName=" + packageName); 455 } 456 457 if (registeredCallbackCount == 0) { 458 return; 459 } 460 List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods(); 461 mCallbacks.broadcast((callback, uid) -> { 462 invokeCallback((int) uid, translatedAppUid, callback, result, enabledInputMethods); 463 }); 464 } 465 getEnabledInputMethods()466 private List<InputMethodInfo> getEnabledInputMethods() { 467 return LocalServices.getService(InputMethodManagerInternal.class) 468 .getEnabledInputMethodListAsUser(mUserId); 469 } 470 createResultForCallback( int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName)471 private Bundle createResultForCallback( 472 int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName) { 473 Bundle result = new Bundle(); 474 result.putInt(EXTRA_STATE, state); 475 // TODO(177500482): Store the locale pair so it can be sent for RESUME events. 476 if (sourceSpec != null) { 477 result.putSerializable(EXTRA_SOURCE_LOCALE, sourceSpec.getLocale()); 478 result.putSerializable(EXTRA_TARGET_LOCALE, targetSpec.getLocale()); 479 } 480 result.putString(EXTRA_PACKAGE_NAME, packageName); 481 return result; 482 } 483 invokeCallback( int callbackSourceUid, int translatedAppUid, IRemoteCallback callback, Bundle result, List<InputMethodInfo> enabledInputMethods)484 private void invokeCallback( 485 int callbackSourceUid, int translatedAppUid, IRemoteCallback callback, 486 Bundle result, List<InputMethodInfo> enabledInputMethods) { 487 if (callbackSourceUid == translatedAppUid) { 488 // Invoke callback for the application being translated. 489 try { 490 callback.sendResult(result); 491 } catch (RemoteException e) { 492 Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e); 493 } 494 return; 495 } 496 497 // TODO(177500482): Only support the *current* Input Method. 498 // Code here is non-optimal since it's temporary.. 499 boolean isIme = false; 500 for (InputMethodInfo inputMethod : enabledInputMethods) { 501 if (callbackSourceUid == inputMethod.getServiceInfo().applicationInfo.uid) { 502 isIme = true; 503 break; 504 } 505 } 506 507 if (!isIme) { 508 return; 509 } 510 try { 511 callback.sendResult(result); 512 } catch (RemoteException e) { 513 Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e); 514 } 515 } 516 517 @GuardedBy("mLock") registerUiTranslationStateCallbackLocked(IRemoteCallback callback, int sourceUid)518 public void registerUiTranslationStateCallbackLocked(IRemoteCallback callback, int sourceUid) { 519 mCallbacks.register(callback, sourceUid); 520 int numActiveTranslations = mActiveTranslations.size(); 521 Slog.i(TAG, "New registered callback for sourceUid=" + sourceUid + " with currently " 522 + numActiveTranslations + " active translations"); 523 if (numActiveTranslations == 0) { 524 return; 525 } 526 527 // Trigger the callback for already active translations. 528 List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods(); 529 for (int i = 0; i < mActiveTranslations.size(); i++) { 530 ActiveTranslation activeTranslation = mActiveTranslations.valueAt(i); 531 int translatedAppUid = activeTranslation.translatedAppUid; 532 String packageName = activeTranslation.packageName; 533 if (DEBUG) { 534 Slog.d(TAG, "Triggering callback for sourceUid=" + sourceUid 535 + " for translated app with uid=" + translatedAppUid 536 + "packageName=" + packageName + " isPaused=" + activeTranslation.isPaused); 537 } 538 539 Bundle startedResult = createResultForCallback(STATE_UI_TRANSLATION_STARTED, 540 activeTranslation.sourceSpec, activeTranslation.targetSpec, 541 packageName); 542 invokeCallback(sourceUid, translatedAppUid, callback, startedResult, 543 enabledInputMethods); 544 if (activeTranslation.isPaused) { 545 // Also send event so callback owners know that translation was started then paused. 546 Bundle pausedResult = createResultForCallback(STATE_UI_TRANSLATION_PAUSED, 547 activeTranslation.sourceSpec, activeTranslation.targetSpec, 548 packageName); 549 invokeCallback(sourceUid, translatedAppUid, callback, pausedResult, 550 enabledInputMethods); 551 } 552 } 553 } 554 unregisterUiTranslationStateCallback(IRemoteCallback callback)555 public void unregisterUiTranslationStateCallback(IRemoteCallback callback) { 556 mCallbacks.unregister(callback); 557 } 558 559 private final RemoteCallbackList<IRemoteCallback> mCallbacks = new RemoteCallbackList<>(); 560 getServiceSettingsActivityLocked()561 public ComponentName getServiceSettingsActivityLocked() { 562 if (mTranslationServiceInfo == null) { 563 return null; 564 } 565 final String activityName = mTranslationServiceInfo.getSettingsActivity(); 566 if (activityName == null) { 567 return null; 568 } 569 final String packageName = mTranslationServiceInfo.getServiceInfo().packageName; 570 return new ComponentName(packageName, activityName); 571 } 572 notifyClientsTranslationCapability(TranslationCapability capability)573 private void notifyClientsTranslationCapability(TranslationCapability capability) { 574 final Bundle res = new Bundle(); 575 res.putParcelable(EXTRA_CAPABILITIES, capability); 576 mTranslationCapabilityCallbacks.broadcast((callback, uid) -> { 577 try { 578 callback.sendResult(res); 579 } catch (RemoteException e) { 580 Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e); 581 } 582 }); 583 } 584 585 private final class TranslationServiceRemoteCallback extends 586 ITranslationServiceCallback.Stub { 587 588 @Override updateTranslationCapability(TranslationCapability capability)589 public void updateTranslationCapability(TranslationCapability capability) { 590 if (capability == null) { 591 Slog.wtf(TAG, "received a null TranslationCapability from TranslationService."); 592 return; 593 } 594 notifyClientsTranslationCapability(capability); 595 } 596 } 597 598 private static final class ActiveTranslation { 599 public final TranslationSpec sourceSpec; 600 public final TranslationSpec targetSpec; 601 public final String packageName; 602 public final int translatedAppUid; 603 public boolean isPaused = false; 604 ActiveTranslation(TranslationSpec sourceSpec, TranslationSpec targetSpec, int translatedAppUid, String packageName)605 private ActiveTranslation(TranslationSpec sourceSpec, TranslationSpec targetSpec, 606 int translatedAppUid, String packageName) { 607 this.sourceSpec = sourceSpec; 608 this.targetSpec = targetSpec; 609 this.translatedAppUid = translatedAppUid; 610 this.packageName = packageName; 611 } 612 } 613 614 @Override binderDied()615 public void binderDied() { 616 // Don't need to implement this with binderDied(IBinder) implemented. 617 } 618 619 @Override binderDied(IBinder who)620 public void binderDied(IBinder who) { 621 synchronized (mLock) { 622 mWaitingFinishedCallbackActivities.remove(who); 623 ActiveTranslation activeTranslation = mActiveTranslations.remove(who); 624 if (activeTranslation != null) { 625 // Let apps with registered callbacks know about the activity's death. 626 invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, activeTranslation.sourceSpec, 627 activeTranslation.targetSpec, activeTranslation.packageName, 628 activeTranslation.translatedAppUid); 629 } 630 } 631 } 632 } 633