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