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