From a25346bf0cec67f9a1e8644bd0dff718adb73d36 Mon Sep 17 00:00:00 2001 From: Nikita Dubrovsky Date: Wed, 28 Oct 2020 16:46:07 -0700 Subject: [PATCH] Update onReceiveContent() logic for app vs platform processing Previously onReceiveContent() would only invoke the app-configured callback if the MIME type of the content matched one of the declared MIME types for the callback. This change updates onReceiveContent() to always invoke the listener if one is set (regardless of the MIME type of the content). To delegate processing to the platform, the app's listener can return some or all of the passed-in content. To make this easy for apps to implement, the Payload class and its Builder now provide some convenience methods to conditionally partition the content. Reasons for this change: * Checking the MIME types could be an expensive operation. On SDKs prior to S, ClipData does not keep track of the MIME types of individual items, so for a ClipData that contains multiple items, checking the MIME types requires making at least one RPC call per item. * Allowing the listener to delegate processing to the platform via its return value enables us to limit the API surface (we don't need to expose TextViewOnReceiveContentListener as a public API, nor equivalent classes for other types of views such as WebView). * An app that wants to customize the platform behavior for coercing content to text would previously need to declare "*/*" as the MIME type for the callback (in order to be invoked for all content). But this would make it impossible for features to know whether the app would actually accept a particular type of content or just coerce it to text (e.g. should the soft keyboard show GIF suggestions when the declared MIME type is "*/*"). With the new logic the app's listener is always invoked and can decide which content to process vs delegate to the platform vs reject completely. Bug: 170191676 Bug: 152068298 Test: atest CtsViewTestCases:ViewOnReceiveContentTest Test: atest CtsWidgetTestCases:TextViewOnReceiveContentTest Test: atest FrameworksCoreTests:TextViewOnReceiveContentTest Change-Id: Ie48b6fe0b2ae4b014c371b5dc40248221947c6bf --- api/current.txt | 31 ++-- core/api/current.txt | 31 ++-- ...ack.java => OnReceiveContentListener.java} | 166 +++++++++++++---- core/java/android/view/View.java | 111 +++++++----- .../view/autofill/AutofillManager.java | 14 +- .../view/inputmethod/BaseInputConnection.java | 23 ++- core/java/android/widget/Editor.java | 18 +- core/java/android/widget/TextView.java | 91 +++------- ... => TextViewOnReceiveContentListener.java} | 168 ++++++------------ .../widget/TextViewOnReceiveContentTest.java | 121 ++++--------- .../statusbar/policy/RemoteInputView.java | 38 ++-- 11 files changed, 390 insertions(+), 422 deletions(-) rename core/java/android/view/{OnReceiveContentCallback.java => OnReceiveContentListener.java} (59%) rename core/java/android/widget/{TextViewOnReceiveContentCallback.java => TextViewOnReceiveContentListener.java} (70%) diff --git a/api/current.txt b/api/current.txt index 6459eb4c548aa..1cc8d785328f9 100644 --- a/api/current.txt +++ b/api/current.txt @@ -53789,16 +53789,17 @@ package android.view { field public int toolType; } - public interface OnReceiveContentCallback { - method public boolean onReceiveContent(@NonNull T, @NonNull android.view.OnReceiveContentCallback.Payload); + public interface OnReceiveContentListener { + method @Nullable public android.view.OnReceiveContentListener.Payload onReceiveContent(@NonNull android.view.View, @NonNull android.view.OnReceiveContentListener.Payload); } - public static final class OnReceiveContentCallback.Payload { + public static final class OnReceiveContentListener.Payload { method @NonNull public android.content.ClipData getClip(); method @Nullable public android.os.Bundle getExtras(); method public int getFlags(); method @Nullable public android.net.Uri getLinkUri(); method public int getSource(); + method @NonNull public java.util.Map partition(@NonNull java.util.function.Predicate); field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1 field public static final int SOURCE_APP = 0; // 0x0 field public static final int SOURCE_AUTOFILL = 4; // 0x4 @@ -53808,12 +53809,15 @@ package android.view { field public static final int SOURCE_PROCESS_TEXT = 5; // 0x5 } - public static final class OnReceiveContentCallback.Payload.Builder { - ctor public OnReceiveContentCallback.Payload.Builder(@NonNull android.content.ClipData, int); - method @NonNull public android.view.OnReceiveContentCallback.Payload build(); - method @NonNull public android.view.OnReceiveContentCallback.Payload.Builder setExtras(@Nullable android.os.Bundle); - method @NonNull public android.view.OnReceiveContentCallback.Payload.Builder setFlags(int); - method @NonNull public android.view.OnReceiveContentCallback.Payload.Builder setLinkUri(@Nullable android.net.Uri); + public static final class OnReceiveContentListener.Payload.Builder { + ctor public OnReceiveContentListener.Payload.Builder(@NonNull android.view.OnReceiveContentListener.Payload); + ctor public OnReceiveContentListener.Payload.Builder(@NonNull android.content.ClipData, int); + method @NonNull public android.view.OnReceiveContentListener.Payload build(); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setClip(@NonNull android.content.ClipData); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setExtras(@Nullable android.os.Bundle); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setFlags(int); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setLinkUri(@Nullable android.net.Uri); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setSource(int); } public abstract class OrientationEventListener { @@ -54562,7 +54566,7 @@ package android.view { method public void onProvideContentCaptureStructure(@NonNull android.view.ViewStructure, int); method public void onProvideStructure(android.view.ViewStructure); method public void onProvideVirtualStructure(android.view.ViewStructure); - method public boolean onReceiveContent(@NonNull android.view.OnReceiveContentCallback.Payload); + method @Nullable public android.view.OnReceiveContentListener.Payload onReceiveContent(@NonNull android.view.OnReceiveContentListener.Payload); method public android.view.PointerIcon onResolvePointerIcon(android.view.MotionEvent, int); method @CallSuper protected void onRestoreInstanceState(android.os.Parcelable); method public void onRtlPropertiesChanged(int); @@ -54720,7 +54724,7 @@ package android.view { method public void setOnHoverListener(android.view.View.OnHoverListener); method public void setOnKeyListener(android.view.View.OnKeyListener); method public void setOnLongClickListener(@Nullable android.view.View.OnLongClickListener); - method public void setOnReceiveContentCallback(@Nullable String[], @Nullable android.view.OnReceiveContentCallback); + method public void setOnReceiveContentListener(@Nullable String[], @Nullable android.view.OnReceiveContentListener); method public void setOnScrollChangeListener(android.view.View.OnScrollChangeListener); method @Deprecated public void setOnSystemUiVisibilityChangeListener(android.view.View.OnSystemUiVisibilityChangeListener); method public void setOnTouchListener(android.view.View.OnTouchListener); @@ -61736,11 +61740,6 @@ package android.widget { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } - public class TextViewOnReceiveContentCallback implements android.view.OnReceiveContentCallback { - ctor public TextViewOnReceiveContentCallback(); - method public boolean onReceiveContent(@NonNull android.widget.TextView, @NonNull android.view.OnReceiveContentCallback.Payload); - } - public interface ThemedSpinnerAdapter extends android.widget.SpinnerAdapter { method @Nullable public android.content.res.Resources.Theme getDropDownViewTheme(); method public void setDropDownViewTheme(@Nullable android.content.res.Resources.Theme); diff --git a/core/api/current.txt b/core/api/current.txt index 05b57af1dccd6..e181754bf1b32 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -51897,16 +51897,17 @@ package android.view { field public int toolType; } - public interface OnReceiveContentCallback { - method public boolean onReceiveContent(@NonNull T, @NonNull android.view.OnReceiveContentCallback.Payload); + public interface OnReceiveContentListener { + method @Nullable public android.view.OnReceiveContentListener.Payload onReceiveContent(@NonNull android.view.View, @NonNull android.view.OnReceiveContentListener.Payload); } - public static final class OnReceiveContentCallback.Payload { + public static final class OnReceiveContentListener.Payload { method @NonNull public android.content.ClipData getClip(); method @Nullable public android.os.Bundle getExtras(); method public int getFlags(); method @Nullable public android.net.Uri getLinkUri(); method public int getSource(); + method @NonNull public java.util.Map partition(@NonNull java.util.function.Predicate); field public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1; // 0x1 field public static final int SOURCE_APP = 0; // 0x0 field public static final int SOURCE_AUTOFILL = 4; // 0x4 @@ -51916,12 +51917,15 @@ package android.view { field public static final int SOURCE_PROCESS_TEXT = 5; // 0x5 } - public static final class OnReceiveContentCallback.Payload.Builder { - ctor public OnReceiveContentCallback.Payload.Builder(@NonNull android.content.ClipData, int); - method @NonNull public android.view.OnReceiveContentCallback.Payload build(); - method @NonNull public android.view.OnReceiveContentCallback.Payload.Builder setExtras(@Nullable android.os.Bundle); - method @NonNull public android.view.OnReceiveContentCallback.Payload.Builder setFlags(int); - method @NonNull public android.view.OnReceiveContentCallback.Payload.Builder setLinkUri(@Nullable android.net.Uri); + public static final class OnReceiveContentListener.Payload.Builder { + ctor public OnReceiveContentListener.Payload.Builder(@NonNull android.view.OnReceiveContentListener.Payload); + ctor public OnReceiveContentListener.Payload.Builder(@NonNull android.content.ClipData, int); + method @NonNull public android.view.OnReceiveContentListener.Payload build(); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setClip(@NonNull android.content.ClipData); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setExtras(@Nullable android.os.Bundle); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setFlags(int); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setLinkUri(@Nullable android.net.Uri); + method @NonNull public android.view.OnReceiveContentListener.Payload.Builder setSource(int); } public abstract class OrientationEventListener { @@ -52670,7 +52674,7 @@ package android.view { method public void onProvideContentCaptureStructure(@NonNull android.view.ViewStructure, int); method public void onProvideStructure(android.view.ViewStructure); method public void onProvideVirtualStructure(android.view.ViewStructure); - method public boolean onReceiveContent(@NonNull android.view.OnReceiveContentCallback.Payload); + method @Nullable public android.view.OnReceiveContentListener.Payload onReceiveContent(@NonNull android.view.OnReceiveContentListener.Payload); method public android.view.PointerIcon onResolvePointerIcon(android.view.MotionEvent, int); method @CallSuper protected void onRestoreInstanceState(android.os.Parcelable); method public void onRtlPropertiesChanged(int); @@ -52828,7 +52832,7 @@ package android.view { method public void setOnHoverListener(android.view.View.OnHoverListener); method public void setOnKeyListener(android.view.View.OnKeyListener); method public void setOnLongClickListener(@Nullable android.view.View.OnLongClickListener); - method public void setOnReceiveContentCallback(@Nullable String[], @Nullable android.view.OnReceiveContentCallback); + method public void setOnReceiveContentListener(@Nullable String[], @Nullable android.view.OnReceiveContentListener); method public void setOnScrollChangeListener(android.view.View.OnScrollChangeListener); method @Deprecated public void setOnSystemUiVisibilityChangeListener(android.view.View.OnSystemUiVisibilityChangeListener); method public void setOnTouchListener(android.view.View.OnTouchListener); @@ -59844,11 +59848,6 @@ package android.widget { field @NonNull public static final android.os.Parcelable.Creator CREATOR; } - public class TextViewOnReceiveContentCallback implements android.view.OnReceiveContentCallback { - ctor public TextViewOnReceiveContentCallback(); - method public boolean onReceiveContent(@NonNull android.widget.TextView, @NonNull android.view.OnReceiveContentCallback.Payload); - } - public interface ThemedSpinnerAdapter extends android.widget.SpinnerAdapter { method @Nullable public android.content.res.Resources.Theme getDropDownViewTheme(); method public void setDropDownViewTheme(@Nullable android.content.res.Resources.Theme); diff --git a/core/java/android/view/OnReceiveContentCallback.java b/core/java/android/view/OnReceiveContentListener.java similarity index 59% rename from core/java/android/view/OnReceiveContentCallback.java rename to core/java/android/view/OnReceiveContentListener.java index d74938c1d1fd6..495528989a830 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). * - *

