diff --git a/core/api/current.txt b/core/api/current.txt index dd0ea286b54fd..5f3d041c15011 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -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(); diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 7455b8bc3cf32..3abc3853626a0 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. + * Returns a {@link ViewTranslationRequest} which represents the content to be translated. * - *
The default implementation does nothing and return null.
+ *The default implementation does nothing and returns null.
* - * @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 + *The default implementation returns null if developers don't set the customized + * {@link ViewTranslationCallback} by {@link #setViewTranslationCallback}
* - *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 - * - *
The default implementation does nothing.
+ * @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 - * - *The default implementation does nothing.
- */ - //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. * *The default implementation does nothing.
* - * @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 d79ecca1426e5..15d01ae6a8fcf 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 PairNOTE: 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. + *
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. * - *
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}. * - *
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}. - * - *
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}. - * - *
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); } } diff --git a/core/java/android/widget/TextViewTranslationCallback.java b/core/java/android/widget/TextViewTranslationCallback.java new file mode 100644 index 0000000000000..296d93c885547 --- /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; + } +}