From a5fa9bc0f7d861b9a083aab3a43b60da1e19b9af Mon Sep 17 00:00:00 2001 From: Joanne Chung Date: Wed, 30 Dec 2020 00:05:02 +0800 Subject: [PATCH] Add initial View's APIs for auto translation The initial APIs for auto translation flow. The implementation of sending request and dealing spans will on the follow up changes. Bug: 172969740 Test: manual build pass. Use a sample app to call api get expected result. Change-Id: Ief45190026e6205ff18366c1aaba8f424a4df00c --- .../TranslationTransformationMethod.java | 93 ++++++++++++++ core/java/android/view/View.java | 69 ++++++++++ core/java/android/widget/TextView.java | 120 ++++++++++++++++++ 3 files changed, 282 insertions(+) create mode 100644 core/java/android/text/method/TranslationTransformationMethod.java diff --git a/core/java/android/text/method/TranslationTransformationMethod.java b/core/java/android/text/method/TranslationTransformationMethod.java new file mode 100644 index 0000000000000..cf66ac7e0d397 --- /dev/null +++ b/core/java/android/text/method/TranslationTransformationMethod.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2020 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.text.method; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.graphics.Rect; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.view.translation.TranslationRequest; +import android.widget.TextView; + +import java.util.regex.Pattern; + +/** + * Transforms source text into an translated string. + * + * @hide + */ +public class TranslationTransformationMethod implements TransformationMethod2 { + + private static final String TAG = "TranslationTransformationMethod"; + private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+"); + + @NonNull + private TranslationRequest mTranslationRequest; + @Nullable + private TransformationMethod mOriginalTranslationMethod; + private boolean mAllowLengthChanges; + + /** + * @param request the translated result from translation service. + * @param method the {@link TextView}'s original {@link TransformationMethod} + */ + public TranslationTransformationMethod(@NonNull TranslationRequest request, + @Nullable TransformationMethod method) { + mTranslationRequest = request; + mOriginalTranslationMethod = method; + } + + /** + * Returns the {@link TextView}'s original {@link TransformationMethod}. This can be used to + * restore to show if the user pauses or finish the ui translation. + */ + public TransformationMethod getOriginalTransformationMethod() { + return mOriginalTranslationMethod; + } + + @Override + public CharSequence getTransformation(CharSequence source, View view) { + if (!mAllowLengthChanges) { + Log.w(TAG, "Caller did not enable length changes; not transforming to translated text"); + return source; + } + CharSequence translatedText = mTranslationRequest.getTranslationText(); + if (TextUtils.isEmpty(translatedText) || isWhitespace(translatedText.toString())) { + return source; + } else { + // TODO(b/174283799): apply the spans to the text + return translatedText; + } + } + + @Override + public void onFocusChanged(View view, CharSequence sourceText, + boolean focused, int direction, + Rect previouslyFocusedRect) { + // do nothing + } + + @Override + public void setLengthChangesAllowed(boolean allowLengthChanges) { + mAllowLengthChanges = allowLengthChanges; + } + + private boolean isWhitespace(String text) { + return PATTERN_WHITESPACE.matcher(text.substring(0, text.length())).matches(); + } +} diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index 04fe8978788ba..5c0e15639491d 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -139,6 +139,7 @@ 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.TranslationRequest; import android.widget.Checkable; import android.widget.FrameLayout; import android.widget.ScrollBarDrawable; @@ -30630,4 +30631,72 @@ public class View implements Drawable.Callback, KeyEvent.Callback, mPrivateFlags4 &= ~PFLAG4_DETACHED; } } + + /** + * Returns a {@link TranslationRequest} to the {@link onStartUiTranslation} which represents + * the content to be translated. + * + *

The default implementation does nothing and return null.

+ * + * @hide + * + * @return the {@link TranslationRequest} which contains the information to be translated. + */ + @Nullable + //TODO(b/178046780): initial version for demo. Will mark public when the design is reviewed. + public TranslationRequest onCreateTranslationRequest() { + return null; + } + + /** + * Called when the user wants to show the original text instead of 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 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. + * + * @hide + * + *

The default implementation does nothing.

+ */ + //TODO(b/178046780): initial version for demo. Will mark public when the design is reviewed. + public void onRestoreUiTranslation() { + // no-op + } + + /** + * 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 + * + *

The default implementation does nothing.

+ * + * @param request 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 TranslationRequest request) { + // no-op + } } diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index b8660255acc1d..8cfbca88c5964 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -129,6 +129,7 @@ import android.text.method.TextKeyListener; import android.text.method.TimeKeyListener; import android.text.method.TransformationMethod; import android.text.method.TransformationMethod2; +import android.text.method.TranslationTransformationMethod; import android.text.method.WordIterator; import android.text.style.CharacterStyle; import android.text.style.ClickableSpan; @@ -193,6 +194,7 @@ import android.view.textclassifier.TextClassifier; import android.view.textclassifier.TextLinks; import android.view.textservice.SpellCheckerSubtype; import android.view.textservice.TextServicesManager; +import android.view.translation.TranslationRequest; import android.widget.RemoteViews.RemoteView; import com.android.internal.annotations.VisibleForTesting; @@ -732,6 +734,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private MovementMethod mMovement; private TransformationMethod mTransformation; + private TranslationTransformationMethod mTranslationTransformation; @UnsupportedAppUsage private boolean mAllowTransformationLengthChange; @UnsupportedAppUsage @@ -13814,4 +13817,121 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener Log.d(LOG_TAG, location + ": " + String.format(msgFormat, msgArgs)); } } + + /** + * Provides a {@link TranslationRequest} that represents the content to be translated via + * translation service. + * + *

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. + * + * @hide + */ + @Nullable + @Override + public TranslationRequest onCreateTranslationRequest() { + if (mText == null || mText.length() == 0) { + 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()) { + 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 informatopn + TranslationRequest request = + new TranslationRequest.Builder() + .setAutofillId(getAutofillId()) + .setTranslationText(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. + * + *

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()); + } + } + + /** + * 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. + * + * @hide + */ + @Override + public void onRestoreUiTranslation() { + if (mTranslationTransformation != null) { + setTransformationMethod(mTranslationTransformation); + } else { + Log.w(LOG_TAG, "onResumeTranslatedText(): 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; + } + } + + /** + * 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 TranslationRequest data) { + // Show the translated text. + TransformationMethod originalTranslationMethod = mTranslationTransformation != null + ? mTranslationTransformation.getOriginalTransformationMethod() : mTransformation; + mTranslationTransformation = + new TranslationTransformationMethod(data, originalTranslationMethod); + // TODO(b/178353965): well-handle setTransformationMethod. + setTransformationMethod(mTranslationTransformation); + } }