From 832edc3cc92584f7a41f84d471b6eeaadbeeccab Mon Sep 17 00:00:00 2001 From: Nikita Dubrovsky Date: Fri, 7 Feb 2020 14:38:23 -0800 Subject: Add unified API for inserting rich content (e.g. pasting an image) The new callback provides a single API that apps can implement to support the different ways in which rich content may be inserted. The API is added to TextView and unifies the following code paths: * paste from the clipboard (TextView.paste) * content insertion from the IME (InputConnection.commitContent) * drag and drop (Editor.onDrop) * autofill (TextView.autofill) Corresponding API in support lib: aosp/1200800 Bug: 152068298 Test: Manual and unit tests atest FrameworksCoreTests:TextViewRichContentReceiverTest atest FrameworksCoreTests:AutofillValueTest atest FrameworksCoreTests:TextViewActivityTest Change-Id: I6e03a398ccb6fa5526d0a282fc114f4e80285099 --- .../widget/TextViewRichContentReceiver.java | 137 +++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 core/java/android/widget/TextViewRichContentReceiver.java (limited to 'core/java/android/widget/TextViewRichContentReceiver.java') diff --git a/core/java/android/widget/TextViewRichContentReceiver.java b/core/java/android/widget/TextViewRichContentReceiver.java new file mode 100644 index 000000000000..125eaa7e6d9f --- /dev/null +++ b/core/java/android/widget/TextViewRichContentReceiver.java @@ -0,0 +1,137 @@ +/* + * 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.widget; + +import android.annotation.NonNull; +import android.content.ClipData; +import android.content.Context; +import android.text.Editable; +import android.text.Selection; +import android.text.SpannableStringBuilder; +import android.text.Spanned; + +import java.util.Collections; +import java.util.Set; + +/** + * Default implementation of {@link RichContentReceiver} for editable {@link TextView} components. + * This class handles insertion of text (plain text, styled text, HTML, etc) but not images or other + * rich content. Typically this class will be used as a delegate by custom implementations of + * {@link RichContentReceiver}, to provide consistent behavior for insertion of text while + * implementing custom behavior for insertion of other content (images, etc). See + * {@link TextView#DEFAULT_RICH_CONTENT_RECEIVER}. + * + * @hide + */ +final class TextViewRichContentReceiver implements RichContentReceiver { + static final TextViewRichContentReceiver INSTANCE = new TextViewRichContentReceiver(); + + private static final Set MIME_TYPES_ALL_TEXT = Collections.singleton("text/*"); + + @Override + public Set getSupportedMimeTypes() { + return MIME_TYPES_ALL_TEXT; + } + + @Override + public boolean onReceive(@NonNull TextView textView, @NonNull ClipData clip, + @Source int source, int flags) { + if (source == SOURCE_AUTOFILL) { + return onReceiveForAutofill(textView, clip, flags); + } + if (source == SOURCE_DRAG_AND_DROP) { + return onReceiveForDragAndDrop(textView, clip, flags); + } + if (source == SOURCE_INPUT_METHOD && !supports(clip.getDescription())) { + return false; + } + + // The code here follows the original paste logic from TextView: + // https://cs.android.com/android/_/android/platform/frameworks/base/+/9fefb65aa9e7beae9ca8306b925b9fbfaeffecc9:core/java/android/widget/TextView.java;l=12644 + // In particular, multiple items within the given ClipData will trigger separate calls to + // replace/insert. This is to preserve the original behavior with respect to TextWatcher + // notifications fired from SpannableStringBuilder when replace/insert is called. + final Editable editable = (Editable) textView.getText(); + final Context context = textView.getContext(); + boolean didFirst = false; + for (int i = 0; i < clip.getItemCount(); i++) { + CharSequence itemText; + if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) { + itemText = clip.getItemAt(i).coerceToText(context); + itemText = (itemText instanceof Spanned) ? itemText.toString() : itemText; + } else { + itemText = clip.getItemAt(i).coerceToStyledText(context); + } + if (itemText != null) { + if (!didFirst) { + final int selStart = Selection.getSelectionStart(editable); + final int selEnd = Selection.getSelectionEnd(editable); + final int start = Math.max(0, Math.min(selStart, selEnd)); + final int end = Math.max(0, Math.max(selStart, selEnd)); + Selection.setSelection(editable, end); + editable.replace(start, end, itemText); + didFirst = true; + } else { + editable.insert(Selection.getSelectionEnd(editable), "\n"); + editable.insert(Selection.getSelectionEnd(editable), itemText); + } + } + } + return didFirst; + } + + private static boolean onReceiveForAutofill(@NonNull TextView textView, @NonNull ClipData clip, + int flags) { + final CharSequence text = coerceToText(clip, textView.getContext(), flags); + if (text.length() == 0) { + return false; + } + // First autofill it... + textView.setText(text); + // ...then move cursor to the end. + final Editable editable = (Editable) textView.getText(); + Selection.setSelection(editable, editable.length()); + return true; + } + + private static boolean onReceiveForDragAndDrop(@NonNull TextView textView, + @NonNull ClipData clip, int flags) { + final CharSequence text = coerceToText(clip, textView.getContext(), flags); + if (text.length() == 0) { + return false; + } + textView.replaceSelectionWithText(text); + return true; + } + + private static CharSequence coerceToText(ClipData clip, Context context, int flags) { + SpannableStringBuilder ssb = new SpannableStringBuilder(); + for (int i = 0; i < clip.getItemCount(); i++) { + CharSequence itemText; + if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) { + itemText = clip.getItemAt(i).coerceToText(context); + itemText = (itemText instanceof Spanned) ? itemText.toString() : itemText; + } else { + itemText = clip.getItemAt(i).coerceToStyledText(context); + } + if (itemText != null) { + ssb.append(itemText); + } + } + return ssb; + } +} -- cgit v1.2.3