Include language information in intents from the TextClassifier.

This CL runs langid on classifyText() and sets the language information
of the classified text in Intent extras that are sent to the destination
app. If the text is too small for langId to detect the text reliably,
include surrounding text as necessary.

Includes flags to tune/disable how sorrounding text is used.
Also fixes failing tests.

Bug: 126453204
Test: atest android.view.textclassifier
Change-Id: I9ef2a33690dc3787f6544ceed0f41c527f4423e5
diff --git a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
index e7ae7a0..9c268f2 100644
--- a/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
+++ b/core/java/android/view/textclassifier/ActionsSuggestionsHelper.java
@@ -143,7 +143,7 @@
         // intent for each action type.
         LabeledIntent.TitleChooser titleChooser =
                 ActionsSuggestionsHelper.createTitleChooser(nativeSuggestion.getActionType());
-        return labeledIntents.get(0).resolve(context, titleChooser);
+        return labeledIntents.get(0).resolve(context, titleChooser, null);
     }
 
     /**
diff --git a/core/java/android/view/textclassifier/ExtrasUtils.java b/core/java/android/view/textclassifier/ExtrasUtils.java
index 05702bf..eadad28 100644
--- a/core/java/android/view/textclassifier/ExtrasUtils.java
+++ b/core/java/android/view/textclassifier/ExtrasUtils.java
@@ -19,6 +19,7 @@
 import android.annotation.Nullable;
 import android.app.RemoteAction;
 import android.content.Intent;
+import android.icu.util.ULocale;
 import android.os.Bundle;
 
 import java.util.ArrayList;
@@ -27,6 +28,7 @@
  * Utility class for inserting and retrieving data in TextClassifier request/response extras.
  * @hide
  */
