Trigger newly-registered callbacks with events from currently active

translations.

Bug: 192331240
Test: atest android.translation.cts.UiTranslationManagerTest
Test: Manually on device:
  1. Kill Gboard
  2. Open translatable Chat conversation, wait for Live Translate
  4. Focus on chat input
  5. Auto-translate on Gboard is opened, and debug logs show
     "TranslationManagerServiceImpl: Triggering callback for..."

Change-Id: Ida82a947491e8157fac5cd43939d6d358fbe6c7b
This commit is contained in:
Mark Punzalan
2022-03-24 20:26:38 +00:00
parent 0effa34013
commit 6c3d2f7b7d
3 changed files with 190 additions and 64 deletions

View File

@@ -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.
* <p>
* 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)}.
*
* <p>
* 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.
* <p>
* 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:
* <ul>
* <li>If translation is <em>not</em> paused,
* {@link UiTranslationStateCallback#onStarted} will be invoked.</li>
* <li>If translation <em>is</em> paused, {@link UiTranslationStateCallback#onStarted}
* will first be invoked, followed by {@link UiTranslationStateCallback#onPaused}.</li>
* </ul>
*
* @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,

View File

@@ -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;

View File

@@ -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<TranslationManagerServiceImpl, TranslationManagerService> {
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<ActivityTokens> mLastActivityTokens;
private ActivityTaskManagerInternal mActivityTaskManagerInternal;
private final ActivityTaskManagerInternal mActivityTaskManagerInternal;
private final TranslationServiceRemoteCallback mRemoteServiceCallback =
new TranslationServiceRemoteCallback();
private final RemoteCallbackList<IRemoteCallback> mTranslationCapabilityCallbacks =
new RemoteCallbackList<>();
private final ArraySet<IBinder> mWaitingFinishedCallbackActivities = new ArraySet();
private final ArraySet<IBinder> mWaitingFinishedCallbackActivities = new ArraySet<>();
/**
* Key is translated activity uid, value is the specification and state for the translation.
*/
@GuardedBy("mLock")
private final SparseArray<ActiveTranslation> 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<InputMethodInfo> enabledInputMethods =
LocalServices.getService(InputMethodManagerInternal.class)
.getEnabledInputMethodListAsUser(mUserId);
List<InputMethodInfo> 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<InputMethodInfo> 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<InputMethodInfo> 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<InputMethodInfo> 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;
}
}
}