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