diff options
| -rw-r--r-- | core/api/current.txt | 10 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 78 | ||||
| -rw-r--r-- | core/java/android/view/translation/UiTranslationController.java | 109 | ||||
| -rw-r--r-- | core/java/android/view/translation/ViewTranslationCallback.java | 51 | ||||
| -rw-r--r-- | core/java/android/widget/TextView.java | 175 | ||||
| -rw-r--r-- | core/java/android/widget/TextViewTranslationCallback.java | 120 |
6 files changed, 368 insertions, 175 deletions
diff --git a/core/api/current.txt b/core/api/current.txt index 2e2147f808df..935cf7067043 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -48364,6 +48364,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); @@ -48584,6 +48585,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(); @@ -48727,6 +48729,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); @@ -48935,6 +48938,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); @@ -52757,6 +52761,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(); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index e573056ddbaa..a03e9e352c60 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -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. - * - * <p>The default implementation does nothing and return null.</p> + * Returns a {@link ViewTranslationRequest} which represents the content to be translated. * - * @hide + * <p>The default implementation does nothing and returns null.</p> * - * @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. - * - * @hide - * - * <p> The default implementation does nothing. - */ - //TODO(b/178046780): initial version for demo. Will mark public when the design is reviewed. - public void onPauseUiTranslation() { - // no-op - } - - /** - * User can switch back to show the original text, this method called when the user wants to - * re-show the translated text again. + * 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.</p> + * @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 onRestoreUiTranslation() { - // no-op + @Nullable + public ViewTranslationCallback getViewTranslationCallback() { + return mViewTranslationCallback; } /** - * Called when the user finish the Ui translation and no longer to show the translated text. + * 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 onFinishUiTranslation() { - // no-op + public void setViewTranslationCallback(@NonNull ViewTranslationCallback callback) { + mViewTranslationCallback = callback; } /** - * 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 } diff --git a/core/java/android/view/translation/UiTranslationController.java b/core/java/android/view/translation/UiTranslationController.java index d79ecca1426e..15d01ae6a8fc 100644 --- a/core/java/android/view/translation/UiTranslationController.java +++ b/core/java/android/view/translation/UiTranslationController.java @@ -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<>(); + // 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); - 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)); - } - }); - } + 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()); } }); } diff --git a/core/java/android/view/translation/ViewTranslationCallback.java b/core/java/android/view/translation/ViewTranslationCallback.java new file mode 100644 index 000000000000..c8959847d64b --- /dev/null +++ b/core/java/android/view/translation/ViewTranslationCallback.java @@ -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); +} diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 940a3c9cccdf..6733c0d3a8e1 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -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; - } - // 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; - } - - /** - * 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. - * - * <p>NOTE: If this method is overridden, other translation related methods such as - * {@link onRestoreUiTranslation}, {@link onFinishUiTranslation}, {@link onTranslationComplete} - * should also be overridden. - * - * @hide - */ - @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."); + 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)); } + return requestBuilder.build(); } /** - * 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. + * 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. * - * @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 onRestoreUiTranslation() { - if (mTranslationTransformation != null) { - setTransformationMethod(mTranslationTransformation); - } else { - // TODO(b/182433547): remove before S release - Log.w(LOG_TAG, "onRestoreUiTranslation(): no translated text."); - } + public ViewTranslationCallback getViewTranslationCallback() { + return getDefaultViewTranslationCallback(); } - /** - * 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."); + private ViewTranslationCallback getDefaultViewTranslationCallback() { + if (mDefaultTranslationCallback == null) { + mDefaultTranslationCallback = new TextViewTranslationCallback(); } + return mDefaultTranslationCallback; } /** - * 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. + * 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 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); } } diff --git a/core/java/android/widget/TextViewTranslationCallback.java b/core/java/android/widget/TextViewTranslationCallback.java new file mode 100644 index 000000000000..296d93c88554 --- /dev/null +++ b/core/java/android/widget/TextViewTranslationCallback.java @@ -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; + } +} |