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 
17 package android.view.textclassifier;
18 
19 import android.annotation.FloatRange;
20 import android.annotation.IntDef;
21 import android.annotation.IntRange;
22 import android.annotation.NonNull;
23 import android.annotation.Nullable;
24 import android.app.ActivityOptions;
25 import android.app.PendingIntent;
26 import android.app.RemoteAction;
27 import android.content.Context;
28 import android.content.Intent;
29 import android.content.res.Resources;
30 import android.graphics.BitmapFactory;
31 import android.graphics.drawable.AdaptiveIconDrawable;
32 import android.graphics.drawable.BitmapDrawable;
33 import android.graphics.drawable.Drawable;
34 import android.graphics.drawable.Icon;
35 import android.os.Bundle;
36 import android.os.LocaleList;
37 import android.os.Parcel;
38 import android.os.Parcelable;
39 import android.text.SpannedString;
40 import android.util.ArrayMap;
41 import android.view.View.OnClickListener;
42 import android.view.textclassifier.TextClassifier.EntityType;
43 import android.view.textclassifier.TextClassifier.Utils;
44 
45 import com.android.internal.annotations.VisibleForTesting;
46 import com.android.internal.util.Preconditions;
47 
48 import java.lang.annotation.Retention;
49 import java.lang.annotation.RetentionPolicy;
50 import java.time.ZonedDateTime;
51 import java.util.ArrayList;
52 import java.util.Collection;
53 import java.util.Collections;
54 import java.util.List;
55 import java.util.Locale;
56 import java.util.Map;
57 import java.util.Objects;
58 
59 /**
60  * Information for generating a widget to handle classified text.
61  *
62  * <p>A TextClassification object contains icons, labels, onClickListeners and intents that may
63  * be used to build a widget that can be used to act on classified text. There is the concept of a
64  * <i>primary action</i> and other <i>secondary actions</i>.
65  *
66  * <p>e.g. building a view that, when clicked, shares the classified text with the preferred app:
67  *
68  * <pre>{@code
69  *   // Called preferably outside the UiThread.
70  *   TextClassification classification = textClassifier.classifyText(allText, 10, 25);
71  *
72  *   // Called on the UiThread.
73  *   Button button = new Button(context);
74  *   button.setCompoundDrawablesWithIntrinsicBounds(classification.getIcon(), null, null, null);
75  *   button.setText(classification.getLabel());
76  *   button.setOnClickListener(v -> classification.getActions().get(0).getActionIntent().send());
77  * }</pre>
78  *
79  * <p>e.g. starting an action mode with menu items that can handle the classified text:
80  *
81  * <pre>{@code
82  *   // Called preferably outside the UiThread.
83  *   final TextClassification classification = textClassifier.classifyText(allText, 10, 25);
84  *
85  *   // Called on the UiThread.
86  *   view.startActionMode(new ActionMode.Callback() {
87  *
88  *       public boolean onCreateActionMode(ActionMode mode, Menu menu) {
89  *           for (int i = 0; i < classification.getActions().size(); ++i) {
90  *              RemoteAction action = classification.getActions().get(i);
91  *              menu.add(Menu.NONE, i, 20, action.getTitle())
92  *                 .setIcon(action.getIcon());
93  *           }
94  *           return true;
95  *       }
96  *
97  *       public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
98  *           classification.getActions().get(item.getItemId()).getActionIntent().send();
99  *           return true;
100  *       }
101  *
102  *       ...
103  *   });
104  * }</pre>
105  */
106 public final class TextClassification implements Parcelable {
107 
108     /**
109      * @hide
110      */
111     public static final TextClassification EMPTY = new TextClassification.Builder().build();
112 
113     private static final String LOG_TAG = "TextClassification";
114     // TODO(toki): investigate a way to derive this based on device properties.
115     private static final int MAX_LEGACY_ICON_SIZE = 192;
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 mLegacyIcon;
127     @Nullable private final String mLegacyLabel;
128     @Nullable private final Intent mLegacyIntent;
129     @Nullable private final OnClickListener mLegacyOnClickListener;
130     @NonNull private final List<RemoteAction> mActions;
131     @NonNull private final EntityConfidence mEntityConfidence;
132     @Nullable private final String mId;
133     @NonNull private final Bundle mExtras;
134 
TextClassification( @ullable String text, @Nullable Drawable legacyIcon, @Nullable String legacyLabel, @Nullable Intent legacyIntent, @Nullable OnClickListener legacyOnClickListener, @NonNull List<RemoteAction> actions, @NonNull EntityConfidence entityConfidence, @Nullable String id, @NonNull Bundle extras)135     private TextClassification(
136             @Nullable String text,
137             @Nullable Drawable legacyIcon,
138             @Nullable String legacyLabel,
139             @Nullable Intent legacyIntent,
140             @Nullable OnClickListener legacyOnClickListener,
141             @NonNull List<RemoteAction> actions,
142             @NonNull EntityConfidence entityConfidence,
143             @Nullable String id,
144             @NonNull Bundle extras) {
145         mText = text;
146         mLegacyIcon = legacyIcon;
147         mLegacyLabel = legacyLabel;
148         mLegacyIntent = legacyIntent;
149         mLegacyOnClickListener = legacyOnClickListener;
150         mActions = Collections.unmodifiableList(actions);
151         mEntityConfidence = Objects.requireNonNull(entityConfidence);
152         mId = id;
153         mExtras = extras;
154     }
155 
156     /**
157      * Gets the classified text.
158      */
159     @Nullable
getText()160     public String getText() {
161         return mText;
162     }
163 
164     /**
165      * Returns the number of entities found in the classified text.
166      */
167     @IntRange(from = 0)
getEntityCount()168     public int getEntityCount() {
169         return mEntityConfidence.getEntities().size();
170     }
171 
172     /**
173      * Returns the entity at the specified index. Entities are ordered from high confidence
174      * to low confidence.
175      *
176      * @throws IndexOutOfBoundsException if the specified index is out of range.
177      * @see #getEntityCount() for the number of entities available.
178      */
179     @NonNull
getEntity(int index)180     public @EntityType String getEntity(int index) {
181         return mEntityConfidence.getEntities().get(index);
182     }
183 
184     /**
185      * Returns the confidence score for the specified entity. The value ranges from
186      * 0 (low confidence) to 1 (high confidence). 0 indicates that the entity was not found for the
187      * classified text.
188      */
189     @FloatRange(from = 0.0, to = 1.0)
getConfidenceScore(@ntityType String entity)190     public float getConfidenceScore(@EntityType String entity) {
191         return mEntityConfidence.getConfidenceScore(entity);
192     }
193 
194     /**
195      * Returns a list of actions that may be performed on the text. The list is ordered based on
196      * the likelihood that a user will use the action, with the most likely action appearing first.
197      */
getActions()198     public List<RemoteAction> getActions() {
199         return mActions;
200     }
201 
202     /**
203      * Returns an icon that may be rendered on a widget used to act on the classified text.
204      *
205      * <p><strong>NOTE: </strong>This field is not parcelable and only represents the icon of the
206      * first {@link RemoteAction} (if one exists) when this object is read from a parcel.
207      *
208      * @deprecated Use {@link #getActions()} instead.
209      */
210     @Deprecated
211     @Nullable
getIcon()212     public Drawable getIcon() {
213         return mLegacyIcon;
214     }
215 
216     /**
217      * Returns a label that may be rendered on a widget used to act on the classified text.
218      *
219      * <p><strong>NOTE: </strong>This field is not parcelable and only represents the label of the
220      * first {@link RemoteAction} (if one exists) when this object is read from a parcel.
221      *
222      * @deprecated Use {@link #getActions()} instead.
223      */
224     @Deprecated
225     @Nullable
getLabel()226     public CharSequence getLabel() {
227         return mLegacyLabel;
228     }
229 
230     /**
231      * Returns an intent that may be fired to act on the classified text.
232      *
233      * <p><strong>NOTE: </strong>This field is not parcelled and will always return null when this
234      * object is read from a parcel.
235      *
236      * @deprecated Use {@link #getActions()} instead.
237      */
238     @Deprecated
239     @Nullable
getIntent()240     public Intent getIntent() {
241         return mLegacyIntent;
242     }
243 
244     /**
245      * Returns the OnClickListener that may be triggered to act on the classified text.
246      *
247      * <p><strong>NOTE: </strong>This field is not parcelable and only represents the first
248      * {@link RemoteAction} (if one exists) when this object is read from a parcel.
249      *
250      * @deprecated Use {@link #getActions()} instead.
251      */
252     @Nullable
getOnClickListener()253     public OnClickListener getOnClickListener() {
254         return mLegacyOnClickListener;
255     }
256 
257     /**
258      * Returns the id, if one exists, for this object.
259      */
260     @Nullable
getId()261     public String getId() {
262         return mId;
263     }
264 
265     /**
266      * Returns the extended data.
267      *
268      * <p><b>NOTE: </b>Do not modify this bundle.
269      */
270     @NonNull
getExtras()271     public Bundle getExtras() {
272         return mExtras;
273     }
274 
275     /** @hide */
toBuilder()276     public Builder toBuilder() {
277         return new Builder()
278                 .setId(mId)
279                 .setText(mText)
280                 .addActions(mActions)
281                 .setEntityConfidence(mEntityConfidence)
282                 .setIcon(mLegacyIcon)
283                 .setLabel(mLegacyLabel)
284                 .setIntent(mLegacyIntent)
285                 .setOnClickListener(mLegacyOnClickListener)
286                 .setExtras(mExtras);
287     }
288 
289     @Override
toString()290     public String toString() {
291         return String.format(Locale.US,
292                 "TextClassification {text=%s, entities=%s, actions=%s, id=%s, extras=%s}",
293                 mText, mEntityConfidence, mActions, mId, mExtras);
294     }
295 
296     /**
297      * Creates an OnClickListener that triggers the specified PendingIntent.
298      *
299      * @hide
300      */
createIntentOnClickListener(@onNull final PendingIntent intent)301     public static OnClickListener createIntentOnClickListener(@NonNull final PendingIntent intent) {
302         Objects.requireNonNull(intent);
303         return v -> {
304             try {
305                 intent.send(ActivityOptions.makeBasic().setPendingIntentBackgroundActivityStartMode(
306                         ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED).toBundle());
307             } catch (PendingIntent.CanceledException e) {
308                 Log.e(LOG_TAG, "Error sending PendingIntent", e);
309             }
310         };
311     }
312 
313     /**
314      * Creates a PendingIntent for the specified intent.
315      * Returns null if the intent is not supported for the specified context.
316      *
317      * @throws IllegalArgumentException if context or intent is null
318      * @hide
319      */
320     public static PendingIntent createPendingIntent(
321             @NonNull final Context context, @NonNull final Intent intent, int requestCode) {
322         return PendingIntent.getActivity(
323                 context, requestCode, intent,
324                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
325     }
326 
327     /**
328      * Builder for building {@link TextClassification} objects.
329      *
330      * <p>e.g.
331      *
332      * <pre>{@code
333      *   TextClassification classification = new TextClassification.Builder()
334      *          .setText(classifiedText)
335      *          .setEntityType(TextClassifier.TYPE_EMAIL, 0.9)
336      *          .setEntityType(TextClassifier.TYPE_OTHER, 0.1)
337      *          .addAction(remoteAction1)
338      *          .addAction(remoteAction2)
339      *          .build();
340      * }</pre>
341      */
342     public static final class Builder {
343 
344         @NonNull private final List<RemoteAction> mActions = new ArrayList<>();
345         @NonNull private final Map<String, Float> mTypeScoreMap = new ArrayMap<>();
346         @Nullable private String mText;
347         @Nullable private Drawable mLegacyIcon;
348         @Nullable private String mLegacyLabel;
349         @Nullable private Intent mLegacyIntent;
350         @Nullable private OnClickListener mLegacyOnClickListener;
351         @Nullable private String mId;
352         @Nullable private Bundle mExtras;
353 
354         /**
355          * Sets the classified text.
356          */
357         @NonNull
358         public Builder setText(@Nullable String text) {
359             mText = text;
360             return this;
361         }
362 
363         /**
364          * Sets an entity type for the classification result and assigns a confidence score.
365          * If a confidence score had already been set for the specified entity type, this will
366          * override that score.
367          *
368          * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence).
369          *      0 implies the entity does not exist for the classified text.
370          *      Values greater than 1 are clamped to 1.
371          */
372         @NonNull
373         public Builder setEntityType(
374                 @NonNull @EntityType String type,
375                 @FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
376             mTypeScoreMap.put(type, confidenceScore);
377             return this;
378         }
379 
380         Builder setEntityConfidence(EntityConfidence scores) {
381             mTypeScoreMap.clear();
382             mTypeScoreMap.putAll(scores.toMap());
383             return this;
384         }
385 
386         /** @hide */
387         public Builder clearEntityTypes() {
388             mTypeScoreMap.clear();
389             return this;
390         }
391 
392         /**
393          * Adds an action that may be performed on the classified text. Actions should be added in
394          * order of likelihood that the user will use them, with the most likely action being added
395          * first.
396          */
397         @NonNull
398         public Builder addAction(@NonNull RemoteAction action) {
399             Preconditions.checkArgument(action != null);
400             mActions.add(action);
401             return this;
402         }
403 
404         /** @hide */
405         public Builder addActions(Collection<RemoteAction> actions) {
406             Objects.requireNonNull(actions);
407             mActions.addAll(actions);
408             return this;
409         }
410 
411         /** @hide */
412         public Builder clearActions() {
413             mActions.clear();
414             return this;
415         }
416 
417         /**
418          * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
419          * on the classified text.
420          *
421          * <p><strong>NOTE: </strong>This field is not parcelled. If read from a parcel, the
422          * returned icon represents the icon of the first {@link RemoteAction} (if one exists).
423          *
424          * @deprecated Use {@link #addAction(RemoteAction)} instead.
425          */
426         @Deprecated
427         @NonNull
428         public Builder setIcon(@Nullable Drawable icon) {
429             mLegacyIcon = icon;
430             return this;
431         }
432 
433         /**
434          * Sets the label for the <i>primary</i> action that may be rendered on a widget used to
435          * act on the classified text.
436          *
437          * <p><strong>NOTE: </strong>This field is not parcelled. If read from a parcel, the
438          * returned label represents the label of the first {@link RemoteAction} (if one exists).
439          *
440          * @deprecated Use {@link #addAction(RemoteAction)} instead.
441          */
442         @Deprecated
443         @NonNull
444         public Builder setLabel(@Nullable String label) {
445             mLegacyLabel = label;
446             return this;
447         }
448 
449         /**
450          * Sets the intent for the <i>primary</i> action that may be fired to act on the classified
451          * text.
452          *
453          * <p><strong>NOTE: </strong>This field is not parcelled.
454          *
455          * @deprecated Use {@link #addAction(RemoteAction)} instead.
456          */
457         @Deprecated
458         @NonNull
459         public Builder setIntent(@Nullable Intent intent) {
460             mLegacyIntent = intent;
461             return this;
462         }
463 
464         /**
465          * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
466          * the classified text.
467          *
468          * <p><strong>NOTE: </strong>This field is not parcelable. If read from a parcel, the
469          * returned OnClickListener represents the first {@link RemoteAction} (if one exists).
470          *
471          * @deprecated Use {@link #addAction(RemoteAction)} instead.
472          */
473         @Deprecated
474         @NonNull
475         public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
476             mLegacyOnClickListener = onClickListener;
477             return this;
478         }
479 
480         /**
481          * Sets an id for the TextClassification object.
482          */
483         @NonNull
484         public Builder setId(@Nullable String id) {
485             mId = id;
486             return this;
487         }
488 
489         /**
490          * Sets the extended data.
491          */
492         @NonNull
493         public Builder setExtras(@Nullable Bundle extras) {
494             mExtras = extras;
495             return this;
496         }
497 
498         /**
499          * Builds and returns a {@link TextClassification} object.
500          */
501         @NonNull
502         public TextClassification build() {
503             EntityConfidence entityConfidence = new EntityConfidence(mTypeScoreMap);
504             return new TextClassification(mText, mLegacyIcon, mLegacyLabel, mLegacyIntent,
505                     mLegacyOnClickListener, mActions, entityConfidence, mId,
506                     mExtras == null ? Bundle.EMPTY : mExtras);
507         }
508     }
509 
510     /**
511      * A request object for generating TextClassification.
512      */
513     public static final class Request implements Parcelable {
514 
515         private final CharSequence mText;
516         private final int mStartIndex;
517         private final int mEndIndex;
518         @Nullable private final LocaleList mDefaultLocales;
519         @Nullable private final ZonedDateTime mReferenceTime;
520         @NonNull private final Bundle mExtras;
521         @Nullable private SystemTextClassifierMetadata mSystemTcMetadata;
522 
523         private Request(
524                 CharSequence text,
525                 int startIndex,
526                 int endIndex,
527                 LocaleList defaultLocales,
528                 ZonedDateTime referenceTime,
529                 Bundle extras) {
530             mText = text;
531             mStartIndex = startIndex;
532             mEndIndex = endIndex;
533             mDefaultLocales = defaultLocales;
534             mReferenceTime = referenceTime;
535             mExtras = extras;
536         }
537 
538         /**
539          * Returns the text providing context for the text to classify (which is specified
540          *      by the sub sequence starting at startIndex and ending at endIndex)
541          */
542         @NonNull
543         public CharSequence getText() {
544             return mText;
545         }
546 
547         /**
548          * Returns start index of the text to classify.
549          */
550         @IntRange(from = 0)
551         public int getStartIndex() {
552             return mStartIndex;
553         }
554 
555         /**
556          * Returns end index of the text to classify.
557          */
558         @IntRange(from = 0)
559         public int getEndIndex() {
560             return mEndIndex;
561         }
562 
563         /**
564          * @return ordered list of locale preferences that can be used to disambiguate
565          *      the provided text.
566          */
567         @Nullable
568         public LocaleList getDefaultLocales() {
569             return mDefaultLocales;
570         }
571 
572         /**
573          * @return reference time based on which relative dates (e.g. "tomorrow") should be
574          *      interpreted.
575          */
576         @Nullable
577         public ZonedDateTime getReferenceTime() {
578             return mReferenceTime;
579         }
580 
581         /**
582          * Returns the name of the package that sent this request.
583          * This returns {@code null} if no calling package name is set.
584          */
585         @Nullable
586         public String getCallingPackageName() {
587             return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : null;
588         }
589 
590         /**
591          * Sets the information about the {@link SystemTextClassifier} that sent this request.
592          *
593          * @hide
594          */
595         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
596         public void setSystemTextClassifierMetadata(
597                 @Nullable SystemTextClassifierMetadata systemTcMetadata) {
598             mSystemTcMetadata = systemTcMetadata;
599         }
600 
601         /**
602          * Returns the information about the {@link SystemTextClassifier} that sent this request.
603          *
604          * @hide
605          */
606         @Nullable
607         public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
608             return mSystemTcMetadata;
609         }
610 
611         /**
612          * Returns the extended data.
613          *
614          * <p><b>NOTE: </b>Do not modify this bundle.
615          */
616         @NonNull
617         public Bundle getExtras() {
618             return mExtras;
619         }
620 
621         /**
622          * A builder for building TextClassification requests.
623          */
624         public static final class Builder {
625 
626             private final CharSequence mText;
627             private final int mStartIndex;
628             private final int mEndIndex;
629             private Bundle mExtras;
630 
631             @Nullable private LocaleList mDefaultLocales;
632             @Nullable private ZonedDateTime mReferenceTime;
633 
634             /**
635              * @param text text providing context for the text to classify (which is specified
636              *      by the sub sequence starting at startIndex and ending at endIndex)
637              * @param startIndex start index of the text to classify
638              * @param endIndex end index of the text to classify
639              */
640             public Builder(
641                     @NonNull CharSequence text,
642                     @IntRange(from = 0) int startIndex,
643                     @IntRange(from = 0) int endIndex) {
644                 Utils.checkArgument(text, startIndex, endIndex);
645                 mText = text;
646                 mStartIndex = startIndex;
647                 mEndIndex = endIndex;
648             }
649 
650             /**
651              * @param defaultLocales ordered list of locale preferences that may be used to
652              *      disambiguate the provided text. If no locale preferences exist, set this to null
653              *      or an empty locale list.
654              *
655              * @return this builder
656              */
657             @NonNull
658             public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) {
659                 mDefaultLocales = defaultLocales;
660                 return this;
661             }
662 
663             /**
664              * @param referenceTime reference time based on which relative dates (e.g. "tomorrow"
665              *      should be interpreted. This should usually be the time when the text was
666              *      originally composed. If no reference time is set, now is used.
667              *
668              * @return this builder
669              */
670             @NonNull
671             public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) {
672                 mReferenceTime = referenceTime;
673                 return this;
674             }
675 
676             /**
677              * Sets the extended data.
678              *
679              * @return this builder
680              */
681             @NonNull
682             public Builder setExtras(@Nullable Bundle extras) {
683                 mExtras = extras;
684                 return this;
685             }
686 
687             /**
688              * Builds and returns the request object.
689              */
690             @NonNull
691             public Request build() {
692                 return new Request(new SpannedString(mText), mStartIndex, mEndIndex,
693                         mDefaultLocales, mReferenceTime,
694                         mExtras == null ? Bundle.EMPTY : mExtras);
695             }
696         }
697 
698         @Override
699         public int describeContents() {
700             return 0;
701         }
702 
703         @Override
704         public void writeToParcel(Parcel dest, int flags) {
705             dest.writeCharSequence(mText);
706             dest.writeInt(mStartIndex);
707             dest.writeInt(mEndIndex);
708             dest.writeParcelable(mDefaultLocales, flags);
709             dest.writeString(mReferenceTime == null ? null : mReferenceTime.toString());
710             dest.writeBundle(mExtras);
711             dest.writeParcelable(mSystemTcMetadata, flags);
712         }
713 
714         private static Request readFromParcel(Parcel in) {
715             final CharSequence text = in.readCharSequence();
716             final int startIndex = in.readInt();
717             final int endIndex = in.readInt();
718             final LocaleList defaultLocales = in.readParcelable(null, android.os.LocaleList.class);
719             final String referenceTimeString = in.readString();
720             final ZonedDateTime referenceTime = referenceTimeString == null
721                     ? null : ZonedDateTime.parse(referenceTimeString);
722             final Bundle extras = in.readBundle();
723             final SystemTextClassifierMetadata systemTcMetadata = in.readParcelable(null, android.view.textclassifier.SystemTextClassifierMetadata.class);
724 
725             final Request request = new Request(text, startIndex, endIndex,
726                     defaultLocales, referenceTime, extras);
727             request.setSystemTextClassifierMetadata(systemTcMetadata);
728             return request;
729         }
730 
731         public static final @android.annotation.NonNull Parcelable.Creator<Request> CREATOR =
732                 new Parcelable.Creator<Request>() {
733                     @Override
734                     public Request createFromParcel(Parcel in) {
735                         return readFromParcel(in);
736                     }
737 
738                     @Override
739                     public Request[] newArray(int size) {
740                         return new Request[size];
741                     }
742                 };
743     }
744 
745     @Override
746     public int describeContents() {
747         return 0;
748     }
749 
750     @Override
751     public void writeToParcel(Parcel dest, int flags) {
752         dest.writeString(mText);
753         // NOTE: legacy fields are not parcelled.
754         dest.writeTypedList(mActions);
755         mEntityConfidence.writeToParcel(dest, flags);
756         dest.writeString(mId);
757         dest.writeBundle(mExtras);
758     }
759 
760     public static final @android.annotation.NonNull Parcelable.Creator<TextClassification> CREATOR =
761             new Parcelable.Creator<TextClassification>() {
762                 @Override
763                 public TextClassification createFromParcel(Parcel in) {
764                     return new TextClassification(in);
765                 }
766 
767                 @Override
768                 public TextClassification[] newArray(int size) {
769                     return new TextClassification[size];
770                 }
771             };
772 
773     private TextClassification(Parcel in) {
774         mText = in.readString();
775         mActions = in.createTypedArrayList(RemoteAction.CREATOR);
776         if (!mActions.isEmpty()) {
777             final RemoteAction action = mActions.get(0);
778             mLegacyIcon = maybeLoadDrawable(action.getIcon());
779             mLegacyLabel = action.getTitle().toString();
780             mLegacyOnClickListener = createIntentOnClickListener(mActions.get(0).getActionIntent());
781         } else {
782             mLegacyIcon = null;
783             mLegacyLabel = null;
784             mLegacyOnClickListener = null;
785         }
786         mLegacyIntent = null; // mLegacyIntent is not parcelled.
787         mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
788         mId = in.readString();
789         mExtras = in.readBundle();
790     }
791 
792     // Best effort attempt to try to load a drawable from the provided icon.
793     @Nullable
794     private static Drawable maybeLoadDrawable(Icon icon) {
795         if (icon == null) {
796             return null;
797         }
798         switch (icon.getType()) {
799             case Icon.TYPE_BITMAP:
800                 return new BitmapDrawable(Resources.getSystem(), icon.getBitmap());
801             case Icon.TYPE_ADAPTIVE_BITMAP:
802                 return new AdaptiveIconDrawable(null,
803                         new BitmapDrawable(Resources.getSystem(), icon.getBitmap()));
804             case Icon.TYPE_DATA:
805                 return new BitmapDrawable(
806                         Resources.getSystem(),
807                         BitmapFactory.decodeByteArray(
808                                 icon.getDataBytes(), icon.getDataOffset(), icon.getDataLength()));
809         }
810         return null;
811     }
812 }
813