AutoTranslate: Pad view text for compatibility reasons.

Some apps operate on the view text based on layout calculations made on
the transformed text. This can cause issues (even crashes) if the
transformed text is longer than the original, as is often the case with
Translate. A previous temporary fix,
I685ae4a9752c817db81f0d736f1d746b2e4a9839, ellipsized the translated
text. This change fixes it properly by instead padding the original
text.

A followup change will make this controllable by API instead of padding
every TextView.

The en space character, U+2002, is used for the padding here. It seems
to work well in practice for multiple languages. We don't make this
configurable through API as it could make debugging harder (issues
triggered by server-side changes).

Bug: 179693024
Test: atest CtsTranslationTestCases
Test: manual - toggling between original and translated, scrolling, new
 views appearing, multiple apps
Change-Id: I9d7db5d2bc200e4042baf3267796f00cfc298f19
This commit is contained in:
Ahaan Ugale
2021-04-15 18:49:42 -07:00
parent 1198ed066b
commit 7110641881
5 changed files with 93 additions and 20 deletions

View File

@@ -78,24 +78,11 @@ public class TranslationTransformationMethod implements TransformationMethod2 {
if (TextUtils.isEmpty(translatedText) || isWhitespace(translatedText.toString())) {
return source;
} else {
// TODO(b/179693024): Remove this once we have the fix to pad the view text instead.
translatedText = ellipsize(translatedText, ((TextView) view).getText().length());
// TODO(b/174283799): apply the spans to the text
return translatedText;
}
}
private static CharSequence ellipsize(CharSequence text, int newLength) {
if (text.length() <= newLength) {
return text;
}
String ellipsis = String.valueOf('\u2026');
if (newLength == 1) {
return ellipsis;
}
return TextUtils.concat(TextUtils.trimToSize(text, newLength - 1), ellipsis);
}
@Override
public void onFocusChanged(View view, CharSequence sourceText,
boolean focused, int direction,

View File

@@ -362,17 +362,20 @@ public class UiTranslationController {
continue;
}
mActivity.runOnUiThread(() -> {
if (view.getViewTranslationCallback() == null) {
final ViewTranslationCallback callback = view.getViewTranslationCallback();
if (callback == null) {
if (DEBUG) {
Log.d(TAG, view + " doesn't support showing translation because of "
+ "null ViewTranslationCallback.");
}
return;
}
// TODO: Do this for specific views on request only.
callback.enableContentPadding();
view.onTranslationResponse(response);
if (view.getViewTranslationCallback() != null) {
view.getViewTranslationCallback().onShowTranslation(view);
}
callback.onShowTranslation(view);
});
}
}

View File

@@ -48,4 +48,16 @@ public interface ViewTranslationCallback {
* @return {@code true} if the View handles clearing the translation.
*/
boolean onClearTranslation(@NonNull View view);
/**
* Enables padding on the view's original content.
* <p>
* This is useful when we do not modify the content directly, rather use a mechanism like
* {@link android.text.method.TransformationMethod}. If the app misbehaves when the displayed
* translation and the underlying content have different sizes, the platform intelligence can
* request that the original content be padded to make the sizes match.
*
* @hide
*/
default void enableContentPadding() {}
}

View File

