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