This callback can be attached to different types of UI components using - * {@link View#setOnReceiveContentCallback}. + *

This listener can be attached to different types of UI components using + * {@link View#setOnReceiveContentListener}. * - *

For editable {@link android.widget.TextView} components, implementations can extend from - * {@link android.widget.TextViewOnReceiveContentCallback} to reuse default platform behavior for - * handling text. - * - *

Example implementation:
+ *

Here is a sample implementation that handles content URIs and delegates the processing for + * text and everything else to the platform:
*

- * // (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());
  *     }
  * 
- * - * @param The type of {@link View} with which this callback can be associated. */ -public interface OnReceiveContentCallback { +public interface OnReceiveContentListener { /** * Receive the given content. * - *

This method is only invoked for content whose MIME type matches a type specified via - * {@link View#setOnReceiveContentCallback}. + *

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. * - *

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. + *

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. * - *

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). + *

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 { @Override public String toString() { return "Payload{" - + "clip=" + mClip.getDescription() + + "clip=" + mClip + ", source=" + sourceToString(mSource) + ", flags=" + flagsToString(mFlags) + ", linkUri=" + mLinkUri @@ -255,16 +269,74 @@ public interface OnReceiveContentCallback { return mExtras; } + /** + * Partitions this payload based on the given predicate. + * + *

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. + * + *

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 partition( + @NonNull Predicate itemPredicate) { + if (mClip.getItemCount() == 1) { + Map result = new ArrayMap<>(1); + result.put(itemPredicate.test(mClip.getItemAt(0)), this); + return result; + } + ArrayList accepted = new ArrayList<>(); + ArrayList 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 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. @@ -275,6 +347,28 @@ public interface OnReceiveContentCallback { mSource = source; } + /** + * 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 diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java index cefddd84ee32f..a88ad9f24c104 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. * - *

Depending on the view, this callback may be invoked for scenarios such as content - * insertion from the IME, Autofill, etc. + *

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: + *

    + *
  1. Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the + * insertion/selection menu) + *
  2. Content insertion from the keyboard (from {@link InputConnection#commitContent}) + *
  3. Drag and drop (drop events from {@link #onDragEvent(DragEvent)}) + *
  4. Autofill + *
  5. Selection replacement via {@link Intent#ACTION_PROCESS_TEXT} + *
* - *

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}). + *

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/*". * - *

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. + *

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"); } - mOnReceiveContentMimeTypes = mimeTypes; - mOnReceiveContentCallback = callback; + 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 = 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. * - *

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". + *

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/*". * *

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/*"). + * 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 supported by {@link #onReceiveContent} for this view. The returned - * MIME types may contain wildcards such as "text/*", "image/*", etc. + * @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 81db62857c176..299c41b02b230 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 62b1b1f8cf53e..a92d1f589e96b 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 4099c8aa67104..9766fab8d59e8 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 3ac78bafdedc8..9485753ce9066 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}. * - *

This callback will be invoked for the following scenarios: - *

    - *
  1. Paste from the clipboard (e.g. "Paste" or "Paste as plain text" action in the - * insertion/selection menu) - *
  2. Content insertion from the keyboard (from {@link InputConnection#commitContent}) - *
  3. Drag and drop (drop events from {@link #onDragEvent(DragEvent)}) - *
  4. Autofill (from {@link #autofill(AutofillValue)}) - *
  5. {@link Intent#ACTION_PROCESS_TEXT} replacement - *
+ *

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. * - *

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}). - * - *

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. - * - * @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}). + *

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 similarity index 70% rename from core/java/android/widget/TextViewOnReceiveContentCallback.java rename to core/java/android/widget/TextViewOnReceiveContentListener.java index 7ed70ec18a7b3..7ef68ec7a4eee 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 { +@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 MIME_TYPES_ALL_TEXT = singleton(MIME_TYPE_ALL_TEXT); - @Nullable private InputConnectionInfo mInputConnectionInfo; - @Nullable private ArraySet 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 * autofill services). 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 getMimeTypes(TextView view) { + @Nullable + public String[] getEditorInfoMimeTypes(@NonNull TextView view) { if (!isUsageOfImeCommitContentEnabled(view)) { - return MIME_TYPES_ALL_TEXT; + return null; } - return getSupportedMimeTypesAugmentedWithImeCommitContentMimeTypes(); - } - - private Set 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 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 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; } /** diff --git a/core/tests/coretests/src/android/widget/TextViewOnReceiveContentTest.java b/core/tests/coretests/src/android/widget/TextViewOnReceiveContentTest.java index 8efd3b4f01351..7b9283b41ff0f 100644 --- a/core/tests/coretests/src/android/widget/TextViewOnReceiveContentTest.java +++ b/core/tests/coretests/src/android/widget/TextViewOnReceiveContentTest.java @@ -16,12 +16,11 @@ package android.widget; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_AUTOFILL; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_CLIPBOARD; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_DRAG_AND_DROP; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_INPUT_METHOD; -import static android.view.OnReceiveContentCallback.Payload.SOURCE_PROCESS_TEXT; -import static android.widget.TextViewOnReceiveContentCallback.canReuse; +import static android.view.OnReceiveContentListener.Payload.SOURCE_AUTOFILL; +import static android.view.OnReceiveContentListener.Payload.SOURCE_CLIPBOARD; +import static android.view.OnReceiveContentListener.Payload.SOURCE_DRAG_AND_DROP; +import static android.view.OnReceiveContentListener.Payload.SOURCE_INPUT_METHOD; +import static android.view.OnReceiveContentListener.Payload.SOURCE_PROCESS_TEXT; import static android.widget.espresso.TextViewActions.clickOnTextAtIndex; import static androidx.test.espresso.Espresso.onView; @@ -42,8 +41,7 @@ import android.content.ClipData; import android.content.ClipDescription; import android.net.Uri; import android.os.Bundle; -import android.util.ArraySet; -import android.view.OnReceiveContentCallback; +import android.view.OnReceiveContentListener; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputConnectionWrapper; import android.view.inputmethod.InputContentInfo; @@ -62,7 +60,7 @@ import org.junit.runner.RunWith; import org.mockito.Mockito; /** - * Tests for {@link TextViewOnReceiveContentCallback}. Most of the test cases are in the CTS test + * Tests for {@link TextViewOnReceiveContentListener}. Most of the test cases are in the CTS test * {@link android.widget.cts.TextViewOnReceiveContentTest}. This class tests some internal * implementation details, e.g. fallback to the keyboard image API. */ @@ -78,35 +76,34 @@ public class TextViewOnReceiveContentTest { private Instrumentation mInstrumentation; private Activity mActivity; private CustomInputConnectionEditText mEditText; - private TextViewOnReceiveContentCallback mDefaultCallback; + private TextViewOnReceiveContentListener mDefaultReceiver; @Before public void before() { mInstrumentation = InstrumentationRegistry.getInstrumentation(); mActivity = mActivityRule.getActivity(); mEditText = mActivity.findViewById(R.id.edittext2); - mDefaultCallback = mEditText.getEditorForTesting().getDefaultOnReceiveContentCallback(); + mDefaultReceiver = mEditText.getEditorForTesting().getDefaultOnReceiveContentListener(); } @Test - public void testGetSupportedMimeTypes_fallbackToCommitContent() throws Throwable { + public void testGetEditorInfoMimeTypes_fallbackToCommitContent() throws Throwable { // Configure the EditText with an EditorInfo/InputConnection that supports some image MIME // types. - mEditText.setContentMimeTypes(new String[] {"image/gif", "image/png"}); + String[] mimeTypes = {"image/gif", "image/png"}; + mEditText.setContentMimeTypes(mimeTypes); MyInputConnection ic = new MyInputConnection(); mEditText.setInputConnectionWrapper(ic); // Focus into the EditText. onView(withId(mEditText.getId())).perform(clickOnTextAtIndex(0)); - // Assert that the callback returns the MIME types declared in the EditorInfo in addition to - // the default. - assertThat(mDefaultCallback.getMimeTypes(mEditText)).containsExactly( - "text/*", "image/gif", "image/png"); + // Assert that the default listener returns the MIME types declared in the EditorInfo. + assertThat(mDefaultReceiver.getEditorInfoMimeTypes(mEditText)).isEqualTo(mimeTypes); } @Test - public void testGetSupportedMimeTypes_fallbackToCommitContent_noMimeTypesInEditorInfo() + public void testGetEditorInfoMimeTypes_fallbackToCommitContent_noMimeTypesInEditorInfo() throws Throwable { // Configure the EditText with an EditorInfo/InputConnection that doesn't declare any MIME // types. @@ -117,8 +114,8 @@ public class TextViewOnReceiveContentTest { // Focus into the EditText. onView(withId(mEditText.getId())).perform(clickOnTextAtIndex(0)); - // Assert that the callback returns the default MIME types. - assertThat(mDefaultCallback.getMimeTypes(mEditText)).containsExactly("text/*"); + // Assert that the default listener returns null as the MIME types. + assertThat(mDefaultReceiver.getEditorInfoMimeTypes(mEditText)).isNull(); } @Test @@ -132,13 +129,13 @@ public class TextViewOnReceiveContentTest { // Focus into the EditText. onView(withId(mEditText.getId())).perform(clickOnTextAtIndex(0)); - // Invoke the callback with SOURCE_AUTOFILL and assert that it triggers a call to + // Invoke the listener with SOURCE_AUTOFILL and assert that it triggers a call to // InputConnection.commitContent. ClipDescription description = new ClipDescription("", new String[] {"image/gif"}); ClipData clip = new ClipData(description, new ClipData.Item(SAMPLE_CONTENT_URI)); - OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_AUTOFILL).build(); - mDefaultCallback.onReceiveContent(mEditText, payload); + OnReceiveContentListener.Payload payload = + new OnReceiveContentListener.Payload.Builder(clip, SOURCE_AUTOFILL).build(); + mDefaultReceiver.onReceiveContent(mEditText, payload); verify(ic.mMock, times(1)) .commitContent(any(InputContentInfo.class), eq(0), eq(null)); verifyNoMoreInteractions(ic.mMock); @@ -155,12 +152,12 @@ public class TextViewOnReceiveContentTest { // Focus into the EditText. onView(withId(mEditText.getId())).perform(clickOnTextAtIndex(0)); - // Invoke the callback and assert that the InputConnection is not invoked. + // Invoke the listener and assert that the InputConnection is not invoked. ClipDescription description = new ClipDescription("", new String[] {"image/gif"}); ClipData clip = new ClipData(description, new ClipData.Item(SAMPLE_CONTENT_URI)); - OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_AUTOFILL).build(); - mDefaultCallback.onReceiveContent(mEditText, payload); + OnReceiveContentListener.Payload payload = + new OnReceiveContentListener.Payload.Builder(clip, SOURCE_AUTOFILL).build(); + mDefaultReceiver.onReceiveContent(mEditText, payload); verifyZeroInteractions(ic.mMock); } @@ -175,71 +172,28 @@ public class TextViewOnReceiveContentTest { // Focus into the EditText. onView(withId(mEditText.getId())).perform(clickOnTextAtIndex(0)); - // Invoke the callback with sources other than SOURCE_AUTOFILL and assert that it does NOT + // Invoke the listener with sources other than SOURCE_AUTOFILL and assert that it does NOT // trigger calls to InputConnection.commitContent. ClipDescription description = new ClipDescription("", new String[] {"image/gif"}); ClipData clip = new ClipData(description, new ClipData.Item(SAMPLE_CONTENT_URI)); - OnReceiveContentCallback.Payload payload = - new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_CLIPBOARD).build(); - mDefaultCallback.onReceiveContent(mEditText, payload); + OnReceiveContentListener.Payload payload = + new OnReceiveContentListener.Payload.Builder(clip, SOURCE_CLIPBOARD).build(); + mDefaultReceiver.onReceiveContent(mEditText, payload); verifyZeroInteractions(ic.mMock); - payload = new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_INPUT_METHOD).build(); - mDefaultCallback.onReceiveContent(mEditText, payload); + payload = new OnReceiveContentListener.Payload.Builder(clip, SOURCE_INPUT_METHOD).build(); + mDefaultReceiver.onReceiveContent(mEditText, payload); verifyZeroInteractions(ic.mMock); - payload = new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_DRAG_AND_DROP).build(); - mDefaultCallback.onReceiveContent(mEditText, payload); + payload = new OnReceiveContentListener.Payload.Builder(clip, SOURCE_DRAG_AND_DROP).build(); + mDefaultReceiver.onReceiveContent(mEditText, payload); verifyZeroInteractions(ic.mMock); - payload = new OnReceiveContentCallback.Payload.Builder(clip, SOURCE_PROCESS_TEXT).build(); - mDefaultCallback.onReceiveContent(mEditText, payload); + payload = new OnReceiveContentListener.Payload.Builder(clip, SOURCE_PROCESS_TEXT).build(); + mDefaultReceiver.onReceiveContent(mEditText, payload); verifyZeroInteractions(ic.mMock); } - @Test - public void testCanReuse() throws Throwable { - ArraySet mimeTypes = null; - String[] editorContentMimeTypes = new String[0]; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isFalse(); - - mimeTypes = new ArraySet<>(); - editorContentMimeTypes = new String[0]; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isTrue(); - - mimeTypes = newArraySet("text/*"); - editorContentMimeTypes = new String[0]; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isTrue(); - - mimeTypes = newArraySet("text/*"); - editorContentMimeTypes = new String[] {"text/*"}; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isTrue(); - - mimeTypes = newArraySet("image/gif", "image/png", "text/*"); - editorContentMimeTypes = new String[] {"image/gif", "image/png"}; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isTrue(); - - mimeTypes = newArraySet("image/gif", "image/png", "text/*"); - editorContentMimeTypes = new String[] {"image/gif", "image/png", "text/*"}; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isTrue(); - - mimeTypes = newArraySet("image/gif", "image/png", "text/*"); - editorContentMimeTypes = new String[] {"image/gif"}; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isFalse(); - - mimeTypes = newArraySet("image/gif", "image/png", "text/*"); - editorContentMimeTypes = new String[] {"image/gif", "image/png", "image/jpg"}; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isFalse(); - - mimeTypes = newArraySet("image/gif", "image/png", "text/*"); - editorContentMimeTypes = new String[] {"image/gif", "image/jpg"}; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isFalse(); - - mimeTypes = newArraySet("image/gif", "image/png", "text/*"); - editorContentMimeTypes = new String[] {"image/gif", "image/jpg", "text/*"}; - assertThat(canReuse(mimeTypes, editorContentMimeTypes)).isFalse(); - } - private static class MyInputConnection extends InputConnectionWrapper { public final InputConnection mMock; @@ -254,9 +208,4 @@ public class TextViewOnReceiveContentTest { return true; } } - - @SafeVarargs - private static ArraySet newArraySet(T ... elements) { - return new ArraySet<>(elements); - } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java index 4552026ced4b4..79f09158fc678 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/RemoteInputView.java @@ -45,7 +45,7 @@ import android.util.Log; import android.view.KeyEvent; import android.view.LayoutInflater; import android.view.MotionEvent; -import android.view.OnReceiveContentCallback; +import android.view.OnReceiveContentListener; import android.view.View; import android.view.ViewAnimationUtils; import android.view.ViewGroup; @@ -74,6 +74,7 @@ import com.android.systemui.statusbar.notification.stack.StackStateAnimator; import com.android.systemui.statusbar.phone.LightBarController; import java.util.HashMap; +import java.util.Map; import java.util.function.Consumer; /** @@ -585,27 +586,32 @@ public class RemoteInputView extends LinearLayout implements View.OnClickListene protected void onFinishInflate() { super.onFinishInflate(); if (mSupportedMimeTypes != null && mSupportedMimeTypes.length > 0) { - setOnReceiveContentCallback(mSupportedMimeTypes, - new OnReceiveContentCallback() { + setOnReceiveContentListener(mSupportedMimeTypes, + new OnReceiveContentListener() { @Override - public boolean onReceiveContent(@NonNull View view, + @Nullable + public Payload onReceiveContent(@NonNull View view, @NonNull Payload payload) { - ClipData clip = payload.getClip(); - if (clip.getItemCount() == 0) { - return false; - } - Uri contentUri = clip.getItemAt(0).getUri(); - ClipDescription description = clip.getDescription(); - String mimeType = null; - if (description.getMimeTypeCount() > 0) { - mimeType = description.getMimeType(0); - } - if (mimeType != null) { + Map split = payload.partition( + item -> item.getUri() != null); + Payload uriItems = split.get(true); + Payload remainingItems = split.get(false); + if (uriItems != null) { + ClipData clip = uriItems.getClip(); + ClipDescription description = clip.getDescription(); + if (clip.getItemCount() > 1 + || description.getMimeTypeCount() < 1 + || remainingItems != null) { + // TODO(b/172363500): Update to loop over all the items + return payload; + } + Uri contentUri = clip.getItemAt(0).getUri(); + String mimeType = description.getMimeType(0); Intent dataIntent = mRemoteInputView .prepareRemoteInputFromData(mimeType, contentUri); mRemoteInputView.sendRemoteInput(dataIntent); } - return true; + return remainingItems; } }); }