diff options
Diffstat (limited to 'core/java/android/widget/TextView.java')
| -rw-r--r-- | core/java/android/widget/TextView.java | 433 |
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()); + } } |
