Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 72e17976 authored by Tony Mak's avatar Tony Mak
Browse files

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
parent 22640b9a
Loading
Loading
Loading
Loading
+1 −1
Original line number Diff line number Diff line
@@ -143,7 +143,7 @@ public final class ActionsSuggestionsHelper {
        // 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);
    }

    /**
+84 −7
Original line number Diff line number Diff line
@@ -19,6 +19,7 @@ package android.view.textclassifier;
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 @@ import java.util.ArrayList;
 * 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 @@ public final class ExtrasUtils {
    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 @@ public final class ExtrasUtils {
    /**
     * 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,12 +69,67 @@ public final class ExtrasUtils {
    /**
     * 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 @@ public final class ExtrasUtils {
     * 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 @@ public final class ExtrasUtils {
     * 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 @@ public final class ExtrasUtils {
     * 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 @@ public final class ExtrasUtils {
     * 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 @@ public final class ExtrasUtils {
     */
    @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);
    }
}
+69 −14
Original line number Diff line number Diff line
@@ -50,6 +50,7 @@ import java.util.StringJoiner;
 * template_intent_factory_enabled                  (boolean)
 * translate_in_classification_enabled              (boolean)
 * detect_languages_from_text_enabled               (boolean)
 * lang_id_context_settings                         (float[])
 * </pre>
 *
 * <p>
@@ -59,11 +60,13 @@ import java.util.StringJoiner;
 * 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
 *      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 @@ public final class TextClassificationConstants {
     * 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 @@ public final class TextClassificationConstants {
     */
    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 @@ public final class TextClassificationConstants {
    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 @@ public final class TextClassificationConstants {
    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,7 +292,8 @@ public final class TextClassificationConstants {
                configParser.getInt(
                        GENERATE_LINKS_LOG_SAMPLE_RATE,
                        GENERATE_LINKS_LOG_SAMPLE_RATE_DEFAULT);
        mEntityListDefault = parseStringList(configParser.getString(
        mEntityListDefault = parseStringList(
                configParser.getString(
                        ENTITY_LIST_DEFAULT,
                        ENTITY_LIST_DEFAULT_VALUE));
        mEntityListNotEditable = parseStringList(
@@ -296,13 +316,22 @@ public final class TextClassificationConstants {
                configParser.getFloat(
                        LANG_ID_THRESHOLD_OVERRIDE,
                        LANG_ID_THRESHOLD_OVERRIDE_DEFAULT);
        mTemplateIntentFactoryEnabled = configParser.getBoolean(
        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);
        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 @@ public final class TextClassificationConstants {
        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 @@ public final class TextClassificationConstants {
        pw.printPair("isTemplateIntentFactoryEnabled", mTemplateIntentFactoryEnabled);
        pw.printPair("isTranslateInClassificationEnabled", mTranslateInClassificationEnabled);
        pw.printPair("isDetectLanguageFromTextEnabled", mDetectLanguagesFromTextEnabled);
        pw.printPair("getLangIdContextSettings", Arrays.toString(mLangIdContextSettings));
        pw.decreaseIndent();
        pw.println();
    }
+47 −1
Original line number Diff line number Diff line
@@ -33,11 +33,13 @@ import android.text.util.Linkify;
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 @@ public interface TextClassifier {
     *  <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;
@@ -665,6 +670,47 @@ public interface TextClassifier {
            Preconditions.checkArgumentInRange(text.length(), 0, maxLength, "text.length()");
        }

        /**
         * 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}.
         */
+142 −43

File changed.

Preview size limit exceeded, changes collapsed.

Loading