/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.core.view;
import android.content.ClipData;
import android.content.ClipDescription;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.util.Pair;
import android.view.ContentInfo;
import androidx.annotation.DoNotInline;
import androidx.annotation.IntDef;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.annotation.RestrictTo;
import androidx.core.util.Preconditions;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* Holds all the relevant data for a request to {@link OnReceiveContentListener}. This is a
* backward-compatible wrapper for the platform class {@link ContentInfo}.
*/
public final class ContentInfoCompat {
/**
* Specifies the UI through which content is being inserted. Future versions of Android may
* support additional values.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@IntDef(value = {SOURCE_APP, SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD, SOURCE_DRAG_AND_DROP,
SOURCE_AUTOFILL, SOURCE_PROCESS_TEXT})
@Retention(RetentionPolicy.SOURCE)
public @interface Source {
}
/**
* Specifies that the operation was triggered by the app that contains the target view.
*/
public static final int SOURCE_APP = 0;
/**
* Specifies that the operation was triggered by a paste from the clipboard (e.g. "Paste" or
* "Paste as plain text" action in the insertion/selection menu).
*/
public static final int SOURCE_CLIPBOARD = 1;
/**
* Specifies that the operation was triggered from the soft keyboard (also known as input
* method editor or IME). See https://developer.android.com/guide/topics/text/image-keyboard
* for more info.
*/
public static final int SOURCE_INPUT_METHOD = 2;
/**
* Specifies that the operation was triggered by the drag/drop framework. See
* https://developer.android.com/guide/topics/ui/drag-drop for more info.
*/
public static final int SOURCE_DRAG_AND_DROP = 3;
/**
* Specifies that the operation was triggered by the autofill framework. See
* https://developer.android.com/guide/topics/text/autofill for more info.
*/
public static final int SOURCE_AUTOFILL = 4;
/**
* Specifies that the operation was triggered by a result from a
* {@link android.content.Intent#ACTION_PROCESS_TEXT PROCESS_TEXT} action in the selection
* menu.
*/
public static final int SOURCE_PROCESS_TEXT = 5;
/**
* Returns the symbolic name of the given source.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@NonNull
static String sourceToString(@Source int source) {
switch (source) {
case SOURCE_APP: return "SOURCE_APP";
case SOURCE_CLIPBOARD: return "SOURCE_CLIPBOARD";
case SOURCE_INPUT_METHOD: return "SOURCE_INPUT_METHOD";
case SOURCE_DRAG_AND_DROP: return "SOURCE_DRAG_AND_DROP";
case SOURCE_AUTOFILL: return "SOURCE_AUTOFILL";
case SOURCE_PROCESS_TEXT: return "SOURCE_PROCESS_TEXT";
}
return String.valueOf(source);
}
/**
* Flags to configure the insertion behavior.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@IntDef(flag = true, value = {FLAG_CONVERT_TO_PLAIN_TEXT})
@Retention(RetentionPolicy.SOURCE)
public @interface Flags {
}
/**
* Flag requesting that the content should be converted to plain text prior to inserting.
*/
@SuppressWarnings("PointlessBitwiseExpression")
public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1 << 0;
/**
* Returns the symbolic names of the set flags or {@code "0"} if no flags are set.
*
* @hide
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@NonNull
static String flagsToString(@Flags int flags) {
if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) {
return "FLAG_CONVERT_TO_PLAIN_TEXT";
}
return String.valueOf(flags);
}
@NonNull
private final Compat mCompat;
ContentInfoCompat(@NonNull Compat compat) {
mCompat = compat;
}
/**
* Provides a backward-compatible wrapper for {@link ContentInfo}.
*
* <p>This method is not supported on devices running SDK <= 30 since the platform
* class will not be available.
*
* @param platContentInfo platform class to wrap, must not be null
* @return wrapped class
*/
@RequiresApi(31)
@NonNull
public static ContentInfoCompat toContentInfoCompat(@NonNull ContentInfo platContentInfo) {
return new ContentInfoCompat(new Compat31Impl(platContentInfo));
}
/**
* Provides the {@link ContentInfo} represented by this object.
*
* <p>This method is not supported on devices running SDK <= 30 since the platform
* class will not be available.
*
* @return platform class object
* @see ContentInfoCompat#toContentInfoCompat
*/
@RequiresApi(31)
@NonNull
public ContentInfo toContentInfo() {
return Objects.requireNonNull(mCompat.getWrapped());
}
@NonNull
@Override
public String toString() {
return mCompat.toString();
}
/**
* The data to be inserted.
*/
@NonNull
public ClipData getClip() {
return mCompat.getClip();
}
/**
* The source of the operation. See {@code SOURCE_} constants. Future versions of Android
* may pass additional values.
*/
@Source
public int getSource() {
return mCompat.getSource();
}
/**
* Optional flags that control the insertion behavior. See {@code FLAG_} constants.
*/
@Flags
public int getFlags() {
return mCompat.getFlags();
}
/**
* Optional http/https URI for the content that may be provided by the IME. This is only
* populated if the source is {@link #SOURCE_INPUT_METHOD} and if a non-empty
* {@link android.view.inputmethod.InputContentInfo#getLinkUri linkUri} was passed by the
* IME.
*/
@Nullable
public Uri getLinkUri() {
return mCompat.getLinkUri();
}
/**
* Optional additional metadata. If the source is {@link #SOURCE_INPUT_METHOD}, this will
* include the {@link android.view.inputmethod.InputConnection#commitContent opts} passed by
* the IME.
*/
@Nullable
public Bundle getExtras() {
return mCompat.getExtras();
}
/**
* Partitions this content based on the given predicate.
*
* <p>This function classifies the content and organizes it into a pair, grouping the items
* that matched vs didn't match the predicate.
*
* <p>Except for the {@link ClipData} items, the returned objects will contain all the same
* metadata as this {@link ContentInfoCompat}.
*
* @param itemPredicate The predicate to test each {@link ClipData.Item} to determine which
* partition to place it into.
* @return A pair containing the partitioned content. The pair's first object will have the
* content that matched the predicate, or null if none of the items matched. The pair's
* second object will have the content that didn't match the predicate, or null if all of
* the items matched.
*/
@NonNull
public Pair<ContentInfoCompat, ContentInfoCompat> partition(
@NonNull androidx.core.util.Predicate<ClipData.Item> itemPredicate) {
ClipData clip = mCompat.getClip();
if (clip.getItemCount() == 1) {
boolean matched = itemPredicate.test(clip.getItemAt(0));
return Pair.create(matched ? this : null, matched ? null : this);
}
Pair<ClipData, ClipData> split = ContentInfoCompat.partition(clip, itemPredicate);
if (split.first == null) {
return Pair.create(null, this);
} else if (split.second == null) {
return Pair.create(this, null);
}
return Pair.create(
new ContentInfoCompat.Builder(this).setClip(split.first).build(),
new ContentInfoCompat.Builder(this).setClip(split.second).build());
}
@NonNull
static Pair<ClipData, ClipData> partition(@NonNull ClipData clip,
@NonNull androidx.core.util.Predicate<ClipData.Item> itemPredicate) {
ArrayList<ClipData.Item> acceptedItems = null;
ArrayList<ClipData.Item> remainingItems = null;
for (int i = 0; i < clip.getItemCount(); i++) {
ClipData.Item item = clip.getItemAt(i);
if (itemPredicate.test(item)) {
acceptedItems = (acceptedItems == null) ? new ArrayList<>() : acceptedItems;
acceptedItems.add(item);
} else {
remainingItems = (remainingItems == null) ? new ArrayList<>() : remainingItems;
remainingItems.add(item);
}
}
if (acceptedItems == null) {
return Pair.create(null, clip);
}
if (remainingItems == null) {
return Pair.create(clip, null);
}
return Pair.create(
buildClipData(clip.getDescription(), acceptedItems),
buildClipData(clip.getDescription(), remainingItems));
}
@NonNull
static ClipData buildClipData(@NonNull ClipDescription description,
@NonNull List<ClipData.Item> items) {
ClipData clip = new ClipData(new ClipDescription(description), items.get(0));
for (int i = 1; i < items.size(); i++) {
clip.addItem(items.get(i));
}
return clip;
}
/**
* Partitions content based on the given predicate.
*
* <p>This function classifies the content and organizes it into a pair, grouping the items
* that matched vs didn't match the predicate.
*
* <p>Except for the {@link ClipData} items, the returned objects will contain all the same
* metadata as the passed-in {@link ContentInfo}.
*
* @param itemPredicate The predicate to test each {@link ClipData.Item} to determine which
* partition to place it into.
* @return A pair containing the partitioned content. The pair's first object will have the
* content that matched the predicate, or null if none of the items matched. The pair's
* second object will have the content that didn't match the predicate, or null if all of
* the items matched.
*/
@RequiresApi(31)
@NonNull
public static Pair<ContentInfo, ContentInfo> partition(@NonNull ContentInfo payload,
@NonNull Predicate<ClipData.Item> itemPredicate) {
return Api31Impl.partition(payload, itemPredicate);
}
@RequiresApi(31)
private static final class Api31Impl {
private Api31Impl() {}
@DoNotInline
@NonNull
public static Pair<ContentInfo, ContentInfo> partition(@NonNull ContentInfo payload,
@NonNull Predicate<ClipData.Item> itemPredicate) {
ClipData clip = payload.getClip();
if (clip.getItemCount() == 1) {
boolean matched = itemPredicate.test(clip.getItemAt(0));
return Pair.create(matched ? payload : null, matched ? null : payload);
}
Pair<ClipData, ClipData> split = ContentInfoCompat.partition(clip, itemPredicate::test);
if (split.first == null) {
return Pair.create(null, payload);
} else if (split.second == null) {
return Pair.create(payload, null);
}
return Pair.create(
new ContentInfo.Builder(payload).setClip(split.first).build(),
new ContentInfo.Builder(payload).setClip(split.second).build());
}
}
private interface Compat {
@Nullable
ContentInfo getWrapped();
@NonNull
ClipData getClip();
@Source
int getSource();
@Flags
int getFlags();
@Nullable
Uri getLinkUri();
@Nullable
Bundle getExtras();
}
private static final class CompatImpl implements Compat {
@NonNull
private final ClipData mClip;
@Source
private final int mSource;
@Flags
private final int mFlags;
@Nullable
private final Uri mLinkUri;
@Nullable
private final Bundle mExtras;
CompatImpl(BuilderCompatImpl b) {
mClip = Preconditions.checkNotNull(b.mClip);
mSource = Preconditions.checkArgumentInRange(b.mSource, 0, SOURCE_PROCESS_TEXT,
"source");
mFlags = Preconditions.checkFlagsArgument(b.mFlags, FLAG_CONVERT_TO_PLAIN_TEXT);
mLinkUri = b.mLinkUri;
mExtras = b.mExtras;
}
@Nullable
@Override
public ContentInfo getWrapped() {
return null;
}
@NonNull
@Override
public ClipData getClip() {
return mClip;
}
@Source
@Override
public int getSource() {
return mSource;
}
@Flags
@Override
public int getFlags() {
return mFlags;
}
@Nullable
@Override
public Uri getLinkUri() {
return mLinkUri;
}
@Nullable
@Override
public Bundle getExtras() {
return mExtras;
}
@NonNull
@Override
public String toString() {
return "ContentInfoCompat{"
+ "clip=" + mClip.getDescription()
+ ", source=" + sourceToString(mSource)
+ ", flags=" + flagsToString(mFlags)
+ (mLinkUri == null ? "" : ", hasLinkUri(" + mLinkUri.toString().length() + ")")
+ (mExtras == null ? "" : ", hasExtras")
+ "}";
}
}
@RequiresApi(31)
private static final class Compat31Impl implements Compat {
@NonNull
private final ContentInfo mWrapped;
Compat31Impl(@NonNull ContentInfo wrapped) {
mWrapped = Preconditions.checkNotNull(wrapped);
}
@NonNull
@Override
public ContentInfo getWrapped() {
return mWrapped;
}
@NonNull
@Override
public ClipData getClip() {
return mWrapped.getClip();
}
@Source
@Override
public int getSource() {
return mWrapped.getSource();
}
@Flags
@Override
public int getFlags() {
return mWrapped.getFlags();
}
@Nullable
@Override
public Uri getLinkUri() {
return mWrapped.getLinkUri();
}
@Nullable
@Override
public Bundle getExtras() {
return mWrapped.getExtras();
}
@NonNull
@Override
public String toString() {
return "ContentInfoCompat{" + mWrapped + "}";
}
}
/**
* Builder for {@link ContentInfoCompat}.
*/
public static final class Builder {
@NonNull
private final BuilderCompat mBuilderCompat;
/**
* Creates a new builder initialized with the data from the given object (shallow copy).
*/
public Builder(@NonNull ContentInfoCompat other) {
if (Build.VERSION.SDK_INT >= 31) {
mBuilderCompat = new BuilderCompat31Impl(other);
} else {
mBuilderCompat = new BuilderCompatImpl(other);
}
}
/**
* Creates a new builder.
*
* @param clip The data to insert.
* @param source The source of the operation. See {@code SOURCE_} constants.
*/
public Builder(@NonNull ClipData clip, @Source int source) {
if (Build.VERSION.SDK_INT >= 31) {
mBuilderCompat = new BuilderCompat31Impl(clip, source);
} else {
mBuilderCompat = new BuilderCompatImpl(clip, source);
}
}
/**
* Sets the data to be inserted.
*
* @param clip The data to insert.
* @return this builder
*/
@NonNull
public Builder setClip(@NonNull ClipData clip) {
mBuilderCompat.setClip(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) {
mBuilderCompat.setSource(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.
* @return this builder
*/
@NonNull
public Builder setFlags(@Flags int flags) {
mBuilderCompat.setFlags(flags);
return this;
}
/**
* Sets the http/https URI for the content. See
* {@link android.view.inputmethod.InputContentInfo#getLinkUri} for more info.
*
* @param linkUri Optional http/https URI for the content.
* @return this builder
*/
@NonNull
public Builder setLinkUri(@Nullable Uri linkUri) {
mBuilderCompat.setLinkUri(linkUri);
return this;
}
/**
* Sets additional metadata.
*
* @param extras Optional bundle with additional metadata.
* @return this builder
*/
@NonNull
public Builder setExtras(@Nullable Bundle extras) {
mBuilderCompat.setExtras(extras);
return this;
}
/**
* @return A new {@link ContentInfoCompat} instance with the data from this builder.
*/
@NonNull
public ContentInfoCompat build() {
return mBuilderCompat.build();
}
}
private interface BuilderCompat {
void setClip(@NonNull ClipData clip);
void setSource(@Source int source);
void setFlags(@Flags int flags);
void setLinkUri(@Nullable Uri linkUri);
void setExtras(@Nullable Bundle extras);
@NonNull
ContentInfoCompat build();
}
private static final class BuilderCompatImpl implements BuilderCompat {
@NonNull
ClipData mClip;
@Source
int mSource;
@Flags
int mFlags;
@Nullable
Uri mLinkUri;
@Nullable
Bundle mExtras;
BuilderCompatImpl(@NonNull ClipData clip, int source) {
mClip = clip;
mSource = source;
}
BuilderCompatImpl(@NonNull ContentInfoCompat other) {
mClip = other.getClip();
mSource = other.getSource();
mFlags = other.getFlags();
mLinkUri = other.getLinkUri();
mExtras = other.getExtras();
}
@Override
public void setClip(@NonNull ClipData clip) {
mClip = clip;
}
@Override
public void setSource(@Source int source) {
mSource = source;
}
@Override
public void setFlags(@Flags int flags) {
mFlags = flags;
}
@Override
public void setLinkUri(@Nullable Uri linkUri) {
mLinkUri = linkUri;
}
@Override
public void setExtras(@Nullable Bundle extras) {
mExtras = extras;
}
@Override
@NonNull
public ContentInfoCompat build() {
return new ContentInfoCompat(new CompatImpl(this));
}
}
@RequiresApi(31)
private static final class BuilderCompat31Impl implements BuilderCompat {
@NonNull
private final ContentInfo.Builder mPlatformBuilder;
BuilderCompat31Impl(@NonNull ClipData clip, int source) {
mPlatformBuilder = new ContentInfo.Builder(clip, source);
}
BuilderCompat31Impl(@NonNull ContentInfoCompat other) {
mPlatformBuilder = new ContentInfo.Builder(other.toContentInfo());
}
@Override
public void setClip(@NonNull ClipData clip) {
mPlatformBuilder.setClip(clip);
}
@Override
public void setSource(@Source int source) {
mPlatformBuilder.setSource(source);
}
@Override
public void setFlags(@Flags int flags) {
mPlatformBuilder.setFlags(flags);
}
@Override
public void setLinkUri(@Nullable Uri linkUri) {
mPlatformBuilder.setLinkUri(linkUri);
}
@Override
public void setExtras(@Nullable Bundle extras) {
mPlatformBuilder.setExtras(extras);
}
@NonNull
@Override
public ContentInfoCompat build() {
return new ContentInfoCompat(new Compat31Impl(mPlatformBuilder.build()));
}
}
}