+// TODO: Make this a TestApi for CTS testing.
 public final class ExtrasUtils {
 
     private static final String ENTITIES_EXTRAS = "entities-extras";
@@ -37,6 +39,7 @@
     private static final String SCORE = "score";
     private static final String MODEL_VERSION = "model-version";
     private static final String MODEL_NAME = "model-name";
+    private static final String TEXT_LANGUAGES = "text-languages";
 
     private ExtrasUtils() {}
 
@@ -56,6 +59,8 @@
     /**
      * Stores {@code extra} as foreign language information in TextClassifier response object's
      * extras {@code container}.
+     *
+     * @see #getForeignLanguageExtra(TextClassification)
      */
     static void putForeignLanguageExtra(Bundle container, Bundle extra) {
         container.putParcelable(FOREIGN_LANGUAGE, extra);
@@ -64,13 +69,68 @@
     /**
      * Returns foreign language detection information contained in the TextClassification object.
      * responses.
+     *
+     * @see #putForeignLanguageExtra(Bundle, Bundle)
      */
     @Nullable
-    public static Bundle getForeignLanguageExtra(TextClassification classification) {
+    public static Bundle getForeignLanguageExtra(@Nullable TextClassification classification) {
+        if (classification == null) {
+            return null;
+        }
         return classification.getExtras().getBundle(FOREIGN_LANGUAGE);
     }
 
     /**
+     * @see #getTopLanguage(Intent)
+     */
+    static void putTopLanguageScores(Bundle container, EntityConfidence languageScores) {
+        final int maxSize = Math.min(3, languageScores.getEntities().size());
+        final String[] languages = languageScores.getEntities().subList(0, maxSize)
+                .toArray(new String[0]);
+        final float[] scores = new float[languages.length];
+        for (int i = 0; i < languages.length; i++) {
+            scores[i] = languageScores.getConfidenceScore(languages[i]);
+        }
+        container.putStringArray(ENTITY_TYPE, languages);
+        container.putFloatArray(SCORE, scores);
+    }
+
+    /**
+     * @see #putTopLanguageScores(Bundle, EntityConfidence)
+     */
+    @Nullable
+    public static ULocale getTopLanguage(@Nullable Intent intent) {
+        if (intent == null) {
+            return null;
+        }
+        final Bundle tcBundle = intent.getBundleExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER);
+        if (tcBundle == null) {
+            return null;
+        }
+        final Bundle textLanguagesExtra = tcBundle.getBundle(TEXT_LANGUAGES);
+        if (textLanguagesExtra == null) {
+            return null;
+        }
+        final String[] languages = textLanguagesExtra.getStringArray(ENTITY_TYPE);
+        final float[] scores = textLanguagesExtra.getFloatArray(SCORE);
+        if (languages == null || scores == null
+                || languages.length == 0 || languages.length != scores.length) {
+            return null;
+        }
+        int highestScoringIndex = 0;
+        for (int i = 1; i < languages.length; i++) {
+            if (scores[highestScoringIndex] < scores[i]) {
+                highestScoringIndex = i;
+            }
+        }
+        return ULocale.forLanguageTag(languages[highestScoringIndex]);
+    }
+
+    public static void putTextLanguagesExtra(Bundle container, Bundle extra) {
+        container.putBundle(TEXT_LANGUAGES, extra);
+    }
+
+    /**
      * Stores {@code actionIntents} information in TextClassifier response object's extras
      * {@code container}.
      */
@@ -122,7 +182,10 @@
      * Returns {@code actionIntents} information contained in the TextClassification object.
      */
     @Nullable
-    public static ArrayList<Intent> getActionsIntents(TextClassification classification) {
+    public static ArrayList<Intent> getActionsIntents(@Nullable TextClassification classification) {
+        if (classification == null) {
+            return null;
+        }
         return classification.getExtras().getParcelableArrayList(ACTIONS_INTENTS);
     }
 
@@ -131,7 +194,11 @@
      * action string, {@code intentAction}.
      */
     @Nullable
-    public static RemoteAction findAction(TextClassification classification, String intentAction) {
+    public static RemoteAction findAction(
+            @Nullable TextClassification classification, @Nullable String intentAction) {
+        if (classification == null || intentAction == null) {
+            return null;
+        }
         final ArrayList<Intent> actionIntents = getActionsIntents(classification);
         if (actionIntents != null) {
             final int size = actionIntents.size();
@@ -149,7 +216,7 @@
      * Returns the first "translate" action found in the {@code classification} object.
      */
     @Nullable
-    public static RemoteAction findTranslateAction(TextClassification classification) {
+    public static RemoteAction findTranslateAction(@Nullable TextClassification classification) {
         return findAction(classification, Intent.ACTION_TRANSLATE);
     }
 
@@ -157,7 +224,10 @@
      * Returns the entity type contained in the {@code extra}.
      */
     @Nullable
-    public static String getEntityType(Bundle extra) {
+    public static String getEntityType(@Nullable Bundle extra) {
+        if (extra == null) {
+            return null;
+        }
         return extra.getString(ENTITY_TYPE);
     }
 
@@ -166,14 +236,21 @@
      */
     @Nullable
     public static float getScore(Bundle extra) {
-        return extra.getFloat(SCORE, -1);
+        final int defaultValue = -1;
+        if (extra == null) {
+            return defaultValue;
+        }
+        return extra.getFloat(SCORE, defaultValue);
     }
 
     /**
      * Returns the model name contained in the {@code extra}.
      */
     @Nullable
-    public static String getModelName(Bundle extra) {
+    public static String getModelName(@Nullable Bundle extra) {
+        if (extra == null) {
+            return null;
+        }
         return extra.getString(MODEL_NAME);
     }
 }
diff --git a/core/java/android/view/textclassifier/TextClassificationConstants.java b/core/java/android/view/textclassifier/TextClassificationConstants.java
index 2627ae6..876e5cc 100644
--- a/core/java/android/view/textclassifier/TextClassificationConstants.java
+++ b/core/java/android/view/textclassifier/TextClassificationConstants.java
@@ -50,6 +50,7 @@
  * template_intent_factory_enabled                  (boolean)
  * translate_in_classification_enabled              (boolean)
  * detect_languages_from_text_enabled               (boolean)
+ * lang_id_context_settings                         (float[])
  * </pre>
  *
  * <p>
@@ -58,12 +59,14 @@
  *
  * Example of setting the values for testing.
  * adb shell settings put global text_classifier_constants \
- *      model_dark_launch_enabled=true,smart_selection_enabled=true,\
- *      entity_list_default=phone:address
+ *      model_dark_launch_enabled=true,smart_selection_enabled=true, \
+ *      entity_list_default=phone:address, \
+ *      lang_id_context_settings=20:1.0:0.4
  * @hide
  */
 public final class TextClassificationConstants {
-    private static final String LOG_TAG = "TextClassificationConstants";
+
+    private static final String LOG_TAG = TextClassifier.DEFAULT_LOG_TAG;
 
     /**
      * Whether the smart linkify feature is enabled.
@@ -148,7 +151,6 @@
      * Whether to enable {@link android.view.textclassifier.TemplateIntentFactory}.
      */
     private static final String TEMPLATE_INTENT_FACTORY_ENABLED = "template_intent_factory_enabled";
-
     /**
      * Whether to enable "translate" action in classifyText.
      */
@@ -160,6 +162,20 @@
      */
     private static final String DETECT_LANGUAGES_FROM_TEXT_ENABLED =
             "detect_languages_from_text_enabled";
+    /**
+     * A colon(:) separated string that specifies the configuration to use when including
+     * surrounding context text in language detection queries.
+     * <p>
+     * Format= minimumTextSize<int>:penalizeRatio<float>:textScoreRatio<float>
+     * <p>
+     * e.g. 20:1.0:0.4
+     * <p>
+     * Accept all text lengths with minimumTextSize=0
+     * <p>
+     * Reject all text less than minimumTextSize with penalizeRatio=0
+     * @see {@code TextClassifierImpl#detectLanguages(String, int, int)} for reference.
+     */
+    private static final String LANG_ID_CONTEXT_SETTINGS = "lang_id_context_settings";
 
     private static final boolean LOCAL_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
     private static final boolean SYSTEM_TEXT_CLASSIFIER_ENABLED_DEFAULT = true;
@@ -205,6 +221,8 @@
     private static final boolean TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT = true;
     private static final boolean TRANSLATE_IN_CLASSIFICATION_ENABLED_DEFAULT = true;
     private static final boolean DETECT_LANGUAGES_FROM_TEXT_ENABLED_DEFAULT = true;
+    private static final String LANG_ID_CONTEXT_SETTINGS_DEFAULT =
+            new StringJoiner(STRING_LIST_DELIMITER).add("20").add("1.0").add("0.4").toString();
 
     private final boolean mSystemTextClassifierEnabled;
     private final boolean mLocalTextClassifierEnabled;
@@ -226,6 +244,7 @@
     private final boolean mTemplateIntentFactoryEnabled;
     private final boolean mTranslateInClassificationEnabled;
     private final boolean mDetectLanguagesFromTextEnabled;
+    private final float[] mLangIdContextSettings;
 
     private TextClassificationConstants(@Nullable String settings) {
         ConfigParser configParser = new ConfigParser(settings);
@@ -273,9 +292,10 @@
                 configParser.getInt(
                         GENERATE_LINKS_LOG_SAMPLE_RATE,
                         GENERATE_LINKS_LOG_SAMPLE_RATE_DEFAULT);
-        mEntityListDefault = parseStringList(configParser.getString(
-                ENTITY_LIST_DEFAULT,
-                ENTITY_LIST_DEFAULT_VALUE));
+        mEntityListDefault = parseStringList(
+                configParser.getString(
+                        ENTITY_LIST_DEFAULT,
+                        ENTITY_LIST_DEFAULT_VALUE));
         mEntityListNotEditable = parseStringList(
                 configParser.getString(
                         ENTITY_LIST_NOT_EDITABLE,
@@ -296,13 +316,22 @@
                 configParser.getFloat(
                         LANG_ID_THRESHOLD_OVERRIDE,
                         LANG_ID_THRESHOLD_OVERRIDE_DEFAULT);
-        mTemplateIntentFactoryEnabled = configParser.getBoolean(
-                TEMPLATE_INTENT_FACTORY_ENABLED,
-                TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT);
-        mTranslateInClassificationEnabled = configParser.getBoolean(
-                TRANSLATE_IN_CLASSIFICATION_ENABLED, TRANSLATE_IN_CLASSIFICATION_ENABLED_DEFAULT);
-        mDetectLanguagesFromTextEnabled = configParser.getBoolean(
-                DETECT_LANGUAGES_FROM_TEXT_ENABLED, DETECT_LANGUAGES_FROM_TEXT_ENABLED_DEFAULT);
+        mTemplateIntentFactoryEnabled =
+                configParser.getBoolean(
+                        TEMPLATE_INTENT_FACTORY_ENABLED,
+                        TEMPLATE_INTENT_FACTORY_ENABLED_DEFAULT);
+        mTranslateInClassificationEnabled =
+                configParser.getBoolean(
+                        TRANSLATE_IN_CLASSIFICATION_ENABLED,
+                        TRANSLATE_IN_CLASSIFICATION_ENABLED_DEFAULT);
+        mDetectLanguagesFromTextEnabled =
+                configParser.getBoolean(
+                        DETECT_LANGUAGES_FROM_TEXT_ENABLED,
+                        DETECT_LANGUAGES_FROM_TEXT_ENABLED_DEFAULT);
+        mLangIdContextSettings = parseFloatArray(
+                configParser,
+                LANG_ID_CONTEXT_SETTINGS,
+                LANG_ID_CONTEXT_SETTINGS_DEFAULT);
     }
 
     /** Load from a settings string. */
@@ -390,10 +419,35 @@
         return mDetectLanguagesFromTextEnabled;
     }
 
+    public float[] getLangIdContextSettings() {
+        return mLangIdContextSettings;
+    }
+
     private static List<String> parseStringList(String listStr) {
         return Collections.unmodifiableList(Arrays.asList(listStr.split(STRING_LIST_DELIMITER)));
     }
 
+    private static float[] parseFloatArray(
+            ConfigParser configParser, String key, String defaultStr) {
+        final String str = configParser.getString(key, defaultStr);
+        final String[] defaultSplit = defaultStr.split(STRING_LIST_DELIMITER);
+        String[] split = str.split(STRING_LIST_DELIMITER);
+        if (split.length != defaultSplit.length) {
+            Log.v(LOG_TAG, "Error parsing " + key + " flag. Using defaults.");
+            split = defaultSplit;
+        }
+        final float[] result = new float[split.length];
+        for (int i = 0; i < split.length; i++) {
+            try {
+                result[i] = Float.parseFloat(split[i]);
+            } catch (NumberFormatException e) {
+                Log.v(LOG_TAG, "Error parsing part of " + key + " flag. Using defaults.");
+                result[i] = Float.parseFloat(defaultSplit[i]);
+            }
+        }
+        return result;
+    }
+
     void dump(IndentingPrintWriter pw) {
         pw.println("TextClassificationConstants:");
         pw.increaseIndent();
@@ -418,6 +472,7 @@
         pw.printPair("isTemplateIntentFactoryEnabled", mTemplateIntentFactoryEnabled);
         pw.printPair("isTranslateInClassificationEnabled", mTranslateInClassificationEnabled);
         pw.printPair("isDetectLanguageFromTextEnabled", mDetectLanguagesFromTextEnabled);
+        pw.printPair("getLangIdContextSettings", Arrays.toString(mLangIdContextSettings));
         pw.decreaseIndent();
         pw.println();
     }
diff --git a/core/java/android/view/textclassifier/TextClassifier.java b/core/java/android/view/textclassifier/TextClassifier.java
index a4e5502..ac8a429 100644
--- a/core/java/android/view/textclassifier/TextClassifier.java
+++ b/core/java/android/view/textclassifier/TextClassifier.java
@@ -33,11 +33,13 @@
 import android.text.util.Linkify.LinkifyMask;
 import android.util.ArrayMap;
 
+import com.android.internal.annotations.GuardedBy;
 import com.android.internal.util.IndentingPrintWriter;
 import com.android.internal.util.Preconditions;
 
 import java.lang.annotation.Retention;
 import java.lang.annotation.RetentionPolicy;
+import java.text.BreakIterator;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
@@ -644,11 +646,14 @@
      *  <li>Provides validation of input parameters to TextClassifier methods
      * </ul>
      *
-     * Intended to be used only in this package.
+     * Intended to be used only for TextClassifier purposes.
      * @hide
      */
     final class Utils {
 
+        @GuardedBy("WORD_ITERATOR")
+        private static final BreakIterator WORD_ITERATOR = BreakIterator.getWordInstance();
+
         /**
          * @throws IllegalArgumentException if text is null; startIndex is negative;
          *      endIndex is greater than text.length() or is not greater than startIndex;
@@ -666,6 +671,47 @@
         }
 
         /**
+         * Returns the substring of {@code text} that contains at least text from index
+         * {@code start} <i>(inclusive)</i> to index {@code end} <i><(exclusive)/i> with the goal of
+         * returning text that is at least {@code minimumLength}. If {@code text} is not long
+         * enough, this will return {@code text}. This method returns text at word boundaries.
+         *
+         * @param text the source text
+         * @param start the start index of text that must be included
+         * @param end the end index of text that must be included
+         * @param minimumLength minimum length of text to return if {@code text} is long enough
+         */
+        public static String getSubString(
+                String text, int start, int end, int minimumLength) {
+            Preconditions.checkArgument(start >= 0);
+            Preconditions.checkArgument(end <= text.length());
+            Preconditions.checkArgument(start <= end);
+
+            if (text.length() < minimumLength) {
+                return text;
+            }
+
+            final int length = end - start;
+            if (length >= minimumLength) {
+                return text.substring(start, end);
+            }
+
+            final int offset = (minimumLength - length) / 2;
+            int iterStart = Math.max(0, Math.min(start - offset, text.length() - minimumLength));
+            int iterEnd = Math.min(text.length(), iterStart + minimumLength);
+
+            synchronized (WORD_ITERATOR) {
+                WORD_ITERATOR.setText(text);
+                iterStart = WORD_ITERATOR.isBoundary(iterStart)
+                        ? iterStart : Math.max(0, WORD_ITERATOR.preceding(iterStart));
+                iterEnd = WORD_ITERATOR.isBoundary(iterEnd)
+                        ? iterEnd : Math.max(iterEnd, WORD_ITERATOR.following(iterEnd));
+                WORD_ITERATOR.setText("");
+                return text.substring(iterStart, iterEnd);
+            }
+        }
+
+        /**
          * Generates links using legacy {@link Linkify}.
          */
         public static TextLinks generateLegacyLinks(@NonNull TextLinks.Request request) {
diff --git a/core/java/android/view/textclassifier/TextClassifierImpl.java b/core/java/android/view/textclassifier/TextClassifierImpl.java
index 76d0313..8f5f0a37 100644
--- a/core/java/android/view/textclassifier/TextClassifierImpl.java
+++ b/core/java/android/view/textclassifier/TextClassifierImpl.java
@@ -21,10 +21,14 @@
 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.intent.ClassificationIntentFactory;
 import android.view.textclassifier.intent.LabeledIntent;
 import android.view.textclassifier.intent.LegacyClassificationIntentFactory;
@@ -38,6 +42,7 @@
 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;
@@ -46,11 +51,12 @@
 import java.time.ZonedDateTime;
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.HashMap;
+import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 
 /**
  * Default implementation of the {@link TextClassifier} interface.
@@ -90,19 +96,19 @@
 
     private final Object mLock = new Object();
 
-    @GuardedBy("mLock") // Do not access outside this lock.
+    @GuardedBy("mLock")
     private ModelFileManager.ModelFile mAnnotatorModelInUse;
-    @GuardedBy("mLock") // Do not access outside this lock.
+    @GuardedBy("mLock")
     private AnnotatorModel mAnnotatorImpl;
 
-    @GuardedBy("mLock") // Do not access outside this lock.
+    @GuardedBy("mLock")
     private ModelFileManager.ModelFile mLangIdModelInUse;
-    @GuardedBy("mLock") // Do not access outside this lock.
+    @GuardedBy("mLock")
     private LangIdModel mLangIdImpl;
 
-    @GuardedBy("mLock") // Do not access outside this lock.
+    @GuardedBy("mLock")
     private ModelFileManager.ModelFile mActionModelInUse;
-    @GuardedBy("mLock") // Do not access outside this lock.
+    @GuardedBy("mLock")
     private ActionsSuggestionsModel mActionsImpl;
 
     private final SelectionSessionLogger mSessionLogger = new SelectionSessionLogger();
@@ -283,7 +289,7 @@
             final ZonedDateTime refTime = ZonedDateTime.now();
             final Collection<String> entitiesToIdentify = request.getEntityConfig() != null
                     ? request.getEntityConfig().resolveEntityListModifications(
-                    getEntitiesForHints(request.getEntityConfig().getHints()))
+                            getEntitiesForHints(request.getEntityConfig().getHints()))
                     : mSettings.getEntityListDefault();
             final String localesString = concatenateLocales(request.getDefaultLocales());
             final String detectLanguageTags = detectLanguageTagsFromText(request.getText());
@@ -304,7 +310,7 @@
                         || !entitiesToIdentify.contains(results[0].getCollection())) {
                     continue;
                 }
-                final Map<String, Float> entityScores = new HashMap<>();
+                final Map<String, Float> entityScores = new ArrayMap<>();
                 for (int i = 0; i < results.length; i++) {
                     entityScores.put(results[i].getCollection(), results[i].getScore());
                 }
@@ -620,50 +626,67 @@
             }
         }
 
-        final Bundle foreignLanguageBundle = detectForeignLanguage(classifiedText);
+        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;
-        List<LabeledIntent> labeledIntents = mClassificationIntentFactory.create(
+        final List<LabeledIntent> labeledIntents = mClassificationIntentFactory.create(
                 mContext,
                 classifiedText,
                 foreignLanguageBundle != null,
                 referenceTime,
                 highestScoringResult);
-        LabeledIntent.TitleChooser titleChooser =
+        final LabeledIntent.TitleChooser titleChooser =
                 (labeledIntent, resolveInfo) -> labeledIntent.titleWithoutEntity;
+
         for (LabeledIntent labeledIntent : labeledIntents) {
-            LabeledIntent.Result result = labeledIntent.resolve(mContext, titleChooser);
+            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(result.resolvedIntent);
+                builder.setIntent(intent);
                 builder.setOnClickListener(TextClassification.createIntentOnClickListener(
-                        TextClassification.createPendingIntent(mContext,
-                                result.resolvedIntent, labeledIntent.requestCode)));
+                        TextClassification.createPendingIntent(
+                                mContext, intent, labeledIntent.requestCode)));
                 isPrimaryAction = false;
             }
-            builder.addAction(action, result.resolvedIntent);
+            builder.addAction(action, intent);
         }
 
         return builder.setId(createId(text, start, end)).build();
     }
 
     /**
-     * Returns a bundle with the language and confidence score if it finds the text to be
-     * in a foreign language. Otherwise returns null. This algorithm defines what the system thinks
-     * is a foreign language.
+     * 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.
-    @Nullable
-    private Bundle detectForeignLanguage(String text) {
+    private Pair<Bundle, Bundle> generateLanguageBundles(String context, int start, int end) {
         if (!mSettings.isTranslateInClassificationEnabled()) {
             return null;
         }
@@ -672,42 +695,118 @@
             if (threshold < 0 || threshold > 1) {
                 Log.w(LOG_TAG,
                         "[detectForeignLanguage] unexpected threshold is found: " + threshold);
-                return null;
+                return Pair.create(null, null);
             }
 
-            final LangIdModel langId = getLangIdImpl();
-            final LangIdModel.LanguageResult[] langResults = langId.detectLanguages(text);
-            if (langResults.length <= 0) {
-                return null;
+            final EntityConfidence languageScores = detectLanguages(context, start, end);
+            if (languageScores.getEntities().isEmpty()) {
+                return Pair.create(null, null);
             }
 
-            LangIdModel.LanguageResult highestScoringResult = langResults[0];
-            for (int i = 1; i < langResults.length; i++) {
-                if (langResults[i].getScore() > highestScoringResult.getScore()) {
-                    highestScoringResult = langResults[i];
-                }
-            }
-            if (highestScoringResult.getScore() < threshold) {
-                return 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("Language detected: <%s:%s>",
-                    highestScoringResult.getLanguage(), highestScoringResult.getScore()));
+            Log.v(LOG_TAG, String.format(
+                    Locale.US, "Language detected: <%s:%.2f>", language, score));
 
-            final Locale detected = new Locale(highestScoringResult.getLanguage());
+            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 null;
+                    return Pair.create(textLanguagesBundle, null);
                 }
             }
-            return ExtrasUtils.createForeignLanguageExtra(
-                    detected.getLanguage(), highestScoringResult.getScore(), langId.getVersion());
+            final Bundle foreignLanguageBundle = ExtrasUtils.createForeignLanguageExtra(
+                    detected.getLanguage(), score, getLangIdImpl().getVersion());
+            return Pair.create(textLanguagesBundle, foreignLanguageBundle);
         } catch (Throwable t) {
-            Log.e(LOG_TAG, "Error detecting foreign text. Ignored.", t);
+            Log.e(LOG_TAG, "Error generating language bundles.", t);
         }
-        return null;
+        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() {
diff --git a/core/java/android/view/textclassifier/intent/ClassificationIntentFactory.java b/core/java/android/view/textclassifier/intent/ClassificationIntentFactory.java
index 4896ae6..b034846 100644
--- a/core/java/android/view/textclassifier/intent/ClassificationIntentFactory.java
+++ b/core/java/android/view/textclassifier/intent/ClassificationIntentFactory.java
@@ -18,7 +18,6 @@
 import android.annotation.Nullable;
 import android.content.Context;
 import android.content.Intent;
-import android.view.textclassifier.TextClassifier;
 
 import com.google.android.textclassifier.AnnotatorModel;
 
@@ -52,8 +51,7 @@
                 new Intent(Intent.ACTION_TRANSLATE)
                         // TODO: Probably better to introduce a "translate" scheme instead of
                         // using EXTRA_TEXT.
-                        .putExtra(Intent.EXTRA_TEXT, text)
-                        .putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true),
+                        .putExtra(Intent.EXTRA_TEXT, text),
                 text.hashCode()));
     }
 }
diff --git a/core/java/android/view/textclassifier/intent/LabeledIntent.java b/core/java/android/view/textclassifier/intent/LabeledIntent.java
index 4b32f1e..11d64f1 100644
--- a/core/java/android/view/textclassifier/intent/LabeledIntent.java
+++ b/core/java/android/view/textclassifier/intent/LabeledIntent.java
@@ -24,9 +24,12 @@
 import android.content.pm.PackageManager;
 import android.content.pm.ResolveInfo;
 import android.graphics.drawable.Icon;
+import android.os.Bundle;
 import android.text.TextUtils;
+import android.view.textclassifier.ExtrasUtils;
 import android.view.textclassifier.Log;
 import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
 
 import com.android.internal.annotations.VisibleForTesting;
 import com.android.internal.util.Preconditions;
@@ -87,10 +90,16 @@
 
     /**
      * Return the resolved result.
+     *
+     * @param context the context to resolve the result's intent and action
+     * @param titleChooser for choosing an action title
+     * @param textLanguagesBundle containing language detection information
      */
     @Nullable
     public Result resolve(
-            Context context, @Nullable TitleChooser titleChooser) {
+            Context context,
+            @Nullable TitleChooser titleChooser,
+            @Nullable Bundle textLanguagesBundle) {
         final PackageManager pm = context.getPackageManager();
         final ResolveInfo resolveInfo = pm.resolveActivity(intent, 0);
 
@@ -106,6 +115,10 @@
         }
         Intent resolvedIntent = new Intent(intent);
         resolvedIntent.setComponent(new ComponentName(packageName, className));
+        resolvedIntent.putExtra(
+                TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER,
+                getFromTextClassifierExtra(textLanguagesBundle));
+
         boolean shouldShowIcon = false;
         Icon icon = null;
         if (!"android".equals(packageName)) {
@@ -117,25 +130,32 @@
         }
         if (icon == null) {
             // RemoteAction requires that there be an icon.
-            icon = Icon.createWithResource("android",
-                    com.android.internal.R.drawable.ic_more_items);
+            icon = Icon.createWithResource(
+                    "android", com.android.internal.R.drawable.ic_more_items);
         }
         final PendingIntent pendingIntent =
                 TextClassification.createPendingIntent(context, resolvedIntent, requestCode);
-        if (titleChooser == null) {
-            titleChooser = DEFAULT_TITLE_CHOOSER;
-        }
+        titleChooser = titleChooser == null ? DEFAULT_TITLE_CHOOSER : titleChooser;
         CharSequence title = titleChooser.chooseTitle(this, resolveInfo);
         if (TextUtils.isEmpty(title)) {
             Log.w(TAG, "Custom titleChooser return null, fallback to the default titleChooser");
             title = DEFAULT_TITLE_CHOOSER.chooseTitle(this, resolveInfo);
         }
-        final RemoteAction action =
-                new RemoteAction(icon, title, description, pendingIntent);
+        final RemoteAction action = new RemoteAction(icon, title, description, pendingIntent);
         action.setShouldShowIcon(shouldShowIcon);
         return new Result(resolvedIntent, action);
     }
 
+    private Bundle getFromTextClassifierExtra(@Nullable Bundle textLanguagesBundle) {
+        if (textLanguagesBundle != null) {
+            final Bundle bundle = new Bundle();
+            ExtrasUtils.putTextLanguagesExtra(bundle, textLanguagesBundle);
+            return bundle;
+        } else {
+            return Bundle.EMPTY;
+        }
+    }
+
     /**
      * Data class that holds the result.
      */
diff --git a/core/java/android/view/textclassifier/intent/LegacyClassificationIntentFactory.java b/core/java/android/view/textclassifier/intent/LegacyClassificationIntentFactory.java
index 1bb5430..7916791 100644
--- a/core/java/android/view/textclassifier/intent/LegacyClassificationIntentFactory.java
+++ b/core/java/android/view/textclassifier/intent/LegacyClassificationIntentFactory.java
@@ -52,8 +52,6 @@
     private static final long MIN_EVENT_FUTURE_MILLIS = TimeUnit.MINUTES.toMillis(5);
     private static final long DEFAULT_EVENT_DURATION = TimeUnit.HOURS.toMillis(1);
 
-    public LegacyClassificationIntentFactory() {}
-
     @NonNull
     @Override
     public List<LabeledIntent> create(Context context, String text, boolean foreignText,
@@ -100,8 +98,6 @@
         if (foreignText) {
             ClassificationIntentFactory.insertTranslateAction(actions, context, text);
         }
-        actions.forEach(
-                action -> action.intent.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
         return actions;
     }
 
diff --git a/core/java/android/view/textclassifier/intent/TemplateIntentFactory.java b/core/java/android/view/textclassifier/intent/TemplateIntentFactory.java
index e630f61..59cd7ab 100644
--- a/core/java/android/view/textclassifier/intent/TemplateIntentFactory.java
+++ b/core/java/android/view/textclassifier/intent/TemplateIntentFactory.java
@@ -66,8 +66,6 @@
                                     ? LabeledIntent.DEFAULT_REQUEST_CODE
                                     : remoteActionTemplate.requestCode));
         }
-        labeledIntents.forEach(
-                action -> action.intent.putExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, true));
         return labeledIntents;
     }
 
diff --git a/core/tests/coretests/src/android/view/textclassifier/FakeContextBuilder.java b/core/tests/coretests/src/android/view/textclassifier/FakeContextBuilder.java
index fef6583..2674e37 100644
--- a/core/tests/coretests/src/android/view/textclassifier/FakeContextBuilder.java
+++ b/core/tests/coretests/src/android/view/textclassifier/FakeContextBuilder.java
@@ -44,8 +44,7 @@
 /**
  * A builder used to build a fake context for testing.
  */
-// TODO: Consider making public.
-final class FakeContextBuilder {
+public final class FakeContextBuilder {
 
     /**
      * A component name that can be used for tests.
@@ -57,7 +56,7 @@
     private final Map<String, ComponentName> mComponents = new HashMap<>();
     private @Nullable ComponentName mAllIntentComponent;
 
-    FakeContextBuilder() {
+    public FakeContextBuilder() {
         mPackageManager = mock(PackageManager.class);
         when(mPackageManager.resolveActivity(any(Intent.class), anyInt())).thenReturn(null);
         mContext = new ContextWrapper(InstrumentationRegistry.getTargetContext()) {
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java
index 066076c..f6bb1bf 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassificationConstantsTest.java
@@ -21,6 +21,7 @@
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
+import org.junit.Assert;
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
@@ -48,7 +49,8 @@
                 + "entity_list_editable=date:datetime,"
                 + "in_app_conversation_action_types_default=text_reply,"
                 + "notification_conversation_action_types_default=send_email:call_phone,"
-                + "lang_id_threshold_override=0.3";
+                + "lang_id_threshold_override=0.3,"
+                + "lang_id_context_settings=10:1:0.5";
         final TextClassificationConstants constants =
                 TextClassificationConstants.loadFromString(s);
 
@@ -91,6 +93,8 @@
                 .containsExactly("send_email", "call_phone");
         assertWithMessage("lang_id_threshold_override")
                 .that(constants.getLangIdThresholdOverride()).isWithin(EPSILON).of(0.3f);
+        Assert.assertArrayEquals("lang_id_context_settings",
+                constants.getLangIdContextSettings(), new float[]{10, 1, 0.5f}, EPSILON);
     }
 
     @Test
@@ -111,7 +115,8 @@
                 + "entity_list_editable=flight,"
                 + "in_app_conversation_action_types_default=view_map:track_flight,"
                 + "notification_conversation_action_types_default=share_location,"
-                + "lang_id_threshold_override=2";
+                + "lang_id_threshold_override=2,"
+                + "lang_id_context_settings=30:0.5:0.3";
         final TextClassificationConstants constants =
                 TextClassificationConstants.loadFromString(s);
 
@@ -154,6 +159,8 @@
                 .containsExactly("share_location");
         assertWithMessage("lang_id_threshold_override")
                 .that(constants.getLangIdThresholdOverride()).isWithin(EPSILON).of(2f);
+        Assert.assertArrayEquals("lang_id_context_settings",
+                constants.getLangIdContextSettings(), new float[]{30, 0.5f, 0.3f}, EPSILON);
     }
 
     @Test
@@ -204,5 +211,7 @@
                         "add_contact", "copy");
         assertWithMessage("lang_id_threshold_override")
                 .that(constants.getLangIdThresholdOverride()).isWithin(EPSILON).of(-1f);
+        Assert.assertArrayEquals("lang_id_context_settings",
+                constants.getLangIdContextSettings(), new float[]{20, 1, 0.4f}, EPSILON);
     }
 }
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java
index 9ceb989..8de5f13 100644
--- a/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassifierTest.java
@@ -32,6 +32,7 @@
 
 import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
 
 import com.google.common.truth.Truth;
 
@@ -41,7 +42,6 @@
 import org.junit.Before;
 import org.junit.Test;
 import org.junit.runner.RunWith;
-import org.junit.runners.Parameterized;
 
 import java.util.Arrays;
 import java.util.Collections;
@@ -53,22 +53,10 @@
  * Tests are skipped if such a textclassifier does not exist.
  */
 @SmallTest
-@RunWith(Parameterized.class)
+@RunWith(AndroidJUnit4.class)
 public class TextClassifierTest {
-    private static final String LOCAL = "local";
-    private static final String SYSTEM = "system";
 
-    @Parameterized.Parameters(name = "{0}")
-    public static Iterable<Object> textClassifierTypes() {
-        return Arrays.asList(LOCAL);
-
-        // TODO: The following will fail on any device that specifies a no-op TextClassifierService.
-        // Enable when we can set a specified TextClassifierService for testing.
-        // return Arrays.asList(LOCAL, SYSTEM);
-    }
-
-    @Parameterized.Parameter
-    public String mTextClassifierType;
+    // TODO: Implement TextClassifierService testing.
 
     private static final TextClassificationConstants TC_CONSTANTS =
             TextClassificationConstants.loadFromString("");
@@ -83,8 +71,7 @@
     public void setup() {
         mContext = InstrumentationRegistry.getTargetContext();
         mTcm = mContext.getSystemService(TextClassificationManager.class);
-        mClassifier = mTcm.getTextClassifier(
-                mTextClassifierType.equals(LOCAL) ? TextClassifier.LOCAL : TextClassifier.SYSTEM);
+        mClassifier = mTcm.getTextClassifier(TextClassifier.LOCAL);
     }
 
     @Test
@@ -278,6 +265,8 @@
         assertEquals("ja", ExtrasUtils.getEntityType(foreignLanguageInfo));
         assertTrue(ExtrasUtils.getScore(foreignLanguageInfo) >= 0);
         assertTrue(ExtrasUtils.getScore(foreignLanguageInfo) <= 1);
+        assertTrue(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER));
+        assertEquals("ja", ExtrasUtils.getTopLanguage(intent).getLanguage());
 
         LocaleList.setDefault(originalLocales);
     }
diff --git a/core/tests/coretests/src/android/view/textclassifier/TextClassifierUtilsTest.java b/core/tests/coretests/src/android/view/textclassifier/TextClassifierUtilsTest.java
new file mode 100644
index 0000000..011866d
--- /dev/null
+++ b/core/tests/coretests/src/android/view/textclassifier/TextClassifierUtilsTest.java
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2019 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 static com.google.common.truth.Truth.assertThat;
+
+import static org.testng.Assert.assertThrows;
+
+import androidx.test.filters.SmallTest;
+import androidx.test.runner.AndroidJUnit4;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+@SmallTest
+@RunWith(AndroidJUnit4.class)
+public class TextClassifierUtilsTest {
+
+    @Test
+    public void testGetSubString() {
+        final String text = "Yakuza call themselves 任侠団体";
+        int start;
+        int end;
+        int minimumLength;
+
+        // End index at end of text.
+        start = text.indexOf("任侠団体");
+        end = text.length();
+        minimumLength = 20;
+        assertThat(TextClassifier.Utils.getSubString(text, start, end, minimumLength))
+                .isEqualTo("call themselves 任侠団体");
+
+        // Start index at beginning of text.
+        start = 0;
+        end = "Yakuza".length();
+        minimumLength = 15;
+        assertThat(TextClassifier.Utils.getSubString(text, start, end, minimumLength))
+                .isEqualTo("Yakuza call themselves");
+
+        // Text in the middle
+        start = text.indexOf("all");
+        end = start + 1;
+        minimumLength = 10;
+        assertThat(TextClassifier.Utils.getSubString(text, start, end, minimumLength))
+                .isEqualTo("Yakuza call themselves");
+
+        // Selection >= minimumLength.
+        start = text.indexOf("themselves");
+        end = start + "themselves".length();
+        minimumLength = end - start;
+        assertThat(TextClassifier.Utils.getSubString(text, start, end, minimumLength))
+                .isEqualTo("themselves");
+
+        // text.length < minimumLength.
+        minimumLength = text.length() + 1;
+        assertThat(TextClassifier.Utils.getSubString(text, start, end, minimumLength))
+                .isEqualTo(text);
+    }
+
+    @Test
+    public void testGetSubString_invalidParams() {
+        final String text = "The Yoruba regard Olodumare as the principal agent of creation";
+        final int length = text.length();
+        final int minimumLength = 10;
+
+        // Null text
+        assertThrows(() -> TextClassifier.Utils.getSubString(null, 0, 1, minimumLength));
+        // start > end
+        assertThrows(() -> TextClassifier.Utils.getSubString(text, 6, 5, minimumLength));
+        // start < 0
+        assertThrows(() -> TextClassifier.Utils.getSubString(text, -1, 5, minimumLength));
+        // end > text.length
+        assertThrows(() -> TextClassifier.Utils.getSubString(text, 6, length + 1, minimumLength));
+    }
+}
diff --git a/core/tests/coretests/src/android/view/textclassifier/intent/LabeledIntentTest.java b/core/tests/coretests/src/android/view/textclassifier/intent/LabeledIntentTest.java
index c8826f01..857408f 100644
--- a/core/tests/coretests/src/android/view/textclassifier/intent/LabeledIntentTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/intent/LabeledIntentTest.java
@@ -23,8 +23,10 @@
 import android.content.Context;
 import android.content.Intent;
 import android.net.Uri;
+import android.os.Bundle;
+import android.view.textclassifier.FakeContextBuilder;
+import android.view.textclassifier.TextClassifier;
 
-import androidx.test.InstrumentationRegistry;
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
 
@@ -41,11 +43,15 @@
     private static final Intent INTENT =
             new Intent(Intent.ACTION_VIEW).setDataAndNormalize(Uri.parse("http://www.android.com"));
     private static final int REQUEST_CODE = 42;
+    private static final Bundle TEXT_LANGUAGES_BUNDLE = Bundle.EMPTY;
+
     private Context mContext;
 
     @Before
     public void setup() {
-        mContext = InstrumentationRegistry.getTargetContext();
+        mContext = new FakeContextBuilder()
+                .setIntentComponent(Intent.ACTION_VIEW, FakeContextBuilder.DEFAULT_COMPONENT)
+                .build();
     }
 
     @Test
@@ -58,8 +64,8 @@
                 REQUEST_CODE
         );
 
-        LabeledIntent.Result result =
-                labeledIntent.resolve(mContext, /*titleChooser*/ null);
+        LabeledIntent.Result result = labeledIntent.resolve(
+                mContext, /*titleChooser*/ null, TEXT_LANGUAGES_BUNDLE);
 
         assertThat(result).isNotNull();
         assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITH_ENTITY);
@@ -67,6 +73,7 @@
         Intent intent = result.resolvedIntent;
         assertThat(intent.getAction()).isEqualTo(intent.getAction());
         assertThat(intent.getComponent()).isNotNull();
+        assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
     }
 
     @Test
@@ -79,8 +86,8 @@
                 REQUEST_CODE
         );
 
-        LabeledIntent.Result result =
-                labeledIntent.resolve(mContext, /*titleChooser*/ null);
+        LabeledIntent.Result result = labeledIntent.resolve(
+                mContext, /*titleChooser*/ null, TEXT_LANGUAGES_BUNDLE);
 
         assertThat(result).isNotNull();
         assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITHOUT_ENTITY);
@@ -100,8 +107,8 @@
                 REQUEST_CODE
         );
 
-        LabeledIntent.Result result =
-                labeledIntent.resolve(mContext, (labeledIntent1, resolveInfo) -> "chooser");
+        LabeledIntent.Result result = labeledIntent.resolve(
+                mContext, (labeledIntent1, resolveInfo) -> "chooser", TEXT_LANGUAGES_BUNDLE);
 
         assertThat(result).isNotNull();
         assertThat(result.remoteAction.getTitle()).isEqualTo("chooser");
@@ -121,8 +128,8 @@
                 REQUEST_CODE
         );
 
-        LabeledIntent.Result result =
-                labeledIntent.resolve(mContext, (labeledIntent1, resolveInfo) -> null);
+        LabeledIntent.Result result = labeledIntent.resolve(
+                mContext, (labeledIntent1, resolveInfo) -> null, TEXT_LANGUAGES_BUNDLE);
 
         assertThat(result).isNotNull();
         assertThat(result.remoteAction.getTitle()).isEqualTo(TITLE_WITHOUT_ENTITY);
@@ -148,15 +155,16 @@
 
     @Test
     public void resolve_noIntentHandler() {
-        Intent intent = new Intent("some.thing.does.not.exist");
+        // See setup(). mContext can only resolve Intent.ACTION_VIEW.
+        Intent unresolvableIntent = new Intent(Intent.ACTION_TRANSLATE);
         LabeledIntent labeledIntent = new LabeledIntent(
                 TITLE_WITHOUT_ENTITY,
                 null,
                 DESCRIPTION,
-                intent,
+                unresolvableIntent,
                 REQUEST_CODE);
 
-        LabeledIntent.Result result = labeledIntent.resolve(mContext, null);
+        LabeledIntent.Result result = labeledIntent.resolve(mContext, null, null);
 
         assertThat(result).isNull();
     }
diff --git a/core/tests/coretests/src/android/view/textclassifier/intent/LegacyIntentClassificationFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/intent/LegacyIntentClassificationFactoryTest.java
index 13863ed..19e5b0a 100644
--- a/core/tests/coretests/src/android/view/textclassifier/intent/LegacyIntentClassificationFactoryTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/intent/LegacyIntentClassificationFactoryTest.java
@@ -77,8 +77,6 @@
         Intent intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(Intent.ACTION_DEFINE);
         assertThat(intent.getStringExtra(Intent.EXTRA_TEXT)).isEqualTo(TEXT);
-        assertThat(
-                intent.getBooleanExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER, false)).isTrue();
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/view/textclassifier/intent/TemplateClassificationIntentFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/intent/TemplateClassificationIntentFactoryTest.java
index 1dc8f01..eaef0a0 100644
--- a/core/tests/coretests/src/android/view/textclassifier/intent/TemplateClassificationIntentFactoryTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/intent/TemplateClassificationIntentFactoryTest.java
@@ -39,7 +39,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 import org.mockito.Mock;
-import org.mockito.Mockito;
 import org.mockito.MockitoAnnotations;
 
 import java.util.List;
