summaryrefslogtreecommitdiff
path: root/core/java/android/widget/TextView.java
diff options
context:
space:
mode:
authorXin Li <delphij@google.com>2021-10-07 23:50:15 +0000
committerGerrit Code Review <noreply-gerritcodereview@google.com>2021-10-07 23:50:15 +0000
commitc03b0fa033117c03430e361d561aa910e95a0478 (patch)
tree9c6aaee5a3023a6c237394b44e06a3fdb46f6747 /core/java/android/widget/TextView.java
parent8cc0f40cf250d9c66dc15d0e8bc3a41db9a7cfa1 (diff)
parent531b8f4f2605c44cf73e8421f674a1c7a9c277ff (diff)
Merge "Merge Android 12"
Diffstat (limited to 'core/java/android/widget/TextView.java')
-rw-r--r--core/java/android/widget/TextView.java433
1 files changed, 343 insertions, 90 deletions
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 22b8ffbd28e3..f5c1bcf2de42 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -17,6 +17,11 @@
package android.widget;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.content.res.Configuration.ORIENTATION_PORTRAIT;
+import static android.view.ContentInfo.FLAG_CONVERT_TO_PLAIN_TEXT;
+import static android.view.ContentInfo.SOURCE_AUTOFILL;
+import static android.view.ContentInfo.SOURCE_CLIPBOARD;
+import static android.view.ContentInfo.SOURCE_PROCESS_TEXT;
import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_RENDERING_INFO_KEY;
import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH;
import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX;
@@ -38,6 +43,7 @@ import android.annotation.RequiresPermission;
import android.annotation.Size;
import android.annotation.StringRes;
import android.annotation.StyleRes;
+import android.annotation.TestApi;
import android.annotation.XmlRes;
import android.app.Activity;
import android.app.PendingIntent;
@@ -75,6 +81,7 @@ import android.os.AsyncTask;
import android.os.Build;
import android.os.Build.VERSION_CODES;
import android.os.Bundle;
+import android.os.Handler;
import android.os.LocaleList;
import android.os.Parcel;
import android.os.Parcelable;
@@ -140,6 +147,7 @@ import android.util.TypedValue;
import android.view.AccessibilityIterators.TextSegmentIterator;
import android.view.ActionMode;
import android.view.Choreographer;
+import android.view.ContentInfo;
import android.view.ContextMenu;
import android.view.DragEvent;
import android.view.Gravity;
@@ -185,11 +193,17 @@ import android.view.textclassifier.TextClassifier;
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.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;
@@ -425,7 +439,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
/**
* @hide
*/
- static final int PROCESS_TEXT_REQUEST_CODE = 100;
+ @TestApi
+ public static final int PROCESS_TEXT_REQUEST_CODE = 100;
/**
* Return code of {@link #doKeyDown}.
@@ -485,6 +500,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private TextUtils.TruncateAt mEllipsize;
+ // A flag to indicate the cursor was hidden by IME.
+ private boolean mImeIsConsumingInput;
+
+ // Whether cursor is visible without regard to {@link mImeConsumesInput}.
+ // {@code true} is the default value.
+ private boolean mCursorVisibleFromAttr = true;
+
static class Drawables {
static final int LEFT = 0;
static final int TOP = 1;
@@ -734,12 +756,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
private boolean mLocalesChanged = false;
private int mTextSizeUnit = -1;
+ // This is used to reflect the current user preference for changing font weight and making text
+ // more bold.
+ private int mFontWeightAdjustment;
+ private Typeface mOriginalTypeface;
+
// True if setKeyListener() has been explicitly called
private boolean mListenerChanged = false;
// True if internationalized input should be used for numbers and date and time.
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
@@ -951,6 +980,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* @hide
*/
public static void preloadFontCache() {
+ if (Typeface.ENABLE_LAZY_TYPEFACE_INITIALIZATION) {
+ return;
+ }
Paint p = new Paint();
p.setAntiAlias(true);
// Ensure that the Typeface is loaded here.
@@ -1447,6 +1479,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;
@@ -1635,6 +1669,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
attributes.mTypefaceIndex = MONOSPACE;
}
+ mFontWeightAdjustment = getContext().getResources().getConfiguration().fontWeightAdjustment;
applyTextAppearance(attributes);
if (isPassword) {
@@ -2128,14 +2163,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
/**
* @hide
*/
+ @TestApi
@Override
- public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
if (requestCode == PROCESS_TEXT_REQUEST_CODE) {
if (resultCode == Activity.RESULT_OK && data != null) {
CharSequence result = data.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT);
if (result != null) {
if (isTextEditable()) {
- replaceSelectionWithText(result);
+ ClipData clip = ClipData.newPlainText("", result);
+ ContentInfo payload =
+ new ContentInfo.Builder(clip, SOURCE_PROCESS_TEXT).build();
+ performReceiveContent(payload);
if (mEditor != null) {
mEditor.refreshTextActionMode();
}
@@ -2334,6 +2373,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@ViewDebug.CapturedViewProperty
@InspectableProperty
public CharSequence getText() {
+ if (mUseTextPaddingForUiTranslation) {
+ ViewTranslationCallback callback = getViewTranslationCallback();
+ if (callback != null && callback instanceof TextViewTranslationCallback) {
+ TextViewTranslationCallback defaultCallback =
+ (TextViewTranslationCallback) callback;
+ if (defaultCallback.isTextPaddingEnabled()
+ && defaultCallback.isShowingTranslation()) {
+ return defaultCallback.getPaddedText(mText, mTransformed);
+ }
+ }
+ }
return mText;
}
@@ -4256,6 +4306,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
invalidate();
}
}
+ if (mFontWeightAdjustment != newConfig.fontWeightAdjustment) {
+ mFontWeightAdjustment = newConfig.fontWeightAdjustment;
+ setTypeface(getTypeface());
+ }
}
/**
@@ -4407,6 +4461,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* @attr ref android.R.styleable#TextView_textStyle
*/
public void setTypeface(@Nullable Typeface tf) {
+ mOriginalTypeface = tf;
+ if (mFontWeightAdjustment != 0
+ && mFontWeightAdjustment != Configuration.FONT_WEIGHT_ADJUSTMENT_UNDEFINED) {
+ if (tf == null) {
+ tf = Typeface.DEFAULT;
+ } else {
+ int newWeight = Math.min(
+ Math.max(tf.getWeight() + mFontWeightAdjustment, FontStyle.FONT_WEIGHT_MIN),
+ FontStyle.FONT_WEIGHT_MAX);
+ int typefaceStyle = tf != null ? tf.getStyle() : 0;
+ boolean italic = (typefaceStyle & Typeface.ITALIC) != 0;
+ tf = Typeface.create(tf, newWeight, italic);
+ }
+ }
if (mTextPaint.getTypeface() != tf) {
mTextPaint.setTypeface(tf);
@@ -4430,7 +4498,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*/
@InspectableProperty
public Typeface getTypeface() {
- return mTextPaint.getTypeface();
+ return mOriginalTypeface;
}
/**
@@ -4706,6 +4774,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* @see #getJustificationMode()
*/
@Layout.JustificationMode
+ @android.view.RemotableViewMethod
public void setJustificationMode(@Layout.JustificationMode int justificationMode) {
mJustificationMode = justificationMode;
if (mLayout != null) {
@@ -5182,6 +5251,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* @see android.view.Gravity
* @attr ref android.R.styleable#TextView_gravity
*/
+ @android.view.RemotableViewMethod
public void setGravity(int gravity) {
if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
gravity |= Gravity.START;
@@ -5776,6 +5846,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*
* @attr ref android.R.styleable#TextView_lineHeight
*/
+ @android.view.RemotableViewMethod
public void setLineHeight(@Px @IntRange(from = 0) int lineHeight) {
Preconditions.checkArgumentNonnegative(lineHeight);
@@ -6216,11 +6287,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
|| needEditableForNotification) {
createEditorIfNeeded();
mEditor.forgetUndoRedo();
+ mEditor.scheduleRestartInputForSetText();
Editable t = mEditableFactory.newEditable(text);
text = t;
setFilters(t, mFilters);
- InputMethodManager imm = getInputMethodManager();
- if (imm != null) imm.restartInput(this);
} else if (precomputed != null) {
if (mTextDir == null) {
mTextDir = getTextDirectionHeuristic();
@@ -6339,8 +6409,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
notifyListeningManagersAfterTextChanged();
}
- // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
- if (mEditor != null) mEditor.prepareCursorControllers();
+ if (mEditor != null) {
+ // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
+ mEditor.prepareCursorControllers();
+
+ mEditor.maybeFireScheduledRestartInputForSetText();
+ }
}
/**
@@ -8699,6 +8773,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
}
}
+ if (getResources().getConfiguration().orientation == ORIENTATION_PORTRAIT) {
+ outAttrs.internalImeOptions |= EditorInfo.IME_INTERNAL_FLAG_APP_WINDOW_PORTRAIT;
+ }
if (isMultilineInputType(outAttrs.inputType)) {
// Multi-line text editors should always show an enter key.
outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
@@ -8711,6 +8788,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
outAttrs.initialSelEnd = getSelectionEnd();
outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
outAttrs.setInitialSurroundingText(mText);
+ outAttrs.contentMimeTypes = getReceiveContentMimeTypes();
return ic;
}
}
@@ -8873,6 +8951,13 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// intentionally empty
}
+ /** @hide */
+ public void onPerformSpellCheck() {
+ if (mEditor != null && mEditor.mSpellChecker != null) {
+ mEditor.mSpellChecker.onPerformSpellCheck();
+ }
+ }
+
/**
* Called by the framework in response to a private command from the
* current method, provided by it calling
@@ -9250,7 +9335,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
for (int i = 0; i < n; i++) {
- max = Math.max(max, layout.getLineWidth(i));
+ max = Math.max(max, layout.getLineMax(i));
}
return (int) Math.ceil(max);
@@ -10216,6 +10301,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
* @see #setTransformationMethod(TransformationMethod)
* @attr ref android.R.styleable#TextView_textAllCaps
*/
+ @android.view.RemotableViewMethod
public void setAllCaps(boolean allCaps) {
if (allCaps) {
setTransformationMethod(new AllCapsTransformationMethod(getContext()));
@@ -10445,7 +10531,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
/**
* Set whether the cursor is visible. The default is true. Note that this property only
- * makes sense for editable TextView.
+ * makes sense for editable TextView. If IME is consuming the input, the cursor will always be
+ * invisible, visibility will be updated as the last state when IME does not consume
+ * the input anymore.
*
* @see #isCursorVisible()
*
@@ -10453,6 +10541,25 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
*/
@android.view.RemotableViewMethod
public void setCursorVisible(boolean visible) {
+ mCursorVisibleFromAttr = visible;
+ updateCursorVisibleInternal();
+ }
+
+ /**
+ * Sets the IME is consuming the input and make the cursor invisible if {@code imeConsumesInput}
+ * is {@code true}. Otherwise, make the cursor visible.
+ *
+ * @param imeConsumesInput {@code true} if IME is consuming the input
+ *
+ * @hide
+ */
+ public void setImeConsumesInput(boolean imeConsumesInput) {
+ mImeIsConsumingInput = imeConsumesInput;
+ updateCursorVisibleInternal();
+ }
+
+ private void updateCursorVisibleInternal() {
+ boolean visible = mCursorVisibleFromAttr && !mImeIsConsumingInput;
if (visible && mEditor == null) return; // visible is the default value with no edit data
createEditorIfNeeded();
if (mEditor.mCursorVisible != visible) {
@@ -10467,7 +10574,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
/**
- * @return whether or not the cursor is visible (assuming this TextView is editable)
+ * @return whether or not the cursor is visible (assuming this TextView is editable). This
+ * method may return {@code false} when the IME is consuming the input even if the
+ * {@code mEditor.mCursorVisible} attribute is {@code true} or {@code #setCursorVisible(true)}
+ * is called.
*
* @see #setCursorVisible(boolean)
*
@@ -10479,6 +10589,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return mEditor == null ? true : mEditor.mCursorVisible;
}
+ /**
+ * @return whether cursor is visible without regard to {@code mImeIsConsumingInput}.
+ * {@code true} is the default value.
+ *
+ * @see #setCursorVisible(boolean)
+ * @hide
+ */
+ public boolean isCursorVisibleFromAttr() {
+ return mCursorVisibleFromAttr;
+ }
+
private boolean canMarquee() {
int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
return width > 0 && (mLayout.getLineWidth(0) > width
@@ -10627,12 +10748,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
Editable text = (Editable) mText;
T[] spans = text.getSpans(start, end, type);
- final int length = spans.length;
- for (int i = 0; i < length; i++) {
- final int spanStart = text.getSpanStart(spans[i]);
- final int spanEnd = text.getSpanEnd(spans[i]);
- if (spanEnd == start || spanStart == end) break;
- text.removeSpan(spans[i]);
+ ArrayList<T> spansToRemove = new ArrayList<>();
+ for (T span : spans) {
+ final int spanStart = text.getSpanStart(span);
+ final int spanEnd = text.getSpanEnd(span);
+ if (spanEnd == start || spanStart == end) continue;
+ spansToRemove.add(span);
+ }
+ for (T span : spansToRemove) {
+ text.removeSpan(span);
}
}
@@ -10706,11 +10830,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
+ notifyContentCaptureTextChanged();
+ }
+
+ /**
+ * Notifies the ContentCapture service that the text of the view has changed (only if
+ * ContentCapture has been notified of this view's existence already).
+ *
+ * @hide
+ */
+ public void notifyContentCaptureTextChanged() {
// TODO(b/121045053): should use a flag / boolean to keep status of SHOWN / HIDDEN instead
// of using isLaidout(), so it's not called in cases where it's laid out but a
// notifyAppeared was not sent.
-
- // ContentCapture
if (isLaidOut() && isImportantForContentCapture() && getNotifiedContentCaptureAppeared()) {
final ContentCaptureManager cm = mContext.getSystemService(ContentCaptureManager.class);
if (cm != null && cm.isContentCaptureEnabled()) {
@@ -11635,6 +11767,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
}
+ String[] mimeTypes = getReceiveContentMimeTypes();
+ if (mimeTypes == null && mEditor != null) {
+ // If the app hasn't set a listener for receiving content on this view (ie,
+ // getReceiveContentMimeTypes() returns null), check if it implements the
+ // keyboard image API and, if possible, use those MIME types as fallback.
+ // This fallback is only in place for autofill, not other mechanisms for
+ // inserting content. See AUTOFILL_NON_TEXT_REQUIRES_ON_RECEIVE_CONTENT_LISTENER
+ // in TextViewOnReceiveContentListener for more info.
+ mimeTypes = mEditor.getDefaultOnReceiveContentListener()
+ .getFallbackMimeTypesForAutofill(this);
+ }
+ structure.setReceiveContentMimeTypes(mimeTypes);
}
if (!isPassword || viewFor == VIEW_STRUCTURE_FOR_AUTOFILL
@@ -11710,23 +11854,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
// Get the text and trim it to the range we are reporting.
CharSequence text = getText();
- if (expandedTopChar > 0 || expandedBottomChar < text.length()) {
- text = text.subSequence(expandedTopChar, expandedBottomChar);
- }
- if (viewFor == VIEW_STRUCTURE_FOR_AUTOFILL) {
- structure.setText(text);
- } else {
- structure.setText(text, selStart - expandedTopChar, selEnd - expandedTopChar);
-
- final int[] lineOffsets = new int[bottomLine - topLine + 1];
- final int[] lineBaselines = new int[bottomLine - topLine + 1];
- final int baselineOffset = getBaselineOffset();
- for (int i = topLine; i <= bottomLine; i++) {
- lineOffsets[i - topLine] = layout.getLineStart(i);
- lineBaselines[i - topLine] = layout.getLineBaseline(i) + baselineOffset;
+ if (text != null) {
+ if (expandedTopChar > 0 || expandedBottomChar < text.length()) {
+ // Cap the offsets to avoid an OOB exception. That can happen if the
+ // displayed/layout text, on which these offsets are calculated, is longer
+ // than the original text (such as when the view is translated by the
+ // platform intelligence).
+ // TODO(b/196433694): Figure out how to better handle the offset
+ // calculations for this case (so we don't unnecessarily cutoff the original
+ // text, for example).
+ expandedTopChar = Math.min(expandedTopChar, text.length());
+ expandedBottomChar = Math.min(expandedBottomChar, text.length());
+ text = text.subSequence(expandedTopChar, expandedBottomChar);
+ }
+
+ if (viewFor == VIEW_STRUCTURE_FOR_AUTOFILL) {
+ structure.setText(text);
+ } else {
+ structure.setText(text,
+ selStart - expandedTopChar,
+ selEnd - expandedTopChar);
+
+ final int[] lineOffsets = new int[bottomLine - topLine + 1];
+ final int[] lineBaselines = new int[bottomLine - topLine + 1];
+ final int baselineOffset = getBaselineOffset();
+ for (int i = topLine; i <= bottomLine; i++) {
+ lineOffsets[i - topLine] = layout.getLineStart(i);
+ lineBaselines[i - topLine] = layout.getLineBaseline(i) + baselineOffset;
+ }
+ structure.setTextLines(lineOffsets, lineBaselines);
}
- structure.setTextLines(lineOffsets, lineBaselines);
}
}
@@ -11807,21 +11965,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
@Override
public void autofill(AutofillValue value) {
- if (!value.isText() || !isTextEditable()) {
- Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+ if (!isTextEditable()) {
+ Log.w(LOG_TAG, "cannot autofill non-editable TextView: " + this);
return;
}
-
- final CharSequence autofilledValue = value.getTextValue();
-
- // First autofill it...
- setText(autofilledValue, mBufferType, true, 0);
-
- // ...then move cursor to the end.
- final CharSequence text = getText();
- if ((text instanceof Spannable)) {
- Selection.setSelection((Spannable) text, text.length());
+ if (!value.isText()) {
+ Log.w(LOG_TAG, "value of type " + value.describeContents()
+ + " cannot be autofilled into " + this);
+ return;
}
+ final ClipData clip = ClipData.newPlainText("", value.getTextValue());
+ final ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_AUTOFILL).build();
+ performReceiveContent(payload);
}
@Override
@@ -12404,11 +12559,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return true; // Returns true even if nothing was undone.
case ID_PASTE:
- paste(min, max, true /* withFormatting */);
+ paste(true /* withFormatting */);
return true;
case ID_PASTE_AS_PLAIN_TEXT:
- paste(min, max, false /* withFormatting */);
+ paste(false /* withFormatting */);
return true;
case ID_CUT:
@@ -12846,17 +13001,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return false;
}
- final ClipData clipData = getClipboardManagerForUser().getPrimaryClip();
- final ClipDescription description = clipData.getDescription();
+ final ClipDescription description =
+ getClipboardManagerForUser().getPrimaryClipDescription();
final boolean isPlainType = description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
- final CharSequence text = clipData.getItemAt(0).getText();
- if (isPlainType && (text instanceof Spanned)) {
- Spanned spanned = (Spanned) text;
- if (TextUtils.hasStyleSpan(spanned)) {
- return true;
- }
- }
- return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
+ return (isPlainType && description.isStyledText())
+ || description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
}
boolean canProcessText() {
@@ -12881,40 +13030,17 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return length > 0;
}
- void replaceSelectionWithText(CharSequence text) {
- ((Editable) mText).replace(getSelectionStart(), getSelectionEnd(), text);
- }
-
- /**
- * Paste clipboard content between min and max positions.
- */
- private void paste(int min, int max, boolean withFormatting) {
+ private void paste(boolean withFormatting) {
ClipboardManager clipboard = getClipboardManagerForUser();
ClipData clip = clipboard.getPrimaryClip();
- if (clip != null) {
- boolean didFirst = false;
- for (int i = 0; i < clip.getItemCount(); i++) {
- final CharSequence paste;
- if (withFormatting) {
- paste = clip.getItemAt(i).coerceToStyledText(getContext());
- } else {
- // Get an item as text and remove all spans by toString().
- final CharSequence text = clip.getItemAt(i).coerceToText(getContext());
- paste = (text instanceof Spanned) ? text.toString() : text;
- }
- if (paste != null) {
- if (!didFirst) {
- Selection.setSelection(mSpannable, max);
- ((Editable) mText).replace(min, max, paste);
- didFirst = true;
- } else {
- ((Editable) mText).insert(getSelectionEnd(), "\n");
- ((Editable) mText).insert(getSelectionEnd(), paste);
- }
- }
- }
- sLastCutCopyOrTextChangedTime = 0;
+ if (clip == null) {
+ return;
}
+ final ContentInfo payload = new ContentInfo.Builder(clip, SOURCE_CLIPBOARD)
+ .setFlags(withFormatting ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT)
+ .build();
+ performReceiveContent(payload);
+ sLastCutCopyOrTextChangedTime = 0;
}
private void shareSelectedText() {
@@ -12988,11 +13114,37 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
return getLayout().getOffsetForHorizontal(line, x);
}
+ /**
+ * Handles drag events sent by the system following a call to
+ * {@link android.view.View#startDragAndDrop(ClipData,DragShadowBuilder,Object,int)
+ * startDragAndDrop()}.
+ *
+ * <p>If this text view is not editable, delegates to the default {@link View#onDragEvent}
+ * implementation.
+ *
+ * <p>If this text view is editable, accepts all drag actions (returns true for an
+ * {@link android.view.DragEvent#ACTION_DRAG_STARTED ACTION_DRAG_STARTED} event and all
+ * subsequent drag events). While the drag is in progress, updates the cursor position
+ * to follow the touch location. Once a drop event is received, handles content insertion
+ * via {@link #performReceiveContent}.
+ *
+ * @param event The {@link android.view.DragEvent} sent by the system.
+ * The {@link android.view.DragEvent#getAction()} method returns an action type constant
+ * defined in DragEvent, indicating the type of drag event represented by this object.
+ * @return Returns true if this text view is editable and delegates to super otherwise.
+ * See {@link View#onDragEvent}.
+ */
@Override
public boolean onDragEvent(DragEvent event) {
+ if (mEditor == null || !mEditor.hasInsertionController()) {
+ // If this TextView is not editable, defer to the default View implementation. This
+ // will check for the presence of an OnReceiveContentListener and accept/reject
+ // drag events depending on whether the listener is/isn't set.
+ return super.onDragEvent(event);
+ }
switch (event.getAction()) {
case DragEvent.ACTION_DRAG_STARTED:
- return mEditor != null && mEditor.hasInsertionController();
+ return true;
case DragEvent.ACTION_DRAG_ENTERED:
TextView.this.requestFocus();
@@ -13695,6 +13847,50 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
}
}
+ /** @hide */
+ @Override
+ public void onInputConnectionOpenedInternal(@NonNull InputConnection ic,
+ @NonNull EditorInfo editorInfo, @Nullable Handler handler) {
+ if (mEditor != null) {
+ mEditor.getDefaultOnReceiveContentListener().setInputConnectionInfo(this, ic,
+ editorInfo);
+ }
+ }
+
+ /** @hide */
+ @Override
+ public void onInputConnectionClosedInternal() {
+ if (mEditor != null) {
+ mEditor.getDefaultOnReceiveContentListener().clearInputConnectionInfo();
+ }
+ }
+
+ /**
+ * Default {@link TextView} implementation for receiving content. Apps wishing to provide
+ * custom behavior should configure a listener via {@link #setOnReceiveContentListener}.
+ *
+ * <p>For non-editable TextViews the default behavior is a no-op (returns the passed-in
+ * content without acting on it).
+ *
+ * <p>For editable TextViews the default behavior is to insert text into the view, coercing
+ * non-text content to text as needed. The MIME types "text/plain" and "text/html" have
+ * well-defined behavior for this, while other MIME types have reasonable fallback behavior
+ * (see {@link ClipData.Item#coerceToStyledText}).
+ *
+ * @param payload The content to insert and related metadata.
+ *
+ * @return The portion of the passed-in content that was not handled (may be all, some, or none
+ * of the passed-in content).
+ */
+ @Nullable
+ @Override
+ public ContentInfo onReceiveContent(@NonNull ContentInfo payload) {
+ if (mEditor != null) {
+ return mEditor.getDefaultOnReceiveContentListener().onReceiveContent(this, payload);
+ }
+ return payload;
+ }
+
private static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) {
if (msgFormat == null) {
Log.d(LOG_TAG, location);
@@ -13702,4 +13898,61 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
Log.d(LOG_TAG, location + ": " + String.format(msgFormat, msgArgs));
}
}
+
+ /**
+ * Collects a {@link ViewTranslationRequest} which represents the content to be translated in
+ * the view.
+ *
+ * <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.
+ *
+ * @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.
+ */
+ @Override
+ public void onCreateViewTranslationRequest(@NonNull int[] supportedFormats,
+ @NonNull Consumer<ViewTranslationRequest> requestsCollector) {
+ if (supportedFormats == null || supportedFormats.length == 0) {
+ if (UiTranslationController.DEBUG) {
+ Log.w(LOG_TAG, "Do not provide the support translation formats.");
+ }
+ return;
+ }
+ ViewTranslationRequest.Builder requestBuilder =
+ new ViewTranslationRequest.Builder(getAutofillId());
+ // Support Text translation
+ if (ArrayUtils.contains(supportedFormats, TranslationSpec.DATA_FORMAT_TEXT)) {
+ if (mText == null || mText.length() == 0) {
+ if (UiTranslationController.DEBUG) {
+ Log.w(LOG_TAG, "Cannot create translation request for the empty text.");
+ }
+ return;
+ }
+ 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()) {
+ if (UiTranslationController.DEBUG) {
+ Log.w(LOG_TAG, "Cannot create translation request. editable = "
+ + isTextEditable() + ", isPassword = " + isPassword + ", selectable = "
+ + isTextSelectable());
+ }
+ return;
+ }
+ // TODO(b/176488462): apply the view's important for translation
+ requestBuilder.setValue(ViewTranslationRequest.ID_TEXT,
+ TranslationRequestValue.forText(mText));
+ if (!TextUtils.isEmpty(getContentDescription())) {
+ requestBuilder.setValue(ViewTranslationRequest.ID_CONTENT_DESCRIPTION,
+ TranslationRequestValue.forText(getContentDescription()));
+ }
+ }
+ requestsCollector.accept(requestBuilder.build());
+ }
}