TextClassification.java revision 6563833cf3c79e8cd211e32357422ae899674437
1/* 2 * Copyright (C) 2017 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 17package android.view.textclassifier; 18 19import android.annotation.FloatRange; 20import android.annotation.IntDef; 21import android.annotation.IntRange; 22import android.annotation.NonNull; 23import android.annotation.Nullable; 24import android.content.Context; 25import android.content.Intent; 26import android.content.pm.PackageManager; 27import android.content.pm.ResolveInfo; 28import android.content.res.Resources; 29import android.graphics.Bitmap; 30import android.graphics.Canvas; 31import android.graphics.drawable.BitmapDrawable; 32import android.graphics.drawable.Drawable; 33import android.os.LocaleList; 34import android.os.Parcel; 35import android.os.Parcelable; 36import android.util.ArrayMap; 37import android.view.View.OnClickListener; 38import android.view.textclassifier.TextClassifier.EntityType; 39 40import com.android.internal.util.Preconditions; 41 42import java.lang.annotation.Retention; 43import java.lang.annotation.RetentionPolicy; 44import java.util.ArrayList; 45import java.util.Calendar; 46import java.util.List; 47import java.util.Locale; 48import java.util.Map; 49 50/** 51 * Information for generating a widget to handle classified text. 52 * 53 * <p>A TextClassification object contains icons, labels, onClickListeners and intents that may 54 * be used to build a widget that can be used to act on classified text. There is the concept of a 55 * <i>primary action</i> and other <i>secondary actions</i>. 56 * 57 * <p>e.g. building a view that, when clicked, shares the classified text with the preferred app: 58 * 59 * <pre>{@code 60 * // Called preferably outside the UiThread. 61 * TextClassification classification = textClassifier.classifyText(allText, 10, 25); 62 * 63 * // Called on the UiThread. 64 * Button button = new Button(context); 65 * button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null); 66 * button.setText(classification.getLabel()); 67 * button.setOnClickListener(v -> context.startActivity(classification.getIntent())); 68 * }</pre> 69 * 70 * <p>e.g. starting an action mode with menu items that can handle the classified text: 71 * 72 * <pre>{@code 73 * // Called preferably outside the UiThread. 74 * final TextClassification classification = textClassifier.classifyText(allText, 10, 25); 75 * 76 * // Called on the UiThread. 77 * view.startActionMode(new ActionMode.Callback() { 78 * 79 * public boolean onCreateActionMode(ActionMode mode, Menu menu) { 80 * // Add the "primary" action. 81 * if (thisAppHasPermissionToInvokeIntent(classification.getIntent())) { 82 * menu.add(Menu.NONE, 0, 20, classification.getLabel()) 83 * .setIcon(classification.getIcon()) 84 * .setIntent(classification.getIntent()); 85 * } 86 * // Add the "secondary" actions. 87 * for (int i = 0; i < classification.getSecondaryActionsCount(); i++) { 88 * if (thisAppHasPermissionToInvokeIntent(classification.getSecondaryIntent(i))) { 89 * menu.add(Menu.NONE, i + 1, 20, classification.getSecondaryLabel(i)) 90 * .setIcon(classification.getSecondaryIcon(i)) 91 * .setIntent(classification.getSecondaryIntent(i)); 92 * } 93 * } 94 * return true; 95 * } 96 * 97 * public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 98 * context.startActivity(item.getIntent()); 99 * return true; 100 * } 101 * 102 * ... 103 * }); 104 * }</pre> 105 */ 106public final class TextClassification implements Parcelable { 107 108 /** 109 * @hide 110 */ 111 static final TextClassification EMPTY = new TextClassification.Builder().build(); 112 113 // TODO(toki): investigate a way to derive this based on device properties. 114 private static final int MAX_PRIMARY_ICON_SIZE = 192; 115 private static final int MAX_SECONDARY_ICON_SIZE = 144; 116 117 @Retention(RetentionPolicy.SOURCE) 118 @IntDef(value = {IntentType.UNSUPPORTED, IntentType.ACTIVITY, IntentType.SERVICE}) 119 private @interface IntentType { 120 int UNSUPPORTED = -1; 121 int ACTIVITY = 0; 122 int SERVICE = 1; 123 } 124 125 @NonNull private final String mText; 126 @Nullable private final Drawable mPrimaryIcon; 127 @Nullable private final String mPrimaryLabel; 128 @Nullable private final Intent mPrimaryIntent; 129 @Nullable private final OnClickListener mPrimaryOnClickListener; 130 @NonNull private final List<Drawable> mSecondaryIcons; 131 @NonNull private final List<String> mSecondaryLabels; 132 @NonNull private final List<Intent> mSecondaryIntents; 133 @NonNull private final EntityConfidence mEntityConfidence; 134 @NonNull private final String mSignature; 135 136 private TextClassification( 137 @Nullable String text, 138 @Nullable Drawable primaryIcon, 139 @Nullable String primaryLabel, 140 @Nullable Intent primaryIntent, 141 @Nullable OnClickListener primaryOnClickListener, 142 @NonNull List<Drawable> secondaryIcons, 143 @NonNull List<String> secondaryLabels, 144 @NonNull List<Intent> secondaryIntents, 145 @NonNull Map<String, Float> entityConfidence, 146 @NonNull String signature) { 147 Preconditions.checkArgument(secondaryLabels.size() == secondaryIntents.size()); 148 Preconditions.checkArgument(secondaryIcons.size() == secondaryIntents.size()); 149 mText = text; 150 mPrimaryIcon = primaryIcon; 151 mPrimaryLabel = primaryLabel; 152 mPrimaryIntent = primaryIntent; 153 mPrimaryOnClickListener = primaryOnClickListener; 154 mSecondaryIcons = secondaryIcons; 155 mSecondaryLabels = secondaryLabels; 156 mSecondaryIntents = secondaryIntents; 157 mEntityConfidence = new EntityConfidence(entityConfidence); 158 mSignature = signature; 159 } 160 161 /** 162 * Gets the classified text. 163 */ 164 @Nullable 165 public String getText() { 166 return mText; 167 } 168 169 /** 170 * Returns the number of entities found in the classified text. 171 */ 172 @IntRange(from = 0) 173 public int getEntityCount() { 174 return mEntityConfidence.getEntities().size(); 175 } 176 177 /** 178 * Returns the entity at the specified index. Entities are ordered from high confidence 179 * to low confidence. 180 * 181 * @throws IndexOutOfBoundsException if the specified index is out of range. 182 * @see #getEntityCount() for the number of entities available. 183 */ 184 @NonNull 185 public @EntityType String getEntity(int index) { 186 return mEntityConfidence.getEntities().get(index); 187 } 188 189 /** 190 * Returns the confidence score for the specified entity. The value ranges from 191 * 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the 192 * classified text. 193 */ 194 @FloatRange(from = 0.0, to = 1.0) 195 public float getConfidenceScore(@EntityType String entity) { 196 return mEntityConfidence.getConfidenceScore(entity); 197 } 198 199 /** 200 * Returns the number of <i>secondary</i> actions that are available to act on the classified 201 * text. 202 * 203 * <p><strong>Note: </strong> that there may or may not be a <i>primary</i> action. 204 * 205 * @see #getSecondaryIntent(int) 206 * @see #getSecondaryLabel(int) 207 * @see #getSecondaryIcon(int) 208 */ 209 @IntRange(from = 0) 210 public int getSecondaryActionsCount() { 211 return mSecondaryIntents.size(); 212 } 213 214 /** 215 * Returns one of the <i>secondary</i> icons that maybe rendered on a widget used to act on the 216 * classified text. 217 * 218 * @param index Index of the action to get the icon for. 219 * @throws IndexOutOfBoundsException if the specified index is out of range. 220 * @see #getSecondaryActionsCount() for the number of actions available. 221 * @see #getSecondaryIntent(int) 222 * @see #getSecondaryLabel(int) 223 * @see #getIcon() 224 */ 225 @Nullable 226 public Drawable getSecondaryIcon(int index) { 227 return mSecondaryIcons.get(index); 228 } 229 230 /** 231 * Returns an icon for the <i>primary</i> intent that may be rendered on a widget used to act 232 * on the classified text. 233 * 234 * @see #getSecondaryIcon(int) 235 */ 236 @Nullable 237 public Drawable getIcon() { 238 return mPrimaryIcon; 239 } 240 241 /** 242 * Returns one of the <i>secondary</i> labels that may be rendered on a widget used to act on 243 * the classified text. 244 * 245 * @param index Index of the action to get the label for. 246 * @throws IndexOutOfBoundsException if the specified index is out of range. 247 * @see #getSecondaryActionsCount() 248 * @see #getSecondaryIntent(int) 249 * @see #getSecondaryIcon(int) 250 * @see #getLabel() 251 */ 252 @Nullable 253 public CharSequence getSecondaryLabel(int index) { 254 return mSecondaryLabels.get(index); 255 } 256 257 /** 258 * Returns a label for the <i>primary</i> intent that may be rendered on a widget used to act 259 * on the classified text. 260 * 261 * @see #getSecondaryLabel(int) 262 */ 263 @Nullable 264 public CharSequence getLabel() { 265 return mPrimaryLabel; 266 } 267 268 /** 269 * Returns one of the <i>secondary</i> intents that may be fired to act on the classified text. 270 * 271 * @param index Index of the action to get the intent for. 272 * @throws IndexOutOfBoundsException if the specified index is out of range. 273 * @see #getSecondaryActionsCount() 274 * @see #getSecondaryLabel(int) 275 * @see #getSecondaryIcon(int) 276 * @see #getIntent() 277 */ 278 @Nullable 279 public Intent getSecondaryIntent(int index) { 280 return mSecondaryIntents.get(index); 281 } 282 283 /** 284 * Returns the <i>primary</i> intent that may be fired to act on the classified text. 285 * 286 * @see #getSecondaryIntent(int) 287 */ 288 @Nullable 289 public Intent getIntent() { 290 return mPrimaryIntent; 291 } 292 293 /** 294 * Returns the <i>primary</i> OnClickListener that may be triggered to act on the classified 295 * text. This field is not parcelable and will be null for all objects read from a parcel. 296 * Instead, call Context#startActivity(Intent) with the result of #getSecondaryIntent(int). 297 * Note that this may fail if the activity doesn't have permission to send the intent. 298 */ 299 @Nullable 300 public OnClickListener getOnClickListener() { 301 return mPrimaryOnClickListener; 302 } 303 304 /** 305 * Returns the signature for this object. 306 * The TextClassifier that generates this object may use it as a way to internally identify 307 * this object. 308 */ 309 @NonNull 310 public String getSignature() { 311 return mSignature; 312 } 313 314 @Override 315 public String toString() { 316 return String.format(Locale.US, "TextClassification {" 317 + "text=%s, entities=%s, " 318 + "primaryLabel=%s, secondaryLabels=%s, " 319 + "primaryIntent=%s, secondaryIntents=%s, " 320 + "signature=%s}", 321 mText, mEntityConfidence, 322 mPrimaryLabel, mSecondaryLabels, 323 mPrimaryIntent, mSecondaryIntents, 324 mSignature); 325 } 326 327 /** 328 * Creates an OnClickListener that triggers the specified intent. 329 * Returns null if the intent is not supported for the specified context. 330 * 331 * @throws IllegalArgumentException if context or intent is null 332 * @hide 333 */ 334 @Nullable 335 public static OnClickListener createIntentOnClickListener( 336 @NonNull final Context context, @NonNull final Intent intent) { 337 switch (getIntentType(intent, context)) { 338 case IntentType.ACTIVITY: 339 return v -> context.startActivity(intent); 340 case IntentType.SERVICE: 341 return v -> context.startService(intent); 342 default: 343 return null; 344 } 345 } 346 347 /** 348 * Triggers the specified intent. 349 * 350 * @throws IllegalArgumentException if context or intent is null 351 * @hide 352 */ 353 public static void fireIntent(@NonNull final Context context, @NonNull final Intent intent) { 354 switch (getIntentType(intent, context)) { 355 case IntentType.ACTIVITY: 356 context.startActivity(intent); 357 return; 358 case IntentType.SERVICE: 359 context.startService(intent); 360 return; 361 default: 362 return; 363 } 364 } 365 366 @IntentType 367 private static int getIntentType(@NonNull Intent intent, @NonNull Context context) { 368 Preconditions.checkArgument(context != null); 369 Preconditions.checkArgument(intent != null); 370 371 final ResolveInfo activityRI = context.getPackageManager().resolveActivity(intent, 0); 372 if (activityRI != null) { 373 if (context.getPackageName().equals(activityRI.activityInfo.packageName)) { 374 return IntentType.ACTIVITY; 375 } 376 final boolean exported = activityRI.activityInfo.exported; 377 if (exported && hasPermission(context, activityRI.activityInfo.permission)) { 378 return IntentType.ACTIVITY; 379 } 380 } 381 382 final ResolveInfo serviceRI = context.getPackageManager().resolveService(intent, 0); 383 if (serviceRI != null) { 384 if (context.getPackageName().equals(serviceRI.serviceInfo.packageName)) { 385 return IntentType.SERVICE; 386 } 387 final boolean exported = serviceRI.serviceInfo.exported; 388 if (exported && hasPermission(context, serviceRI.serviceInfo.permission)) { 389 return IntentType.SERVICE; 390 } 391 } 392 393 return IntentType.UNSUPPORTED; 394 } 395 396 private static boolean hasPermission(@NonNull Context context, @NonNull String permission) { 397 return permission == null 398 || context.checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; 399 } 400 401 /** 402 * Returns a Bitmap representation of the Drawable 403 * 404 * @param drawable The drawable to convert. 405 * @param maxDims The maximum edge length of the resulting bitmap (in pixels). 406 */ 407 @Nullable 408 private static Bitmap drawableToBitmap(@Nullable Drawable drawable, int maxDims) { 409 if (drawable == null) { 410 return null; 411 } 412 final int actualWidth = Math.max(1, drawable.getIntrinsicWidth()); 413 final int actualHeight = Math.max(1, drawable.getIntrinsicHeight()); 414 final double scaleWidth = ((double) maxDims) / actualWidth; 415 final double scaleHeight = ((double) maxDims) / actualHeight; 416 final double scale = Math.min(1.0, Math.min(scaleWidth, scaleHeight)); 417 final int width = (int) (actualWidth * scale); 418 final int height = (int) (actualHeight * scale); 419 if (drawable instanceof BitmapDrawable) { 420 final BitmapDrawable bitmapDrawable = (BitmapDrawable) drawable; 421 if (actualWidth != width || actualHeight != height) { 422 return Bitmap.createScaledBitmap( 423 bitmapDrawable.getBitmap(), width, height, /*filter=*/false); 424 } else { 425 return bitmapDrawable.getBitmap(); 426 } 427 } else { 428 final Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); 429 final Canvas canvas = new Canvas(bitmap); 430 drawable.setBounds(0, 0, canvas.getWidth(), canvas.getHeight()); 431 drawable.draw(canvas); 432 return bitmap; 433 } 434 } 435 436 /** 437 * Returns a list of drawables converted to Bitmaps 438 * 439 * @param drawables The drawables to convert. 440 * @param maxDims The maximum edge length of the resulting bitmaps (in pixels). 441 */ 442 private static List<Bitmap> drawablesToBitmaps(List<Drawable> drawables, int maxDims) { 443 final List<Bitmap> bitmaps = new ArrayList<>(drawables.size()); 444 for (Drawable drawable : drawables) { 445 bitmaps.add(drawableToBitmap(drawable, maxDims)); 446 } 447 return bitmaps; 448 } 449 450 /** Returns a list of drawable wrappers for a list of bitmaps. */ 451 private static List<Drawable> bitmapsToDrawables(List<Bitmap> bitmaps) { 452 final List<Drawable> drawables = new ArrayList<>(bitmaps.size()); 453 for (Bitmap bitmap : bitmaps) { 454 if (bitmap != null) { 455 drawables.add(new BitmapDrawable(Resources.getSystem(), bitmap)); 456 } else { 457 drawables.add(null); 458 } 459 } 460 return drawables; 461 } 462 463 /** 464 * Builder for building {@link TextClassification} objects. 465 * 466 * <p>e.g. 467 * 468 * <pre>{@code 469 * TextClassification classification = new TextClassification.Builder() 470 * .setText(classifiedText) 471 * .setEntityType(TextClassifier.TYPE_EMAIL, 0.9) 472 * .setEntityType(TextClassifier.TYPE_OTHER, 0.1) 473 * .setPrimaryAction(intent, label, icon) 474 * .addSecondaryAction(intent1, label1, icon1) 475 * .addSecondaryAction(intent2, label2, icon2) 476 * .build(); 477 * }</pre> 478 */ 479 public static final class Builder { 480 481 @NonNull private String mText; 482 @NonNull private final List<Drawable> mSecondaryIcons = new ArrayList<>(); 483 @NonNull private final List<String> mSecondaryLabels = new ArrayList<>(); 484 @NonNull private final List<Intent> mSecondaryIntents = new ArrayList<>(); 485 @NonNull private final Map<String, Float> mEntityConfidence = new ArrayMap<>(); 486 @Nullable Drawable mPrimaryIcon; 487 @Nullable String mPrimaryLabel; 488 @Nullable Intent mPrimaryIntent; 489 @Nullable OnClickListener mPrimaryOnClickListener; 490 @NonNull private String mSignature = ""; 491 492 /** 493 * Sets the classified text. 494 */ 495 public Builder setText(@Nullable String text) { 496 mText = text; 497 return this; 498 } 499 500 /** 501 * Sets an entity type for the classification result and assigns a confidence score. 502 * If a confidence score had already been set for the specified entity type, this will 503 * override that score. 504 * 505 * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence). 506 * 0 implies the entity does not exist for the classified text. 507 * Values greater than 1 are clamped to 1. 508 */ 509 public Builder setEntityType( 510 @NonNull @EntityType String type, 511 @FloatRange(from = 0.0, to = 1.0) float confidenceScore) { 512 mEntityConfidence.put(type, confidenceScore); 513 return this; 514 } 515 516 /** 517 * Adds an <i>secondary</i> action that may be performed on the classified text. 518 * Secondary actions are in addition to the <i>primary</i> action which may or may not 519 * exist. 520 * 521 * <p>The label and icon are used for rendering of widgets that offer the intent. 522 * Actions should be added in order of priority. 523 * 524 * <p><stong>Note: </stong> If all input parameters are set to null, this method will be a 525 * no-op. 526 * 527 * @see #setPrimaryAction(Intent, String, Drawable) 528 */ 529 public Builder addSecondaryAction( 530 @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) { 531 if (intent != null || label != null || icon != null) { 532 mSecondaryIntents.add(intent); 533 mSecondaryLabels.add(label); 534 mSecondaryIcons.add(icon); 535 } 536 return this; 537 } 538 539 /** 540 * Removes all the <i>secondary</i> actions. 541 */ 542 public Builder clearSecondaryActions() { 543 mSecondaryIntents.clear(); 544 mSecondaryLabels.clear(); 545 mSecondaryIcons.clear(); 546 return this; 547 } 548 549 /** 550 * Sets the <i>primary</i> action that may be performed on the classified text. This is 551 * equivalent to calling {@code setIntent(intent).setLabel(label).setIcon(icon)}. 552 * 553 * <p><strong>Note: </strong>If all input parameters are null, there will be no 554 * <i>primary</i> action but there may still be <i>secondary</i> actions. 555 * 556 * @see #addSecondaryAction(Intent, String, Drawable) 557 */ 558 public Builder setPrimaryAction( 559 @Nullable Intent intent, @Nullable String label, @Nullable Drawable icon) { 560 return setIntent(intent).setLabel(label).setIcon(icon); 561 } 562 563 /** 564 * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act 565 * on the classified text. 566 * 567 * @see #setPrimaryAction(Intent, String, Drawable) 568 */ 569 public Builder setIcon(@Nullable Drawable icon) { 570 mPrimaryIcon = icon; 571 return this; 572 } 573 574 /** 575 * Sets the label for the <i>primary</i> action that may be rendered on a widget used to 576 * act on the classified text. 577 * 578 * @see #setPrimaryAction(Intent, String, Drawable) 579 */ 580 public Builder setLabel(@Nullable String label) { 581 mPrimaryLabel = label; 582 return this; 583 } 584 585 /** 586 * Sets the intent for the <i>primary</i> action that may be fired to act on the classified 587 * text. 588 * 589 * @see #setPrimaryAction(Intent, String, Drawable) 590 */ 591 public Builder setIntent(@Nullable Intent intent) { 592 mPrimaryIntent = intent; 593 return this; 594 } 595 596 /** 597 * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on 598 * the classified text. This field is not parcelable and will always be null when the 599 * object is read from a parcel. 600 */ 601 public Builder setOnClickListener(@Nullable OnClickListener onClickListener) { 602 mPrimaryOnClickListener = onClickListener; 603 return this; 604 } 605 606 /** 607 * Sets a signature for the TextClassification object. 608 * The TextClassifier that generates the TextClassification object may use it as a way to 609 * internally identify the TextClassification object. 610 */ 611 public Builder setSignature(@NonNull String signature) { 612 mSignature = Preconditions.checkNotNull(signature); 613 return this; 614 } 615 616 /** 617 * Builds and returns a {@link TextClassification} object. 618 */ 619 public TextClassification build() { 620 return new TextClassification( 621 mText, 622 mPrimaryIcon, mPrimaryLabel, mPrimaryIntent, mPrimaryOnClickListener, 623 mSecondaryIcons, mSecondaryLabels, mSecondaryIntents, 624 mEntityConfidence, mSignature); 625 } 626 } 627 628 /** 629 * Optional input parameters for generating TextClassification. 630 */ 631 public static final class Options implements Parcelable { 632 633 private @Nullable LocaleList mDefaultLocales; 634 private @Nullable Calendar mReferenceTime; 635 636 public Options() {} 637 638 /** 639 * @param defaultLocales ordered list of locale preferences that may be used to disambiguate 640 * the provided text. If no locale preferences exist, set this to null or an empty 641 * locale list. 642 */ 643 public Options setDefaultLocales(@Nullable LocaleList defaultLocales) { 644 mDefaultLocales = defaultLocales; 645 return this; 646 } 647 648 /** 649 * @param referenceTime reference time based on which relative dates (e.g. "tomorrow" should 650 * be interpreted. This should usually be the time when the text was originally 651 * composed. If no reference time is set, now is used. 652 */ 653 public Options setReferenceTime(Calendar referenceTime) { 654 mReferenceTime = referenceTime; 655 return this; 656 } 657 658 /** 659 * @return ordered list of locale preferences that can be used to disambiguate 660 * the provided text. 661 */ 662 @Nullable 663 public LocaleList getDefaultLocales() { 664 return mDefaultLocales; 665 } 666 667 /** 668 * @return reference time based on which relative dates (e.g. "tomorrow") should be 669 * interpreted. 670 */ 671 @Nullable 672 public Calendar getReferenceTime() { 673 return mReferenceTime; 674 } 675 676 @Override 677 public int describeContents() { 678 return 0; 679 } 680 681 @Override 682 public void writeToParcel(Parcel dest, int flags) { 683 dest.writeInt(mDefaultLocales != null ? 1 : 0); 684 if (mDefaultLocales != null) { 685 mDefaultLocales.writeToParcel(dest, flags); 686 } 687 dest.writeInt(mReferenceTime != null ? 1 : 0); 688 if (mReferenceTime != null) { 689 dest.writeSerializable(mReferenceTime); 690 } 691 } 692 693 public static final Parcelable.Creator<Options> CREATOR = 694 new Parcelable.Creator<Options>() { 695 @Override 696 public Options createFromParcel(Parcel in) { 697 return new Options(in); 698 } 699 700 @Override 701 public Options[] newArray(int size) { 702 return new Options[size]; 703 } 704 }; 705 706 private Options(Parcel in) { 707 if (in.readInt() > 0) { 708 mDefaultLocales = LocaleList.CREATOR.createFromParcel(in); 709 } 710 if (in.readInt() > 0) { 711 mReferenceTime = (Calendar) in.readSerializable(); 712 } 713 } 714 } 715 716 @Override 717 public int describeContents() { 718 return 0; 719 } 720 721 @Override 722 public void writeToParcel(Parcel dest, int flags) { 723 dest.writeString(mText); 724 final Bitmap primaryIconBitmap = drawableToBitmap(mPrimaryIcon, MAX_PRIMARY_ICON_SIZE); 725 dest.writeInt(primaryIconBitmap != null ? 1 : 0); 726 if (primaryIconBitmap != null) { 727 primaryIconBitmap.writeToParcel(dest, flags); 728 } 729 dest.writeString(mPrimaryLabel); 730 dest.writeInt(mPrimaryIntent != null ? 1 : 0); 731 if (mPrimaryIntent != null) { 732 mPrimaryIntent.writeToParcel(dest, flags); 733 } 734 // mPrimaryOnClickListener is not parcelable. 735 dest.writeTypedList(drawablesToBitmaps(mSecondaryIcons, MAX_SECONDARY_ICON_SIZE)); 736 dest.writeStringList(mSecondaryLabels); 737 dest.writeTypedList(mSecondaryIntents); 738 mEntityConfidence.writeToParcel(dest, flags); 739 dest.writeString(mSignature); 740 } 741 742 public static final Parcelable.Creator<TextClassification> CREATOR = 743 new Parcelable.Creator<TextClassification>() { 744 @Override 745 public TextClassification createFromParcel(Parcel in) { 746 return new TextClassification(in); 747 } 748 749 @Override 750 public TextClassification[] newArray(int size) { 751 return new TextClassification[size]; 752 } 753 }; 754 755 private TextClassification(Parcel in) { 756 mText = in.readString(); 757 mPrimaryIcon = in.readInt() == 0 758 ? null 759 : new BitmapDrawable(Resources.getSystem(), Bitmap.CREATOR.createFromParcel(in)); 760 mPrimaryLabel = in.readString(); 761 mPrimaryIntent = in.readInt() == 0 ? null : Intent.CREATOR.createFromParcel(in); 762 mPrimaryOnClickListener = null; // not parcelable 763 mSecondaryIcons = bitmapsToDrawables(in.createTypedArrayList(Bitmap.CREATOR)); 764 mSecondaryLabels = in.createStringArrayList(); 765 mSecondaryIntents = in.createTypedArrayList(Intent.CREATOR); 766 mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in); 767 mSignature = in.readString(); 768 } 769} 770