diff options
| author | Nikita Dubrovsky <dubrovsky@google.com> | 2020-02-07 14:38:23 -0800 |
|---|---|---|
| committer | Nikita Dubrovsky <dubrovsky@google.com> | 2020-03-31 15:15:36 -0700 |
| commit | 832edc3cc92584f7a41f84d471b6eeaadbeeccab (patch) | |
| tree | 9821a8a02998c85b1796e53020cc5c895cbffba8 /core/java/android/widget/TextViewRichContentReceiver.java | |
| parent | 53bedad0f1044b80289fbf54ad49f9ee796b6f51 (diff) | |
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
Diffstat (limited to 'core/java/android/widget/TextViewRichContentReceiver.java')
| -rw-r--r-- | core/java/android/widget/TextViewRichContentReceiver.java | 137 |
1 files changed, 137 insertions, 0 deletions
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<TextView> { + static final TextViewRichContentReceiver INSTANCE = new TextViewRichContentReceiver(); + + private static final Set<String> MIME_TYPES_ALL_TEXT = Collections.singleton("text/*"); + + @Override + public Set<String> 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; + } +} |
