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