• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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