diff --git a/api/current.txt b/api/current.txt index 576f3f241a7a6..8c23fb77f43f8 100644 --- a/api/current.txt +++ b/api/current.txt @@ -53860,16 +53860,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 @@ -53879,12 +53880,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 { @@ -54633,7 +54637,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); @@ -54791,7 +54795,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); @@ -61808,11 +61812,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 0e461c54cba02..8a139a6a9b4ed 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -51965,16 +51965,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 @@ -51984,12 +51985,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 { @@ -52738,7 +52742,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); @@ -52896,7 +52900,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); @@ -59913,11 +59917,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 e36243cc7948e..da14f2cfbd5af 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; } }); }