1 /* 2 * Copyright (C) 2020 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package android.view; 18 19 import android.annotation.IntDef; 20 import android.annotation.NonNull; 21 import android.annotation.Nullable; 22 import android.annotation.SuppressLint; 23 import android.annotation.TestApi; 24 import android.content.ClipData; 25 import android.content.ClipDescription; 26 import android.net.Uri; 27 import android.os.Bundle; 28 import android.os.Parcel; 29 import android.os.Parcelable; 30 import android.util.Pair; 31 import android.view.inputmethod.InputContentInfo; 32 33 import com.android.internal.util.Preconditions; 34 35 import java.lang.annotation.Retention; 36 import java.lang.annotation.RetentionPolicy; 37 import java.util.ArrayList; 38 import java.util.Objects; 39 import java.util.function.Predicate; 40 41 /** 42 * Holds all the relevant data for a request to {@link View#performReceiveContent}. 43 */ 44 public final class ContentInfo implements Parcelable { 45 46 /** 47 * Specifies the UI through which content is being inserted. Future versions of Android may 48 * support additional values. 49 * 50 * @hide 51 */ 52 @IntDef(prefix = {"SOURCE_"}, value = {SOURCE_APP, SOURCE_CLIPBOARD, SOURCE_INPUT_METHOD, 53 SOURCE_DRAG_AND_DROP, SOURCE_AUTOFILL, SOURCE_PROCESS_TEXT}) 54 @Retention(RetentionPolicy.SOURCE) 55 public @interface Source {} 56 57 /** 58 * Specifies that the operation was triggered by the app that contains the target view. 59 */ 60 public static final int SOURCE_APP = 0; 61 62 /** 63 * Specifies that the operation was triggered by a paste from the clipboard (e.g. "Paste" or 64 * "Paste as plain text" action in the insertion/selection menu). 65 */ 66 public static final int SOURCE_CLIPBOARD = 1; 67 68 /** 69 * Specifies that the operation was triggered from the soft keyboard (also known as input 70 * method editor or IME). See https://developer.android.com/guide/topics/text/image-keyboard 71 * for more info. 72 */ 73 public static final int SOURCE_INPUT_METHOD = 2; 74 75 /** 76 * Specifies that the operation was triggered by the drag/drop framework. See 77 * https://developer.android.com/guide/topics/ui/drag-drop for more info. 78 */ 79 public static final int SOURCE_DRAG_AND_DROP = 3; 80 81 /** 82 * Specifies that the operation was triggered by the autofill framework. See 83 * https://developer.android.com/guide/topics/text/autofill for more info. 84 */ 85 public static final int SOURCE_AUTOFILL = 4; 86 87 /** 88 * Specifies that the operation was triggered by a result from a 89 * {@link android.content.Intent#ACTION_PROCESS_TEXT PROCESS_TEXT} action in the selection 90 * menu. 91 */ 92 public static final int SOURCE_PROCESS_TEXT = 5; 93 94 /** 95 * Returns the symbolic name of the given source. 96 * 97 * @hide 98 */ sourceToString(@ource int source)99 static String sourceToString(@Source int source) { 100 switch (source) { 101 case SOURCE_APP: return "SOURCE_APP"; 102 case SOURCE_CLIPBOARD: return "SOURCE_CLIPBOARD"; 103 case SOURCE_INPUT_METHOD: return "SOURCE_INPUT_METHOD"; 104 case SOURCE_DRAG_AND_DROP: return "SOURCE_DRAG_AND_DROP"; 105 case SOURCE_AUTOFILL: return "SOURCE_AUTOFILL"; 106 case SOURCE_PROCESS_TEXT: return "SOURCE_PROCESS_TEXT"; 107 } 108 return String.valueOf(source); 109 } 110 111 /** 112 * Flags to configure the insertion behavior. 113 * 114 * @hide 115 */ 116 @IntDef(flag = true, prefix = {"FLAG_"}, value = {FLAG_CONVERT_TO_PLAIN_TEXT}) 117 @Retention(RetentionPolicy.SOURCE) 118 public @interface Flags {} 119 120 /** 121 * Flag requesting that the content should be converted to plain text prior to inserting. 122 */ 123 public static final int FLAG_CONVERT_TO_PLAIN_TEXT = 1 << 0; 124 125 /** 126 * Returns the symbolic names of the set flags or {@code "0"} if no flags are set. 127 * 128 * @hide 129 */ flagsToString(@lags int flags)130 static String flagsToString(@Flags int flags) { 131 if ((flags & FLAG_CONVERT_TO_PLAIN_TEXT) != 0) { 132 return "FLAG_CONVERT_TO_PLAIN_TEXT"; 133 } 134 return String.valueOf(flags); 135 } 136 137 @NonNull 138 private final ClipData mClip; 139 @Source 140 private final int mSource; 141 @Flags 142 private final int mFlags; 143 @Nullable 144 private final Uri mLinkUri; 145 @Nullable 146 private final Bundle mExtras; 147 @Nullable 148 private final InputContentInfo mInputContentInfo; 149 @Nullable 150 private final DragAndDropPermissions mDragAndDropPermissions; 151 ContentInfo(Builder b)152 private ContentInfo(Builder b) { 153 this.mClip = Objects.requireNonNull(b.mClip); 154 this.mSource = Preconditions.checkArgumentInRange(b.mSource, 0, SOURCE_PROCESS_TEXT, 155 "source"); 156 this.mFlags = Preconditions.checkFlagsArgument(b.mFlags, FLAG_CONVERT_TO_PLAIN_TEXT); 157 this.mLinkUri = b.mLinkUri; 158 this.mExtras = b.mExtras; 159 this.mInputContentInfo = b.mInputContentInfo; 160 this.mDragAndDropPermissions = b.mDragAndDropPermissions; 161 } 162 163 /** 164 * If the content came from a source that supports proactive release of URI permissions 165 * (e.g. IME), releases permissions; otherwise a no-op. 166 * 167 * @hide 168 */ 169 @TestApi releasePermissions()170 public void releasePermissions() { 171 if (mInputContentInfo != null) { 172 mInputContentInfo.releasePermission(); 173 } 174 if (mDragAndDropPermissions != null) { 175 mDragAndDropPermissions.release(); 176 } 177 } 178 179 @NonNull 180 @Override toString()181 public String toString() { 182 return "ContentInfo{" 183 + "clip=" + mClip 184 + ", source=" + sourceToString(mSource) 185 + ", flags=" + flagsToString(mFlags) 186 + ", linkUri=" + mLinkUri 187 + ", extras=" + mExtras 188 + "}"; 189 } 190 191 /** 192 * The data to be inserted. 193 */ 194 @NonNull getClip()195 public ClipData getClip() { 196 return mClip; 197 } 198 199 /** 200 * The source of the operation. See {@code SOURCE_} constants. Future versions of Android 201 * may pass additional values. 202 */ 203 @Source getSource()204 public int getSource() { 205 return mSource; 206 } 207 208 /** 209 * Optional flags that control the insertion behavior. See {@code FLAG_} constants. 210 */ 211 @Flags getFlags()212 public int getFlags() { 213 return mFlags; 214 } 215 216 /** 217 * Optional http/https URI for the content that may be provided by the IME. This is only 218 * populated if the source is {@link #SOURCE_INPUT_METHOD} and if a non-empty 219 * {@link android.view.inputmethod.InputContentInfo#getLinkUri linkUri} was passed by the 220 * IME. 221 */ 222 @Nullable getLinkUri()223 public Uri getLinkUri() { 224 return mLinkUri; 225 } 226 227 /** 228 * Optional additional metadata. If the source is {@link #SOURCE_INPUT_METHOD}, this will 229 * include the {@link android.view.inputmethod.InputConnection#commitContent opts} passed by 230 * the IME. 231 */ 232 @Nullable 233 @SuppressLint("NullableCollection") getExtras()234 public Bundle getExtras() { 235 return mExtras; 236 } 237 238 /** 239 * Partitions this content based on the given predicate. 240 * 241 * <p>This function classifies the content and organizes it into a pair, grouping the items 242 * that matched vs didn't match the predicate. 243 * 244 * <p>Except for the {@link ClipData} items, the returned objects will contain all the same 245 * metadata as this {@link ContentInfo}. 246 * 247 * @param itemPredicate The predicate to test each {@link ClipData.Item} to determine which 248 * partition to place it into. 249 * @return A pair containing the partitioned content. The pair's first object will have the 250 * content that matched the predicate, or null if none of the items matched. The pair's 251 * second object will have the content that didn't match the predicate, or null if all of 252 * the items matched. 253 * 254 * @hide 255 */ 256 @TestApi 257 @NonNull partition( @onNull Predicate<ClipData.Item> itemPredicate)258 public Pair<ContentInfo, ContentInfo> partition( 259 @NonNull Predicate<ClipData.Item> itemPredicate) { 260 if (mClip.getItemCount() == 1) { 261 boolean matched = itemPredicate.test(mClip.getItemAt(0)); 262 return Pair.create(matched ? this : null, matched ? null : this); 263 } 264 ArrayList<ClipData.Item> acceptedItems = new ArrayList<>(); 265 ArrayList<ClipData.Item> remainingItems = new ArrayList<>(); 266 for (int i = 0; i < mClip.getItemCount(); i++) { 267 ClipData.Item item = mClip.getItemAt(i); 268 if (itemPredicate.test(item)) { 269 acceptedItems.add(item); 270 } else { 271 remainingItems.add(item); 272 } 273 } 274 if (acceptedItems.isEmpty()) { 275 return Pair.create(null, this); 276 } 277 if (remainingItems.isEmpty()) { 278 return Pair.create(this, null); 279 } 280 ContentInfo accepted = new Builder(this) 281 .setClip(new ClipData(new ClipDescription(mClip.getDescription()), acceptedItems)) 282 .build(); 283 ContentInfo remaining = new Builder(this) 284 .setClip(new ClipData(new ClipDescription(mClip.getDescription()), remainingItems)) 285 .build(); 286 return Pair.create(accepted, remaining); 287 } 288 289 /** 290 * Builder for {@link ContentInfo}. 291 */ 292 public static final class Builder { 293 @NonNull 294 private ClipData mClip; 295 @Source 296 private int mSource; 297 @Flags 298 private int mFlags; 299 @Nullable 300 private Uri mLinkUri; 301 @Nullable 302 private Bundle mExtras; 303 @Nullable 304 private InputContentInfo mInputContentInfo; 305 @Nullable 306 private DragAndDropPermissions mDragAndDropPermissions; 307 308 /** 309 * Creates a new builder initialized with the data from the given builder. 310 */ Builder(@onNull ContentInfo other)311 public Builder(@NonNull ContentInfo other) { 312 mClip = other.mClip; 313 mSource = other.mSource; 314 mFlags = other.mFlags; 315 mLinkUri = other.mLinkUri; 316 mExtras = other.mExtras; 317 mInputContentInfo = other.mInputContentInfo; 318 mDragAndDropPermissions = other.mDragAndDropPermissions; 319 } 320 321 /** 322 * Creates a new builder. 323 * @param clip The data to insert. 324 * @param source The source of the operation. See {@code SOURCE_} constants. 325 */ Builder(@onNull ClipData clip, @Source int source)326 public Builder(@NonNull ClipData clip, @Source int source) { 327 mClip = clip; 328 mSource = source; 329 } 330 331 /** 332 * Sets the data to be inserted. 333 * @param clip The data to insert. 334 * @return this builder 335 */ 336 @NonNull setClip(@onNull ClipData clip)337 public Builder setClip(@NonNull ClipData clip) { 338 mClip = clip; 339 return this; 340 } 341 342 /** 343 * Sets the source of the operation. 344 * @param source The source of the operation. See {@code SOURCE_} constants. 345 * @return this builder 346 */ 347 @NonNull setSource(@ource int source)348 public Builder setSource(@Source int source) { 349 mSource = source; 350 return this; 351 } 352 353 /** 354 * Sets flags that control content insertion behavior. 355 * @param flags Optional flags to configure the insertion behavior. Use 0 for default 356 * behavior. See {@code FLAG_} constants. 357 * @return this builder 358 */ 359 @NonNull setFlags(@lags int flags)360 public Builder setFlags(@Flags int flags) { 361 mFlags = flags; 362 return this; 363 } 364 365 /** 366 * Sets the http/https URI for the content. See 367 * {@link android.view.inputmethod.InputContentInfo#getLinkUri} for more info. 368 * @param linkUri Optional http/https URI for the content. 369 * @return this builder 370 */ 371 @NonNull setLinkUri(@ullable Uri linkUri)372 public Builder setLinkUri(@Nullable Uri linkUri) { 373 mLinkUri = linkUri; 374 return this; 375 } 376 377 /** 378 * Sets additional metadata. 379 * @param extras Optional bundle with additional metadata. 380 * @return this builder 381 */ 382 @NonNull setExtras(@uppressLint"NullableCollection") @ullable Bundle extras)383 public Builder setExtras(@SuppressLint("NullableCollection") @Nullable Bundle extras) { 384 mExtras = extras; 385 return this; 386 } 387 388 /** 389 * Set the {@link InputContentInfo} object if the content is coming from the IME. This can 390 * be used for proactive cleanup of permissions. 391 * 392 * @hide 393 */ 394 @TestApi 395 @SuppressLint("MissingGetterMatchingBuilder") 396 @NonNull setInputContentInfo(@ullable InputContentInfo inputContentInfo)397 public Builder setInputContentInfo(@Nullable InputContentInfo inputContentInfo) { 398 mInputContentInfo = inputContentInfo; 399 return this; 400 } 401 402 /** 403 * Set the {@link DragAndDropPermissions} object if the content is coming via drag-and-drop. 404 * This can be used for proactive cleanup of permissions. 405 * 406 * @hide 407 */ 408 @TestApi 409 @SuppressLint("MissingGetterMatchingBuilder") 410 @NonNull setDragAndDropPermissions(@ullable DragAndDropPermissions permissions)411 public Builder setDragAndDropPermissions(@Nullable DragAndDropPermissions permissions) { 412 mDragAndDropPermissions = permissions; 413 return this; 414 } 415 416 417 /** 418 * @return A new {@link ContentInfo} instance with the data from this builder. 419 */ 420 @NonNull build()421 public ContentInfo build() { 422 return new ContentInfo(this); 423 } 424 } 425 426 /** 427 * {@inheritDoc} 428 */ 429 @Override describeContents()430 public int describeContents() { 431 return 0; 432 } 433 434 /** 435 * Writes this object into the given parcel. 436 * 437 * @param dest The parcel to write into. 438 * @param flags The flags to use for parceling. 439 */ 440 @Override writeToParcel(@onNull Parcel dest, int flags)441 public void writeToParcel(@NonNull Parcel dest, int flags) { 442 mClip.writeToParcel(dest, flags); 443 dest.writeInt(mSource); 444 dest.writeInt(mFlags); 445 Uri.writeToParcel(dest, mLinkUri); 446 dest.writeBundle(mExtras); 447 if (mInputContentInfo == null) { 448 dest.writeInt(0); 449 } else { 450 dest.writeInt(1); 451 mInputContentInfo.writeToParcel(dest, flags); 452 } 453 if (mDragAndDropPermissions == null) { 454 dest.writeInt(0); 455 } else { 456 dest.writeInt(1); 457 mDragAndDropPermissions.writeToParcel(dest, flags); 458 } 459 } 460 461 /** 462 * Creates {@link ContentInfo} instances from parcels. 463 */ 464 @NonNull 465 public static final Parcelable.Creator<ContentInfo> CREATOR = 466 new Parcelable.Creator<ContentInfo>() { 467 @Override 468 public ContentInfo createFromParcel(Parcel parcel) { 469 ClipData clip = ClipData.CREATOR.createFromParcel(parcel); 470 int source = parcel.readInt(); 471 int flags = parcel.readInt(); 472 Uri linkUri = Uri.CREATOR.createFromParcel(parcel); 473 Bundle extras = parcel.readBundle(); 474 InputContentInfo inputContentInfo = null; 475 if (parcel.readInt() != 0) { 476 inputContentInfo = InputContentInfo.CREATOR.createFromParcel(parcel); 477 } 478 DragAndDropPermissions dragAndDropPermissions = null; 479 if (parcel.readInt() != 0) { 480 dragAndDropPermissions = DragAndDropPermissions.CREATOR.createFromParcel(parcel); 481 } 482 return new ContentInfo.Builder(clip, source) 483 .setFlags(flags) 484 .setLinkUri(linkUri) 485 .setExtras(extras) 486 .setInputContentInfo(inputContentInfo) 487 .setDragAndDropPermissions(dragAndDropPermissions) 488 .build(); 489 } 490 491 @Override 492 public ContentInfo[] newArray(int size) { 493 return new ContentInfo[size]; 494 } 495 }; 496 } 497