Update View translation APIs.

This change doesn't contain the virtual view part and the API
dispatchTranslationRequests, it will be done in another change.

Bug: 178046780
Test: manual
Test: atest CtsTranslationTestCases
CTS-Coverage-Bug: 177960696

Change-Id: I65fe1db19c9dff21c0caca425fbb7d08559e730b
This commit is contained in:
Joanne Chung
2021-03-15 22:31:53 +08:00
parent 1363aa9538
commit a749f2aadb
6 changed files with 371 additions and 178 deletions

View File

@@ -48273,6 +48273,7 @@ package android.view {
method protected int computeVerticalScrollRange();
method public android.view.accessibility.AccessibilityNodeInfo createAccessibilityNodeInfo();
method public void createContextMenu(android.view.ContextMenu);
method @Nullable public android.view.translation.ViewTranslationRequest createTranslationRequest(@NonNull int[]);
method @Deprecated public void destroyDrawingCache();
method public android.view.WindowInsets dispatchApplyWindowInsets(android.view.WindowInsets);
method public boolean dispatchCapturedPointerEvent(android.view.MotionEvent);
@@ -48493,6 +48494,7 @@ package android.view {
method @Nullable public android.graphics.drawable.Drawable getVerticalScrollbarThumbDrawable();
method @Nullable public android.graphics.drawable.Drawable getVerticalScrollbarTrackDrawable();
method public int getVerticalScrollbarWidth();
method @Nullable public android.view.translation.ViewTranslationCallback getViewTranslationCallback();
method public android.view.ViewTreeObserver getViewTreeObserver();
method public int getVisibility();
method public final int getWidth();
@@ -48636,6 +48638,7 @@ package android.view {
method public void onStartTemporaryDetach();
method public boolean onTouchEvent(android.view.MotionEvent);
method public boolean onTrackballEvent(android.view.MotionEvent);
method public void onTranslationResponse(@NonNull android.view.translation.ViewTranslationResponse);
method @CallSuper public void onVisibilityAggregated(boolean);
method protected void onVisibilityChanged(@NonNull android.view.View, int);
method public void onWindowFocusChanged(boolean);
@@ -48844,6 +48847,7 @@ package android.view {
method public void setVerticalScrollbarPosition(int);
method public void setVerticalScrollbarThumbDrawable(@Nullable android.graphics.drawable.Drawable);
method public void setVerticalScrollbarTrackDrawable(@Nullable android.graphics.drawable.Drawable);
method public void setViewTranslationCallback(@NonNull android.view.translation.ViewTranslationCallback);
method public void setVisibility(int);
method @Deprecated public void setWillNotCacheDrawing(boolean);
method public void setWillNotDraw(boolean);
@@ -52665,6 +52669,12 @@ package android.view.translation {
method public void onStarted(@NonNull String, @NonNull String);
}
@UiThread public interface ViewTranslationCallback {
method public boolean onClearTranslation(@NonNull android.view.View);
method public boolean onHideTranslation(@NonNull android.view.View);
method public boolean onShowTranslation(@NonNull android.view.View);
}
public final class ViewTranslationRequest implements android.os.Parcelable {
method public int describeContents();
method @NonNull public android.view.autofill.AutofillId getAutofillId();

View File

@@ -151,6 +151,8 @@ import android.view.inputmethod.InputConnection;
import android.view.inspector.InspectableProperty;
import android.view.inspector.InspectableProperty.EnumEntry;
import android.view.inspector.InspectableProperty.FlagEntry;
import android.view.translation.TranslationSpec.DataFormat;
import android.view.translation.ViewTranslationCallback;
import android.view.translation.ViewTranslationRequest;
import android.view.translation.ViewTranslationResponse;
import android.widget.Checkable;
@@ -5253,6 +5255,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
@Nullable
private String[] mOnReceiveContentMimeTypes;
@Nullable
private ViewTranslationCallback mViewTranslationCallback;
/**
* Simple constructor to use when creating a view from code.
*
@@ -30717,71 +30722,62 @@ public class View implements Drawable.Callback, KeyEvent.Callback,
}
}
//TODO(b/1960696): update javadoc when dispatchRequestTranslation is ready.
/**
* Returns a {@link ViewTranslationRequest} to the {@link onStartUiTranslation} which represents
* the content to be translated.
* Returns a {@link ViewTranslationRequest} which represents the content to be translated.
*
* <p>The default implementation does nothing and return null.</p>
* <p>The default implementation does nothing and returns null.</p>
*
* @hide
*
* @return the {@link ViewTranslationRequest} which contains the information to be translated.
* @param supportedFormats the supported translation formats. For now, the only possible value
* is the {@link android.view.translation.TranslationSpec#DATA_FORMAT_TEXT}.
* @return the {@link ViewTranslationRequest} which contains the information to be translated or
* {@code null} if this View doesn't support translation.
* The {@link AutofillId} must be set on the returned value.
*/
@Nullable
//TODO(b/178046780): initial version for demo. Will mark public when the design is reviewed.
public ViewTranslationRequest onCreateTranslationRequest() {
public ViewTranslationRequest createTranslationRequest(
@NonNull @DataFormat int[] supportedFormats) {
return null;
}
/**
* Called when the user wants to show the original text instead of the translated text.
* Returns a {@link ViewTranslationCallback} that is used to display/hide the translated
* information. If the View supports displaying translated content, it should implement
* {@link ViewTranslationCallback}.
*
* @hide
* <p>The default implementation returns null if developers don't set the customized
* {@link ViewTranslationCallback} by {@link #setViewTranslationCallback} </p>
*
* <p> The default implementation does nothing.
* @return a {@link ViewTranslationCallback} that is used to control how to display the
* translated information or {@code null} if this View doesn't support translation.
*/
//TODO(b/178046780): initial version for demo. Will mark public when the design is reviewed.
public void onPauseUiTranslation() {
// no-op
@Nullable
public ViewTranslationCallback getViewTranslationCallback() {
return mViewTranslationCallback;
}
/**
* User can switch back to show the original text, this method called when the user wants to
* re-show the translated text again.
* Sets a {@link ViewTranslationCallback} that is used to display/hide the translated
* information. Developers can provide the customized implementation for show/hide translated
* information.
*
* @hide
*
* <p> The default implementation does nothing.</p>
* @param callback a {@link ViewTranslationCallback} that is used to control how to display the
* translated information
*/
//TODO(b/178046780): initial version for demo. Will mark public when the design is reviewed.
public void onRestoreUiTranslation() {
// no-op
public void setViewTranslationCallback(@NonNull ViewTranslationCallback callback) {
mViewTranslationCallback = callback;
}
/**
* Called when the user finish the Ui translation and no longer to show the translated text.
*
* @hide
*
* <p> The default implementation does nothing.</p>
*/
//TODO(b/178046780): initial version for demo. Will mark public when the design is reviewed.
public void onFinishUiTranslation() {
// no-op
}
/**
* Called when the request from {@link onStartUiTranslation} is completed by the translation
* service so that the translation result can be shown.
*
* @hide
* Called when the content from {@link #createTranslationRequest} had been translated by the
* TranslationService.
*
* <p> The default implementation does nothing.</p>
*
* @param response the translated information which can be shown in the view.
* @param response a {@link ViewTranslationResponse} that contains the translated information
* which can be shown in the view.
*/
//TODO(b/178046780): initial version for demo. Will mark public when the design is reviewed.
public void onTranslationComplete(@NonNull ViewTranslationResponse response) {
public void onTranslationResponse(@NonNull ViewTranslationResponse response) {
// no-op
}

View File

@@ -46,7 +46,7 @@ import java.io.PrintWriter;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;
import java.util.function.BiConsumer;
/**
* A controller to manage the ui translation requests for the {@link Activity}.
@@ -77,6 +77,7 @@ public class UiTranslationController {
private final HandlerThread mWorkerThread;
@NonNull
private final Handler mWorkerHandler;
private int mCurrentState;
public UiTranslationController(Activity activity, Context context) {
mActivity = activity;
@@ -101,6 +102,9 @@ public class UiTranslationController {
}
Log.i(TAG, "updateUiTranslationState state: " + stateToString(state)
+ (DEBUG ? ", views: " + views : ""));
synchronized (mLock) {
mCurrentState = state;
}
switch (state) {
case STATE_UI_TRANSLATION_STARTED:
final Pair<TranslationSpec, TranslationSpec> specs =
@@ -114,14 +118,14 @@ public class UiTranslationController {
}
break;
case STATE_UI_TRANSLATION_PAUSED:
runForEachView(View::onPauseUiTranslation);
runForEachView((view, callback) -> callback.onHideTranslation(view));
break;
case STATE_UI_TRANSLATION_RESUMED:
runForEachView(View::onRestoreUiTranslation);
runForEachView((view, callback) -> callback.onShowTranslation(view));
break;
case STATE_UI_TRANSLATION_FINISHED:
destroyTranslators();
runForEachView(View::onFinishUiTranslation);
runForEachView((view, callback) -> callback.onClearTranslation(view));
synchronized (mLock) {
mViews.clear();
}
@@ -232,11 +236,16 @@ public class UiTranslationController {
}
final SparseArray<ViewTranslationResponse> translatedResult =
response.getViewTranslationResponses();
// TODO(b/177960696): handle virtual views. Check the result if the AutofillId is virtual
// AutofillId?
onTranslationCompleted(translatedResult);
}
private void onTranslationCompleted(SparseArray<ViewTranslationResponse> translatedResult) {
if (!mActivity.isResumed()) {
if (DEBUG) {
Log.v(TAG, "onTranslationCompleted: Activity is not resumed.");
}
return;
}
final int resultCount = translatedResult.size();
@@ -244,6 +253,11 @@ public class UiTranslationController {
Log.v(TAG, "onTranslationCompleted: receive " + resultCount + " responses.");
}
synchronized (mLock) {
if (mCurrentState == STATE_UI_TRANSLATION_FINISHED) {
Log.w(TAG, "onTranslationCompleted: the translation state is finished now. "
+ "Skip to show the translated text.");
return;
}
for (int i = 0; i < resultCount; i++) {
final ViewTranslationResponse response = translatedResult.get(i);
final AutofillId autofillId = response.getAutofillId();
@@ -256,18 +270,28 @@ public class UiTranslationController {
+ " may be gone.");
continue;
}
mActivity.runOnUiThread(() -> view.onTranslationComplete(response));
mActivity.runOnUiThread(() -> {
if (view.getViewTranslationCallback() == null) {
if (DEBUG) {
Log.d(TAG, view + " doesn't support showing translation because of "
+ "null ViewTranslationCallback.");
}
return;
}
view.onTranslationResponse(response);
view.getViewTranslationCallback().onShowTranslation(view);
});
}
}
}
/**
* Called when there is an ui translation request comes to request view translation.
* Creates a Translator for the given source and target translation specs and start the ui
* translation when the Translator is created successfully.
*/
@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:"
@@ -295,31 +319,51 @@ public class UiTranslationController {
*/
private void onUiTranslationStarted(Translator translator, List<AutofillId> views) {
synchronized (mLock) {
// Find Views collect the translation data
final ArrayList<ViewTranslationRequest> requests = new ArrayList<>();
final ArrayList<View> foundViews = new ArrayList<>();
findViewsTraversalByAutofillIds(views, foundViews);
for (int i = 0; i < foundViews.size(); i++) {
final View view = foundViews.get(i);
final int currentCount = i;
mActivity.runOnUiThread(() -> {
final ViewTranslationRequest request = view.onCreateTranslationRequest();
if (request != null
&& request.getKeys().size() > 0) {
requests.add(request);
}
if (currentCount == (foundViews.size() - 1)) {
Log.v(TAG, "onUiTranslationStarted: collect " + requests.size()
+ " requests.");
mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
UiTranslationController::sendTranslationRequest,
UiTranslationController.this, translator, requests));
}
});
}
// TODO(b/177960696): handle virtual views. Need to check the requested view list is
// virtual AutofillId or not
findViewsAndCollectViewTranslationRequest(translator, views);
}
}
/**
* If the translation requested views are not virtual view, we need to traverse the tree to
* find the views and get the View's ViewTranslationRequest.
*/
private void findViewsAndCollectViewTranslationRequest(Translator translator,
List<AutofillId> views) {
// Find Views collect the translation data
final ArrayList<ViewTranslationRequest> requests = new ArrayList<>();
final ArrayList<View> foundViews = new ArrayList<>();
findViewsTraversalByAutofillIds(views, foundViews);
final int[] supportedFormats = getSupportedFormatsLocked();
for (int i = 0; i < foundViews.size(); i++) {
final View view = foundViews.get(i);
final int currentCount = i;
mActivity.runOnUiThread(() -> {
final ViewTranslationRequest request =
view.createTranslationRequest(supportedFormats);
// TODO(b/177960696): handle null case, the developers may want to handle the
// translation, call dispatchRequestTranslation() instead.
if (request != null
&& request.getKeys().size() > 0) {
requests.add(request);
}
if (currentCount == (foundViews.size() - 1)) {
Log.v(TAG, "onUiTranslationStarted: collect " + requests.size()
+ " requests.");
mWorkerHandler.sendMessage(PooledLambda.obtainMessage(
UiTranslationController::sendTranslationRequest,
UiTranslationController.this, translator, requests));
}
});
}
}
private int[] getSupportedFormatsLocked() {
// We only support text now
return new int[] {TranslationSpec.DATA_FORMAT_TEXT};
}
private void findViewsTraversalByAutofillIds(List<AutofillId> sourceViewIds,
ArrayList<View> foundViews) {
final ArrayList<ViewRootImpl> roots =
@@ -356,20 +400,21 @@ public class UiTranslationController {
}
}
private void runForEachView(Consumer<View> action) {
private void runForEachView(BiConsumer<View, ViewTranslationCallback> action) {
synchronized (mLock) {
final ArrayMap<AutofillId, WeakReference<View>> views = new ArrayMap<>(mViews);
mActivity.runOnUiThread(() -> {
final int viewCounts = views.size();
for (int i = 0; i < viewCounts; i++) {
final View view = views.valueAt(i).get();
if (view == null) {
if (view == null || view.getViewTranslationCallback() == null) {
if (DEBUG) {
Log.d(TAG, "View was gone for autofillid = " + views.keyAt(i));
Log.d(TAG, "View was gone or ViewTranslationCallback for autofillid "
+ "= " + views.keyAt(i));
}
continue;
}
action.accept(view);
action.accept(view, view.getViewTranslationCallback());
}
});
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.view.translation;
import android.annotation.NonNull;
import android.annotation.UiThread;
import android.view.View;
/**
* Callback for handling the translated information show or hide in the {@link View}. See {@link
* View#onTranslationResponse} for how to get the translated information.
*/
@UiThread
public interface ViewTranslationCallback {
/**
* Called when the translated text is ready to show or if the user has requested to reshow the
* translated content after hiding it. This may be called before the translation results are
* returned through the {@link View#onTranslationResponse}.
*
* @return {@code true} if the View handles showing the translation.
*/
boolean onShowTranslation(@NonNull View view);
/**
* Called when the user wants to show the original text instead of the translated text. This
* may be called before the translation results are returned through the
* {@link View#onTranslationResponse}.
*
* @return {@code true} if the View handles hiding the translation.
*/
boolean onHideTranslation(@NonNull View view);
/**
* Called when the user finish the Ui translation and no longer to show the translated text.
*
* @return {@code true} if the View handles clearing the translation.
*/
boolean onClearTranslation(@NonNull View view);
}

View File

@@ -195,7 +195,9 @@ import android.view.textclassifier.TextLinks;
import android.view.textservice.SpellCheckerSubtype;
import android.view.textservice.TextServicesManager;
import android.view.translation.TranslationRequestValue;
import android.view.translation.TranslationSpec;
import android.view.translation.UiTranslationController;
import android.view.translation.ViewTranslationCallback;
import android.view.translation.ViewTranslationRequest;
import android.view.translation.ViewTranslationResponse;
import android.widget.RemoteViews.RemoteView;
@@ -203,6 +205,7 @@ import android.widget.RemoteViews.RemoteView;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.internal.util.ArrayUtils;
import com.android.internal.util.FastMath;
import com.android.internal.util.Preconditions;
import com.android.internal.widget.EditableInputConnection;
@@ -737,7 +740,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private MovementMethod mMovement;
private TransformationMethod mTransformation;
private TranslationTransformationMethod mTranslationTransformation;
private TextViewTranslationCallback mDefaultTranslationCallback;
@UnsupportedAppUsage
private boolean mAllowTransformationLengthChange;
@UnsupportedAppUsage
@@ -13857,136 +13860,104 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
* Provides a {@link ViewTranslationRequest} that represents the content to be translated via
* translation service.
* Returns a {@link ViewTranslationRequest} which represents the content to be translated.
*
* <p>NOTE: When overriding the method, it should not translate the password. We also suggest
* that not translating the text is selectable or editable. We use the transformation method to
* implement showing the translated text. The TextView does not support the transformation
* method text length change. If the text is selectable or editable, it will crash while
* selecting the text. To support it, it needs broader changes to text APIs, we only allow to
* translate non selectable and editable text now.
* <p>NOTE: When overriding the method, it should not translate the password. If the subclass
* uses {@link TransformationMethod} to display the translated result, it's also not recommend
* to translate text is selectable or editable.
*
* @hide
* @param supportedFormats the supported translation format. The value could be {@link
* android.view.translation.TranslationSpec#DATA_FORMAT_TEXT}.
* @return the {@link ViewTranslationRequest} which contains the information to be translated.
*/
@Nullable
@Override
public ViewTranslationRequest onCreateTranslationRequest() {
if (mText == null || mText.length() == 0) {
public ViewTranslationRequest createTranslationRequest(@NonNull int[] supportedFormats) {
if (supportedFormats == null || supportedFormats.length == 0) {
// TODO(b/182433547): remove before S release
if (UiTranslationController.DEBUG) {
Log.w(LOG_TAG, "Cannot create translation request for the empty text.");
Log.w(LOG_TAG, "Do not provide the support translation formats.");
}
return null;
}
// Not translate password, editable text and not important for translation
// TODO(b/177214256): support selectable text translation. It needs to broader changes to
// text selection apis, not support in S.
boolean isPassword = isAnyPasswordInputType() || hasPasswordTransformationMethod();
if (isTextEditable() || isPassword || isTextSelectable()) {
// TODO(b/182433547): remove before S release
if (UiTranslationController.DEBUG) {
Log.w(LOG_TAG, "Cannot create translation request. editable = " + isTextEditable()
+ ", isPassword = " + isPassword + ", selectable = " + isTextSelectable());
ViewTranslationRequest.Builder requestBuilder =
new ViewTranslationRequest.Builder(getAutofillId());
// Support Text translation
if (ArrayUtils.contains(supportedFormats, TranslationSpec.DATA_FORMAT_TEXT)) {
if (mText == null || mText.length() == 0) {
// TODO(b/182433547): remove before S release
if (UiTranslationController.DEBUG) {
Log.w(LOG_TAG, "Cannot create translation request for the empty text.");
}
return null;
}
return null;
boolean isPassword = isAnyPasswordInputType() || hasPasswordTransformationMethod();
// TODO(b/177214256): support selectable text translation.
// We use the TransformationMethod to implement showing the translated text. The
// TextView does not support the text length change for TransformationMethod. If the
// text is selectable or editable, it will crash while selecting the text. To support
// it, it needs broader changes to text APIs, we only allow to translate non selectable
// and editable text in S.
if (isTextEditable() || isPassword || isTextSelectable()) {
// TODO(b/182433547): remove before S release
if (UiTranslationController.DEBUG) {
Log.w(LOG_TAG, "Cannot create translation request. editable = "
+ isTextEditable() + ", isPassword = " + isPassword + ", selectable = "
+ isTextSelectable());
}
return null;
}
// TODO(b/176488462): apply the view's important for translation
requestBuilder.setValue(ViewTranslationRequest.ID_TEXT,
TranslationRequestValue.forText(mText));
}
// TODO(b/176488462): apply the view's important for translation property
// TODO(b/174283799): remove the spans from the mText and save the spans information
// TODO: use fixed ids for request texts.
ViewTranslationRequest request =
new ViewTranslationRequest.Builder(getAutofillId())
.setValue(ViewTranslationRequest.ID_TEXT,
TranslationRequestValue.forText(mText))
.build();
return request;
return requestBuilder.build();
}
/**
* Provides the implementation that pauses the ongoing Ui translation, it will show the original
* text instead of the translated text and restore the original transformation method.
* Returns a {@link ViewTranslationCallback} that is used to display the translated information.
* The default implementation will use a {@link TransformationMethod} that allow to replace the
* current {@link TransformationMethod} to transform the original text to the translated text
* display.
*
* <p>NOTE: If this method is overridden, other translation related methods such as
* {@link onRestoreUiTranslation}, {@link onFinishUiTranslation}, {@link onTranslationComplete}
* should also be overridden.
*
* @hide
* @return a {@link ViewTranslationCallback} that is used to control how to display the
* translated information or {@code null} if this View doesn't support translation.
*/
@Nullable
@Override
public void onPauseUiTranslation() {
// Restore to original text content.
if (mTranslationTransformation != null) {
setTransformationMethod(mTranslationTransformation.getOriginalTransformationMethod());
} else {
// TODO(b/182433547): remove before S release
Log.w(LOG_TAG, "onPauseUiTranslation(): no translated text.");
public ViewTranslationCallback getViewTranslationCallback() {
return getDefaultViewTranslationCallback();
}
private ViewTranslationCallback getDefaultViewTranslationCallback() {
if (mDefaultTranslationCallback == null) {
mDefaultTranslationCallback = new TextViewTranslationCallback();
}
return mDefaultTranslationCallback;
}
/**
* Provides the implementation that restoes the paused Ui translation, it will show the
* translated text again if the text had been translated. This method will replace the current
* tansformation method with {@link TranslationTransformationMethod}.
*
* <p>NOTE: If this method is overridden, other translation related methods such as
* {@link onPauseUiTranslation}, {@link onFinishUiTranslation}, {@link onTranslationComplete}
* should also be overridden.
* Called when the content from {@link #createTranslationRequest} had been translated by the
* TranslationService. The default implementation will replace the current
* {@link TransformationMethod} to transform the original text to the translated text display.
*
* @hide
* @param response a {@link ViewTranslationResponse} that contains the translated information
* which can be shown in the view.
*/
@Override
public void onRestoreUiTranslation() {
if (mTranslationTransformation != null) {
setTransformationMethod(mTranslationTransformation);
} else {
// TODO(b/182433547): remove before S release
Log.w(LOG_TAG, "onRestoreUiTranslation(): no translated text.");
}
}
/**
* Provides the implementation that finishes the current Ui translation and it's no longer to
* show the translated text. This method restores the original transformation method and resets
* the saved {@link TranslationTransformationMethod}.
*
* <p>NOTE: If this method is overridden, other translation related methods such as
* {@link onPauseUiTranslation}, {@link onRestoreUiTranslation}, {@link onTranslationComplete}
* should also be overridden.
*
* @hide
*/
@Override
public void onFinishUiTranslation() {
// Restore to original text content and clear TranslationTransformation
if (mTranslationTransformation != null) {
setTransformationMethod(mTranslationTransformation.getOriginalTransformationMethod());
mTranslationTransformation = null;
} else {
// TODO(b/182433547): remove before S release
Log.w(LOG_TAG, "onFinishUiTranslation(): no translated text.");
}
}
/**
* Default {@link TextView} implementation after the translation request is done by the
* translation service, it's ok to show the translated text. This method will save the original
* transformation method and replace the current transformation method with
* {@link TranslationTransformationMethod}.
*
* <p>NOTE: If this method is overridden, other translation related methods such as
* {@link onPauseUiTranslation}, {@link onRestoreUiTranslation}, {@link onFinishUiTranslation}
* should also be overridden.
*
* @hide
*/
@Override
public void onTranslationComplete(@NonNull ViewTranslationResponse response) {
// Show the translated text.
TransformationMethod originalTranslationMethod = mTranslationTransformation != null
? mTranslationTransformation.getOriginalTransformationMethod() : mTransformation;
mTranslationTransformation =
public void onTranslationResponse(@NonNull ViewTranslationResponse response) {
// TODO(b/183467275): Use the overridden ViewTranslationCallback instead of our default
// implementation if the view has overridden getViewTranslationCallback.
TextViewTranslationCallback callback =
(TextViewTranslationCallback) getDefaultViewTranslationCallback();
TranslationTransformationMethod oldTranslationMethod =
callback.getTranslationTransformation();
TransformationMethod originalTranslationMethod = oldTranslationMethod != null
? oldTranslationMethod.getOriginalTransformationMethod() : mTransformation;
TranslationTransformationMethod newTranslationMethod =
new TranslationTransformationMethod(response, originalTranslationMethod);
// TODO(b/178353965): well-handle setTransformationMethod.
setTransformationMethod(mTranslationTransformation);
callback.setTranslationTransformation(newTranslationMethod);
}
}

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package android.widget;
import android.annotation.NonNull;
import android.text.method.TranslationTransformationMethod;
import android.util.Log;
import android.view.View;
import android.view.translation.UiTranslationManager;
import android.view.translation.ViewTranslationCallback;
import android.view.translation.ViewTranslationResponse;
/**
* Default implementation for {@link ViewTranslationCallback} for {@link TextView} components.
* This class handles how to display the translated information for {@link TextView}.
*
* @hide
*/
public class TextViewTranslationCallback implements ViewTranslationCallback {
private static final String TAG = "TextViewTranslationCallback";
private static final boolean DEBUG = Log.isLoggable(UiTranslationManager.LOG_TAG, Log.DEBUG);
private TranslationTransformationMethod mTranslationTransformation;
/**
* Invoked by the platform when receiving the successful {@link ViewTranslationResponse} for the
* view that provides the translatable information by {@link View#createTranslationRequest} and
* sent by the platform.
*/
void setTranslationTransformation(TranslationTransformationMethod method) {
if (method == null) {
if (DEBUG) {
Log.w(TAG, "setTranslationTransformation: should not set null "
+ "TranslationTransformationMethod");
}
return;
}
mTranslationTransformation = method;
}
TranslationTransformationMethod getTranslationTransformation() {
return mTranslationTransformation;
}
private void clearTranslationTransformation() {
if (DEBUG) {
Log.v(TAG, "clearTranslationTransformation: " + mTranslationTransformation);
}
mTranslationTransformation = null;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onShowTranslation(@NonNull View view) {
if (mTranslationTransformation != null) {
((TextView) view).setTransformationMethod(mTranslationTransformation);
} else {
if (DEBUG) {
// TODO(b/182433547): remove before S release
Log.w(TAG, "onShowTranslation(): no translated text.");
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onHideTranslation(@NonNull View view) {
// Restore to original text content.
if (mTranslationTransformation != null) {
((TextView) view).setTransformationMethod(
mTranslationTransformation.getOriginalTransformationMethod());
} else {
if (DEBUG) {
// TODO(b/182433547): remove before S release
Log.w(TAG, "onHideTranslation(): no translated text.");
}
}
return true;
}
/**
* {@inheritDoc}
*/
@Override
public boolean onClearTranslation(@NonNull View view) {
// Restore to original text content and clear TranslationTransformation
if (mTranslationTransformation != null) {
((TextView) view).setTransformationMethod(
mTranslationTransformation.getOriginalTransformationMethod());
clearTranslationTransformation();
} else {
if (DEBUG) {
// TODO(b/182433547): remove before S release
Log.w(TAG, "onClearTranslation(): no translated text.");
}
}
return true;
}
}