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