diff --git a/core/java/android/view/translation/UiTranslationManager.java b/core/java/android/view/translation/UiTranslationManager.java index 01acdfec6c27a..9fc7b07200ae6 100644 --- a/core/java/android/view/translation/UiTranslationManager.java +++ b/core/java/android/view/translation/UiTranslationManager.java @@ -163,7 +163,6 @@ public final class UiTranslationManager { /** * @removed Use {@link #startTranslation(TranslationSpec, TranslationSpec, List, ActivityId, * UiTranslationSpec)} instead. - * * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) @@ -180,13 +179,13 @@ public final class UiTranslationManager { /** * Request ui translation for a given Views. * - * @param sourceSpec {@link TranslationSpec} for the data to be translated. - * @param targetSpec {@link TranslationSpec} for the translated data. - * @param viewIds A list of the {@link View}'s {@link AutofillId} which needs to be translated - * @param activityId the identifier for the Activity which needs ui translation + * @param sourceSpec {@link TranslationSpec} for the data to be translated. + * @param targetSpec {@link TranslationSpec} for the translated data. + * @param viewIds A list of the {@link View}'s {@link AutofillId} which needs to be + * translated + * @param activityId the identifier for the Activity which needs ui translation * @param uiTranslationSpec configuration for translation of the specified views * @throws IllegalArgumentException if the no {@link View}'s {@link AutofillId} in the list - * * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) @@ -220,8 +219,7 @@ public final class UiTranslationManager { * * @param activityId the identifier for the Activity which needs ui translation * @throws NullPointerException the activityId or - * {@link android.app.assist.ActivityId#getToken()} is {@code null} - * + * {@link android.app.assist.ActivityId#getToken()} is {@code null} * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) @@ -245,8 +243,7 @@ public final class UiTranslationManager { * * @param activityId the identifier for the Activity which needs ui translation * @throws NullPointerException the activityId or - * {@link android.app.assist.ActivityId#getToken()} is {@code null} - * + * {@link android.app.assist.ActivityId#getToken()} is {@code null} * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) @@ -270,8 +267,7 @@ public final class UiTranslationManager { * * @param activityId the identifier for the Activity which needs ui translation * @throws NullPointerException the activityId or - * {@link android.app.assist.ActivityId#getToken()} is {@code null} - * + * {@link android.app.assist.ActivityId#getToken()} is {@code null} * @hide */ @RequiresPermission(android.Manifest.permission.MANAGE_UI_TRANSLATION) @@ -290,20 +286,29 @@ public final class UiTranslationManager { } /** - * Register for notifications of UI Translation state changes on the foreground activity. This + * Register for notifications of UI Translation state changes on the foreground Activity. This * is available to the owning application itself and also the current input method. *

* The application whose UI is being translated can use this to customize the UI Translation * behavior in ways that aren't made easy by methods like * {@link View#onCreateViewTranslationRequest(int[], Consumer)}. - * *

* Input methods can use this to offer complementary features to UI Translation; for example, * enabling outgoing message translation when the system is translating incoming messages in a * communication app. + *

+ * Starting from {@link android.os.Build.VERSION_CODES#TIRAMISU}, if Activities are already + * being translated when a callback is registered, methods on the callback will be invoked for + * each translated activity, depending on the state of translation: + *

* * @param callback the callback to register for receiving the state change - * notifications + * notifications */ public void registerUiTranslationStateCallback( @NonNull @CallbackExecutor Executor executor, @@ -355,9 +360,8 @@ public final class UiTranslationManager { * called or Activity is destroyed. * * @param activityDestroyed if the ui translation is finished because of activity destroyed. - * @param activityId the identifier for the Activity which needs ui translation - * @param componentName the ui translated Activity componentName. - * + * @param activityId the identifier for the Activity which needs ui translation + * @param componentName the ui translated Activity componentName. * @hide */ public void onTranslationFinished(boolean activityDestroyed, ActivityId activityId, diff --git a/services/translation/java/com/android/server/translation/TranslationManagerService.java b/services/translation/java/com/android/server/translation/TranslationManagerService.java index 27b254a304ff2..e8c989280a0e5 100644 --- a/services/translation/java/com/android/server/translation/TranslationManagerService.java +++ b/services/translation/java/com/android/server/translation/TranslationManagerService.java @@ -225,12 +225,12 @@ public final class TranslationManagerService @Override public void registerUiTranslationStateCallback(IRemoteCallback callback, int userId) { - TranslationManagerServiceImpl service; synchronized (mLock) { - service = getServiceForUserLocked(userId); - } - if (service != null) { - service.registerUiTranslationStateCallback(callback, Binder.getCallingUid()); + final TranslationManagerServiceImpl service = getServiceForUserLocked(userId); + if (service != null) { + service.registerUiTranslationStateCallbackLocked(callback, + Binder.getCallingUid()); + } } } @@ -297,7 +297,7 @@ public final class TranslationManagerService /** * Dump the service state into the given stream. You run "adb shell dumpsys translation". - */ + */ @Override public void dump(FileDescriptor fd, PrintWriter pw, String[] args) { if (!DumpUtils.checkDumpPermission(getContext(), TAG, pw)) return; diff --git a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java index 3833ceb3122ec..d3a589668808e 100644 --- a/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java +++ b/services/translation/java/com/android/server/translation/TranslationManagerServiceImpl.java @@ -22,9 +22,13 @@ import static android.view.translation.UiTranslationManager.EXTRA_SOURCE_LOCALE; import static android.view.translation.UiTranslationManager.EXTRA_STATE; import static android.view.translation.UiTranslationManager.EXTRA_TARGET_LOCALE; import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_FINISHED; +import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_PAUSED; +import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_RESUMED; +import static android.view.translation.UiTranslationManager.STATE_UI_TRANSLATION_STARTED; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.SuppressLint; import android.app.Activity; import android.content.ComponentName; import android.content.Context; @@ -38,7 +42,9 @@ import android.os.RemoteException; import android.os.ResultReceiver; import android.service.translation.TranslationServiceInfo; import android.util.ArraySet; +import android.util.Log; import android.util.Slog; +import android.util.SparseArray; import android.view.autofill.AutofillId; import android.view.inputmethod.InputMethodInfo; import android.view.translation.ITranslationServiceCallback; @@ -68,6 +74,8 @@ final class TranslationManagerServiceImpl extends AbstractPerUserSystemService { private static final String TAG = "TranslationManagerServiceImpl"; + @SuppressLint("IsLoggableTagLength") + private static final boolean DEBUG = Log.isLoggable(TAG, Log.DEBUG); @GuardedBy("mLock") @Nullable @@ -83,13 +91,19 @@ final class TranslationManagerServiceImpl extends @GuardedBy("mLock") private WeakReference mLastActivityTokens; - private ActivityTaskManagerInternal mActivityTaskManagerInternal; + private final ActivityTaskManagerInternal mActivityTaskManagerInternal; private final TranslationServiceRemoteCallback mRemoteServiceCallback = new TranslationServiceRemoteCallback(); private final RemoteCallbackList mTranslationCapabilityCallbacks = new RemoteCallbackList<>(); - private final ArraySet mWaitingFinishedCallbackActivities = new ArraySet(); + private final ArraySet mWaitingFinishedCallbackActivities = new ArraySet<>(); + + /** + * Key is translated activity uid, value is the specification and state for the translation. + */ + @GuardedBy("mLock") + private final SparseArray mActiveTranslations = new SparseArray<>(); protected TranslationManagerServiceImpl( @NonNull TranslationManagerService master, @@ -231,6 +245,7 @@ final class TranslationManagerServiceImpl extends if (state == STATE_UI_TRANSLATION_FINISHED) { mWaitingFinishedCallbackActivities.add(token); } + IBinder activityToken = taskTopActivityTokens.getActivityToken(); try { taskTopActivityTokens.getApplicationThread().updateUiTranslationState( @@ -243,9 +258,46 @@ final class TranslationManagerServiceImpl extends ComponentName componentName = mActivityTaskManagerInternal.getActivityName(activityToken); int translationActivityUid = getActivityUidByComponentName(getContext(), componentName, getUserId()); + String packageName = componentName.getPackageName(); if (state != STATE_UI_TRANSLATION_FINISHED) { - invokeCallbacks(state, sourceSpec, targetSpec, componentName.getPackageName(), + invokeCallbacks(state, sourceSpec, targetSpec, packageName, translationActivityUid); + updateActiveTranslations(state, sourceSpec, targetSpec, packageName, translationActivityUid); + } else { + if (mActiveTranslations.contains(translationActivityUid)) { + mActiveTranslations.delete(translationActivityUid); + } else { + Slog.w(TAG, "Finishing translation for activity with uid=" + translationActivityUid + + " but no active translation was found for it"); + } + } + } + + @GuardedBy("mLock") + private void updateActiveTranslations(int state, TranslationSpec sourceSpec, + TranslationSpec targetSpec, String packageName, int translationActivityUid) { + // Keep track of active translations so that we can trigger callbacks that are + // registered after translation has started. + switch (state) { + case STATE_UI_TRANSLATION_STARTED: { + ActiveTranslation activeTranslation = new ActiveTranslation(sourceSpec, + targetSpec, packageName); + mActiveTranslations.put(translationActivityUid, activeTranslation); + break; + } + case STATE_UI_TRANSLATION_PAUSED: + case STATE_UI_TRANSLATION_RESUMED: { + ActiveTranslation activeTranslation = mActiveTranslations.get( + translationActivityUid); + if (activeTranslation != null) { + activeTranslation.isPaused = (state == STATE_UI_TRANSLATION_PAUSED); + } else { + Slog.w(TAG, "Pausing or resuming translation for activity with uid=" + + translationActivityUid + + " but no active translation was found for it"); + } + break; + } } } @@ -289,49 +341,105 @@ final class TranslationManagerServiceImpl extends private void invokeCallbacks( int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName, int translationActivityUid) { - Bundle res = new Bundle(); - res.putInt(EXTRA_STATE, state); - // TODO(177500482): Store the locale pair so it can be sent for RESUME events. - if (sourceSpec != null) { - res.putSerializable(EXTRA_SOURCE_LOCALE, sourceSpec.getLocale()); - res.putSerializable(EXTRA_TARGET_LOCALE, targetSpec.getLocale()); + Bundle result = createResultForCallback(state, sourceSpec, targetSpec, packageName); + if (mCallbacks.getRegisteredCallbackCount() == 0) { + return; } - res.putString(EXTRA_PACKAGE_NAME, packageName); - // TODO(177500482): Only support the *current* Input Method. - List enabledInputMethods = - LocalServices.getService(InputMethodManagerInternal.class) - .getEnabledInputMethodListAsUser(mUserId); + List enabledInputMethods = getEnabledInputMethods(); mCallbacks.broadcast((callback, uid) -> { - if ((int) uid == translationActivityUid) { - try { - callback.sendResult(res); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e); - } - return; - } - // Code here is non-optimal since it's temporary.. - boolean isIme = false; - for (InputMethodInfo inputMethod : enabledInputMethods) { - if ((int) uid == inputMethod.getServiceInfo().applicationInfo.uid) { - isIme = true; - } - } - // TODO(177500482): Invoke it for the application being translated too. - if (!isIme) { - return; - } - try { - callback.sendResult(res); - } catch (RemoteException e) { - Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e); - } + invokeCallback((int) uid, translationActivityUid, callback, result, + enabledInputMethods); }); } - public void registerUiTranslationStateCallback(IRemoteCallback callback, int sourceUid) { + private List getEnabledInputMethods() { + return LocalServices.getService(InputMethodManagerInternal.class) + .getEnabledInputMethodListAsUser(mUserId); + } + + private Bundle createResultForCallback( + int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName) { + Bundle result = new Bundle(); + result.putInt(EXTRA_STATE, state); + // TODO(177500482): Store the locale pair so it can be sent for RESUME events. + if (sourceSpec != null) { + result.putSerializable(EXTRA_SOURCE_LOCALE, sourceSpec.getLocale()); + result.putSerializable(EXTRA_TARGET_LOCALE, targetSpec.getLocale()); + } + result.putString(EXTRA_PACKAGE_NAME, packageName); + return result; + } + + private void invokeCallback( + int callbackSourceUid, int translationActivityUid, IRemoteCallback callback, + Bundle result, List enabledInputMethods) { + if (callbackSourceUid == translationActivityUid) { + // Invoke callback for the application being translated. + try { + callback.sendResult(result); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e); + } + return; + } + + // TODO(177500482): Only support the *current* Input Method. + // Code here is non-optimal since it's temporary.. + boolean isIme = false; + for (InputMethodInfo inputMethod : enabledInputMethods) { + if (callbackSourceUid == inputMethod.getServiceInfo().applicationInfo.uid) { + isIme = true; + break; + } + } + + if (!isIme) { + return; + } + try { + callback.sendResult(result); + } catch (RemoteException e) { + Slog.w(TAG, "Failed to invoke UiTranslationStateCallback: " + e); + } + } + + @GuardedBy("mLock") + public void registerUiTranslationStateCallbackLocked(IRemoteCallback callback, int sourceUid) { mCallbacks.register(callback, sourceUid); - // TODO(177500482): trigger the callback here if we're already translating the UI. + + if (mActiveTranslations.size() == 0) { + return; + } + + // Trigger the callback for already active translations. + List enabledInputMethods = getEnabledInputMethods(); + for (int i = 0; i < mActiveTranslations.size(); i++) { + int activeTranslationUid = mActiveTranslations.keyAt(i); + ActiveTranslation activeTranslation = mActiveTranslations.valueAt(i); + if (activeTranslation == null) { + continue; + } + String packageName = activeTranslation.packageName; + if (DEBUG) { + Slog.d(TAG, "Triggering callback for sourceUid=" + sourceUid + + " for translated activity with uid=" + activeTranslationUid + + "packageName=" + packageName + " isPaused=" + activeTranslation.isPaused); + } + + Bundle startedResult = createResultForCallback(STATE_UI_TRANSLATION_STARTED, + activeTranslation.sourceSpec, activeTranslation.targetSpec, + packageName); + invokeCallback(sourceUid, activeTranslationUid, callback, startedResult, + enabledInputMethods); + if (activeTranslation.isPaused) { + // Also send event so callback owners know that translation was started then paused. + Bundle pausedResult = createResultForCallback(STATE_UI_TRANSLATION_PAUSED, + activeTranslation.sourceSpec, activeTranslation.targetSpec, + packageName); + invokeCallback(sourceUid, activeTranslationUid, callback, pausedResult, + enabledInputMethods); + } + } } public void unregisterUiTranslationStateCallback(IRemoteCallback callback) { @@ -376,4 +484,18 @@ final class TranslationManagerServiceImpl extends notifyClientsTranslationCapability(capability); } } + + private static final class ActiveTranslation { + public final TranslationSpec sourceSpec; + public final TranslationSpec targetSpec; + public final String packageName; + public boolean isPaused = false; + + private ActiveTranslation(TranslationSpec sourceSpec, TranslationSpec targetSpec, + String packageName) { + this.sourceSpec = sourceSpec; + this.targetSpec = targetSpec; + this.packageName = packageName; + } + } }