1 /** 2 * Copyright (c) 2010, 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.content; 18 19 import android.annotation.FloatRange; 20 import android.annotation.IntDef; 21 import android.annotation.NonNull; 22 import android.os.Bundle; 23 import android.os.Parcel; 24 import android.os.Parcelable; 25 import android.os.PersistableBundle; 26 import android.text.TextUtils; 27 import android.util.ArrayMap; 28 import android.util.TimeUtils; 29 import android.util.proto.ProtoOutputStream; 30 import android.view.textclassifier.TextClassifier; 31 import android.view.textclassifier.TextLinks; 32 33 import java.lang.annotation.Retention; 34 import java.lang.annotation.RetentionPolicy; 35 import java.util.ArrayList; 36 import java.util.Arrays; 37 import java.util.Map; 38 39 /** 40 * Meta-data describing the contents of a {@link ClipData}. Provides enough 41 * information to know if you can handle the ClipData, but not the data 42 * itself. 43 * 44 * <div class="special reference"> 45 * <h3>Developer Guides</h3> 46 * <p>For more information about using the clipboard framework, read the 47 * <a href="{@docRoot}guide/topics/clipboard/copy-paste.html">Copy and Paste</a> 48 * developer guide.</p> 49 * </div> 50 */ 51 public class ClipDescription implements Parcelable { 52 /** 53 * The MIME type for a clip holding plain text. 54 */ 55 public static final String MIMETYPE_TEXT_PLAIN = "text/plain"; 56 57 /** 58 * The MIME type for a clip holding HTML text. 59 */ 60 public static final String MIMETYPE_TEXT_HTML = "text/html"; 61 62 /** 63 * The MIME type for a clip holding one or more URIs. This should be 64 * used for URIs that are meaningful to a user (such as an http: URI). 65 * It should <em>not</em> be used for a content: URI that references some 66 * other piece of data; in that case the MIME type should be the type 67 * of the referenced data. 68 */ 69 public static final String MIMETYPE_TEXT_URILIST = "text/uri-list"; 70 71 /** 72 * The MIME type for a clip holding an Intent. 73 */ 74 public static final String MIMETYPE_TEXT_INTENT = "text/vnd.android.intent"; 75 76 /** 77 * The MIME type for an activity. The ClipData must include intents with required extras 78 * {@link #EXTRA_PENDING_INTENT} and {@link Intent#EXTRA_USER}, and an optional 79 * {@link #EXTRA_ACTIVITY_OPTIONS}. 80 * @hide 81 */ 82 public static final String MIMETYPE_APPLICATION_ACTIVITY = "application/vnd.android.activity"; 83 84 /** 85 * The MIME type for a shortcut. The ClipData must include intents with required extras 86 * {@link Intent#EXTRA_SHORTCUT_ID}, {@link Intent#EXTRA_PACKAGE_NAME} and 87 * {@link Intent#EXTRA_USER}, and an optional {@link #EXTRA_ACTIVITY_OPTIONS}. 88 * @hide 89 */ 90 public static final String MIMETYPE_APPLICATION_SHORTCUT = "application/vnd.android.shortcut"; 91 92 /** 93 * The MIME type for a task. The ClipData must include an intent with a required extra 94 * {@link Intent#EXTRA_TASK_ID} of the task to launch. 95 * @hide 96 */ 97 public static final String MIMETYPE_APPLICATION_TASK = "application/vnd.android.task"; 98 99 /** 100 * The MIME type for data whose type is otherwise unknown. 101 * <p> 102 * Per RFC 2046, the "application" media type is to be used for discrete 103 * data which do not fit in any of the other categories, and the 104 * "octet-stream" subtype is used to indicate that a body contains arbitrary 105 * binary data. 106 */ 107 public static final String MIMETYPE_UNKNOWN = "application/octet-stream"; 108 109 /** 110 * The pending intent for the activity to launch. 111 * <p> 112 * Type: PendingIntent 113 * </p> 114 * @hide 115 */ 116 public static final String EXTRA_PENDING_INTENT = "android.intent.extra.PENDING_INTENT"; 117 118 /** 119 * The activity options bundle to use when launching an activity. 120 * <p> 121 * Type: Bundle 122 * </p> 123 * @hide 124 */ 125 public static final String EXTRA_ACTIVITY_OPTIONS = "android.intent.extra.ACTIVITY_OPTIONS"; 126 127 /** @hide */ 128 @Retention(RetentionPolicy.SOURCE) 129 @IntDef(value = 130 { CLASSIFICATION_NOT_COMPLETE, CLASSIFICATION_NOT_PERFORMED, CLASSIFICATION_COMPLETE}) 131 @interface ClassificationStatus {} 132 133 /** 134 * Value returned by {@link #getConfidenceScore(String)} if text classification has not been 135 * completed on the associated clip. This will be always be the case if the clip has not been 136 * copied to clipboard, or if there is no associated clip. 137 */ 138 public static final int CLASSIFICATION_NOT_COMPLETE = 1; 139 140 /** 141 * Value returned by {@link #getConfidenceScore(String)} if text classification was not and will 142 * not be performed on the associated clip. This may be the case if the clip does not contain 143 * text in its first item, or if the text is too long. 144 */ 145 public static final int CLASSIFICATION_NOT_PERFORMED = 2; 146 147 /** 148 * Value returned by {@link #getConfidenceScore(String)} if text classification has been 149 * completed. 150 */ 151 public static final int CLASSIFICATION_COMPLETE = 3; 152 153 final CharSequence mLabel; 154 private final ArrayList<String> mMimeTypes; 155 private PersistableBundle mExtras; 156 private long mTimeStamp; 157 private boolean mIsStyledText; 158 private final ArrayMap<String, Float> mEntityConfidence = new ArrayMap<>(); 159 private int mClassificationStatus = CLASSIFICATION_NOT_COMPLETE; 160 161 /** 162 * Create a new clip. 163 * 164 * @param label Label to show to the user describing this clip. 165 * @param mimeTypes An array of MIME types this data is available as. 166 */ ClipDescription(CharSequence label, String[] mimeTypes)167 public ClipDescription(CharSequence label, String[] mimeTypes) { 168 if (mimeTypes == null) { 169 throw new NullPointerException("mimeTypes is null"); 170 } 171 mLabel = label; 172 mMimeTypes = new ArrayList<String>(Arrays.asList(mimeTypes)); 173 } 174 175 /** 176 * Create a copy of a ClipDescription. 177 */ ClipDescription(ClipDescription o)178 public ClipDescription(ClipDescription o) { 179 mLabel = o.mLabel; 180 mMimeTypes = new ArrayList<String>(o.mMimeTypes); 181 mTimeStamp = o.mTimeStamp; 182 } 183 184 /** 185 * Helper to compare two MIME types, where one may be a pattern. 186 * @param concreteType A fully-specified MIME type. 187 * @param desiredType A desired MIME type that may be a pattern such as */*. 188 * @return Returns true if the two MIME types match. 189 */ compareMimeTypes(String concreteType, String desiredType)190 public static boolean compareMimeTypes(String concreteType, String desiredType) { 191 final int typeLength = desiredType.length(); 192 if (typeLength == 3 && desiredType.equals("*/*")) { 193 return true; 194 } 195 196 final int slashpos = desiredType.indexOf('/'); 197 if (slashpos > 0) { 198 if (typeLength == slashpos+2 && desiredType.charAt(slashpos+1) == '*') { 199 if (desiredType.regionMatches(0, concreteType, 0, slashpos+1)) { 200 return true; 201 } 202 } else if (desiredType.equals(concreteType)) { 203 return true; 204 } 205 } 206 207 return false; 208 } 209 210 /** 211 * Used for setting the timestamp at which the associated {@link ClipData} is copied to 212 * global clipboard. 213 * 214 * @param timeStamp at which the associated {@link ClipData} is copied to clipboard in 215 * {@link System#currentTimeMillis()} time base. 216 * @hide 217 */ setTimestamp(long timeStamp)218 public void setTimestamp(long timeStamp) { 219 mTimeStamp = timeStamp; 220 } 221 222 /** 223 * Return the timestamp at which the associated {@link ClipData} is copied to global clipboard 224 * in the {@link System#currentTimeMillis()} time base. 225 * 226 * @return timestamp at which the associated {@link ClipData} is copied to global clipboard 227 * or {@code 0} if it is not copied to clipboard. 228 */ getTimestamp()229 public long getTimestamp() { 230 return mTimeStamp; 231 } 232 233 /** 234 * Return the label for this clip. 235 */ getLabel()236 public CharSequence getLabel() { 237 return mLabel; 238 } 239 240 /** 241 * Check whether the clip description contains the given MIME type. 242 * 243 * @param mimeType The desired MIME type. May be a pattern. 244 * @return Returns true if one of the MIME types in the clip description 245 * matches the desired MIME type, else false. 246 */ hasMimeType(String mimeType)247 public boolean hasMimeType(String mimeType) { 248 final int size = mMimeTypes.size(); 249 for (int i=0; i<size; i++) { 250 if (compareMimeTypes(mMimeTypes.get(i), mimeType)) { 251 return true; 252 } 253 } 254 return false; 255 } 256 257 /** 258 * Check whether the clip description contains any of the given MIME types. 259 * 260 * @param targetMimeTypes The target MIME types. May use patterns. 261 * @return Returns true if at least one of the MIME types in the clip description matches at 262 * least one of the target MIME types, else false. 263 * 264 * @hide 265 */ hasMimeType(@onNull String[] targetMimeTypes)266 public boolean hasMimeType(@NonNull String[] targetMimeTypes) { 267 for (String targetMimeType : targetMimeTypes) { 268 if (hasMimeType(targetMimeType)) { 269 return true; 270 } 271 } 272 return false; 273 } 274 275 /** 276 * Filter the clip description MIME types by the given MIME type. Returns 277 * all MIME types in the clip that match the given MIME type. 278 * 279 * @param mimeType The desired MIME type. May be a pattern. 280 * @return Returns an array of all matching MIME types. If there are no 281 * matching MIME types, null is returned. 282 */ filterMimeTypes(String mimeType)283 public String[] filterMimeTypes(String mimeType) { 284 ArrayList<String> array = null; 285 final int size = mMimeTypes.size(); 286 for (int i=0; i<size; i++) { 287 if (compareMimeTypes(mMimeTypes.get(i), mimeType)) { 288 if (array == null) { 289 array = new ArrayList<String>(); 290 } 291 array.add(mMimeTypes.get(i)); 292 } 293 } 294 if (array == null) { 295 return null; 296 } 297 String[] rawArray = new String[array.size()]; 298 array.toArray(rawArray); 299 return rawArray; 300 } 301 302 /** 303 * Return the number of MIME types the clip is available in. 304 */ getMimeTypeCount()305 public int getMimeTypeCount() { 306 return mMimeTypes.size(); 307 } 308 309 /** 310 * Return one of the possible clip MIME types. 311 */ getMimeType(int index)312 public String getMimeType(int index) { 313 return mMimeTypes.get(index); 314 } 315 316 /** 317 * Add MIME types to the clip description. 318 */ addMimeTypes(String[] mimeTypes)319 void addMimeTypes(String[] mimeTypes) { 320 for (int i=0; i!=mimeTypes.length; i++) { 321 final String mimeType = mimeTypes[i]; 322 if (!mMimeTypes.contains(mimeType)) { 323 mMimeTypes.add(mimeType); 324 } 325 } 326 } 327 328 /** 329 * Retrieve extended data from the clip description. 330 * 331 * @return the bundle containing extended data previously set with 332 * {@link #setExtras(PersistableBundle)}, or null if no extras have been set. 333 * 334 * @see #setExtras(PersistableBundle) 335 */ getExtras()336 public PersistableBundle getExtras() { 337 return mExtras; 338 } 339 340 /** 341 * Add extended data to the clip description. 342 * 343 * @see #getExtras() 344 */ setExtras(PersistableBundle extras)345 public void setExtras(PersistableBundle extras) { 346 mExtras = new PersistableBundle(extras); 347 } 348 349 /** @hide */ validate()350 public void validate() { 351 if (mMimeTypes == null) { 352 throw new NullPointerException("null mime types"); 353 } 354 final int size = mMimeTypes.size(); 355 if (size <= 0) { 356 throw new IllegalArgumentException("must have at least 1 mime type"); 357 } 358 for (int i=0; i<size; i++) { 359 if (mMimeTypes.get(i) == null) { 360 throw new NullPointerException("mime type at " + i + " is null"); 361 } 362 } 363 } 364 365 /** 366 * Returns true if the first item of the associated {@link ClipData} contains styled text, i.e. 367 * if it contains spans such as {@link android.text.style.CharacterStyle CharacterStyle}, {@link 368 * android.text.style.ParagraphStyle ParagraphStyle}, or {@link 369 * android.text.style.UpdateAppearance UpdateAppearance}. Returns false if it does not, or if 370 * there is no associated clip data. 371 */ isStyledText()372 public boolean isStyledText() { 373 return mIsStyledText; 374 } 375 376 /** 377 * Sets whether the associated {@link ClipData} contains styled text in its first item. This 378 * should be called when this description is associated with clip data or when the first item 379 * is added to the associated clip data. 380 */ setIsStyledText(boolean isStyledText)381 void setIsStyledText(boolean isStyledText) { 382 mIsStyledText = isStyledText; 383 } 384 385 /** 386 * Sets the current status of text classification for the associated clip. 387 * 388 * @hide 389 */ setClassificationStatus(@lassificationStatus int status)390 public void setClassificationStatus(@ClassificationStatus int status) { 391 mClassificationStatus = status; 392 } 393 394 /** 395 * Returns a score indicating confidence that an instance of the given entity is present in the 396 * first item of the clip data, if that item is plain text and text classification has been 397 * performed. The value ranges from 0 (low confidence) to 1 (high confidence). 0 indicates that 398 * the entity was not found in the classified text. 399 * 400 * <p>Entities should be as defined in the {@link TextClassifier} class, such as 401 * {@link TextClassifier#TYPE_ADDRESS}, {@link TextClassifier#TYPE_URL}, or 402 * {@link TextClassifier#TYPE_EMAIL}. 403 * 404 * <p>If the result is positive for any entity, the full classification result as a 405 * {@link TextLinks} object may be obtained using the {@link ClipData.Item#getTextLinks()} 406 * method. 407 * 408 * @throws IllegalStateException if {@link #getClassificationStatus()} is not 409 * {@link #CLASSIFICATION_COMPLETE} 410 */ 411 @FloatRange(from = 0.0, to = 1.0) getConfidenceScore(@onNull @extClassifier.EntityType String entity)412 public float getConfidenceScore(@NonNull @TextClassifier.EntityType String entity) { 413 if (mClassificationStatus != CLASSIFICATION_COMPLETE) { 414 throw new IllegalStateException("Classification not complete"); 415 } 416 return mEntityConfidence.getOrDefault(entity, 0f); 417 } 418 419 /** 420 * Returns {@link #CLASSIFICATION_COMPLETE} if text classification has been performed on the 421 * associated {@link ClipData}. If this is the case then {@link #getConfidenceScore} may be used 422 * to retrieve information about entities within the text. Otherwise, returns 423 * {@link #CLASSIFICATION_NOT_COMPLETE} if classification has not yet returned results, or 424 * {@link #CLASSIFICATION_NOT_PERFORMED} if classification was not attempted (e.g. because the 425 * text was too long). 426 */ getClassificationStatus()427 public @ClassificationStatus int getClassificationStatus() { 428 return mClassificationStatus; 429 } 430 431 /** 432 * @hide 433 */ setConfidenceScores(Map<String, Float> confidences)434 public void setConfidenceScores(Map<String, Float> confidences) { 435 mEntityConfidence.clear(); 436 mEntityConfidence.putAll(confidences); 437 mClassificationStatus = CLASSIFICATION_COMPLETE; 438 } 439 440 @Override toString()441 public String toString() { 442 StringBuilder b = new StringBuilder(128); 443 444 b.append("ClipDescription { "); 445 toShortString(b, true); 446 b.append(" }"); 447 448 return b.toString(); 449 } 450 451 /** 452 * Appends this description to the given builder. 453 * @param redactContent If true, redacts common forms of PII; otherwise appends full details. 454 * @hide 455 */ toShortString(StringBuilder b, boolean redactContent)456 public boolean toShortString(StringBuilder b, boolean redactContent) { 457 boolean first = !toShortStringTypesOnly(b); 458 if (mLabel != null) { 459 if (!first) { 460 b.append(' '); 461 } 462 first = false; 463 if (redactContent) { 464 b.append("hasLabel(").append(mLabel.length()).append(')'); 465 } else { 466 b.append('"').append(mLabel).append('"'); 467 } 468 } 469 if (mExtras != null) { 470 if (!first) { 471 b.append(' '); 472 } 473 first = false; 474 if (redactContent) { 475 if (mExtras.isParcelled()) { 476 // We don't want this toString function to trigger un-parcelling. 477 b.append("hasExtras"); 478 } else { 479 b.append("hasExtras(").append(mExtras.size()).append(')'); 480 } 481 } else { 482 b.append(mExtras.toString()); 483 } 484 } 485 if (mTimeStamp > 0) { 486 if (!first) { 487 b.append(' '); 488 } 489 first = false; 490 b.append('<'); 491 b.append(TimeUtils.logTimeOfDay(mTimeStamp)); 492 b.append('>'); 493 } 494 return !first; 495 } 496 497 /** @hide */ toShortStringTypesOnly(StringBuilder b)498 public boolean toShortStringTypesOnly(StringBuilder b) { 499 boolean first = true; 500 final int size = mMimeTypes.size(); 501 for (int i=0; i<size; i++) { 502 if (!first) { 503 b.append(' '); 504 } 505 first = false; 506 b.append(mMimeTypes.get(i)); 507 } 508 return !first; 509 } 510 511 /** @hide */ dumpDebug(ProtoOutputStream proto, long fieldId)512 public void dumpDebug(ProtoOutputStream proto, long fieldId) { 513 final long token = proto.start(fieldId); 514 515 final int size = mMimeTypes.size(); 516 for (int i = 0; i < size; i++) { 517 proto.write(ClipDescriptionProto.MIME_TYPES, mMimeTypes.get(i)); 518 } 519 520 if (mLabel != null) { 521 proto.write(ClipDescriptionProto.LABEL, mLabel.toString()); 522 } 523 if (mExtras != null) { 524 mExtras.dumpDebug(proto, ClipDescriptionProto.EXTRAS); 525 } 526 if (mTimeStamp > 0) { 527 proto.write(ClipDescriptionProto.TIMESTAMP_MS, mTimeStamp); 528 } 529 530 proto.end(token); 531 } 532 533 @Override describeContents()534 public int describeContents() { 535 return 0; 536 } 537 538 @Override writeToParcel(Parcel dest, int flags)539 public void writeToParcel(Parcel dest, int flags) { 540 TextUtils.writeToParcel(mLabel, dest, flags); 541 dest.writeStringList(mMimeTypes); 542 dest.writePersistableBundle(mExtras); 543 dest.writeLong(mTimeStamp); 544 dest.writeBoolean(mIsStyledText); 545 dest.writeInt(mClassificationStatus); 546 dest.writeBundle(confidencesToBundle()); 547 } 548 confidencesToBundle()549 private Bundle confidencesToBundle() { 550 Bundle bundle = new Bundle(); 551 int size = mEntityConfidence.size(); 552 for (int i = 0; i < size; i++) { 553 bundle.putFloat(mEntityConfidence.keyAt(i), mEntityConfidence.valueAt(i)); 554 } 555 return bundle; 556 } 557 readBundleToConfidences(Bundle bundle)558 private void readBundleToConfidences(Bundle bundle) { 559 for (String key : bundle.keySet()) { 560 mEntityConfidence.put(key, bundle.getFloat(key)); 561 } 562 } 563 ClipDescription(Parcel in)564 ClipDescription(Parcel in) { 565 mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in); 566 mMimeTypes = in.createStringArrayList(); 567 mExtras = in.readPersistableBundle(); 568 mTimeStamp = in.readLong(); 569 mIsStyledText = in.readBoolean(); 570 mClassificationStatus = in.readInt(); 571 readBundleToConfidences(in.readBundle()); 572 } 573 574 public static final @android.annotation.NonNull Parcelable.Creator<ClipDescription> CREATOR = 575 new Parcelable.Creator<ClipDescription>() { 576 577 public ClipDescription createFromParcel(Parcel source) { 578 return new ClipDescription(source); 579 } 580 581 public ClipDescription[] newArray(int size) { 582 return new ClipDescription[size]; 583 } 584 }; 585 } 586