summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/Activity.java15
-rw-r--r--core/java/android/view/translation/Translator.java48
-rw-r--r--core/java/android/view/translation/UiTranslationController.java249
3 files changed, 304 insertions, 8 deletions
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 415105f88f25..f366df55be4c 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -2669,6 +2669,10 @@ public class Activity extends ContextThemeWrapper
dispatchActivityDestroyed();
notifyContentCaptureManagerIfNeeded(CONTENT_CAPTURE_STOP);
+
+ if (mUiTranslationController != null) {
+ mUiTranslationController.onActivityDestroyed();
+ }
}
/**
@@ -8450,9 +8454,8 @@ public class Activity extends ContextThemeWrapper
}
/** @hide */
- @Override
@Nullable
- public final View autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId) {
+ public View findViewByAutofillIdTraversal(@NonNull AutofillId autofillId) {
final ArrayList<ViewRootImpl> roots =
WindowManagerGlobal.getInstance().getRootViews(getActivityToken());
for (int rootNum = 0; rootNum < roots.size(); rootNum++) {
@@ -8465,12 +8468,18 @@ public class Activity extends ContextThemeWrapper
}
}
}
-
return null;
}
/** @hide */
@Override
+ @Nullable
+ public final View autofillClientFindViewByAutofillIdTraversal(AutofillId autofillId) {
+ return findViewByAutofillIdTraversal(autofillId);
+ }
+
+ /** @hide */
+ @Override
public final @NonNull boolean[] autofillClientGetViewVisibility(
@NonNull AutofillId[] autofillIds) {
final int autofillIdCount = autofillIds.length;
diff --git a/core/java/android/view/translation/Translator.java b/core/java/android/view/translation/Translator.java
index 675f32b19d17..22c3e57ecc95 100644
--- a/core/java/android/view/translation/Translator.java
+++ b/core/java/android/view/translation/Translator.java
@@ -28,6 +28,7 @@ import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.os.RemoteException;
+import android.service.translation.ITranslationCallback;
import android.util.Log;
import com.android.internal.annotations.GuardedBy;
@@ -36,9 +37,11 @@ import com.android.internal.util.SyncResultReceiver;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
+import java.util.List;
import java.util.Objects;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
+import java.util.function.Consumer;
/**
* The {@link Translator} for translation, defined by a source and a dest {@link TranslationSpec}.
@@ -295,4 +298,49 @@ public class Translator {
}
// TODO: add methods for UI-toolkit case.
+ /** @hide */
+ public void requestUiTranslate(@NonNull List<TranslationRequest> requests,
+ @NonNull Consumer<TranslationResponse> responseCallback) {
+ if (mDirectServiceBinder == null) {
+ Log.wtf(TAG, "Translator created without proper initialization.");
+ return;
+ }
+ final android.service.translation.TranslationRequest request =
+ new android.service.translation.TranslationRequest
+ .Builder(getNextRequestId(), mSourceSpec, mDestSpec, requests)
+ .build();
+ final ITranslationCallback callback =
+ new TranslationResponseCallbackImpl(responseCallback);
+ try {
+ mDirectServiceBinder.onTranslationRequest(request, mId, callback, null);
+ } catch (RemoteException e) {
+ Log.w(TAG, "RemoteException calling flushRequest");
+ }
+ }
+
+ private static class TranslationResponseCallbackImpl extends ITranslationCallback.Stub {
+
+ private final WeakReference<Consumer<TranslationResponse>> mResponseCallback;
+
+ TranslationResponseCallbackImpl(Consumer<TranslationResponse> responseCallback) {
+ mResponseCallback = new WeakReference<>(responseCallback);
+ }
+
+ @Override
+ public void onTranslationComplete(TranslationResponse response) throws RemoteException {
+ provideTranslationResponse(response);
+ }
+
+ @Override
+ public void onError() throws RemoteException {
+ provideTranslationResponse(null);
+ }
+
+ private void provideTranslationResponse(TranslationResponse response) {
+ final Consumer<TranslationResponse> responseCallback = mResponseCallback.get();
+ if (responseCallback != null) {
+ responseCallback.accept(response);
+ }
+ }
+ }
}
diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java
index a810c2e6fb41..fa4614628102 100644
--- a/core/java/android/view/translation/UiTranslationController.java
+++ b/core/java/android/view/translation/UiTranslationController.java
@@ -16,35 +16,274 @@
package android.view.translation;
+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.WorkerThread;
import android.app.Activity;
import android.content.Context;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.util.Pair;
+import android.view.View;
import android.view.autofill.AutofillId;
+import android.view.translation.UiTranslationManager.UiTranslationState;
+
+import com.android.internal.util.function.pooled.PooledLambda;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
import java.util.List;
+import java.util.function.Consumer;
/**
- * A controller to manage the ui translation requests.
+ * A controller to manage the ui translation requests for the {@link Activity}.
*
* @hide
*/
public class UiTranslationController {
private static final String TAG = "UiTranslationController";
-
+ @NonNull
private final Activity mActivity;
-
+ @NonNull
private final Context mContext;
+ @NonNull
+ private final Object mLock = new Object();
+
+ // Each Translator is distinguished by sourceSpec and desSepc.
+ @NonNull
+ private final ArrayMap<Pair<TranslationSpec, TranslationSpec>, Translator> mTranslators;
+ @NonNull
+ private final ArrayMap<AutofillId, WeakReference<View>> mViews;
+ @NonNull
+ private final HandlerThread mWorkerThread;
+ @NonNull
+ private final Handler mWorkerHandler;
public UiTranslationController(Activity activity, Context context) {
mActivity = activity;
mContext = context;
+ mViews = new ArrayMap<>();
+ mTranslators = new ArrayMap<>();
+
+ mWorkerThread =
+ new HandlerThread("UiTranslationController_" + mActivity.getComponentName(),
+ Process.THREAD_PRIORITY_FOREGROUND);
+ mWorkerThread.start();
+ mWorkerHandler = mWorkerThread.getThreadHandler();
}
/**
* Update the Ui translation state.
*/
- public void updateUiTranslationState(int state, TranslationSpec sourceSpec,
+ public void updateUiTranslationState(@UiTranslationState int state, TranslationSpec sourceSpec,
TranslationSpec destSpec, List<AutofillId> views) {
- // Implement it. Deal with the each states
+ if (!mActivity.isResumed()) {
+ return;
+ }
+ switch (state) {
+ case STATE_UI_TRANSLATION_STARTED:
+ final Pair<TranslationSpec, TranslationSpec> specs =
+ new Pair<>(sourceSpec, destSpec);
+ if (!mTranslators.containsKey(specs)) {
+ mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
+ UiTranslationController::createTranslatorAndStart,
+ UiTranslationController.this, sourceSpec, destSpec, views));
+ } else {
+ onUiTranslationStarted(mTranslators.get(specs), views);
+ }
+ break;
+ case STATE_UI_TRANSLATION_PAUSED:
+ runForEachView((view) -> view.onPauseUiTranslation(), STATE_UI_TRANSLATION_PAUSED);
+ break;
+ case STATE_UI_TRANSLATION_RESUMED:
+ runForEachView((view) -> view.onRestoreUiTranslation(),
+ STATE_UI_TRANSLATION_PAUSED);
+ break;
+ case STATE_UI_TRANSLATION_FINISHED:
+ destroyTranslators();
+ runForEachView((view) -> view.onFinishUiTranslation(), STATE_UI_TRANSLATION_PAUSED);
+ break;
+ default:
+ Log.w(TAG, "onAutoTranslationStateChange(): unknown state: " + state);
+ }
+ }
+
+ /**
+ * Called when the Activity is destroyed.
+ */
+ public void onActivityDestroyed() {
+ synchronized (mLock) {
+ mViews.clear();
+ destroyTranslators();
+ mWorkerThread.quitSafely();
+ }
+ }
+
+ /**
+ * The method is used by {@link Translator}, it will be called when the translation is done. The
+ * translation result can be get from here.
+ */
+ public void onTranslationCompleted(TranslationResponse response) {
+ if (response == null || response.getTranslationStatus()
+ != TranslationResponse.TRANSLATION_STATUS_SUCCESS) {
+ Log.w(TAG, "Fail result from TranslationService, response: " + response);
+ return;
+ }
+ final List<TranslationRequest> translatedResult = response.getTranslations();
+ onTranslationCompleted(translatedResult);
+ }
+
+ private void onTranslationCompleted(List<TranslationRequest> translatedResult) {
+ if (!mActivity.isResumed()) {
+ return;
+ }
+ final int resultCount = translatedResult.size();
+ synchronized (mLock) {
+ for (int i = 0; i < resultCount; i++) {
+ final TranslationRequest request = translatedResult.get(i);
+ final AutofillId autofillId = request.getAutofillId();
+ if (autofillId == null) {
+ continue;
+ }
+ final View view = mViews.get(autofillId).get();
+ if (view == null) {
+ Log.w(TAG, "onTranslationCompleted: the Veiew for autofill id " + autofillId
+ + " may be gone.");
+ continue;
+ }
+ mActivity.runOnUiThread(() -> view.onTranslationComplete(request));
+ }
+ }
+ }
+
+ /**
+ * Called when there is an ui translation request comes to request view translation.
+ */
+ @WorkerThread
+ private void createTranslatorAndStart(TranslationSpec sourceSpec, TranslationSpec destSpec,
+ List<AutofillId> views) {
+ // Create Translator
+ final Translator translator = createTranslatorIfNeeded(sourceSpec, destSpec);
+ if (translator == null) {
+ Log.w(TAG, "Can not create Translator for sourceSpec:" + sourceSpec + " destSpec:"
+ + destSpec);
+ return;
+ }
+ onUiTranslationStarted(translator, views);
+ }
+
+ @WorkerThread
+ private void sendTranslationRequest(Translator translator,
+ ArrayList<TranslationRequest> requests) {
+ translator.requestUiTranslate(requests, this::onTranslationCompleted);
+ }
+
+ /**
+ * Called when there is an ui translation request comes to request view translation.
+ */
+ private void onUiTranslationStarted(Translator translator, List<AutofillId> views) {
+ synchronized (mLock) {
+ if (views == null || views.size() == 0) {
+ throw new IllegalArgumentException("Invalid empty views: " + views);
+ }
+ // Find Views collect the translation data
+ // TODO(b/178084101): try to optimize, e.g. to this in a single traversal
+ final int viewCounts = views.size();
+ final ArrayList<TranslationRequest> requests = new ArrayList<>();
+ for (int i = 0; i < viewCounts; i++) {
+ final AutofillId viewAutofillId = views.get(i);
+ final View view = mActivity.findViewByAutofillIdTraversal(viewAutofillId);
+ if (view == null) {
+ Log.w(TAG, "Can not find the View for autofill id= " + viewAutofillId);
+ continue;
+ }
+ mViews.put(viewAutofillId, new WeakReference<>(view));
+ mActivity.runOnUiThread(() -> {
+ final TranslationRequest translationRequest = view.onCreateTranslationRequest();
+ if (translationRequest != null
+ && translationRequest.getTranslationText().length() > 0) {
+ requests.add(translationRequest);
+ }
+ if (requests.size() == viewCounts) {
+ Log.v(TAG, "onUiTranslationStarted: send " + requests.size() + " request.");
+ mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
+ UiTranslationController::sendTranslationRequest,
+ UiTranslationController.this, translator, requests));
+ }
+ });
+ }
+ }
+ }
+
+ private void runForEachView(Consumer<View> action, @UiTranslationState int state) {
+ synchronized (mLock) {
+ mActivity.runOnUiThread(() -> {
+ final int viewCounts = mViews.size();
+ for (int i = 0; i < viewCounts; i++) {
+ final View view = mViews.valueAt(i).get();
+ if (view == null) {
+ Log.w(TAG, "The View for autofill id " + mViews.keyAt(i)
+ + " may be gone for state " + stateToString(state));
+ continue;
+ }
+ action.accept(view);
+ }
+ if (state == STATE_UI_TRANSLATION_FINISHED) {
+ mViews.clear();
+ }
+ });
+ }
+ }
+
+ private Translator createTranslatorIfNeeded(
+ TranslationSpec sourceSpec, TranslationSpec destSpec) {
+ final TranslationManager tm = mContext.getSystemService(TranslationManager.class);
+ if (tm == null) {
+ Log.e(TAG, "Can not find TranslationManager when trying to create translator.");
+ return null;
+ }
+ final Translator translator = tm.createTranslator(sourceSpec, destSpec);
+ if (translator != null) {
+ final Pair<TranslationSpec, TranslationSpec> specs = new Pair<>(sourceSpec, destSpec);
+ mTranslators.put(specs, translator);
+ }
+ return translator;
+ }
+
+ private void destroyTranslators() {
+ synchronized (mLock) {
+ final int count = mTranslators.size();
+ for (int i = 0; i < count; i++) {
+ Translator translator = mTranslators.valueAt(i);
+ translator.destroy();
+ }
+ mTranslators.clear();
+ }
+ }
+
+ /**
+ * Returns a string representation of the state.
+ */
+ public static String stateToString(@UiTranslationState int state) {
+ switch (state) {
+ case STATE_UI_TRANSLATION_STARTED:
+ return "UI_TRANSLATION_STARTED";
+ case STATE_UI_TRANSLATION_PAUSED:
+ return "UI_TRANSLATION_PAUSED";
+ case STATE_UI_TRANSLATION_RESUMED:
+ return "UI_TRANSLATION_RESUMED";
+ case STATE_UI_TRANSLATION_FINISHED:
+ return "UI_TRANSLATION_FINISHED";
+ default:
+ return "Unknown state (" + state + ")";
+ }
}
}