Merge "Implement the behavior after receiving auto translation request." into sc-dev

This commit is contained in:
TreeHugger Robot
2021-01-28 06:28:49 +00:00
committed by Android (Google) Code Review
3 changed files with 304 additions and 8 deletions

View File

@@ -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,10 +8468,16 @@ 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(

View File

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

View File

@@ -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 + ")";
}
}
}