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:
+ *
+ * - If translation is not paused,
+ * {@link UiTranslationStateCallback#onStarted} will be invoked.
+ * - If translation is paused, {@link UiTranslationStateCallback#onStarted}
+ * will first be invoked, followed by {@link UiTranslationStateCallback#onPaused}.
+ *
*
* @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;
+ }
+ }
}