diff options
Diffstat (limited to 'core/java')
| -rw-r--r-- | core/java/android/view/OnReceiveContentListener.java (renamed from core/java/android/view/OnReceiveContentCallback.java) | 166 | ||||
| -rw-r--r-- | core/java/android/view/View.java | 113 | ||||
| -rw-r--r-- | core/java/android/view/autofill/AutofillManager.java | 14 | ||||
| -rw-r--r-- | core/java/android/view/inputmethod/BaseInputConnection.java | 23 | ||||
| -rw-r--r-- | core/java/android/widget/Editor.java | 18 | ||||
| -rw-r--r-- | core/java/android/widget/TextView.java | 91 | ||||
| -rw-r--r-- | core/java/android/widget/TextViewOnReceiveContentListener.java (renamed from core/java/android/widget/TextViewOnReceiveContentCallback.java) | 168 |
7 files changed, 304 insertions, 289 deletions
diff --git a/core/java/android/view/OnReceiveContentCallback.java b/core/java/android/view/OnReceiveContentListener.java index d74938c1d1fd..495528989a83 100644 --- a/core/java/android/view/OnReceiveContentCallback.java +++ b/core/java/android/view/OnReceiveContentListener.java @@ -22,77 +22,91 @@ import android.annotation.Nullable; import android.content.ClipData; import android.net.Uri; import android.os.Bundle; +import android.util.ArrayMap; import com.android.internal.util.Preconditions; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.ArrayList; +import java.util.Map; import java.util.Objects; +import java.util.function.Predicate; /** - * Callback for apps to implement handling for insertion of content. Content may be both text and + * Listener for apps to implement handling for insertion of content. Content may be both text and * non-text (plain/styled text, HTML, images, videos, audio files, etc). * - * <p>This callback can be attached to different types of UI components using - * {@link View#setOnReceiveContentCallback}. + * <p>This listener can be attached to different types of UI components using + * {@link View#setOnReceiveContentListener}. * - * <p>For editable {@link android.widget.TextView} components, implementations can extend from - * {@link android.widget.TextViewOnReceiveContentCallback} to reuse default platform behavior for - * handling text. - * - * <p>Example implementation:<br> + * <p>Here is a sample implementation that handles content URIs and delegates the processing for + * text and everything else to the platform:<br> * <pre class="prettyprint"> - * // (1) Define the callback - * public class MyOnReceiveContentCallback implements OnReceiveContentCallback<TextView> { - * public static final Set<String> MIME_TYPES = Collections.unmodifiableSet( - * Set.of("image/*", "video/*")); + * // (1) Define the listener + * public class MyReceiver implements OnReceiveContentListener { + * public static final String[] MIME_TYPES = new String[] {"image/*", "video/*"}; * * @Override - * public boolean onReceiveContent(@NonNull TextView view, @NonNull Payload payload) { - * // ... app-specific logic to handle the content in the payload ... + * public Payload onReceiveContent(TextView view, Payload payload) { + * Map<Boolean, Payload> split = payload.partition(item -> item.getUri() != null); + * if (split.get(true) != null) { + * ClipData clip = payload.getClip(); + * for (int i = 0; i < clip.getItemCount(); i++) { + * Uri uri = clip.getItemAt(i).getUri(); + * // ... app-specific logic to handle the URI ... + * } + * } + * // Return anything that we didn't handle ourselves. This preserves the default platform + * // behavior for text and anything else for which we are not implementing custom handling. + * return split.get(false); * } * } * - * // (2) Register the callback + * // (2) Register the listener * public class MyActivity extends Activity { * @Override * public void onCreate(Bundle savedInstanceState) { * // ... * * EditText myInput = findViewById(R.id.my_input); - * myInput.setOnReceiveContentCallback( - * MyOnReceiveContentCallback.MIME_TYPES, - * new MyOnReceiveContentCallback()); + * myInput.setOnReceiveContentListener(MyReceiver.MIME_TYPES, new MyReceiver()); * } * </pre> - * - * @param <T> The type of {@link View} with which this callback can be associated. */ -public interface OnReceiveContentCallback<T extends View> { +public interface OnReceiveContentListener { /** * Receive the given content. * - * <p>This method is only invoked for content whose MIME type matches a type specified via - * {@link View#setOnReceiveContentCallback}. + * <p>Implementations should handle any content items of interest and return all unhandled + * items to preserve the default platform behavior for content that does not have app-specific + * handling. For example, an implementation may provide handling for content URIs (to provide + * support for inserting images, etc) and delegate the processing of text to the platform to + * preserve the common behavior for inserting text. See the class javadoc for a sample + * implementation and see {@link Payload#partition} for a convenient way to split the passed-in + * content. * - * <p>For text, if the view has a selection, the selection should be overwritten by the clip; if - * there's no selection, this method should insert the content at the current cursor position. + * <p>If implementing handling for text: if the view has a selection, the selection should + * be overwritten by the passed-in content; if there's no selection, the passed-in content + * should be inserted at the current cursor position. * - * <p>For non-text content (e.g. an image), the content may be inserted inline, or it may be - * added as an attachment (could potentially be shown in a completely separate view). + * <p>If implementing handling for non-text content (e.g. images): the content may be + * inserted inline, or it may be added as an attachment (could potentially be shown in a + * completely separate view). * * @param view The view where the content insertion was requested. * @param payload The content to insert and related metadata. * - * @return Returns true if the content was handled in some way, false otherwise. Actual - * insertion may be processed asynchronously in the background and may or may not succeed even - * if this method returns true. For example, an app may not end up inserting an item if it - * exceeds the app's size limit for that type of content. + * @return The portion of the passed-in content whose processing should be delegated to + * the platform. Return null if all content was handled in some way. Actual insertion of + * the content may be processed asynchronously in the background and may or may not + * succeed even if this method returns null. For example, an app may end up not inserting + * an item if it exceeds the app's size limit for that type of content. */ - boolean onReceiveContent(@NonNull T view, @NonNull Payload payload); + @Nullable Payload onReceiveContent(@NonNull View view, @NonNull Payload payload); /** - * Holds all the relevant data for a request to {@link OnReceiveContentCallback}. + * Holds all the relevant data for a request to {@link OnReceiveContentListener}. */ final class Payload { @@ -206,7 +220,7 @@ public interface OnReceiveContentCallback<T extends View> { @Override public String toString() { return "Payload{" - + "clip=" + mClip.getDescription() + + "clip=" + mClip + ", source=" + sourceToString(mSource) + ", flags=" + flagsToString(mFlags) + ", linkUri=" + mLinkUri @@ -256,16 +270,74 @@ public interface OnReceiveContentCallback<T extends View> { } /** + * Partitions this payload based on the given predicate. + * + * <p>Similar to a + * {@link java.util.stream.Collectors#partitioningBy(Predicate) partitioning collector}, + * this function classifies the content in this payload and organizes it into a map, + * grouping the content that matched vs didn't match the predicate. + * + * <p>Except for the {@link ClipData} items, the returned payloads will contain all the same + * metadata as the original payload. + * + * @param itemPredicate The predicate to test each {@link ClipData.Item} to determine which + * partition to place it into. + * @return A map containing the partitioned content. The map will contain a single entry if + * all items were classified into the same partition (all matched or all didn't match the + * predicate) or two entries (if there's at least one item that matched the predicate and at + * least one item that didn't match the predicate). + */ + public @NonNull Map<Boolean, Payload> partition( + @NonNull Predicate<ClipData.Item> itemPredicate) { + if (mClip.getItemCount() == 1) { + Map<Boolean, Payload> result = new ArrayMap<>(1); + result.put(itemPredicate.test(mClip.getItemAt(0)), this); + return result; + } + ArrayList<ClipData.Item> accepted = new ArrayList<>(); + ArrayList<ClipData.Item> remaining = new ArrayList<>(); + for (int i = 0; i < mClip.getItemCount(); i++) { + ClipData.Item item = mClip.getItemAt(i); + if (itemPredicate.test(item)) { + accepted.add(item); + } else { + remaining.add(item); + } + } + Map<Boolean, Payload> result = new ArrayMap<>(2); + if (!accepted.isEmpty()) { + ClipData acceptedClip = new ClipData(mClip.getDescription(), accepted); + result.put(true, new Builder(this).setClip(acceptedClip).build()); + } + if (!remaining.isEmpty()) { + ClipData remainingClip = new ClipData(mClip.getDescription(), remaining); + result.put(false, new Builder(this).setClip(remainingClip).build()); + } + return result; + } + + /** * Builder for {@link Payload}. */ public static final class Builder { - @NonNull private final ClipData mClip; - private final @Source int mSource; + @NonNull private ClipData mClip; + private @Source int mSource; private @Flags int mFlags; @Nullable private Uri mLinkUri; @Nullable private Bundle mExtras; /** + * Creates a new builder initialized with the data from the given builder. + */ + public Builder(@NonNull Payload payload) { + mClip = payload.mClip; + mSource = payload.mSource; + mFlags = payload.mFlags; + mLinkUri = payload.mLinkUri; + mExtras = payload.mExtras; + } + + /** * Creates a new builder. * @param clip The data to insert. * @param source The source of the operation. See {@code SOURCE_} constants. @@ -276,6 +348,28 @@ public interface OnReceiveContentCallback<T extends View> { } /** + * Sets the data to be inserted. + * @param clip The data to insert. + * @return this builder + */ + @NonNull + public Builder setClip(@NonNull ClipData clip) { + mClip = clip; + return this; + } + + /** + * Sets the source of the operation. + * @param source The source of the operation. See {@code SOURCE_} constants. + * @return this builder + */ + @NonNull + public Builder setSource(@Source int source) { + mSource = source; + return this; + } + + /** * Sets flags that control content insertion behavior. * @param flags Optional flags to configure the insertion behavior. Use 0 for default * behavior. See {@code FLAG_} constants. diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index cefddd84ee32..a88ad9f24c10 100644 --- a/core/java/android/view/View.java +++ b/core/java/android/view/View.java @@ -112,6 +112,7 @@ import android.view.AccessibilityIterators.TextSegmentIterator; import android.view.AccessibilityIterators.WordTextSegmentIterator; import android.view.ContextMenu.ContextMenuInfo; import android.view.InputDevice.InputSourceClass; +import android.view.OnReceiveContentListener.Payload; import android.view.Window.OnContentApplyWindowInsetsListener; import android.view.WindowInsets.Type; import android.view.WindowInsetsAnimation.Bounds; @@ -143,6 +144,7 @@ import android.widget.FrameLayout; import android.widget.ScrollBarDrawable; import com.android.internal.R; +import com.android.internal.util.ArrayUtils; import com.android.internal.util.FrameworkStatsLog; import com.android.internal.util.Preconditions; import com.android.internal.view.ScrollCaptureInternal; @@ -4713,6 +4715,9 @@ public class View implements Drawable.Callback, KeyEvent.Callback, * Allows the application to implement custom scroll capture support. */ ScrollCaptureCallback mScrollCaptureCallback; + + @Nullable + private OnReceiveContentListener mOnReceiveContentListener; } @UnsupportedAppUsage @@ -5246,8 +5251,6 @@ public class View implements Drawable.Callback, KeyEvent.Callback, @Nullable private String[] mOnReceiveContentMimeTypes; - @Nullable - private OnReceiveContentCallback mOnReceiveContentCallback; /** * Simple constructor to use when creating a view from code. @@ -9005,72 +9008,92 @@ public class View implements Drawable.Callback, KeyEvent.Callback, } /** - * Sets the callback to handle insertion of content into this view. + * Sets the listener to be {@link #onReceiveContent used} to handle insertion of + * content into this view. * - * <p>Depending on the view, this callback may be invoked for scenarios such as content - * insertion from the IME, Autofill, etc. + * <p>Depending on the type of view, this listener may be invoked for different scenarios. For + * example, for editable TextViews, this listener will be invoked for the following scenarios: + * <ol> + * <li>Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the + * insertion/selection menu) + * <li>Content insertion from the keyboard (from {@link InputConnection#commitContent}) + * <li>Drag and drop (drop events from {@link #onDragEvent(DragEvent)}) + * <li>Autofill + * <li>Selection replacement via {@link Intent#ACTION_PROCESS_TEXT} + * </ol> * - * <p>This callback is only invoked for content whose MIME type matches a type specified via - * the {code mimeTypes} parameter. If the MIME type is not supported by the callback, the - * default platform handling will be executed instead (no-op for the default {@link View}). + * <p>When setting a listener, clients should also declare the MIME types accepted by it. + * When invoked with other types of content, the listener may reject the content (defer to + * the default platform behavior) or execute some other fallback logic. The MIME types + * declared here allow different features to optionally alter their behavior. For example, + * the soft keyboard may choose to hide its UI for inserting GIFs for a particular input + * field if the MIME types set here for that field don't include "image/gif" or "image/*". * - * <p><em>Note: MIME type matching in the Android framework is case-sensitive, unlike formal RFC - * MIME types. As a result, you should always write your MIME types with lower case letters, or - * use {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to lower - * case.</em> + * <p>Note: MIME type matching in the Android framework is case-sensitive, unlike formal RFC + * MIME types. As a result, you should always write your MIME types with lowercase letters, + * or use {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to + * lowercase. * - * @param mimeTypes The type of content for which the callback should be invoked. This may use - * wildcards such as "text/*", "image/*", etc. This must not be null or empty if a non-null - * callback is passed in. - * @param callback The callback to use. This can be null to reset to the default behavior. + * @param mimeTypes The MIME types accepted by the given listener. These may use patterns + * such as "image/*", but may not start with a wildcard. This argument must + * not be null or empty if a non-null listener is passed in. + * @param listener The listener to use. This can be null to reset to the default behavior. */ @SuppressWarnings("rawtypes") - public void setOnReceiveContentCallback(@Nullable String[] mimeTypes, - @Nullable OnReceiveContentCallback callback) { - if (callback != null) { + public void setOnReceiveContentListener(@Nullable String[] mimeTypes, + @Nullable OnReceiveContentListener listener) { + if (listener != null) { Preconditions.checkArgument(mimeTypes != null && mimeTypes.length > 0, - "When the callback is set, MIME types must also be set"); + "When the listener is set, MIME types must also be set"); + } + if (mimeTypes != null) { + Preconditions.checkArgument(Arrays.stream(mimeTypes).noneMatch(t -> t.startsWith("*")), + "A MIME type set here must not start with *: " + Arrays.toString(mimeTypes)); } - mOnReceiveContentMimeTypes = mimeTypes; - mOnReceiveContentCallback = callback; + mOnReceiveContentMimeTypes = ArrayUtils.isEmpty(mimeTypes) ? null : mimeTypes; + getListenerInfo().mOnReceiveContentListener = listener; } /** - * Receives the given content. The default implementation invokes the callback set via - * {@link #setOnReceiveContentCallback}. If no callback is set or if the callback does not - * support the given content (based on the MIME type), returns false. + * Receives the given content. Invokes the listener configured via + * {@link #setOnReceiveContentListener}; if no listener is set, the default implementation is a + * no-op (returns the passed-in content without acting on it). * * @param payload The content to insert and related metadata. * - * @return Returns true if the content was handled in some way, false otherwise. Actual - * insertion may be processed asynchronously in the background and may or may not succeed even - * if this method returns true. For example, an app may not end up inserting an item if it - * exceeds the app's size limit for that type of content. + * @return The portion of the passed-in content that was not accepted (may be all, some, or none + * of the passed-in content). */ - public boolean onReceiveContent(@NonNull OnReceiveContentCallback.Payload payload) { - ClipDescription description = payload.getClip().getDescription(); - if (mOnReceiveContentCallback != null && mOnReceiveContentMimeTypes != null - && description.hasMimeType(mOnReceiveContentMimeTypes)) { - return mOnReceiveContentCallback.onReceiveContent(this, payload); + @SuppressWarnings({"rawtypes", "unchecked"}) + public @Nullable Payload onReceiveContent(@NonNull Payload payload) { + final OnReceiveContentListener listener = (mListenerInfo == null) ? null + : getListenerInfo().mOnReceiveContentListener; + if (listener != null) { + return listener.onReceiveContent(this, payload); } - return false; + return payload; } /** - * Returns the MIME types that can be handled by {@link #onReceiveContent} for this view, as - * configured via {@link #setOnReceiveContentCallback}. By default returns null. + * Returns the MIME types accepted by {@link #onReceiveContent} for this view, as + * configured via {@link #setOnReceiveContentListener}. By default returns null. * - * <p>Different platform features (e.g. pasting from the clipboard, inserting stickers from the - * keyboard, etc) may use this function to conditionally alter their behavior. For example, the - * soft keyboard may choose to hide its UI for inserting GIFs for a particular input field if - * the MIME types returned here for that field don't include "image/gif". + * <p>Different features (e.g. pasting from the clipboard, inserting stickers from the soft + * keyboard, etc) may optionally use this metadata to conditionally alter their behavior. For + * example, a soft keyboard may choose to hide its UI for inserting GIFs for a particular + * input field if the MIME types returned here for that field don't include "image/gif" or + * "image/*". * * <p>Note: Comparisons of MIME types should be performed using utilities such as * {@link ClipDescription#compareMimeTypes} rather than simple string equality, in order to - * correctly handle patterns (e.g. "text/*"). - * - * @return The MIME types supported by {@link #onReceiveContent} for this view. The returned - * MIME types may contain wildcards such as "text/*", "image/*", etc. + * correctly handle patterns such as "text/*", "image/*", etc. Note that MIME type matching + * in the Android framework is case-sensitive, unlike formal RFC MIME types. As a result, + * you should always write your MIME types with lowercase letters, or use + * {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to + * lowercase. + * + * @return The MIME types accepted by {@link #onReceiveContent} for this view (may + * include patterns such as "image/*"). */ public @Nullable String[] getOnReceiveContentMimeTypes() { return mOnReceiveContentMimeTypes; diff --git a/core/java/android/view/autofill/AutofillManager.java b/core/java/android/view/autofill/AutofillManager.java index 81db62857c17..299c41b02b23 100644 --- a/core/java/android/view/autofill/AutofillManager.java +++ b/core/java/android/view/autofill/AutofillManager.java @@ -19,7 +19,7 @@ package android.view.autofill; import static android.service.autofill.FillRequest.FLAG_MANUAL_REQUEST; import static android.service.autofill.FillRequest.FLAG_PASSWORD_INPUT_TYPE; import static android.service.autofill.FillRequest.FLAG_VIEW_NOT_FOCUSED; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_AUTOFILL; +import static android.view.OnReceiveContentListener.Payload.SOURCE_AUTOFILL; import static android.view.autofill.Helper.sDebug; import static android.view.autofill.Helper.sVerbose; import static android.view.autofill.Helper.toList; @@ -62,7 +62,7 @@ import android.util.Slog; import android.util.SparseArray; import android.view.Choreographer; import android.view.KeyEvent; -import android.view.OnReceiveContentCallback; +import android.view.OnReceiveContentListener.Payload; import android.view.View; import android.view.accessibility.AccessibilityEvent; import android.view.accessibility.AccessibilityManager; @@ -2371,12 +2371,10 @@ public final class AutofillManager { reportAutofillContentFailure(id); return; } - OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_AUTOFILL) - .build(); - boolean handled = view.onReceiveContent(payload); - if (!handled) { - Log.w(TAG, "autofillContent(): receiver returned false: id=" + id + Payload payload = new Payload.Builder(clip, SOURCE_AUTOFILL).build(); + Payload result = view.onReceiveContent(payload); + if (result != null) { + Log.w(TAG, "autofillContent(): receiver could not insert content: id=" + id + ", view=" + view + ", clip=" + clip); reportAutofillContentFailure(id); return; diff --git a/core/java/android/view/inputmethod/BaseInputConnection.java b/core/java/android/view/inputmethod/BaseInputConnection.java index 62b1b1f8cf53..a92d1f589e96 100644 --- a/core/java/android/view/inputmethod/BaseInputConnection.java +++ b/core/java/android/view/inputmethod/BaseInputConnection.java @@ -16,7 +16,7 @@ package android.view.inputmethod; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_INPUT_METHOD; +import static android.view.OnReceiveContentListener.Payload.SOURCE_INPUT_METHOD; import android.annotation.CallSuper; import android.annotation.IntRange; @@ -40,7 +40,7 @@ import android.util.Log; import android.util.LogPrinter; import android.view.KeyCharacterMap; import android.view.KeyEvent; -import android.view.OnReceiveContentCallback; +import android.view.OnReceiveContentListener; import android.view.View; class ComposingText implements NoCopySpan { @@ -928,17 +928,14 @@ public class BaseInputConnection implements InputConnection { /** * Default implementation which invokes {@link View#onReceiveContent} on the target view if the - * MIME type of the content matches one of the MIME types returned by - * {@link View#getOnReceiveContentMimeTypes()}. If the MIME type of the content is not matched, - * returns false without any side effects. + * view {@link View#getOnReceiveContentMimeTypes allows} content insertion; otherwise returns + * false without any side effects. */ public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { ClipDescription description = inputContentInfo.getDescription(); - final String[] viewMimeTypes = mTargetView.getOnReceiveContentMimeTypes(); - if (viewMimeTypes == null || !description.hasMimeType(viewMimeTypes)) { + if (mTargetView.getOnReceiveContentMimeTypes() == null) { if (DEBUG) { - Log.d(TAG, "Can't insert content from IME; unsupported MIME type: content=" - + description + ", viewMimeTypes=" + viewMimeTypes); + Log.d(TAG, "Can't insert content from IME: content=" + description); } return false; } @@ -950,13 +947,13 @@ public class BaseInputConnection implements InputConnection { return false; } } - final ClipData clip = new ClipData(description, + final ClipData clip = new ClipData(inputContentInfo.getDescription(), new ClipData.Item(inputContentInfo.getContentUri())); - final OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_INPUT_METHOD) + final OnReceiveContentListener.Payload payload = + new OnReceiveContentListener.Payload.Builder(clip, SOURCE_INPUT_METHOD) .setLinkUri(inputContentInfo.getLinkUri()) .setExtras(opts) .build(); - return mTargetView.onReceiveContent(payload); + return mTargetView.onReceiveContent(payload) == null; } } diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java index e36243cc7948..da14f2cfbd5a 100644 --- a/core/java/android/widget/Editor.java +++ b/core/java/android/widget/Editor.java @@ -16,7 +16,7 @@ package android.widget; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_DRAG_AND_DROP; +import static android.view.OnReceiveContentListener.Payload.SOURCE_DRAG_AND_DROP; import android.R; import android.animation.ValueAnimator; @@ -98,7 +98,7 @@ import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; -import android.view.OnReceiveContentCallback; +import android.view.OnReceiveContentListener; import android.view.SubMenu; import android.view.View; import android.view.View.DragShadowBuilder; @@ -207,8 +207,8 @@ public class Editor { } // Default content insertion handler. - private final TextViewOnReceiveContentCallback mDefaultOnReceiveContentCallback = - new TextViewOnReceiveContentCallback(); + private final TextViewOnReceiveContentListener mDefaultOnReceiveContentListener = + new TextViewOnReceiveContentListener(); // Each Editor manages its own undo stack. private final UndoManager mUndoManager = new UndoManager(); @@ -589,8 +589,8 @@ public class Editor { } @VisibleForTesting - public @NonNull TextViewOnReceiveContentCallback getDefaultOnReceiveContentCallback() { - return mDefaultOnReceiveContentCallback; + public @NonNull TextViewOnReceiveContentListener getDefaultOnReceiveContentListener() { + return mDefaultOnReceiveContentListener; } /** @@ -719,7 +719,7 @@ public class Editor { hideCursorAndSpanControllers(); stopTextActionModeWithPreservingSelection(); - mDefaultOnReceiveContentCallback.clearInputConnectionInfo(); + mDefaultOnReceiveContentListener.clearInputConnectionInfo(); } private void discardTextDisplayLists() { @@ -2869,8 +2869,8 @@ public class Editor { final int originalLength = mTextView.getText().length(); Selection.setSelection((Spannable) mTextView.getText(), offset); final ClipData clip = event.getClipData(); - final OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_DRAG_AND_DROP) + final OnReceiveContentListener.Payload payload = + new OnReceiveContentListener.Payload.Builder(clip, SOURCE_DRAG_AND_DROP) .build(); mTextView.onReceiveContent(payload); if (dragDropIntoItself) { diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 3ac78bafdedc..9485753ce906 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -17,10 +17,10 @@ package android.widget; import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL; -import static android.view.OnReceiveContentCallback.Payload.FLAG_CONVERT_TO_PLAIN_TEXT; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_AUTOFILL; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_CLIPBOARD; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_PROCESS_TEXT; +import static android.view.OnReceiveContentListener.Payload.FLAG_CONVERT_TO_PLAIN_TEXT; +import static android.view.OnReceiveContentListener.Payload.SOURCE_AUTOFILL; +import static android.view.OnReceiveContentListener.Payload.SOURCE_CLIPBOARD; +import static android.view.OnReceiveContentListener.Payload.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; @@ -154,7 +154,7 @@ import android.view.InputDevice; import android.view.KeyCharacterMap; import android.view.KeyEvent; import android.view.MotionEvent; -import android.view.OnReceiveContentCallback; +import android.view.OnReceiveContentListener.Payload; import android.view.PointerIcon; import android.view.View; import android.view.ViewConfiguration; @@ -2151,10 +2151,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (result != null) { if (isTextEditable()) { ClipData clip = ClipData.newPlainText("", result); - OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder( - clip, SOURCE_PROCESS_TEXT) - .build(); + Payload payload = new Payload.Builder(clip, SOURCE_PROCESS_TEXT).build(); onReceiveContent(payload); if (mEditor != null) { mEditor.refreshTextActionMode(); @@ -11858,8 +11855,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener + " cannot be autofilled into " + this); return; } - final OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_AUTOFILL).build(); + final Payload payload = new Payload.Builder(clip, SOURCE_AUTOFILL).build(); onReceiveContent(payload); } @@ -12926,8 +12922,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (clip == null) { return; } - final OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_CLIPBOARD) + final Payload payload = new Payload.Builder(clip, SOURCE_CLIPBOARD) .setFlags(withFormatting ? 0 : FLAG_CONVERT_TO_PLAIN_TEXT) .build(); onReceiveContent(payload); @@ -13717,7 +13712,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener public void onInputConnectionOpenedInternal(@NonNull InputConnection ic, @NonNull EditorInfo editorInfo, @Nullable Handler handler) { if (mEditor != null) { - mEditor.getDefaultOnReceiveContentCallback().setInputConnectionInfo(ic, editorInfo); + mEditor.getDefaultOnReceiveContentListener().setInputConnectionInfo(this, ic, + editorInfo); } } @@ -13725,68 +13721,35 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public void onInputConnectionClosedInternal() { if (mEditor != null) { - mEditor.getDefaultOnReceiveContentCallback().clearInputConnectionInfo(); + mEditor.getDefaultOnReceiveContentListener().clearInputConnectionInfo(); } } /** - * Sets the callback to handle insertion of content into this view. + * Receives the given content. Clients wishing to provide custom behavior should configure a + * listener via {@link #setOnReceiveContentListener}. * - * <p>This callback will be invoked for the following scenarios: - * <ol> - * <li>Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the - * insertion/selection menu) - * <li>Content insertion from the keyboard (from {@link InputConnection#commitContent}) - * <li>Drag and drop (drop events from {@link #onDragEvent(DragEvent)}) - * <li>Autofill (from {@link #autofill(AutofillValue)}) - * <li>{@link Intent#ACTION_PROCESS_TEXT} replacement - * </ol> + * <p>If a listener is set, invokes the listener. If the listener returns a non-null result, + * executes the default platform handling for the portion of the content returned by the + * listener. * - * <p>This callback is only invoked for content whose MIME type matches a type specified via - * the {code mimeTypes} parameter. If the MIME type is not supported by the callback, the - * default platform handling will be executed instead (no-op for the default {@link View}). - * - * <p><em>Note: MIME type matching in the Android framework is case-sensitive, unlike formal RFC - * MIME types. As a result, you should always write your MIME types with lower case letters, or - * use {@link android.content.Intent#normalizeMimeType} to ensure that it is converted to lower - * case.</em> - * - * @param mimeTypes The type of content for which the callback should be invoked. This may use - * wildcards such as "text/*", "image/*", etc. This must not be null or empty if a non-null - * callback is passed in. - * @param callback The callback to use. This can be null to reset to the default behavior. - */ - @SuppressWarnings("rawtypes") - @Override - public void setOnReceiveContentCallback( - @Nullable String[] mimeTypes, - @Nullable OnReceiveContentCallback callback) { - super.setOnReceiveContentCallback(mimeTypes, callback); - } - - /** - * Receives the given content. The default implementation invokes the callback set via - * {@link #setOnReceiveContentCallback}. If no callback is set or if the callback does not - * support the given content (based on the MIME type), executes the default platform handling - * (e.g. coerces content to text if the source is - * {@link OnReceiveContentCallback.Payload#SOURCE_CLIPBOARD} and this is an editable - * {@link TextView}). + * <p>If no listener is set, executes the default platform behavior. For non-editable TextViews + * the default behavior is a no-op (returns the passed-in content without acting on it). For + * editable TextViews the default behavior coerces all content to text and inserts into the + * view. * * @param payload The content to insert and related metadata. * - * @return Returns true if the content was handled in some way, false otherwise. Actual - * insertion may be processed asynchronously in the background and may or may not succeed even - * if this method returns true. For example, an app may not end up inserting an item if it - * exceeds the app's size limit for that type of content. + * @return The portion of the passed-in content that was not handled (may be all, some, or none + * of the passed-in content). */ @Override - public boolean onReceiveContent(@NonNull OnReceiveContentCallback.Payload payload) { - if (super.onReceiveContent(payload)) { - return true; - } else if (mEditor != null) { - return mEditor.getDefaultOnReceiveContentCallback().onReceiveContent(this, payload); + public @Nullable Payload onReceiveContent(@NonNull Payload payload) { + Payload remaining = super.onReceiveContent(payload); + if (remaining != null && mEditor != null) { + return mEditor.getDefaultOnReceiveContentListener().onReceiveContent(this, remaining); } - return false; + return remaining; } private static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) { diff --git a/core/java/android/widget/TextViewOnReceiveContentCallback.java b/core/java/android/widget/TextViewOnReceiveContentListener.java index 7ed70ec18a7b..7ef68ec7a4ee 100644 --- a/core/java/android/widget/TextViewOnReceiveContentCallback.java +++ b/core/java/android/widget/TextViewOnReceiveContentListener.java @@ -17,12 +17,10 @@ package android.widget; import static android.content.ContentResolver.SCHEME_CONTENT; -import static android.view.OnReceiveContentCallback.Payload.FLAG_CONVERT_TO_PLAIN_TEXT; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_AUTOFILL; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_DRAG_AND_DROP; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_INPUT_METHOD; - -import static java.util.Collections.singleton; +import static android.view.OnReceiveContentListener.Payload.FLAG_CONVERT_TO_PLAIN_TEXT; +import static android.view.OnReceiveContentListener.Payload.SOURCE_AUTOFILL; +import static android.view.OnReceiveContentListener.Payload.SOURCE_DRAG_AND_DROP; +import static android.view.OnReceiveContentListener.Payload.SOURCE_INPUT_METHOD; import android.annotation.NonNull; import android.annotation.Nullable; @@ -39,11 +37,10 @@ import android.text.Editable; import android.text.Selection; import android.text.SpannableStringBuilder; import android.text.Spanned; -import android.util.ArraySet; import android.util.Log; -import android.view.OnReceiveContentCallback; -import android.view.OnReceiveContentCallback.Payload.Flags; -import android.view.OnReceiveContentCallback.Payload.Source; +import android.view.OnReceiveContentListener; +import android.view.OnReceiveContentListener.Payload.Flags; +import android.view.OnReceiveContentListener.Payload.Source; import android.view.View; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -54,42 +51,38 @@ import com.android.internal.annotations.VisibleForTesting; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Arrays; -import java.util.Set; /** - * Default implementation of {@link android.view.OnReceiveContentCallback} for editable + * Default implementation of {@link OnReceiveContentListener} for editable * {@link TextView} components. This class handles insertion of text (plain text, styled text, HTML, - * etc) but not images or other content. This class can be used as a base class for an - * implementation of {@link android.view.OnReceiveContentCallback} for a {@link TextView}, to - * provide consistent behavior for insertion of text. + * etc) but not images or other content. + * + * @hide */ -public class TextViewOnReceiveContentCallback implements OnReceiveContentCallback<TextView> { +@VisibleForTesting +public final class TextViewOnReceiveContentListener implements OnReceiveContentListener { private static final String LOG_TAG = "OnReceiveContent"; - private static final String MIME_TYPE_ALL_TEXT = "text/*"; - private static final Set<String> MIME_TYPES_ALL_TEXT = singleton(MIME_TYPE_ALL_TEXT); - @Nullable private InputConnectionInfo mInputConnectionInfo; - @Nullable private ArraySet<String> mCachedSupportedMimeTypes; @Override - public boolean onReceiveContent(@NonNull TextView view, @NonNull Payload payload) { + public @Nullable Payload onReceiveContent(@NonNull View view, @NonNull Payload payload) { if (Log.isLoggable(LOG_TAG, Log.DEBUG)) { Log.d(LOG_TAG, "onReceive: " + payload); } - ClipData clip = payload.getClip(); - @Source int source = payload.getSource(); - @Flags int flags = payload.getFlags(); + 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 false; + return payload; } if (source == SOURCE_AUTOFILL) { - return onReceiveForAutofill(view, clip, flags); + onReceiveForAutofill((TextView) view, payload); + return null; } if (source == SOURCE_DRAG_AND_DROP) { - return onReceiveForDragAndDrop(view, clip, flags); + onReceiveForDragAndDrop((TextView) view, payload); + return null; } // The code here follows the original paste logic from TextView: @@ -97,7 +90,9 @@ public class TextViewOnReceiveContentCallback implements OnReceiveContentCallbac // 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) view.getText(); + final ClipData clip = payload.getClip(); + final @Flags int flags = payload.getFlags(); + final Editable editable = (Editable) ((TextView) view).getText(); final Context context = view.getContext(); boolean didFirst = false; for (int i = 0; i < clip.getItemCount(); i++) { @@ -118,7 +113,7 @@ public class TextViewOnReceiveContentCallback implements OnReceiveContentCallbac } } } - return didFirst; + return null; } private static void replaceSelection(@NonNull Editable editable, @@ -131,37 +126,33 @@ public class TextViewOnReceiveContentCallback implements OnReceiveContentCallbac editable.replace(start, end, replacement); } - private boolean onReceiveForAutofill(@NonNull TextView view, @NonNull ClipData clip, - @Flags int flags) { + private void onReceiveForAutofill(@NonNull TextView view, @NonNull Payload payload) { + ClipData clip = payload.getClip(); if (isUsageOfImeCommitContentEnabled(view)) { clip = handleNonTextViaImeCommitContent(clip); if (clip == null) { if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "onReceive: Handled via IME"); } - return true; + return; } } - final CharSequence text = coerceToText(clip, view.getContext(), flags); + final CharSequence text = coerceToText(clip, view.getContext(), payload.getFlags()); // First autofill it... view.setText(text); // ...then move cursor to the end. final Editable editable = (Editable) view.getText(); Selection.setSelection(editable, editable.length()); - return true; } - private static boolean onReceiveForDragAndDrop(@NonNull TextView textView, - @NonNull ClipData clip, @Flags int flags) { - final CharSequence text = coerceToText(clip, textView.getContext(), flags); - if (text.length() == 0) { - return false; - } - replaceSelection((Editable) textView.getText(), text); - return true; + private static void onReceiveForDragAndDrop(@NonNull TextView view, @NonNull Payload payload) { + final CharSequence text = coerceToText(payload.getClip(), view.getContext(), + payload.getFlags()); + replaceSelection((Editable) view.getText(), text); } - private static CharSequence coerceToText(ClipData clip, Context context, @Flags int flags) { + private static @NonNull CharSequence coerceToText(@NonNull ClipData clip, + @NonNull Context context, @Flags int flags) { SpannableStringBuilder ssb = new SpannableStringBuilder(); for (int i = 0; i < clip.getItemCount(); i++) { CharSequence itemText; @@ -183,17 +174,17 @@ public class TextViewOnReceiveContentCallback implements OnReceiveContentCallbac * augmented autofill framework (see * <a href="/guide/topics/text/autofill-services">autofill services</a>). In order for an app to * be able to handle these suggestions, it must normally implement the - * {@link android.view.OnReceiveContentCallback} API. To make the adoption of this smoother for + * {@link android.view.OnReceiveContentListener} API. To make the adoption of this smoother for * apps that have previously implemented the * {@link android.view.inputmethod.InputConnection#commitContent(InputContentInfo, int, Bundle)} - * API, we reuse that API as a fallback if {@link android.view.OnReceiveContentCallback} is not + * API, we reuse that API as a fallback if {@link android.view.OnReceiveContentListener} is not * yet implemented by the app. This fallback is only enabled on Android S. This change ID * disables the fallback, such that apps targeting Android T and above must implement the - * {@link android.view.OnReceiveContentCallback} API in order to accept non-text suggestions. + * {@link android.view.OnReceiveContentListener} API in order to accept non-text suggestions. */ @ChangeId @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.S) // Enabled on Android T and higher - private static final long AUTOFILL_NON_TEXT_REQUIRES_ON_RECEIVE_CONTENT_CALLBACK = 163400105L; + private static final long AUTOFILL_NON_TEXT_REQUIRES_ON_RECEIVE_CONTENT_LISTENER = 163400105L; /** * Returns true if we can use the IME {@link InputConnection#commitContent} API in order handle @@ -206,7 +197,7 @@ public class TextViewOnReceiveContentCallback implements OnReceiveContentCallbac } return false; } - if (Compatibility.isChangeEnabled(AUTOFILL_NON_TEXT_REQUIRES_ON_RECEIVE_CONTENT_CALLBACK)) { + if (Compatibility.isChangeEnabled(AUTOFILL_NON_TEXT_REQUIRES_ON_RECEIVE_CONTENT_LISTENER)) { if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "Fallback to commitContent disabled (target SDK is above S)"); } @@ -238,11 +229,16 @@ public class TextViewOnReceiveContentCallback implements OnReceiveContentCallbac * Invoked by the platform when an {@link InputConnection} is successfully created for the view * that owns this callback instance. */ - void setInputConnectionInfo(@NonNull InputConnection ic, @NonNull EditorInfo editorInfo) { + void setInputConnectionInfo(@NonNull TextView view, @NonNull InputConnection ic, + @NonNull EditorInfo editorInfo) { if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { Log.v(LOG_TAG, "setInputConnectionInfo: " + Arrays.toString(editorInfo.contentMimeTypes)); } + if (!isUsageOfImeCommitContentEnabled(view)) { + mInputConnectionInfo = null; + return; + } String[] contentMimeTypes = editorInfo.contentMimeTypes; if (contentMimeTypes == null || contentMimeTypes.length == 0) { mInputConnectionInfo = null; @@ -262,82 +258,26 @@ public class TextViewOnReceiveContentCallback implements OnReceiveContentCallbac mInputConnectionInfo = null; } - // TODO(b/168253885): Use this to populate the assist structure for Autofill - /** @hide */ @VisibleForTesting - public Set<String> getMimeTypes(TextView view) { + @Nullable + public String[] getEditorInfoMimeTypes(@NonNull TextView view) { if (!isUsageOfImeCommitContentEnabled(view)) { - return MIME_TYPES_ALL_TEXT; + return null; } - return getSupportedMimeTypesAugmentedWithImeCommitContentMimeTypes(); - } - - private Set<String> getSupportedMimeTypesAugmentedWithImeCommitContentMimeTypes() { - InputConnectionInfo icInfo = mInputConnectionInfo; + final InputConnectionInfo icInfo = mInputConnectionInfo; if (icInfo == null) { if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { - Log.v(LOG_TAG, "getSupportedMimeTypes: No usable EditorInfo/InputConnection"); + Log.v(LOG_TAG, "getEditorInfoMimeTypes: No usable EditorInfo"); } - return MIME_TYPES_ALL_TEXT; + return null; } - String[] editorInfoContentMimeTypes = icInfo.mEditorInfoContentMimeTypes; + final String[] editorInfoContentMimeTypes = icInfo.mEditorInfoContentMimeTypes; if (Log.isLoggable(LOG_TAG, Log.VERBOSE)) { - Log.v(LOG_TAG, "getSupportedMimeTypes: Augmenting with EditorInfo.contentMimeTypes: " + Log.v(LOG_TAG, "getEditorInfoMimeTypes: " + Arrays.toString(editorInfoContentMimeTypes)); } - ArraySet<String> supportedMimeTypes = mCachedSupportedMimeTypes; - if (canReuse(supportedMimeTypes, editorInfoContentMimeTypes)) { - return supportedMimeTypes; - } - supportedMimeTypes = new ArraySet<>(editorInfoContentMimeTypes); - supportedMimeTypes.add(MIME_TYPE_ALL_TEXT); - mCachedSupportedMimeTypes = supportedMimeTypes; - return supportedMimeTypes; - } - - /** - * We want to avoid creating a new set on every invocation of - * {@link #getSupportedMimeTypesAugmentedWithImeCommitContentMimeTypes()}. - * This method will check if the cached set of MIME types matches the data in the given array - * from {@link EditorInfo} or if a new set should be created. The custom logic is needed for - * comparing the data because the set contains the additional "text/*" MIME type. - * - * @param cachedMimeTypes Previously cached set of MIME types. - * @param newEditorInfoMimeTypes MIME types from {@link EditorInfo}. - * - * @return Returns true if the data in the given cached set matches the data in the array. - * - * @hide - */ - @VisibleForTesting - public static boolean canReuse(@Nullable ArraySet<String> cachedMimeTypes, - @NonNull String[] newEditorInfoMimeTypes) { - if (cachedMimeTypes == null) { - return false; - } - if (newEditorInfoMimeTypes.length != cachedMimeTypes.size() - && newEditorInfoMimeTypes.length != (cachedMimeTypes.size() - 1)) { - return false; - } - final boolean ignoreAllTextMimeType = - newEditorInfoMimeTypes.length == (cachedMimeTypes.size() - 1); - for (String mimeType : cachedMimeTypes) { - if (ignoreAllTextMimeType && mimeType.equals(MIME_TYPE_ALL_TEXT)) { - continue; - } - boolean present = false; - for (String editorInfoContentMimeType : newEditorInfoMimeTypes) { - if (editorInfoContentMimeType.equals(mimeType)) { - present = true; - break; - } - } - if (!present) { - return false; - } - } - return true; + return editorInfoContentMimeTypes; } /** |
