• Home
  • 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,
322                 PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
323     }
324 
325     /**
326      * Builder for building {@link TextClassification} objects.
327      *
328      * <p>e.g.
329      *
330      * <pre>{@code
331      *   TextClassification classification = new TextClassification.Builder()
332      *          .setText(classifiedText)
333      *          .setEntityType(TextClassifier.TYPE_EMAIL, 0.9)
334      *          .setEntityType(TextClassifier.TYPE_OTHER, 0.1)
335      *          .addAction(remoteAction1)
336      *          .addAction(remoteAction2)
337      *          .build();
338      * }</pre>
339      */
340     public static final class Builder {
341 
342         @NonNull private final List<RemoteAction> mActions = new ArrayList<>();
343         @NonNull private final Map<String, Float> mTypeScoreMap = new ArrayMap<>();
344         @Nullable private String mText;
345         @Nullable private Drawable mLegacyIcon;
346         @Nullable private String mLegacyLabel;
347         @Nullable private Intent mLegacyIntent;
348         @Nullable private OnClickListener mLegacyOnClickListener;
349         @Nullable private String mId;
350         @Nullable private Bundle mExtras;
351 
352         /**
353          * Sets the classified text.
354          */
355         @NonNull
356         public Builder setText(@Nullable String text) {
357             mText = text;
358             return this;
359         }
360 
361         /**
362          * Sets an entity type for the classification result and assigns a confidence score.
363          * If a confidence score had already been set for the specified entity type, this will
364          * override that score.
365          *
366          * @param confidenceScore a value from 0 (low confidence) to 1 (high confidence).
367          *      0 implies the entity does not exist for the classified text.
368          *      Values greater than 1 are clamped to 1.
369          */
370         @NonNull
371         public Builder setEntityType(
372                 @NonNull @EntityType String type,
373                 @FloatRange(from = 0.0, to = 1.0) float confidenceScore) {
374             mTypeScoreMap.put(type, confidenceScore);
375             return this;
376         }
377 
378         Builder setEntityConfidence(EntityConfidence scores) {
379             mTypeScoreMap.clear();
380             mTypeScoreMap.putAll(scores.toMap());
381             return this;
382         }
383 
384         /** @hide */
385         public Builder clearEntityTypes() {
386             mTypeScoreMap.clear();
387             return this;
388         }
389 
390         /**
391          * Adds an action that may be performed on the classified text. Actions should be added in
392          * order of likelihood that the user will use them, with the most likely action being added
393          * first.
394          */
395         @NonNull
396         public Builder addAction(@NonNull RemoteAction action) {
397             Preconditions.checkArgument(action != null);
398             mActions.add(action);
399             return this;
400         }
401 
402         /** @hide */
403         public Builder addActions(Collection<RemoteAction> actions) {
404             Objects.requireNonNull(actions);
405             mActions.addAll(actions);
406             return this;
407         }
408 
409         /** @hide */
410         public Builder clearActions() {
411             mActions.clear();
412             return this;
413         }
414 
415         /**
416          * Sets the icon for the <i>primary</i> action that may be rendered on a widget used to act
417          * on the classified text.
418          *
419          * <p><strong>NOTE: </strong>This field is not parcelled. If read from a parcel, the
420          * returned icon represents the icon of the first {@link RemoteAction} (if one exists).
421          *
422          * @deprecated Use {@link #addAction(RemoteAction)} instead.
423          */
424         @Deprecated
425         @NonNull
426         public Builder setIcon(@Nullable Drawable icon) {
427             mLegacyIcon = icon;
428             return this;
429         }
430 
431         /**
432          * Sets the label for the <i>primary</i> action that may be rendered on a widget used to
433          * act on the classified text.
434          *
435          * <p><strong>NOTE: </strong>This field is not parcelled. If read from a parcel, the
436          * returned label represents the label of the first {@link RemoteAction} (if one exists).
437          *
438          * @deprecated Use {@link #addAction(RemoteAction)} instead.
439          */
440         @Deprecated
441         @NonNull
442         public Builder setLabel(@Nullable String label) {
443             mLegacyLabel = label;
444             return this;
445         }
446 
447         /**
448          * Sets the intent for the <i>primary</i> action that may be fired to act on the classified
449          * text.
450          *
451          * <p><strong>NOTE: </strong>This field is not parcelled.
452          *
453          * @deprecated Use {@link #addAction(RemoteAction)} instead.
454          */
455         @Deprecated
456         @NonNull
457         public Builder setIntent(@Nullable Intent intent) {
458             mLegacyIntent = intent;
459             return this;
460         }
461 
462         /**
463          * Sets the OnClickListener for the <i>primary</i> action that may be triggered to act on
464          * the classified text.
465          *
466          * <p><strong>NOTE: </strong>This field is not parcelable. If read from a parcel, the
467          * returned OnClickListener represents the first {@link RemoteAction} (if one exists).
468          *
469          * @deprecated Use {@link #addAction(RemoteAction)} instead.
470          */
471         @Deprecated
472         @NonNull
473         public Builder setOnClickListener(@Nullable OnClickListener onClickListener) {
474             mLegacyOnClickListener = onClickListener;
475             return this;
476         }
477 
478         /**
479          * Sets an id for the TextClassification object.
480          */
481         @NonNull
482         public Builder setId(@Nullable String id) {
483             mId = id;
484             return this;
485         }
486 
487         /**
488          * Sets the extended data.
489          */
490         @NonNull
491         public Builder setExtras(@Nullable Bundle extras) {
492             mExtras = extras;
493             return this;
494         }
495 
496         /**
497          * Builds and returns a {@link TextClassification} object.
498          */
499         @NonNull
500         public TextClassification build() {
501             EntityConfidence entityConfidence = new EntityConfidence(mTypeScoreMap);
502             return new TextClassification(mText, mLegacyIcon, mLegacyLabel, mLegacyIntent,
503                     mLegacyOnClickListener, mActions, entityConfidence, mId,
504                     mExtras == null ? Bundle.EMPTY : mExtras);
505         }
506     }
507 
508     /**
509      * A request object for generating TextClassification.
510      */
511     public static final class Request implements Parcelable {
512 
513         private final CharSequence mText;
514         private final int mStartIndex;
515         private final int mEndIndex;
516         @Nullable private final LocaleList mDefaultLocales;
517         @Nullable private final ZonedDateTime mReferenceTime;
518         @NonNull private final Bundle mExtras;
519         @Nullable private SystemTextClassifierMetadata mSystemTcMetadata;
520 
521         private Request(
522                 CharSequence text,
523                 int startIndex,
524                 int endIndex,
525                 LocaleList defaultLocales,
526                 ZonedDateTime referenceTime,
527                 Bundle extras) {
528             mText = text;
529             mStartIndex = startIndex;
530             mEndIndex = endIndex;
531             mDefaultLocales = defaultLocales;
532             mReferenceTime = referenceTime;
533             mExtras = extras;
534         }
535 
536         /**
537          * Returns the text providing context for the text to classify (which is specified
538          *      by the sub sequence starting at startIndex and ending at endIndex)
539          */
540         @NonNull
541         public CharSequence getText() {
542             return mText;
543         }
544 
545         /**
546          * Returns start index of the text to classify.
547          */
548         @IntRange(from = 0)
549         public int getStartIndex() {
550             return mStartIndex;
551         }
552 
553         /**
554          * Returns end index of the text to classify.
555          */
556         @IntRange(from = 0)
557         public int getEndIndex() {
558             return mEndIndex;
559         }
560 
561         /**
562          * @return ordered list of locale preferences that can be used to disambiguate
563          *      the provided text.
564          */
565         @Nullable
566         public LocaleList getDefaultLocales() {
567             return mDefaultLocales;
568         }
569 
570         /**
571          * @return reference time based on which relative dates (e.g. "tomorrow") should be
572          *      interpreted.
573          */
574         @Nullable
575         public ZonedDateTime getReferenceTime() {
576             return mReferenceTime;
577         }
578 
579         /**
580          * Returns the name of the package that sent this request.
581          * This returns {@code null} if no calling package name is set.
582          */
583         @Nullable
584         public String getCallingPackageName() {
585             return mSystemTcMetadata != null ? mSystemTcMetadata.getCallingPackageName() : null;
586         }
587 
588         /**
589          * Sets the information about the {@link SystemTextClassifier} that sent this request.
590          *
591          * @hide
592          */
593         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
594         public void setSystemTextClassifierMetadata(
595                 @Nullable SystemTextClassifierMetadata systemTcMetadata) {
596             mSystemTcMetadata = systemTcMetadata;
597         }
598 
599         /**
600          * Returns the information about the {@link SystemTextClassifier} that sent this request.
601          *
602          * @hide
603          */
604         @Nullable
605         public SystemTextClassifierMetadata getSystemTextClassifierMetadata() {
606             return mSystemTcMetadata;
607         }
608 
609         /**
610          * Returns the extended data.
611          *
612          * <p><b>NOTE: </b>Do not modify this bundle.
613          */
614         @NonNull
615         public Bundle getExtras() {
616             return mExtras;
617         }
618 
619         /**
620          * A builder for building TextClassification requests.
621          */
622         public static final class Builder {
623 
624             private final CharSequence mText;
625             private final int mStartIndex;
626             private final int mEndIndex;
627             private Bundle mExtras;
628 
629             @Nullable private LocaleList mDefaultLocales;
630             @Nullable private ZonedDateTime mReferenceTime;
631 
632             /**
633              * @param text text providing context for the text to classify (which is specified
634              *      by the sub sequence starting at startIndex and ending at endIndex)
635              * @param startIndex start index of the text to classify
636              * @param endIndex end index of the text to classify
637              */
638             public Builder(
639                     @NonNull CharSequence text,
640                     @IntRange(from = 0) int startIndex,
641                     @IntRange(from = 0) int endIndex) {
642                 Utils.checkArgument(text, startIndex, endIndex);
643                 mText = text;
644                 mStartIndex = startIndex;
645                 mEndIndex = endIndex;
646             }
647 
648             /**
649              * @param defaultLocales ordered list of locale preferences that may be used to
650              *      disambiguate the provided text. If no locale preferences exist, set this to null
651              *      or an empty locale list.
652              *
653              * @return this builder
654              */
655             @NonNull
656             public Builder setDefaultLocales(@Nullable LocaleList defaultLocales) {
657                 mDefaultLocales = defaultLocales;
658                 return this;
659             }
660 
661             /**
662              * @param referenceTime reference time based on which relative dates (e.g. "tomorrow"
663              *      should be interpreted. This should usually be the time when the text was
664              *      originally composed. If no reference time is set, now is used.
665              *
666              * @return this builder
667              */
668             @NonNull
669             public Builder setReferenceTime(@Nullable ZonedDateTime referenceTime) {
670                 mReferenceTime = referenceTime;
671                 return this;
672             }
673 
674             /**
675              * Sets the extended data.
676              *
677              * @return this builder
678              */
679             @NonNull
680             public Builder setExtras(@Nullable Bundle extras) {
681                 mExtras = extras;
682                 return this;
683             }
684 
685             /**
686              * Builds and returns the request object.
687              */
688             @NonNull
689             public Request build() {
690                 return new Request(new SpannedString(mText), mStartIndex, mEndIndex,
691                         mDefaultLocales, mReferenceTime,
692                         mExtras == null ? Bundle.EMPTY : mExtras);
693             }
694         }
695 
696         @Override
697         public int describeContents() {
698             return 0;
699         }
700 
701         @Override
702         public void writeToParcel(Parcel dest, int flags) {
703             dest.writeCharSequence(mText);
704             dest.writeInt(mStartIndex);
705             dest.writeInt(mEndIndex);
706             dest.writeParcelable(mDefaultLocales, flags);
707             dest.writeString(mReferenceTime == null ? null : mReferenceTime.toString());
708             dest.writeBundle(mExtras);
709             dest.writeParcelable(mSystemTcMetadata, flags);
710         }
711 
712         private static Request readFromParcel(Parcel in) {
713             final CharSequence text = in.readCharSequence();
714             final int startIndex = in.readInt();
715             final int endIndex = in.readInt();
716             final LocaleList defaultLocales = in.readParcelable(null);
717             final String referenceTimeString = in.readString();
718             final ZonedDateTime referenceTime = referenceTimeString == null
719                     ? null : ZonedDateTime.parse(referenceTimeString);
720             final Bundle extras = in.readBundle();
721             final SystemTextClassifierMetadata systemTcMetadata = in.readParcelable(null);
722 
723             final Request request = new Request(text, startIndex, endIndex,
724                     defaultLocales, referenceTime, extras);
725             request.setSystemTextClassifierMetadata(systemTcMetadata);
726             return request;
727         }
728 
729         public static final @android.annotation.NonNull Parcelable.Creator<Request> CREATOR =
730                 new Parcelable.Creator<Request>() {
731                     @Override
732                     public Request createFromParcel(Parcel in) {
733                         return readFromParcel(in);
734                     }
735 
736                     @Override
737                     public Request[] newArray(int size) {
738                         return new Request[size];
739                     }
740                 };
741     }
742 
743     @Override
744     public int describeContents() {
745         return 0;
746     }
747 
748     @Override
749     public void writeToParcel(Parcel dest, int flags) {
750         dest.writeString(mText);
751         // NOTE: legacy fields are not parcelled.
752         dest.writeTypedList(mActions);
753         mEntityConfidence.writeToParcel(dest, flags);
754         dest.writeString(mId);
755         dest.writeBundle(mExtras);
756     }
757 
758     public static final @android.annotation.NonNull Parcelable.Creator<TextClassification> CREATOR =
759             new Parcelable.Creator<TextClassification>() {
760                 @Override
761                 public TextClassification createFromParcel(Parcel in) {
762                     return new TextClassification(in);
763                 }
764 
765                 @Override
766                 public TextClassification[] newArray(int size) {
767                     return new TextClassification[size];
768                 }
769             };
770 
771     private TextClassification(Parcel in) {
772         mText = in.readString();
773         mActions = in.createTypedArrayList(RemoteAction.CREATOR);
774         if (!mActions.isEmpty()) {
775             final RemoteAction action = mActions.get(0);
776             mLegacyIcon = maybeLoadDrawable(action.getIcon());
777             mLegacyLabel = action.getTitle().toString();
778             mLegacyOnClickListener = createIntentOnClickListener(mActions.get(0).getActionIntent());
779         } else {
780             mLegacyIcon = null;
781             mLegacyLabel = null;
782             mLegacyOnClickListener = null;
783         }
784         mLegacyIntent = null; // mLegacyIntent is not parcelled.
785         mEntityConfidence = EntityConfidence.CREATOR.createFromParcel(in);
786         mId = in.readString();
787         mExtras = in.readBundle();
788     }
789 
790     // Best effort attempt to try to load a drawable from the provided icon.
791     @Nullable
792     private static Drawable maybeLoadDrawable(Icon icon) {
793         if (icon == null) {
794             return null;
795         }
796         switch (icon.getType()) {
797             case Icon.TYPE_BITMAP:
798                 return new BitmapDrawable(Resources.getSystem(), icon.getBitmap());
799             case Icon.TYPE_ADAPTIVE_BITMAP:
800                 return new AdaptiveIconDrawable(null,
801                         new BitmapDrawable(Resources.getSystem(), icon.getBitmap()));
802             case Icon.TYPE_DATA:
803                 return new BitmapDrawable(
804                         Resources.getSystem(),
805                         BitmapFactory.decodeByteArray(
806                                 icon.getDataBytes(), icon.getDataOffset(), icon.getDataLength()));
807         }
808         return null;
809     }
810 }
811