TextViewOnReceiveContentListener.java
/*
* Copyright 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 androidx.core.widget;
import static androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX;
import static androidx.core.view.ContentInfoCompat.FLAG_CONVERT_TO_PLAIN_TEXT;
import static androidx.core.view.ContentInfoCompat.SOURCE_INPUT_METHOD;
import android.content.ClipData;
import android.content.Context;
import android.os.Build;
import android.text.Editable;
import android.text.Selection;
import android.text.Spanned;
import android.util.Log;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.view.ContentInfoCompat;
import androidx.core.view.ContentInfoCompat.Flags;
import androidx.core.view.ContentInfoCompat.Source;
import androidx.core.view.OnReceiveContentListener;
/**
* Default implementation inserting content into editable {@link TextView} components. This class
* handles insertion of text (plain text, styled text, HTML, etc) but not images or other content.
*
* @hide
*/
@RestrictTo(LIBRARY_GROUP_PREFIX)
public final class TextViewOnReceiveContentListener implements OnReceiveContentListener {
private static final String LOG_TAG = "ReceiveContent";
@Nullable
@Override
public ContentInfoCompat onReceiveContent(@NonNull View view,
@NonNull ContentInfoCompat payload) {
if (Log.isLoggable(LOG_TAG, Log.DEBUG)) {
Log.d(LOG_TAG, "onReceive: " + payload);
}
final @Source int source = payload.getSource();
if (source == SOURCE_INPUT_METHOD) {
// InputConnection.commitContent() should only be used for non-text input which is not
// supported by the default implementation.
return payload;
}
// The code here follows the platform logic in 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 platform behavior with respect to TextWatcher
// notifications fired from SpannableStringBuilder when replace/insert is called.
final ClipData clip = payload.getClip();
final @Flags int flags = payload.getFlags();
final TextView textView = (TextView) view;
final Editable editable = (Editable) textView.getText();
final Context context = textView.getContext();
boolean didFirst = false;
for (int i = 0; i < clip.getItemCount(); i++) {
CharSequence itemText = coerceToText(context, clip.getItemAt(i), flags);
if (itemText != null) {
if (!didFirst) {
replaceSelection(editable, itemText);
didFirst = true;
} else {
editable.insert(Selection.getSelectionEnd(editable), "\n");
editable.insert(Selection.getSelectionEnd(editable), itemText);
}
}
}
return null;
}
private static CharSequence coerceToText(@NonNull Context context, @NonNull ClipData.Item item,
@Flags int flags) {
if (Build.VERSION.SDK_INT >= 16) {
return Api16Impl.coerce(context, item, flags);
} else {
return ApiImpl.coerce(context, item, flags);
}
}
private static void replaceSelection(@NonNull Editable editable,
@NonNull CharSequence replacement) {
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, replacement);
}
@RequiresApi(16) // For ClipData.Item.coerceToStyledText()
private static final class Api16Impl {
private Api16Impl() {}
static CharSequence coerce(@NonNull Context context, @NonNull ClipData.Item item,
@Flags int flags) {
if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
CharSequence text = item.coerceToText(context);
return (text instanceof Spanned) ? text.toString() : text;
} else {
return item.coerceToStyledText(context);
}
}
}
private static final class ApiImpl {
private ApiImpl() {}
static CharSequence coerce(@NonNull Context context, @NonNull ClipData.Item item,
@Flags int flags) {
CharSequence text = item.coerceToText(context);
if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0 && text instanceof Spanned) {
text = text.toString();
}
return text;
}
}
}