content: /*
   * Copyright (C) 2017 The Android Open Source Project
   *
   * Licensed under the Apache License, Version 2.0 (the "License");
   * you may not use this file except in compliance with the License.
   * You may obtain a copy of the License at
   *
   *      http://www.apache.org/licenses/LICENSE-2.0
   *
   * Unless required by applicable law or agreed to in writing, software
   * distributed under the License is distributed on an "AS IS" BASIS,
   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   * See the License for the specific language governing permissions and
   * limitations under the License.
   */
  
  package android.view.textclassifier;
  
  import android.annotation.NonNull;
  import android.annotation.Nullable;
  import android.annotation.WorkerThread;
  import android.app.RemoteAction;
  import android.content.Context;
  import android.content.Intent;
  import android.icu.util.ULocale;
  import android.os.Bundle;
  import android.os.LocaleList;
  import android.os.ParcelFileDescriptor;
  import android.util.ArrayMap;
  import android.util.ArraySet;
  import android.util.Pair;
  import android.view.textclassifier.ActionsModelParamsSupplier.ActionsModelParams;
  import android.view.textclassifier.intent.ClassificationIntentFactory;
  import android.view.textclassifier.intent.LabeledIntent;
  import android.view.textclassifier.intent.LegacyClassificationIntentFactory;
  import android.view.textclassifier.intent.TemplateClassificationIntentFactory;
  import android.view.textclassifier.intent.TemplateIntentFactory;
  
  import com.android.internal.annotations.GuardedBy;
  import com.android.internal.util.IndentingPrintWriter;
  import com.android.internal.util.Preconditions;
  
  import com.google.android.textclassifier.ActionsSuggestionsModel;
  import com.google.android.textclassifier.AnnotatorModel;
  import com.google.android.textclassifier.LangIdModel;
  import com.google.android.textclassifier.LangIdModel.LanguageResult;
  
  import java.io.File;
  import java.io.FileNotFoundException;
  import java.io.IOException;
  import java.time.Instant;
  import java.time.ZonedDateTime;
  import java.util.ArrayList;
  import java.util.Collection;
  import java.util.Collections;
  import java.util.List;
  import java.util.Locale;
  import java.util.Map;
  import java.util.Objects;
  import java.util.Set;
  import java.util.function.Supplier;
  
  /**
   * Default implementation of the {@link TextClassifier} interface.
   *
   * <p>This class uses machine learning to recognize entities in text.
   * Unless otherwise stated, methods of this class are blocking operations and should most
   * likely not be called on the UI thread.
   *
   * @hide
   */
  public final class TextClassifierImpl implements TextClassifier {
  
      private static final String LOG_TAG = DEFAULT_LOG_TAG;
  
      private static final boolean DEBUG = false;
  
      private static final File FACTORY_MODEL_DIR = new File("/etc/textclassifier/");
      // Annotator
      private static final String ANNOTATOR_FACTORY_MODEL_FILENAME_REGEX =
              "textclassifier\\.(.*)\\.model";
      private static final File ANNOTATOR_UPDATED_MODEL_FILE =
              new File("/data/misc/textclassifier/textclassifier.model");
  
      // LangID
      private static final String LANG_ID_FACTORY_MODEL_FILENAME_REGEX = "lang_id.model";
      private static final File UPDATED_LANG_ID_MODEL_FILE =
              new File("/data/misc/textclassifier/lang_id.model");
  
      // Actions
      private static final String ACTIONS_FACTORY_MODEL_FILENAME_REGEX =
              "actions_suggestions\\.(.*)\\.model";
      private static final File UPDATED_ACTIONS_MODEL =
              new File("/data/misc/textclassifier/actions_suggestions.model");
  
      private final Context mContext;
      private final TextClassifier mFallback;
      private final GenerateLinksLogger mGenerateLinksLogger;
  
      private final Object mLock = new Object();
  
      @GuardedBy("mLock")
      private ModelFileManager.ModelFile mAnnotatorModelInUse;
      @GuardedBy("mLock")
      private AnnotatorModel mAnnotatorImpl;
  
      @GuardedBy("mLock")
      private ModelFileManager.ModelFile mLangIdModelInUse;
      @GuardedBy("mLock")
      private LangIdModel mLangIdImpl;
  
      @GuardedBy("mLock")
      private ModelFileManager.ModelFile mActionModelInUse;
      @GuardedBy("mLock")
      private ActionsSuggestionsModel mActionsImpl;
  
      private final SelectionSessionLogger mSessionLogger = new SelectionSessionLogger();
      private final TextClassifierEventTronLogger mTextClassifierEventTronLogger =
              new TextClassifierEventTronLogger();
  
      private final TextClassificationConstants mSettings;
  
      private final ModelFileManager mAnnotatorModelFileManager;
      private final ModelFileManager mLangIdModelFileManager;
      private final ModelFileManager mActionsModelFileManager;
  
      private final ClassificationIntentFactory mClassificationIntentFactory;
      private final TemplateIntentFactory mTemplateIntentFactory;
      private final Supplier<ActionsModelParams> mActionsModelParamsSupplier;
  
      public TextClassifierImpl(
              Context context, TextClassificationConstants settings, TextClassifier fallback) {
          mContext = Preconditions.checkNotNull(context);
          mFallback = Preconditions.checkNotNull(fallback);
          mSettings = Preconditions.checkNotNull(settings);
          mGenerateLinksLogger = new GenerateLinksLogger(mSettings.getGenerateLinksLogSampleRate());
          mAnnotatorModelFileManager = new ModelFileManager(
                  new ModelFileManager.ModelFileSupplierImpl(
                          FACTORY_MODEL_DIR,
                          ANNOTATOR_FACTORY_MODEL_FILENAME_REGEX,
                          ANNOTATOR_UPDATED_MODEL_FILE,
                          AnnotatorModel::getVersion,
                          AnnotatorModel::getLocales));
          mLangIdModelFileManager = new ModelFileManager(
                  new ModelFileManager.ModelFileSupplierImpl(
                          FACTORY_MODEL_DIR,
                          LANG_ID_FACTORY_MODEL_FILENAME_REGEX,
                          UPDATED_LANG_ID_MODEL_FILE,
                          LangIdModel::getVersion,
                          fd -> ModelFileManager.ModelFile.LANGUAGE_INDEPENDENT));
          mActionsModelFileManager = new ModelFileManager(
                  new ModelFileManager.ModelFileSupplierImpl(
                          FACTORY_MODEL_DIR,
                          ACTIONS_FACTORY_MODEL_FILENAME_REGEX,
                          UPDATED_ACTIONS_MODEL,
                          ActionsSuggestionsModel::getVersion,
                          ActionsSuggestionsModel::getLocales));
  
          mTemplateIntentFactory = new TemplateIntentFactory();
          mClassificationIntentFactory = mSettings.isTemplateIntentFactoryEnabled()
                  ? new TemplateClassificationIntentFactory(
                  mTemplateIntentFactory, new LegacyClassificationIntentFactory())
                  : new LegacyClassificationIntentFactory();
          mActionsModelParamsSupplier = new ActionsModelParamsSupplier(mContext,
                  () -> {
                      synchronized (mLock) {
                          // Clear mActionsImpl here, so that we will create a new
                          // ActionsSuggestionsModel object with the new flag in the next request.
                          mActionsImpl = null;
                          mActionModelInUse = null;
                      }
                  });
      }
  
      public TextClassifierImpl(Context context, TextClassificationConstants settings) {
          this(context, settings, TextClassifier.NO_OP);
      }
  
      /** @inheritDoc */
      @Override
      @WorkerThread
      public TextSelection suggestSelection(TextSelection.Request request) {
          Preconditions.checkNotNull(request);
          Utils.checkMainThread();
          try {
              final int rangeLength = request.getEndIndex() - request.getStartIndex();
              final String string = request.getText().toString();
              if (string.length() > 0
                      && rangeLength <= mSettings.getSuggestSelectionMaxRangeLength()) {
                  final String localesString = concatenateLocales(request.getDefaultLocales());
                  final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
                  final ZonedDateTime refTime = ZonedDateTime.now();
                  final AnnotatorModel annotatorImpl = getAnnotatorImpl(request.getDefaultLocales());
                  final int start;
                  final int end;
                  if (mSettings.isModelDarkLaunchEnabled() && !request.isDarkLaunchAllowed()) {
                      start = request.getStartIndex();
                      end = request.getEndIndex();
                  } else {
                      final int[] startEnd = annotatorImpl.suggestSelection(
                              string, request.getStartIndex(), request.getEndIndex(),
                              new AnnotatorModel.SelectionOptions(localesString, detectLanguageTags));
                      start = startEnd[0];
                      end = startEnd[1];
                  }
                  if (start < end
                          && start >= 0 && end <= string.length()
                          && start <= request.getStartIndex() && end >= request.getEndIndex()) {
                      final TextSelection.Builder tsBuilder = new TextSelection.Builder(start, end);
                      final AnnotatorModel.ClassificationResult[] results =
                              annotatorImpl.classifyText(
                                      string, start, end,
                                      new AnnotatorModel.ClassificationOptions(
                                              refTime.toInstant().toEpochMilli(),
                                              refTime.getZone().getId(),
                                              localesString,
                                              detectLanguageTags),
                                      // Passing null here to suppress intent generation
                                      // TODO: Use an explicit flag to suppress it.
                                      /* appContext */ null,
                                      /* deviceLocales */null);
                      final int size = results.length;
                      for (int i = 0; i < size; i++) {
                          tsBuilder.setEntityType(results[i].getCollection(), results[i].getScore());
                      }
                      return tsBuilder.setId(createId(
                              string, request.getStartIndex(), request.getEndIndex()))
                              .build();
                  } else {
                      // We can not trust the result. Log the issue and ignore the result.
                      Log.d(LOG_TAG, "Got bad indices for input text. Ignoring result.");
                  }
              }
          } catch (Throwable t) {
              // Avoid throwing from this method. Log the error.
              Log.e(LOG_TAG,
                      "Error suggesting selection for text. No changes to selection suggested.",
                      t);
          }
          // Getting here means something went wrong, return a NO_OP result.
          return mFallback.suggestSelection(request);
      }
  
      /** @inheritDoc */
      @Override
      @WorkerThread
      public TextClassification classifyText(TextClassification.Request request) {
          Preconditions.checkNotNull(request);
          Utils.checkMainThread();
          try {
              final int rangeLength = request.getEndIndex() - request.getStartIndex();
              final String string = request.getText().toString();
              if (string.length() > 0 && rangeLength <= mSettings.getClassifyTextMaxRangeLength()) {
                  final String localesString = concatenateLocales(request.getDefaultLocales());
                  final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
                  final ZonedDateTime refTime = request.getReferenceTime() != null
                          ? request.getReferenceTime() : ZonedDateTime.now();
                  final AnnotatorModel.ClassificationResult[] results =
                          getAnnotatorImpl(request.getDefaultLocales())
                                  .classifyText(
                                          string, request.getStartIndex(), request.getEndIndex(),
                                          new AnnotatorModel.ClassificationOptions(
                                                  refTime.toInstant().toEpochMilli(),
                                                  refTime.getZone().getId(),
                                                  localesString,
                                                  detectLanguageTags),
                                          mContext,
                                          getResourceLocalesString()
                                  );
                  if (results.length > 0) {
                      return createClassificationResult(
                              results, string,
                              request.getStartIndex(), request.getEndIndex(), refTime.toInstant());
                  }
              }
          } catch (Throwable t) {
              // Avoid throwing from this method. Log the error.
              Log.e(LOG_TAG, "Error getting text classification info.", t);
          }
          // Getting here means something went wrong, return a NO_OP result.
          return mFallback.classifyText(request);
      }
  
      /** @inheritDoc */
      @Override
      @WorkerThread
      public TextLinks generateLinks(@NonNull TextLinks.Request request) {
          Preconditions.checkNotNull(request);
          Utils.checkTextLength(request.getText(), getMaxGenerateLinksTextLength());
          Utils.checkMainThread();
  
          if (!mSettings.isSmartLinkifyEnabled() && request.isLegacyFallback()) {
              return Utils.generateLegacyLinks(request);
          }
  
          final String textString = request.getText().toString();
          final TextLinks.Builder builder = new TextLinks.Builder(textString);
  
          try {
              final long startTimeMs = System.currentTimeMillis();
              final ZonedDateTime refTime = ZonedDateTime.now();
              final Collection<String> entitiesToIdentify = request.getEntityConfig() != null
                      ? request.getEntityConfig().resolveEntityListModifications(
                      getEntitiesForHints(request.getEntityConfig().getHints()))
                      : mSettings.getEntityListDefault();
              final String localesString = concatenateLocales(request.getDefaultLocales());
              final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
              final AnnotatorModel annotatorImpl =
                      getAnnotatorImpl(request.getDefaultLocales());
              final boolean isSerializedEntityDataEnabled =
                      ExtrasUtils.isSerializedEntityDataEnabled(request);
              final AnnotatorModel.AnnotatedSpan[] annotations =
                      annotatorImpl.annotate(
                              textString,
                              new AnnotatorModel.AnnotationOptions(
                                      refTime.toInstant().toEpochMilli(),
                                      refTime.getZone().getId(),
                                      localesString,
                                      detectLanguageTags,
                                      entitiesToIdentify,
                                      AnnotatorModel.AnnotationUsecase.SMART.getValue(),
                                      isSerializedEntityDataEnabled));
              for (AnnotatorModel.AnnotatedSpan span : annotations) {
                  final AnnotatorModel.ClassificationResult[] results =
                          span.getClassification();
                  if (results.length == 0
                          || !entitiesToIdentify.contains(results[0].getCollection())) {
                      continue;
                  }
                  final Map<String, Float> entityScores = new ArrayMap<>();
                  for (int i = 0; i < results.length; i++) {
                      entityScores.put(results[i].getCollection(), results[i].getScore());
                  }
                  Bundle extras = new Bundle();
                  if (isSerializedEntityDataEnabled) {
                      ExtrasUtils.putEntities(extras, results);
                  }
                  builder.addLink(span.getStartIndex(), span.getEndIndex(), entityScores, extras);
              }
              final TextLinks links = builder.build();
              final long endTimeMs = System.currentTimeMillis();
              final String callingPackageName = request.getCallingPackageName() == null
                      ? mContext.getPackageName()  // local (in process) TC.
                      : request.getCallingPackageName();
              mGenerateLinksLogger.logGenerateLinks(
                      request.getText(), links, callingPackageName, endTimeMs - startTimeMs);
              return links;
          } catch (Throwable t) {
              // Avoid throwing from this method. Log the error.
              Log.e(LOG_TAG, "Error getting links info.", t);
          }
          return mFallback.generateLinks(request);
      }
  
      /** @inheritDoc */
      @Override
      public int getMaxGenerateLinksTextLength() {
          return mSettings.getGenerateLinksMaxTextLength();
      }
  
      private Collection<String> getEntitiesForHints(Collection<String> hints) {
          final boolean editable = hints.contains(HINT_TEXT_IS_EDITABLE);
          final boolean notEditable = hints.contains(HINT_TEXT_IS_NOT_EDITABLE);
  
          // Use the default if there is no hint, or conflicting ones.
          final boolean useDefault = editable == notEditable;
          if (useDefault) {
              return mSettings.getEntityListDefault();
          } else if (editable) {
              return mSettings.getEntityListEditable();
          } else {  // notEditable
              return mSettings.getEntityListNotEditable();
          }
      }
  
      /** @inheritDoc */
      @Override
      public void onSelectionEvent(SelectionEvent event) {
          mSessionLogger.writeEvent(event);
      }
  
      @Override
      public void onTextClassifierEvent(TextClassifierEvent event) {
          if (DEBUG) {
              Log.d(DEFAULT_LOG_TAG, "onTextClassifierEvent() called with: event = [" + event + "]");
          }
          try {
              final SelectionEvent selEvent = event.toSelectionEvent();
              if (selEvent != null) {
                  mSessionLogger.writeEvent(selEvent);
              } else {
                  mTextClassifierEventTronLogger.writeEvent(event);
              }
          } catch (Exception e) {
              Log.e(LOG_TAG, "Error writing event", e);
          }
      }
  
      /** @inheritDoc */
      @Override
      public TextLanguage detectLanguage(@NonNull TextLanguage.Request request) {
          Preconditions.checkNotNull(request);
          Utils.checkMainThread();
          try {
              final TextLanguage.Builder builder = new TextLanguage.Builder();
              final LangIdModel.LanguageResult[] langResults =
                      getLangIdImpl().detectLanguages(request.getText().toString());
              for (int i = 0; i < langResults.length; i++) {
                  builder.putLocale(
                          ULocale.forLanguageTag(langResults[i].getLanguage()),
                          langResults[i].getScore());
              }
              return builder.build();
          } catch (Throwable t) {
              // Avoid throwing from this method. Log the error.
              Log.e(LOG_TAG, "Error detecting text language.", t);
          }
          return mFallback.detectLanguage(request);
      }
  
      @Override
      public ConversationActions suggestConversationActions(ConversationActions.Request request) {
          Preconditions.checkNotNull(request);
          Utils.checkMainThread();
          try {
              ActionsSuggestionsModel actionsImpl = getActionsImpl();
              if (actionsImpl == null) {
                  // Actions model is optional, fallback if it is not available.
                  return mFallback.suggestConversationActions(request);
              }
              ActionsSuggestionsModel.ConversationMessage[] nativeMessages =
                      ActionsSuggestionsHelper.toNativeMessages(
                              request.getConversation(), this::detectLanguageTagsFromText);
              if (nativeMessages.length == 0) {
                  return mFallback.suggestConversationActions(request);
              }
              ActionsSuggestionsModel.Conversation nativeConversation =
                      new ActionsSuggestionsModel.Conversation(nativeMessages);
  
              ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions =
                      actionsImpl.suggestActionsWithIntents(
                              nativeConversation,
                              null,
                              mContext,
                              getResourceLocalesString(),
                              getAnnotatorImpl(LocaleList.getDefault()));
              return createConversationActionResult(request, nativeSuggestions);
          } catch (Throwable t) {
              // Avoid throwing from this method. Log the error.
              Log.e(LOG_TAG, "Error suggesting conversation actions.", t);
          }
          return mFallback.suggestConversationActions(request);
      }
  
      /**
       * Returns the {@link ConversationAction} result, with a non-null extras.
       * <p>
       * Whenever the RemoteAction is non-null, you can expect its corresponding intent
       * with a non-null component name is in the extras.
       */
      private ConversationActions createConversationActionResult(
              ConversationActions.Request request,
              ActionsSuggestionsModel.ActionSuggestion[] nativeSuggestions) {
          Collection<String> expectedTypes = resolveActionTypesFromRequest(request);
          List<ConversationAction> conversationActions = new ArrayList<>();
          for (ActionsSuggestionsModel.ActionSuggestion nativeSuggestion : nativeSuggestions) {
              String actionType = nativeSuggestion.getActionType();
              if (!expectedTypes.contains(actionType)) {
                  continue;
              }
              LabeledIntent.Result labeledIntentResult =
                      ActionsSuggestionsHelper.createLabeledIntentResult(
                              mContext,
                              mTemplateIntentFactory,
                              nativeSuggestion);
              RemoteAction remoteAction = null;
              Bundle extras = new Bundle();
              if (labeledIntentResult != null) {
                  remoteAction = labeledIntentResult.remoteAction;
                  ExtrasUtils.putActionIntent(extras, labeledIntentResult.resolvedIntent);
              }
              ExtrasUtils.putSerializedEntityData(extras, nativeSuggestion.getSerializedEntityData());
              ExtrasUtils.putEntitiesExtras(
                      extras,
                      TemplateIntentFactory.nameVariantsToBundle(nativeSuggestion.getEntityData()));
              conversationActions.add(
                      new ConversationAction.Builder(actionType)
                              .setConfidenceScore(nativeSuggestion.getScore())
                              .setTextReply(nativeSuggestion.getResponseText())
                              .setAction(remoteAction)
                              .setExtras(extras)
                              .build());
          }
          conversationActions =
                  ActionsSuggestionsHelper.removeActionsWithDuplicates(conversationActions);
          if (request.getMaxSuggestions() >= 0
                  && conversationActions.size() > request.getMaxSuggestions()) {
              conversationActions = conversationActions.subList(0, request.getMaxSuggestions());
          }
          String resultId = ActionsSuggestionsHelper.createResultId(
                  mContext,
                  request.getConversation(),
                  mActionModelInUse.getVersion(),
                  mActionModelInUse.getSupportedLocales());
          return new ConversationActions(conversationActions, resultId);
      }
  
      @Nullable
      private String detectLanguageTagsFromText(CharSequence text) {
          if (!mSettings.isDetectLanguagesFromTextEnabled()) {
              return null;
          }
          final float threshold = getLangIdThreshold();
          if (threshold < 0 || threshold > 1) {
              Log.w(LOG_TAG,
                      "[detectLanguageTagsFromText] unexpected threshold is found: " + threshold);
              return null;
          }
          TextLanguage.Request request = new TextLanguage.Request.Builder(text).build();
          TextLanguage textLanguage = detectLanguage(request);
          int localeHypothesisCount = textLanguage.getLocaleHypothesisCount();
          List<String> languageTags = new ArrayList<>();
          for (int i = 0; i < localeHypothesisCount; i++) {
              ULocale locale = textLanguage.getLocale(i);
              if (textLanguage.getConfidenceScore(locale) < threshold) {
                  break;
              }
              languageTags.add(locale.toLanguageTag());
          }
          if (languageTags.isEmpty()) {
              return null;
          }
          return String.join(",", languageTags);
      }
  
      private Collection<String> resolveActionTypesFromRequest(ConversationActions.Request request) {
          List<String> defaultActionTypes =
                  request.getHints().contains(ConversationActions.Request.HINT_FOR_NOTIFICATION)
                          ? mSettings.getNotificationConversationActionTypes()
                          : mSettings.getInAppConversationActionTypes();
          return request.getTypeConfig().resolveEntityListModifications(defaultActionTypes);
      }
  
      private AnnotatorModel getAnnotatorImpl(LocaleList localeList)
              throws FileNotFoundException {
          synchronized (mLock) {
              localeList = localeList == null ? LocaleList.getDefault() : localeList;
              final ModelFileManager.ModelFile bestModel =
                      mAnnotatorModelFileManager.findBestModelFile(localeList);
              if (bestModel == null) {
                  throw new FileNotFoundException(
                          "No annotator model for " + localeList.toLanguageTags());
              }
              if (mAnnotatorImpl == null || !Objects.equals(mAnnotatorModelInUse, bestModel)) {
                  Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
                  final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                          new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
                  try {
                      if (pfd != null) {
                          // The current annotator model may be still used by another thread / model.
                          // Do not call close() here, and let the GC to clean it up when no one else
                          // is using it.
                          mAnnotatorImpl = new AnnotatorModel(pfd.getFd());
                          mAnnotatorModelInUse = bestModel;
                      }
                  } finally {
                      maybeCloseAndLogError(pfd);
                  }
              }
              return mAnnotatorImpl;
          }
      }
  
      private LangIdModel getLangIdImpl() throws FileNotFoundException {
          synchronized (mLock) {
              final ModelFileManager.ModelFile bestModel =
                      mLangIdModelFileManager.findBestModelFile(null);
              if (bestModel == null) {
                  throw new FileNotFoundException("No LangID model is found");
              }
              if (mLangIdImpl == null || !Objects.equals(mLangIdModelInUse, bestModel)) {
                  Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
                  final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                          new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
                  try {
                      if (pfd != null) {
                          mLangIdImpl = new LangIdModel(pfd.getFd());
                          mLangIdModelInUse = bestModel;
                      }
                  } finally {
                      maybeCloseAndLogError(pfd);
                  }
              }
              return mLangIdImpl;
          }
      }
  
      @Nullable
      private ActionsSuggestionsModel getActionsImpl() throws FileNotFoundException {
          synchronized (mLock) {
              // TODO: Use LangID to determine the locale we should use here?
              final ModelFileManager.ModelFile bestModel =
                      mActionsModelFileManager.findBestModelFile(LocaleList.getDefault());
              if (bestModel == null) {
                  return null;
              }
              if (mActionsImpl == null || !Objects.equals(mActionModelInUse, bestModel)) {
                  Log.d(DEFAULT_LOG_TAG, "Loading " + bestModel);
                  final ParcelFileDescriptor pfd = ParcelFileDescriptor.open(
                          new File(bestModel.getPath()), ParcelFileDescriptor.MODE_READ_ONLY);
                  try {
                      if (pfd == null) {
                          Log.d(LOG_TAG, "Failed to read the model file: " + bestModel.getPath());
                          return null;
                      }
                      ActionsModelParams params = mActionsModelParamsSupplier.get();
                      mActionsImpl = new ActionsSuggestionsModel(
                              pfd.getFd(), params.getSerializedPreconditions(bestModel));
                      mActionModelInUse = bestModel;
                  } finally {
                      maybeCloseAndLogError(pfd);
                  }
              }
              return mActionsImpl;
          }
      }
  
      private String createId(String text, int start, int end) {
          synchronized (mLock) {
              return SelectionSessionLogger.createId(text, start, end, mContext,
                      mAnnotatorModelInUse.getVersion(),
                      mAnnotatorModelInUse.getSupportedLocales());
          }
      }
  
      private static String concatenateLocales(@Nullable LocaleList locales) {
          return (locales == null) ? "" : locales.toLanguageTags();
      }
  
      private TextClassification createClassificationResult(
              AnnotatorModel.ClassificationResult[] classifications,
              String text, int start, int end, @Nullable Instant referenceTime) {
          final String classifiedText = text.substring(start, end);
          final TextClassification.Builder builder = new TextClassification.Builder()
                  .setText(classifiedText);
  
          final int typeCount = classifications.length;
          AnnotatorModel.ClassificationResult highestScoringResult =
                  typeCount > 0 ? classifications[0] : null;
          for (int i = 0; i < typeCount; i++) {
              builder.setEntityType(classifications[i]);
              if (classifications[i].getScore() > highestScoringResult.getScore()) {
                  highestScoringResult = classifications[i];
              }
          }
  
          final Pair<Bundle, Bundle> languagesBundles = generateLanguageBundles(text, start, end);
          final Bundle textLanguagesBundle = languagesBundles.first;
          final Bundle foreignLanguageBundle = languagesBundles.second;
          builder.setForeignLanguageExtra(foreignLanguageBundle);
  
          boolean isPrimaryAction = true;
          final List<LabeledIntent> labeledIntents = mClassificationIntentFactory.create(
                  mContext,
                  classifiedText,
                  foreignLanguageBundle != null,
                  referenceTime,
                  highestScoringResult);
          final LabeledIntent.TitleChooser titleChooser =
                  (labeledIntent, resolveInfo) -> labeledIntent.titleWithoutEntity;
  
          for (LabeledIntent labeledIntent : labeledIntents) {
              final LabeledIntent.Result result =
                      labeledIntent.resolve(mContext, titleChooser, textLanguagesBundle);
              if (result == null) {
                  continue;
              }
  
              final Intent intent = result.resolvedIntent;
              final RemoteAction action = result.remoteAction;
              if (isPrimaryAction) {
                  // For O backwards compatibility, the first RemoteAction is also written to the
                  // legacy API fields.
                  builder.setIcon(action.getIcon().loadDrawable(mContext));
                  builder.setLabel(action.getTitle().toString());
                  builder.setIntent(intent);
                  builder.setOnClickListener(TextClassification.createIntentOnClickListener(
                          TextClassification.createPendingIntent(
                                  mContext, intent, labeledIntent.requestCode)));
                  isPrimaryAction = false;
              }
              builder.addAction(action, intent);
          }
          return builder.setId(createId(text, start, end)).build();
      }
  
      /**
       * Returns a bundle pair with language detection information for extras.
       * <p>
       * Pair.first = textLanguagesBundle - A bundle containing information about all detected
       * languages in the text. May be null if language detection fails or is disabled. This is
       * typically expected to be added to a textClassifier generated remote action intent.
       * See {@link ExtrasUtils#putTextLanguagesExtra(Bundle, Bundle)}.
       * See {@link ExtrasUtils#getTopLanguage(Intent)}.
       * <p>
       * Pair.second = foreignLanguageBundle - A bundle with the language and confidence score if the
       * system finds the text to be in a foreign language. Otherwise is null.
       * See {@link TextClassification.Builder#setForeignLanguageExtra(Bundle)}.
       *
       * @param context the context of the text to detect languages for
       * @param start the start index of the text
       * @param end the end index of the text
       */
      // TODO: Revisit this algorithm.
      // TODO: Consider making this public API.
      private Pair<Bundle, Bundle> generateLanguageBundles(String context, int start, int end) {
          if (!mSettings.isTranslateInClassificationEnabled()) {
              return null;
          }
          try {
              final float threshold = getLangIdThreshold();
              if (threshold < 0 || threshold > 1) {
                  Log.w(LOG_TAG,
                          "[detectForeignLanguage] unexpected threshold is found: " + threshold);
                  return Pair.create(null, null);
              }
  
              final EntityConfidence languageScores = detectLanguages(context, start, end);
              if (languageScores.getEntities().isEmpty()) {
                  return Pair.create(null, null);
              }
  
              final Bundle textLanguagesBundle = new Bundle();
              ExtrasUtils.putTopLanguageScores(textLanguagesBundle, languageScores);
  
              final String language = languageScores.getEntities().get(0);
              final float score = languageScores.getConfidenceScore(language);
              if (score < threshold) {
                  return Pair.create(textLanguagesBundle, null);
              }
  
              Log.v(LOG_TAG, String.format(
                      Locale.US, "Language detected: <%s:%.2f>", language, score));
  
              final Locale detected = new Locale(language);
              final LocaleList deviceLocales = LocaleList.getDefault();
              final int size = deviceLocales.size();
              for (int i = 0; i < size; i++) {
                  if (deviceLocales.get(i).getLanguage().equals(detected.getLanguage())) {
                      return Pair.create(textLanguagesBundle, null);
                  }
              }
              final Bundle foreignLanguageBundle = ExtrasUtils.createForeignLanguageExtra(
                      detected.getLanguage(), score, getLangIdImpl().getVersion());
              return Pair.create(textLanguagesBundle, foreignLanguageBundle);
          } catch (Throwable t) {
              Log.e(LOG_TAG, "Error generating language bundles.", t);
          }
          return Pair.create(null, null);
      }
  
      /**
       * Detect the language of a piece of text by taking surrounding text into consideration.
       *
       * @param text text providing context for the text for which its language is to be detected
       * @param start the start index of the text to detect its language
       * @param end the end index of the text to detect its language
       */
      // TODO: Revisit this algorithm.
      private EntityConfidence detectLanguages(String text, int start, int end)
              throws FileNotFoundException {
          Preconditions.checkArgument(start >= 0);
          Preconditions.checkArgument(end <= text.length());
          Preconditions.checkArgument(start <= end);
  
          final float[] langIdContextSettings = mSettings.getLangIdContextSettings();
          // The minimum size of text to prefer for detection.
          final int minimumTextSize = (int) langIdContextSettings[0];
          // For reducing the score when text is less than the preferred size.
          final float penalizeRatio = langIdContextSettings[1];
          // Original detection score to surrounding text detection score ratios.
          final float subjectTextScoreRatio = langIdContextSettings[2];
          final float moreTextScoreRatio = 1f - subjectTextScoreRatio;
          Log.v(LOG_TAG,
                  String.format(Locale.US, "LangIdContextSettings: "
                                  + "minimumTextSize=%d, penalizeRatio=%.2f, "
                                  + "subjectTextScoreRatio=%.2f, moreTextScoreRatio=%.2f",
                          minimumTextSize, penalizeRatio, subjectTextScoreRatio, moreTextScoreRatio));
  
          if (end - start < minimumTextSize && penalizeRatio <= 0) {
              return new EntityConfidence(Collections.emptyMap());
          }
  
          final String subject = text.substring(start, end);
          final EntityConfidence scores = detectLanguages(subject);
  
          if (subject.length() >= minimumTextSize
                  || subject.length() == text.length()
                  || subjectTextScoreRatio * penalizeRatio >= 1) {
              return scores;
          }
  
          final EntityConfidence moreTextScores;
          if (moreTextScoreRatio >= 0) {
              // Attempt to grow the detection text to be at least minimumTextSize long.
              final String moreText = Utils.getSubString(text, start, end, minimumTextSize);
              moreTextScores = detectLanguages(moreText);
          } else {
              moreTextScores = new EntityConfidence(Collections.emptyMap());
          }
  
          // Combine the original detection scores with the those returned after including more text.
          final Map<String, Float> newScores = new ArrayMap<>();
          final Set<String> languages = new ArraySet<>();
          languages.addAll(scores.getEntities());
          languages.addAll(moreTextScores.getEntities());
          for (String language : languages) {
              final float score = (subjectTextScoreRatio * scores.getConfidenceScore(language)
                      + moreTextScoreRatio * moreTextScores.getConfidenceScore(language))
                      * penalizeRatio;
              newScores.put(language, score);
          }
          return new EntityConfidence(newScores);
      }
  
      /**
       * Detect languages for the specified text.
       */
      private EntityConfidence detectLanguages(String text) throws FileNotFoundException {
          final LangIdModel langId = getLangIdImpl();
          final LangIdModel.LanguageResult[] langResults = langId.detectLanguages(text);
          final Map<String, Float> languagesMap = new ArrayMap<>();
          for (LanguageResult langResult : langResults) {
              languagesMap.put(langResult.getLanguage(), langResult.getScore());
          }
          return new EntityConfidence(languagesMap);
      }
  
      private float getLangIdThreshold() {
          try {
              return mSettings.getLangIdThresholdOverride() >= 0
                      ? mSettings.getLangIdThresholdOverride()
                      : getLangIdImpl().getLangIdThreshold();
          } catch (FileNotFoundException e) {
              final float defaultThreshold = 0.5f;
              Log.v(LOG_TAG, "Using default foreign language threshold: " + defaultThreshold);
              return defaultThreshold;
          }
      }
  
      @Override
      public void dump(@NonNull IndentingPrintWriter printWriter) {
          synchronized (mLock) {
              printWriter.println("TextClassifierImpl:");
              printWriter.increaseIndent();
              printWriter.println("Annotator model file(s):");
              printWriter.increaseIndent();
              for (ModelFileManager.ModelFile modelFile :
                      mAnnotatorModelFileManager.listModelFiles()) {
                  printWriter.println(modelFile.toString());
              }
              printWriter.decreaseIndent();
              printWriter.println("LangID model file(s):");
              printWriter.increaseIndent();
              for (ModelFileManager.ModelFile modelFile :
                      mLangIdModelFileManager.listModelFiles()) {
                  printWriter.println(modelFile.toString());
              }
              printWriter.decreaseIndent();
              printWriter.println("Actions model file(s):");
              printWriter.increaseIndent();
              for (ModelFileManager.ModelFile modelFile :
                      mActionsModelFileManager.listModelFiles()) {
                  printWriter.println(modelFile.toString());
              }
              printWriter.decreaseIndent();
              printWriter.printPair("mFallback", mFallback);
              printWriter.decreaseIndent();
              printWriter.println();
          }
      }
  
      /**
       * Closes the ParcelFileDescriptor, if non-null, and logs any errors that occur.
       */
      private static void maybeCloseAndLogError(@Nullable ParcelFileDescriptor fd) {
          if (fd == null) {
              return;
          }
  
          try {
              fd.close();
          } catch (IOException e) {
              Log.e(LOG_TAG, "Error closing file.", e);
          }
      }
  
      /**
       * Returns the locales string for the current resources configuration.
       */
      private String getResourceLocalesString() {
          try {
              return mContext.getResources().getConfiguration().getLocales().toLanguageTags();
          } catch (NullPointerException e) {
              // NPE is unexpected. Erring on the side of caution.
              return LocaleList.getDefault().toLanguageTags();
          }
      }
  }
  