@@ -770,6 +770,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private final boolean mUseInternationalizedInput;
// True if fallback fonts that end up getting used should be allowed to affect line spacing.
/* package */ boolean mUseFallbackLineSpacing;
// True if the view text can be padded for compat reasons, when the view is translated.
private final boolean mUseTextPaddingForUiTranslation;
@ViewDebug.ExportedProperty(category = "text")
@UnsupportedAppUsage
@@ -1480,6 +1482,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
mUseInternationalizedInput = targetSdkVersion >= VERSION_CODES.O;
mUseFallbackLineSpacing = targetSdkVersion >= VERSION_CODES.P;
// TODO(b/179693024): Use a ChangeId instead.
mUseTextPaddingForUiTranslation = targetSdkVersion <= Build.VERSION_CODES.R;
if (inputMethod != null) {
Class<?> c;
@@ -2372,6 +2376,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@ViewDebug.CapturedViewProperty
@InspectableProperty
public CharSequence getText() {
if (mUseTextPaddingForUiTranslation
&& mDefaultTranslationCallback != null
&& mDefaultTranslationCallback.isTextPaddingEnabled()
&& mDefaultTranslationCallback.isShowingTranslation()) {
return mDefaultTranslationCallback.getPaddedText(mText, mTransformed);
}
return mText;
}

View File

@@ -17,6 +17,7 @@
package android.widget;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.os.Build;
import android.text.method.TranslationTransformationMethod;
import android.util.Log;
@@ -33,7 +34,7 @@ import android.view.translation.ViewTranslationResponse;
*/
public class TextViewTranslationCallback implements ViewTranslationCallback {
private static final String TAG = "TextViewTranslationCallback";
private static final String TAG = "TextViewTranslationCb";
// TODO(b/182433547): remove Build.IS_DEBUGGABLE before ship. Enable the logging in debug build
// to help the debug during the development phase
@@ -41,6 +42,9 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
|| Build.IS_DEBUGGABLE;
private TranslationTransformationMethod mTranslationTransformation;
private boolean mIsShowingTranslation = false;
private boolean mIsTextPaddingEnabled = false;
private CharSequence mPaddedText;
/**
* Invoked by the platform when receiving the successful {@link ViewTranslationResponse} for the
@@ -74,6 +78,7 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
*/
@Override
public boolean onShowTranslation(@NonNull View view) {
mIsShowingTranslation = true;
if (mTranslationTransformation != null) {
((TextView) view).setTransformationMethod(mTranslationTransformation);
} else {
@@ -90,6 +95,7 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
*/
@Override
public boolean onHideTranslation(@NonNull View view) {
mIsShowingTranslation = false;
// Restore to original text content.
if (mTranslationTransformation != null) {
((TextView) view).setTransformationMethod(
@@ -110,9 +116,9 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
public boolean onClearTranslation(@NonNull View view) {
// Restore to original text content and clear TranslationTransformation
if (mTranslationTransformation != null) {
((TextView) view).setTransformationMethod(
mTranslationTransformation.getOriginalTransformationMethod());
onHideTranslation(view);
clearTranslationTransformation();
mPaddedText = null;
} else {
if (DEBUG) {
// TODO(b/182433547): remove before S release
@@ -121,4 +127,59 @@ public class TextViewTranslationCallback implements ViewTranslationCallback {
}
return true;
}
boolean isShowingTranslation() {
return mIsShowingTranslation;
}
@Override
public void enableContentPadding() {
mIsTextPaddingEnabled = true;
}
/**
* Returns whether readers of the view text should receive padded text for compatibility
* reasons. The view's original text will be padded to match the length of the translated text.
*/
boolean isTextPaddingEnabled() {
return mIsTextPaddingEnabled;
}
/**
* Returns the view's original text with padding added. If the translated text isn't longer than
* the original text, returns the original text itself.
*
* @param text the view's original text
* @param translatedText the view's translated text
* @see #isTextPaddingEnabled()
*/
@Nullable
CharSequence getPaddedText(CharSequence text, CharSequence translatedText) {
if (text == null) {
return null;
}
if (mPaddedText == null) {
mPaddedText = computePaddedText(text, translatedText);
}
return mPaddedText;
}
@NonNull
private CharSequence computePaddedText(CharSequence text, CharSequence translatedText) {
if (translatedText == null) {
return text;
}
int newLength = translatedText.length();
if (newLength <= text.length()) {
return text;
}
StringBuilder sb = new StringBuilder(newLength);
sb.append(text);
for (int i = text.length(); i < newLength; i++) {
sb.append(COMPAT_PAD_CHARACTER);
}
return sb;
}
private static final char COMPAT_PAD_CHARACTER = '\u2002';
}