497 lines
16 KiB
Java
497 lines
16 KiB
Java
/*
|
|
* Copyright (C) 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 android.view;
|
|
|
|
import android.annotation.IntDef;
|
|
import android.annotation.NonNull;
|
|
import android.annotation.Nullable;
|
|
import android.annotation.SuppressLint;
|
|
import android.annotation.TestApi;
|
|
import android.content.ClipData;
|
|
import android.content.ClipDescription;
|
|
import android.net.Uri;
|
|
import android.os.Bundle;
|
|
import android.os.Parcel;
|
|
import android.os.Parcelable;
|
|
import android.util.Pair;
|
|
import android.view.inputmethod.InputContentInfo;
|
|
|
|
import com.android.internal.util.Preconditions;
|
|
|
|
import java.lang.annotation.Retention;
|
|
import java.lang.annotation.RetentionPolicy;
|
|
import java.util.ArrayList;
|
|
import java.util.Objects;
|
|
import java.util.function.Predicate;
|
|
|
|
/**
|
|
* Holds all the relevant data for a request to {@link View#performReceiveContent}.
|
|
*/
|
|
public final class ContentInfo implements Parcelable {
|
|
|
|
/**
|
|
* Specifies the UI through which content is being inserted. Future versions of Android may
|
|
* support additional values.
|
|
*
|
|
* @hide
|
|
*/
|
|
@IntDef(prefix = {"SOURCE_"}, 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
|
|
*/
|
|
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
|
|
*/
|
|
@IntDef(flag = true, prefix = {"FLAG_"}, 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.
|
|
*/
|
|
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
|
|
*/
|
|
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 ClipData mClip;
|
|
@Source
|
|
private final int mSource;
|
|
@Flags
|
|
private final int mFlags;
|
|
@Nullable
|
|
private final Uri mLinkUri;
|
|
@Nullable
|
|
private final Bundle mExtras;
|
|
@Nullable
|
|
private final InputContentInfo mInputContentInfo;
|
|
@Nullable
|
|
private final DragAndDropPermissions mDragAndDropPermissions;
|
|
|
|
private ContentInfo(Builder b) {
|
|
this.mClip = Objects.requireNonNull(b.mClip);
|
|
this.mSource = Preconditions.checkArgumentInRange(b.mSource, 0, SOURCE_PROCESS_TEXT,
|
|
"source");
|
|
this.mFlags = Preconditions.checkFlagsArgument(b.mFlags, FLAG_CONVERT_TO_PLAIN_TEXT);
|
|
this.mLinkUri = b.mLinkUri;
|
|
this.mExtras = b.mExtras;
|
|
this.mInputContentInfo = b.mInputContentInfo;
|
|
this.mDragAndDropPermissions = b.mDragAndDropPermissions;
|
|
}
|
|
|
|
/**
|
|
* If the content came from a source that supports proactive release of URI permissions
|
|
* (e.g. IME), releases permissions; otherwise a no-op.
|
|
*
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
public void releasePermissions() {
|
|
if (mInputContentInfo != null) {
|
|
mInputContentInfo.releasePermission();
|
|
}
|
|
if (mDragAndDropPermissions != null) {
|
|
mDragAndDropPermissions.release();
|
|
}
|
|
}
|
|
|
|
@NonNull
|
|
@Override
|
|
public String toString() {
|
|
return "ContentInfo{"
|
|
+ "clip=" + mClip
|
|
+ ", source=" + sourceToString(mSource)
|
|
+ ", flags=" + flagsToString(mFlags)
|
|
+ ", linkUri=" + mLinkUri
|
|
+ ", extras=" + mExtras
|
|
+ "}";
|
|
}
|
|
|
|
/**
|
|
* The data to be inserted.
|
|
*/
|
|
@NonNull
|
|
public ClipData getClip() {
|
|
return mClip;
|
|
}
|
|
|
|
/**
|
|
* The source of the operation. See {@code SOURCE_} constants. Future versions of Android
|
|
* may pass additional values.
|
|
*/
|
|
@Source
|
|
public int getSource() {
|
|
return mSource;
|
|
}
|
|
|
|
/**
|
|
* Optional flags that control the insertion behavior. See {@code FLAG_} constants.
|
|
*/
|
|
@Flags
|
|
public int getFlags() {
|
|
return mFlags;
|
|
}
|
|
|
|
/**
|
|
* 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 mLinkUri;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
@SuppressLint("NullableCollection")
|
|
public Bundle getExtras() {
|
|
return mExtras;
|
|
}
|
|
|
|
/**
|
|
* 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 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.
|
|
*
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
@NonNull
|
|
public Pair<ContentInfo, ContentInfo> partition(
|
|
@NonNull Predicate<ClipData.Item> itemPredicate) {
|
|
if (mClip.getItemCount() == 1) {
|
|
boolean matched = itemPredicate.test(mClip.getItemAt(0));
|
|
return Pair.create(matched ? this : null, matched ? null : this);
|
|
}
|
|
ArrayList<ClipData.Item> acceptedItems = new ArrayList<>();
|
|
ArrayList<ClipData.Item> remainingItems = new ArrayList<>();
|
|
for (int i = 0; i < mClip.getItemCount(); i++) {
|
|
ClipData.Item item = mClip.getItemAt(i);
|
|
if (itemPredicate.test(item)) {
|
|
acceptedItems.add(item);
|
|
} else {
|
|
remainingItems.add(item);
|
|
}
|
|
}
|
|
if (acceptedItems.isEmpty()) {
|
|
return Pair.create(null, this);
|
|
}
|
|
if (remainingItems.isEmpty()) {
|
|
return Pair.create(this, null);
|
|
}
|
|
ContentInfo accepted = new Builder(this)
|
|
.setClip(new ClipData(new ClipDescription(mClip.getDescription()), acceptedItems))
|
|
.build();
|
|
ContentInfo remaining = new Builder(this)
|
|
.setClip(new ClipData(new ClipDescription(mClip.getDescription()), remainingItems))
|
|
.build();
|
|
return Pair.create(accepted, remaining);
|
|
}
|
|
|
|
/**
|
|
* Builder for {@link ContentInfo}.
|
|
*/
|
|
public static final class Builder {
|
|
@NonNull
|
|
private ClipData mClip;
|
|
@Source
|
|
private int mSource;
|
|
@Flags
|
|
private int mFlags;
|
|
@Nullable
|
|
private Uri mLinkUri;
|
|
@Nullable
|
|
private Bundle mExtras;
|
|
@Nullable
|
|
private InputContentInfo mInputContentInfo;
|
|
@Nullable
|
|
private DragAndDropPermissions mDragAndDropPermissions;
|
|
|
|
/**
|
|
* Creates a new builder initialized with the data from the given builder.
|
|
*/
|
|
public Builder(@NonNull ContentInfo other) {
|
|
mClip = other.mClip;
|
|
mSource = other.mSource;
|
|
mFlags = other.mFlags;
|
|
mLinkUri = other.mLinkUri;
|
|
mExtras = other.mExtras;
|
|
mInputContentInfo = other.mInputContentInfo;
|
|
mDragAndDropPermissions = other.mDragAndDropPermissions;
|
|
}
|
|
|
|
/**
|
|
* 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) {
|
|
mClip = clip;
|
|
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
|
|
* behavior. See {@code FLAG_} constants.
|
|
* @return this builder
|
|
*/
|
|
@NonNull
|
|
public Builder setFlags(@Flags int flags) {
|
|
mFlags = 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) {
|
|
mLinkUri = linkUri;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Sets additional metadata.
|
|
* @param extras Optional bundle with additional metadata.
|
|
* @return this builder
|
|
*/
|
|
@NonNull
|
|
public Builder setExtras(@SuppressLint("NullableCollection") @Nullable Bundle extras) {
|
|
mExtras = extras;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set the {@link InputContentInfo} object if the content is coming from the IME. This can
|
|
* be used for proactive cleanup of permissions.
|
|
*
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
@SuppressLint("MissingGetterMatchingBuilder")
|
|
@NonNull
|
|
public Builder setInputContentInfo(@Nullable InputContentInfo inputContentInfo) {
|
|
mInputContentInfo = inputContentInfo;
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Set the {@link DragAndDropPermissions} object if the content is coming via drag-and-drop.
|
|
* This can be used for proactive cleanup of permissions.
|
|
*
|
|
* @hide
|
|
*/
|
|
@TestApi
|
|
@SuppressLint("MissingGetterMatchingBuilder")
|
|
@NonNull
|
|
public Builder setDragAndDropPermissions(@Nullable DragAndDropPermissions permissions) {
|
|
mDragAndDropPermissions = permissions;
|
|
return this;
|
|
}
|
|
|
|
|
|
/**
|
|
* @return A new {@link ContentInfo} instance with the data from this builder.
|
|
*/
|
|
@NonNull
|
|
public ContentInfo build() {
|
|
return new ContentInfo(this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* {@inheritDoc}
|
|
*/
|
|
@Override
|
|
public int describeContents() {
|
|
return 0;
|
|
}
|
|
|
|
/**
|
|
* Writes this object into the given parcel.
|
|
*
|
|
* @param dest The parcel to write into.
|
|
* @param flags The flags to use for parceling.
|
|
*/
|
|
@Override
|
|
public void writeToParcel(@NonNull Parcel dest, int flags) {
|
|
mClip.writeToParcel(dest, flags);
|
|
dest.writeInt(mSource);
|
|
dest.writeInt(mFlags);
|
|
Uri.writeToParcel(dest, mLinkUri);
|
|
dest.writeBundle(mExtras);
|
|
if (mInputContentInfo == null) {
|
|
dest.writeInt(0);
|
|
} else {
|
|
dest.writeInt(1);
|
|
mInputContentInfo.writeToParcel(dest, flags);
|
|
}
|
|
if (mDragAndDropPermissions == null) {
|
|
dest.writeInt(0);
|
|
} else {
|
|
dest.writeInt(1);
|
|
mDragAndDropPermissions.writeToParcel(dest, flags);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Creates {@link ContentInfo} instances from parcels.
|
|
*/
|
|
@NonNull
|
|
public static final Parcelable.Creator<ContentInfo> CREATOR =
|
|
new Parcelable.Creator<ContentInfo>() {
|
|
@Override
|
|
public ContentInfo createFromParcel(Parcel parcel) {
|
|
ClipData clip = ClipData.CREATOR.createFromParcel(parcel);
|
|
int source = parcel.readInt();
|
|
int flags = parcel.readInt();
|
|
Uri linkUri = Uri.CREATOR.createFromParcel(parcel);
|
|
Bundle extras = parcel.readBundle();
|
|
InputContentInfo inputContentInfo = null;
|
|
if (parcel.readInt() != 0) {
|
|
inputContentInfo = InputContentInfo.CREATOR.createFromParcel(parcel);
|
|
}
|
|
DragAndDropPermissions dragAndDropPermissions = null;
|
|
if (parcel.readInt() != 0) {
|
|
dragAndDropPermissions = DragAndDropPermissions.CREATOR.createFromParcel(parcel);
|
|
}
|
|
return new ContentInfo.Builder(clip, source)
|
|
.setFlags(flags)
|
|
.setLinkUri(linkUri)
|
|
.setExtras(extras)
|
|
.setInputContentInfo(inputContentInfo)
|
|
.setDragAndDropPermissions(dragAndDropPermissions)
|
|
.build();
|
|
}
|
|
|
|
@Override
|
|
public ContentInfo[] newArray(int size) {
|
|
return new ContentInfo[size];
|
|
}
|
|
};
|
|
}
|