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