@@ -97,12 +96,10 @@
         assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
         Intent intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(ACTION);
-        assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
 
         labeledIntent = intents.get(1);
         intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(Intent.ACTION_TRANSLATE);
-        assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
     }
 
     @Test
@@ -137,7 +134,6 @@
         assertThat(labeledIntent.titleWithoutEntity).isEqualTo(TITLE_WITHOUT_ENTITY);
         Intent intent = labeledIntent.intent;
         assertThat(intent.getAction()).isEqualTo(ACTION);
-        assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
     }
 
     @Test
diff --git a/core/tests/coretests/src/android/view/textclassifier/intent/TemplateIntentFactoryTest.java b/core/tests/coretests/src/android/view/textclassifier/intent/TemplateIntentFactoryTest.java
index 1cc8004..6e3de2d 100644
--- a/core/tests/coretests/src/android/view/textclassifier/intent/TemplateIntentFactoryTest.java
+++ b/core/tests/coretests/src/android/view/textclassifier/intent/TemplateIntentFactoryTest.java
@@ -19,7 +19,6 @@
 
 import android.content.Intent;
 import android.net.Uri;
-import android.view.textclassifier.TextClassifier;
 
 import androidx.test.filters.SmallTest;
 import androidx.test.runner.AndroidJUnit4;
@@ -102,7 +101,6 @@
         assertThat(intent.getPackage()).isNull();
         assertThat(intent.getStringExtra(KEY_ONE)).isEqualTo(VALUE_ONE);
         assertThat(intent.getIntExtra(KEY_TWO, 0)).isEqualTo(VALUE_TWO);
-        assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
     }
 
     @Test
@@ -161,7 +159,6 @@
         assertThat(intent.getFlags()).isEqualTo(0);
         assertThat(intent.getCategories()).isNull();
         assertThat(intent.getPackage()).isNull();
-        assertThat(intent.hasExtra(TextClassifier.EXTRA_FROM_TEXT_CLASSIFIER)).isTrue();
     }
 
     @Test