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