Merge changes from topic "b192331240-singleInvoke" into tm-dev

* changes:
  Rename translationActivityUid to translatedAppUid.
  Reduce duplicate UiTranslationStateCallback calls.
This commit is contained in:
Mark Punzalan
2022-04-22 07:01:15 +00:00
committed by Android (Google) Code Review
2 changed files with 210 additions and 66 deletions

View File

@@ -25,14 +25,28 @@ import java.util.concurrent.Executor;
* Callback for listening to UI Translation state changes. See {@link * Callback for listening to UI Translation state changes. See {@link
* UiTranslationManager#registerUiTranslationStateCallback(Executor, UiTranslationStateCallback)}. * UiTranslationManager#registerUiTranslationStateCallback(Executor, UiTranslationStateCallback)}.
* <p> * <p>
* Prior to Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}, callback methods * Prior to Android version {@link android.os.Build.VERSION_CODES#TIRAMISU}:
* <em>without</em> {@code packageName} are invoked. Apps with minSdkVersion lower than {@link * <ul>
* android.os.Build.VERSION_CODES#TIRAMISU} <em>must</em> implement those methods if they want to * <li>Callback methods <em>without</em> {@code packageName} are invoked. Apps with
* handle the events. * minSdkVersion lower than {@link android.os.Build.VERSION_CODES#TIRAMISU} <em>must</em>
* implement those methods if they want to handle the events.</li>
* <li>Callback methods for a particular event <em>may</em> be called multiple times
* consecutively, even when the translation state has not changed (e.g.,
* {@link #onStarted(ULocale, ULocale, String)} may be called multiple times even after
* translation has already started).</li>
* </ul>
* <p> * <p>
* In Android version {@link android.os.Build.VERSION_CODES#TIRAMISU} and later, if both methods * In Android version {@link android.os.Build.VERSION_CODES#TIRAMISU} and later:
* with and without {@code packageName} are implemented (e.g., {@link #onFinished()} and {@link * <ul>
* #onFinished(String)}, only the one <em>with</em> {@code packageName} will be called. * <li>If both methods with and without {@code packageName} are implemented (e.g.,
* {@link #onFinished()} and {@link #onFinished(String)}, only the one <em>with</em> {@code
* packageName} will be called.</li>
* <li>Callback methods for a particular event will <em>not</em> be called multiple times
* consecutively. They will only be called when the translation state has actually changed
* (e.g., from "started" to "paused"). Note: "resumed" is not considered a separate state
* from "started", so {@link #onResumed(ULocale, ULocale, String)} will never be called after
* {@link #onStarted(ULocale, ULocale, String)}.<</li>
* </ul>
*/ */
public interface UiTranslationStateCallback { public interface UiTranslationStateCallback {

View File

@@ -41,10 +41,10 @@ import android.os.RemoteCallbackList;
import android.os.RemoteException; import android.os.RemoteException;
import android.os.ResultReceiver; import android.os.ResultReceiver;
import android.service.translation.TranslationServiceInfo; import android.service.translation.TranslationServiceInfo;
import android.util.ArrayMap;
import android.util.ArraySet; import android.util.ArraySet;
import android.util.Log; import android.util.Log;
import android.util.Slog; import android.util.Slog;
import android.util.SparseArray;
import android.view.autofill.AutofillId; import android.view.autofill.AutofillId;
import android.view.inputmethod.InputMethodInfo; import android.view.inputmethod.InputMethodInfo;
import android.view.translation.ITranslationServiceCallback; import android.view.translation.ITranslationServiceCallback;
@@ -71,7 +71,8 @@ import java.lang.ref.WeakReference;
import java.util.List; import java.util.List;
final class TranslationManagerServiceImpl extends final class TranslationManagerServiceImpl extends
AbstractPerUserSystemService<TranslationManagerServiceImpl, TranslationManagerService> { AbstractPerUserSystemService<TranslationManagerServiceImpl, TranslationManagerService>
implements IBinder.DeathRecipient {
private static final String TAG = "TranslationManagerServiceImpl"; private static final String TAG = "TranslationManagerServiceImpl";
@SuppressLint("IsLoggableTagLength") @SuppressLint("IsLoggableTagLength")
@@ -100,10 +101,10 @@ final class TranslationManagerServiceImpl extends
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. * Key is translated activity token, value is the specification and state for the translation.
*/ */
@GuardedBy("mLock") @GuardedBy("mLock")
private final SparseArray<ActiveTranslation> mActiveTranslations = new SparseArray<>(); private final ArrayMap<IBinder, ActiveTranslation> mActiveTranslations = new ArrayMap<>();
protected TranslationManagerServiceImpl( protected TranslationManagerServiceImpl(
@NonNull TranslationManagerService master, @NonNull TranslationManagerService master,
@@ -190,25 +191,24 @@ final class TranslationManagerServiceImpl extends
} }
} }
private int getActivityUidByComponentName(Context context, ComponentName componentName, private int getAppUidByComponentName(Context context, ComponentName componentName, int userId) {
int userId) { int translatedAppUid = -1;
int translationActivityUid = -1;
try { try {
if (componentName != null) { if (componentName != null) {
translationActivityUid = context.getPackageManager().getApplicationInfoAsUser( translatedAppUid = context.getPackageManager().getApplicationInfoAsUser(
componentName.getPackageName(), 0, userId).uid; componentName.getPackageName(), 0, userId).uid;
} }
} catch (PackageManager.NameNotFoundException e) { } catch (PackageManager.NameNotFoundException e) {
Slog.d(TAG, "Cannot find packageManager for" + componentName); Slog.d(TAG, "Cannot find packageManager for" + componentName);
} }
return translationActivityUid; return translatedAppUid;
} }
@GuardedBy("mLock") @GuardedBy("mLock")
public void onTranslationFinishedLocked(boolean activityDestroyed, IBinder token, public void onTranslationFinishedLocked(boolean activityDestroyed, IBinder token,
ComponentName componentName) { ComponentName componentName) {
final int translationActivityUid = final int translatedAppUid =
getActivityUidByComponentName(getContext(), componentName, getUserId()); getAppUidByComponentName(getContext(), componentName, getUserId());
final String packageName = componentName.getPackageName(); final String packageName = componentName.getPackageName();
if (activityDestroyed) { if (activityDestroyed) {
// In the Activity destroy case, we only calls onTranslationFinished() in // In the Activity destroy case, we only calls onTranslationFinished() in
@@ -216,13 +216,13 @@ final class TranslationManagerServiceImpl extends
// should remove the waiting callback to avoid callback twice. // should remove the waiting callback to avoid callback twice.
invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, invokeCallbacks(STATE_UI_TRANSLATION_FINISHED,
/* sourceSpec= */ null, /* targetSpec= */ null, /* sourceSpec= */ null, /* targetSpec= */ null,
packageName, translationActivityUid); packageName, translatedAppUid);
mWaitingFinishedCallbackActivities.remove(token); mWaitingFinishedCallbackActivities.remove(token);
} else { } else {
if (mWaitingFinishedCallbackActivities.contains(token)) { if (mWaitingFinishedCallbackActivities.contains(token)) {
invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, invokeCallbacks(STATE_UI_TRANSLATION_FINISHED,
/* sourceSpec= */ null, /* targetSpec= */ null, /* sourceSpec= */ null, /* targetSpec= */ null,
packageName, translationActivityUid); packageName, translatedAppUid);
mWaitingFinishedCallbackActivities.remove(token); mWaitingFinishedCallbackActivities.remove(token);
} }
} }
@@ -259,48 +259,161 @@ final class TranslationManagerServiceImpl extends
} }
ComponentName componentName = mActivityTaskManagerInternal.getActivityName(activityToken); ComponentName componentName = mActivityTaskManagerInternal.getActivityName(activityToken);
int translationActivityUid = int translatedAppUid =
getActivityUidByComponentName(getContext(), componentName, getUserId()); getAppUidByComponentName(getContext(), componentName, getUserId());
String packageName = componentName.getPackageName(); String packageName = componentName.getPackageName();
if (state != STATE_UI_TRANSLATION_FINISHED) {
invokeCallbacks(state, sourceSpec, targetSpec, packageName, translationActivityUid); invokeCallbacksIfNecessaryLocked(state, sourceSpec, targetSpec, packageName, activityToken,
updateActiveTranslations(state, sourceSpec, targetSpec, packageName, translatedAppUid);
translationActivityUid); updateActiveTranslationsLocked(state, sourceSpec, targetSpec, packageName, activityToken,
} else { translatedAppUid);
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 updateActiveTranslationsLocked(int state, TranslationSpec sourceSpec,
TranslationSpec targetSpec, String packageName, IBinder activityToken,
int translatedAppUid) {
// We keep track of active translations and their state so that we can:
// 1. Trigger callbacks that are registered after translation has started.
// See registerUiTranslationStateCallbackLocked().
// 2. NOT trigger callbacks when the state didn't change.
// See invokeCallbacksIfNecessaryLocked().
ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken);
switch (state) {
case STATE_UI_TRANSLATION_STARTED: {
if (activeTranslation == null) {
try {
activityToken.linkToDeath(this, /* flags= */ 0);
} catch (RemoteException e) {
Slog.w(TAG, "Failed to call linkToDeath for translated app with uid="
+ translatedAppUid + "; activity is already dead", e);
// Apps with registered callbacks were just notified that translation
// started. We should let them know translation is finished too.
invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, sourceSpec, targetSpec,
packageName, translatedAppUid);
return;
}
mActiveTranslations.put(activityToken,
new ActiveTranslation(sourceSpec, targetSpec, translatedAppUid,
packageName));
}
break;
}
case STATE_UI_TRANSLATION_PAUSED: {
if (activeTranslation != null) {
activeTranslation.isPaused = true;
}
break;
}
case STATE_UI_TRANSLATION_RESUMED: {
if (activeTranslation != null) {
activeTranslation.isPaused = false;
}
break;
}
case STATE_UI_TRANSLATION_FINISHED: {
if (activeTranslation != null) {
mActiveTranslations.remove(activityToken);
}
break;
}
}
if (DEBUG) {
Slog.d(TAG,
"Updating to translation state=" + state + " for app with uid="
+ translatedAppUid + " packageName=" + packageName);
} }
} }
@GuardedBy("mLock") @GuardedBy("mLock")
private void updateActiveTranslations(int state, TranslationSpec sourceSpec, private void invokeCallbacksIfNecessaryLocked(int state, TranslationSpec sourceSpec,
TranslationSpec targetSpec, String packageName, int translationActivityUid) { TranslationSpec targetSpec, String packageName, IBinder activityToken,
// Keep track of active translations so that we can trigger callbacks that are int translatedAppUid) {
// registered after translation has started. boolean shouldInvokeCallbacks = true;
switch (state) { int stateForCallbackInvocation = state;
case STATE_UI_TRANSLATION_STARTED: {
ActiveTranslation activeTranslation = new ActiveTranslation(sourceSpec, ActiveTranslation activeTranslation = mActiveTranslations.get(activityToken);
targetSpec, packageName); if (activeTranslation == null) {
mActiveTranslations.put(translationActivityUid, activeTranslation); if (state != STATE_UI_TRANSLATION_STARTED) {
break; shouldInvokeCallbacks = false;
} Slog.w(TAG,
case STATE_UI_TRANSLATION_PAUSED: "Updating to translation state=" + state + " for app with uid="
case STATE_UI_TRANSLATION_RESUMED: { + translatedAppUid + " packageName=" + packageName
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"); + " but no active translation was found for it");
} }
} else {
switch (state) {
case STATE_UI_TRANSLATION_STARTED: {
boolean specsAreIdentical = activeTranslation.sourceSpec.getLocale().equals(
sourceSpec.getLocale())
&& activeTranslation.targetSpec.getLocale().equals(
targetSpec.getLocale());
if (specsAreIdentical) {
if (activeTranslation.isPaused) {
// Ideally UiTranslationManager.resumeTranslation() should be first
// used to resume translation, but for the purposes of invoking the
// callback, we want to call onResumed() instead of onStarted(). This
// way there can only be one call to onStarted() for the lifetime of
// a translated activity and this will simplify the number of states
// apps have to handle.
stateForCallbackInvocation = STATE_UI_TRANSLATION_RESUMED;
} else {
// Don't invoke callbacks if the state or specs didn't change. For a
// given activity, startTranslation() will be called every time there
// are new views to be translated, but we don't need to repeatedly
// notify apps about it.
shouldInvokeCallbacks = false;
}
}
break; break;
} }
case STATE_UI_TRANSLATION_PAUSED: {
if (activeTranslation.isPaused) {
// Don't invoke callbacks if the state didn't change.
shouldInvokeCallbacks = false;
}
break;
}
case STATE_UI_TRANSLATION_RESUMED: {
if (!activeTranslation.isPaused) {
// Don't invoke callbacks if the state didn't change. Either
// resumeTranslation() was called consecutive times, or right after
// startTranslation(). The latter case shouldn't happen normally, so we
// don't want apps to have to handle that particular transition.
shouldInvokeCallbacks = false;
}
break;
}
case STATE_UI_TRANSLATION_FINISHED: {
// Note: Here finishTranslation() was called but we don't want to invoke
// onFinished() on the callbacks. They will be invoked when
// UiTranslationManager.onTranslationFinished() is called (see
// onTranslationFinishedLocked()).
shouldInvokeCallbacks = false;
break;
}
}
}
if (DEBUG) {
Slog.d(TAG,
(shouldInvokeCallbacks ? "" : "NOT ")
+ "Invoking callbacks for translation state="
+ stateForCallbackInvocation + " for app with uid=" + translatedAppUid
+ " packageName=" + packageName);
}
if (shouldInvokeCallbacks) {
invokeCallbacks(stateForCallbackInvocation, sourceSpec, targetSpec, packageName,
translatedAppUid);
} }
} }
@@ -343,15 +456,14 @@ final class TranslationManagerServiceImpl extends
private void invokeCallbacks( private void invokeCallbacks(
int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName, int state, TranslationSpec sourceSpec, TranslationSpec targetSpec, String packageName,
int translationActivityUid) { int translatedAppUid) {
Bundle result = createResultForCallback(state, sourceSpec, targetSpec, packageName); Bundle result = createResultForCallback(state, sourceSpec, targetSpec, packageName);
if (mCallbacks.getRegisteredCallbackCount() == 0) { if (mCallbacks.getRegisteredCallbackCount() == 0) {
return; return;
} }
List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods(); List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods();
mCallbacks.broadcast((callback, uid) -> { mCallbacks.broadcast((callback, uid) -> {
invokeCallback((int) uid, translationActivityUid, callback, result, invokeCallback((int) uid, translatedAppUid, callback, result, enabledInputMethods);
enabledInputMethods);
}); });
} }
@@ -374,9 +486,9 @@ final class TranslationManagerServiceImpl extends
} }
private void invokeCallback( private void invokeCallback(
int callbackSourceUid, int translationActivityUid, IRemoteCallback callback, int callbackSourceUid, int translatedAppUid, IRemoteCallback callback,
Bundle result, List<InputMethodInfo> enabledInputMethods) { Bundle result, List<InputMethodInfo> enabledInputMethods) {
if (callbackSourceUid == translationActivityUid) { if (callbackSourceUid == translatedAppUid) {
// Invoke callback for the application being translated. // Invoke callback for the application being translated.
try { try {
callback.sendResult(result); callback.sendResult(result);
@@ -417,29 +529,26 @@ final class TranslationManagerServiceImpl extends
// Trigger the callback for already active translations. // Trigger the callback for already active translations.
List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods(); List<InputMethodInfo> enabledInputMethods = getEnabledInputMethods();
for (int i = 0; i < mActiveTranslations.size(); i++) { for (int i = 0; i < mActiveTranslations.size(); i++) {
int activeTranslationUid = mActiveTranslations.keyAt(i);
ActiveTranslation activeTranslation = mActiveTranslations.valueAt(i); ActiveTranslation activeTranslation = mActiveTranslations.valueAt(i);
if (activeTranslation == null) { int translatedAppUid = activeTranslation.translatedAppUid;
continue;
}
String packageName = activeTranslation.packageName; String packageName = activeTranslation.packageName;
if (DEBUG) { if (DEBUG) {
Slog.d(TAG, "Triggering callback for sourceUid=" + sourceUid Slog.d(TAG, "Triggering callback for sourceUid=" + sourceUid
+ " for translated activity with uid=" + activeTranslationUid + " for translated app with uid=" + translatedAppUid
+ "packageName=" + packageName + " isPaused=" + activeTranslation.isPaused); + "packageName=" + packageName + " isPaused=" + activeTranslation.isPaused);
} }
Bundle startedResult = createResultForCallback(STATE_UI_TRANSLATION_STARTED, Bundle startedResult = createResultForCallback(STATE_UI_TRANSLATION_STARTED,
activeTranslation.sourceSpec, activeTranslation.targetSpec, activeTranslation.sourceSpec, activeTranslation.targetSpec,
packageName); packageName);
invokeCallback(sourceUid, activeTranslationUid, callback, startedResult, invokeCallback(sourceUid, translatedAppUid, callback, startedResult,
enabledInputMethods); enabledInputMethods);
if (activeTranslation.isPaused) { if (activeTranslation.isPaused) {
// Also send event so callback owners know that translation was started then paused. // Also send event so callback owners know that translation was started then paused.
Bundle pausedResult = createResultForCallback(STATE_UI_TRANSLATION_PAUSED, Bundle pausedResult = createResultForCallback(STATE_UI_TRANSLATION_PAUSED,
activeTranslation.sourceSpec, activeTranslation.targetSpec, activeTranslation.sourceSpec, activeTranslation.targetSpec,
packageName); packageName);
invokeCallback(sourceUid, activeTranslationUid, callback, pausedResult, invokeCallback(sourceUid, translatedAppUid, callback, pausedResult,
enabledInputMethods); enabledInputMethods);
} }
} }
@@ -492,13 +601,34 @@ final class TranslationManagerServiceImpl extends
public final TranslationSpec sourceSpec; public final TranslationSpec sourceSpec;
public final TranslationSpec targetSpec; public final TranslationSpec targetSpec;
public final String packageName; public final String packageName;
public final int translatedAppUid;
public boolean isPaused = false; public boolean isPaused = false;
private ActiveTranslation(TranslationSpec sourceSpec, TranslationSpec targetSpec, private ActiveTranslation(TranslationSpec sourceSpec, TranslationSpec targetSpec,
String packageName) { int translatedAppUid, String packageName) {
this.sourceSpec = sourceSpec; this.sourceSpec = sourceSpec;
this.targetSpec = targetSpec; this.targetSpec = targetSpec;
this.translatedAppUid = translatedAppUid;
this.packageName = packageName; this.packageName = packageName;
} }
} }
@Override
public void binderDied() {
// Don't need to implement this with binderDied(IBinder) implemented.
}
@Override
public void binderDied(IBinder who) {
synchronized (mLock) {
mWaitingFinishedCallbackActivities.remove(who);
ActiveTranslation activeTranslation = mActiveTranslations.remove(who);
if (activeTranslation != null) {
// Let apps with registered callbacks know about the activity's death.
invokeCallbacks(STATE_UI_TRANSLATION_FINISHED, activeTranslation.sourceSpec,
activeTranslation.targetSpec, activeTranslation.packageName,
activeTranslation.translatedAppUid);
}
}
}
} }