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