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

Commit 77dd59fc authored by Yohei Yukawa's avatar Yohei Yukawa
Browse files

Revert "Introduce script matching for enabling default IME subtypes."

This reverts commit 9e7a1c98.

Seems that that CL causes ArrayIndexOutOfBoundsException when
initializing InputMethodManagerService, which results in an infinite
boot animation.

Bug: 27129703
Bug: 27348943
Change-Id: I474a87876670ac018c675ac7b4608e90fbb2434b
parent b1c23e2c
Loading
Loading
Loading
Loading
+74 −140
Original line number Diff line number Diff line
@@ -18,17 +18,15 @@ package com.android.internal.inputmethod;

import com.android.internal.annotations.VisibleForTesting;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.icu.util.ULocale;
import android.text.TextUtils;
import android.util.LocaleList;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Objects;

public final class LocaleUtils {

@@ -38,120 +36,12 @@ public final class LocaleUtils {
        Locale get(@Nullable T source);
    }

    /**
     * Calculates a matching score for the single desired locale.
     *
     * <p>See {@link LocaleUtils#calculateMatchingScore(ULocale, LocaleList, byte[])} for
     * details.</p>
     */
    @IntRange(from=1, to=3)
    private static byte calculateMatchingSubScore(@NonNull final ULocale supported,
            @NonNull final ULocale desired) {

        // Assuming supported/desired is fully expanded.
        if (supported.equals(desired)) {
            return 3;  // Exact match.
        }

        // Skip language matching since it was already done in calculateMatchingScore.

        final String supportedScript = supported.getScript();
        if (supportedScript.isEmpty() || !supportedScript.equals(desired.getScript())) {
            // TODO: Need subscript matching. For example, Hanb should match with Bopo.
            return 1;
        }

        final String supportedCountry = supported.getCountry();
        if (supportedCountry.isEmpty() || !supportedCountry.equals(desired.getCountry())) {
            return 2;
        }

        // Ignore others e.g. variants, extensions.
        return 3;
    }

    /**
     * Calculates a matching score for the desired locale list.
     *
     * <p>The supported locale gets a matching score of 3 if all language, script and country of the
     * supported locale matches with the desired locale.  The supported locale gets a matching
     * score of 2 if the language and script of the supported locale matches with the desired
     * locale. The supported locale gets a matching score of 1 if only language of the supported
     * locale matches with the desired locale.  The supported locale gets a matching score of 0 if
     * the language of the supported locale doesn't match with the desired locale.</p>
     *
     * <p>This function returns {@code false} if supported locale doesn't match with any desired
     * locale list.  Otherwise, this function returns {@code true}.</p>
     */
    private static boolean calculateMatchingScore(@NonNull final ULocale supported,
            @NonNull final LocaleList desired, @NonNull byte[] out) {
        if (desired.isEmpty()) {
            return false;
        }

        boolean allZeros = true;
        for (int i = 0; i < desired.size(); ++i) {
            final Locale loc = desired.get(i);

            if (!loc.getLanguage().equals(supported.getLanguage())) {
                // TODO: cache the result of addLikelySubtags if it is slow.
                out[i] = 0;
            } else {
                out[i] = calculateMatchingSubScore(
                        supported, ULocale.addLikelySubtags(ULocale.forLocale(loc)));
                if (allZeros && out[i] != 0) {
                    allZeros = false;
                }
            }
        }
        return !allZeros;
    }

    private static final class ScoreEntry implements Comparable<ScoreEntry> {
        public int index = -1;
        @NonNull public byte[] score;  // matching score of the i-th system languages.

        ScoreEntry(int capacity) {
            score = new byte[capacity];
        }

        /**
         * Update score and index if the given score is better than this.
         */
        public void updateIfBetter(@NonNull byte[] newScore, int newIndex) {
            if (isBetterThan(score) != 1) {
                return;
            }

            for (int i = 0; i < score.length; ++i) {
                score[i] = newScore[i];
            }
            index = newIndex;
        }

        /**
         * Determines given score is better than current.
         *
         * <p>Compares the matching score for the first priority locale. If the given score has
         * higher score than current score, returns 1.  If the current score has higher score than
         * given score, returns -1. Otherwise, do the same comparison for the next priority locale.
         * If given score and current score is same for the all system locale, returns 0.</p>
         */
        private int isBetterThan(@NonNull byte[] other) {
            for (int i = 0; i < score.length; ++i) {
                if (score[i] < other[i]) {
                    return 1;
                } else if (score[i] > other[i]) {
                    return -1;
                }
            }
            return 0;
        }

        @Override
        public int compareTo(ScoreEntry other) {
            return isBetterThan(score);
    @Nullable
    private static String getLanguage(@Nullable Locale locale) {
        if (locale == null) {
            return null;
        }
        return locale.getLanguage();
    }

    /**
@@ -162,8 +52,14 @@ public final class LocaleUtils {
     * {@code "en-GB", "ja", "en-AU", "fr-CA", "en-IN"} is specified to {@code preferredLanguages},
     * this method tries to copy at most one English locale, at most one Japanese, and at most one
     * French locale from {@code source} to {@code dest}.  Here the best matching English locale
     * will be searched from {@code source} based on matching score. For the score design, see
     * {@link LocaleUtils#calculateMatchingScore(ULocale, LocaleList, byte[])}</p>
     * will be searched from {@code source} as follows.
     * <ol>
     *     <li>The first instance in {@code sources} that exactly matches {@code "en-GB"}</li>
     *     <li>The first instance in {@code sources} that exactly matches {@code "en-AU"}</li>
     *     <li>The first instance in {@code sources} that exactly matches {@code "en-IN"}</li>
     *     <li>The first instance in {@code sources} that partially matches {@code "en"}</li>
     * </ol>
     * <p>Then this method iterates the same algorithm for Japanese then French.</p>
     *
     * @param sources Source items to be filtered.
     * @param extractor Type converter from the source items to {@link Locale} object.
@@ -178,31 +74,69 @@ public final class LocaleUtils {
            @NonNull LocaleExtractor<T> extractor,
            @NonNull LocaleList preferredLanguages,
            @NonNull ArrayList<T> dest) {
        final HashMap<String, ScoreEntry> scoreboard = new HashMap<>();
        final byte[] score = new byte[preferredLanguages.size()];

        for (int i = 0; i < sources.size(); ++i) {
            final Locale loc = extractor.get(sources.get(i));
            if (loc == null ||
                    !calculateMatchingScore(ULocale.addLikelySubtags(ULocale.forLocale(loc)),
                            preferredLanguages, score)) {
                continue;
        final Locale[] availableLocales = new Locale[sources.size()];
        for (int i = 0; i < availableLocales.length; ++i) {
            availableLocales[i] = extractor.get(sources.get(i));
        }
        final Locale[] sortedPreferredLanguages = new Locale[preferredLanguages.size()];
        if (sortedPreferredLanguages.length > 0) {
            int nextIndex = 0;
            final int N = preferredLanguages.size();
            languageLoop:
            for (int i = 0; i < N; ++i) {
                final String language = getLanguage(preferredLanguages.get(i));
                for (int j = 0; j < nextIndex; ++j) {
                    if (TextUtils.equals(getLanguage(sortedPreferredLanguages[j]), language)) {
                        continue languageLoop;
                    }
                }
                for (int j = i; j < N; ++j) {
                    final Locale locale = preferredLanguages.get(j);
                    if (TextUtils.equals(language, getLanguage(locale))) {
                        sortedPreferredLanguages[nextIndex] = locale;
                        ++nextIndex;
                    }
                }
            }
        }

            final String lang = loc.getLanguage();
            ScoreEntry bestScore = scoreboard.get(lang);
            if (bestScore == null) {
                bestScore = new ScoreEntry(score.length);
                scoreboard.put(lang, bestScore);

        for (int languageIndex = 0; languageIndex < sortedPreferredLanguages.length;) {
            // Finding the range.
            final String language = getLanguage(sortedPreferredLanguages[languageIndex]);
            int nextLanguageIndex = languageIndex;
            for (; nextLanguageIndex < sortedPreferredLanguages.length; ++nextLanguageIndex) {
                final Locale locale = sortedPreferredLanguages[nextLanguageIndex];
                if (!TextUtils.equals(getLanguage(locale), language)) {
                    break;
                }
            }

            bestScore.updateIfBetter(score, i);
            // Check exact match
            boolean found = false;
            for (int i = languageIndex; !found && i < nextLanguageIndex; ++i) {
                final Locale locale = sortedPreferredLanguages[i];
                for (int j = 0; j < availableLocales.length; ++j) {
                    if (!Objects.equals(locale, availableLocales[j])) {
                        continue;
                    }
                    dest.add(sources.get(j));
                    found = true;
                    break;
                }
            }

        final ScoreEntry[] result = scoreboard.values().toArray(new ScoreEntry[scoreboard.size()]);
        Arrays.sort(result);
        for (final ScoreEntry entry : result) {
            dest.add(sources.get(entry.index));
            if (!found) {
                // No exact match.  Use language match.
                for (int j = 0; j < availableLocales.length; ++j) {
                    if (!TextUtils.equals(language, getLanguage(availableLocales[j]))) {
                        continue;
                    }
                    dest.add(sources.get(j));
                    break;
                }
            }
            languageIndex = nextLanguageIndex;
        }
    }
}
 No newline at end of file
+0 −161
Original line number Diff line number Diff line
@@ -191,165 +191,4 @@ public class LocaleUtilsTest extends InstrumentationTestCase {
            assertEquals(availableLocales.get(1), dest.get(0));  // "en-CA"
        }
    }

    @SmallTest
    public void testFilterByLanguageFallbackRules() throws Exception {
        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-Latn-RS");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-BA"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-CS"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-ME"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-RS"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-BA"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-CS"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-ME"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-RS"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(7), dest.get(0));  // "sr-Latn-RS"
        }
        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-Latn-RS-x-android");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-BA"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-CS"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-ME"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-RS"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-BA"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-CS"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-ME"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-RS"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(7), dest.get(0));  // "sr-Latn-RS"
        }
        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-Latn-RS");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-BA-x-android"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-CS-x-android"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-ME-x-android"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-RS-x-android"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-BA-x-android"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-CS-x-android"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-ME-x-android"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-RS-x-android"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(7), dest.get(0));  // "sr-Latn-RS-x-android"
        }

        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-Latn-RS");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(2), dest.get(0));  // "sr-Latn"
        }

        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-RS");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr"));
            availableLocales.add(Locale.forLanguageTag("sr-RS"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(0), dest.get(0));  // "sr"
        }

        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-Latn");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr"));
            availableLocales.add(Locale.forLanguageTag("sr-RS"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(2), dest.get(0));  // "sr-Latn"
        }

        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr"));
            availableLocales.add(Locale.forLanguageTag("sr-RS"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(0), dest.get(0));  // "sr"
        }
        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr-Latn"));
            availableLocales.add(Locale.forLanguageTag("sr-RS"));
            availableLocales.add(Locale.forLanguageTag("sr"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(1), dest.get(0));  // "sr-RS"
        }

        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-RS"));
            availableLocales.add(Locale.forLanguageTag("sr-Latn-RS"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(0), dest.get(0));  // "sr-Cyrl-RS"
        }
        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("sr-Latn");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("sr-Latn-RS"));
            availableLocales.add(Locale.forLanguageTag("sr-Cyrl-RS"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            assertEquals(availableLocales.get(0), dest.get(0));  // "sr-Latn-RS"
        }
    }

    public void testFilterKnownLimitation() throws Exception {
        // Following test cases are not for intentional behavior but checks for preventing the
        // behavior from becoming worse.
        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("ja-Hrkt");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("ja-Jpan"));
            availableLocales.add(Locale.forLanguageTag("ja-Hrkt"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            // Should be ja-Jpan since it supports ja-Hrkt and listed before ja-Hrkt.
            assertEquals(availableLocales.get(1), dest.get(0));
        }
        {
            final LocaleList preferredLocales = LocaleList.forLanguageTags("zh-Hani");
            final ArrayList<Locale> availableLocales = new ArrayList<>();
            availableLocales.add(Locale.forLanguageTag("zh-Hans"));
            availableLocales.add(Locale.forLanguageTag("zh-Hant"));
            availableLocales.add(Locale.forLanguageTag("zh-Hanb"));
            availableLocales.add(Locale.forLanguageTag("zh-Hani"));
            final ArrayList<Locale> dest = new ArrayList<>();
            LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest);
            assertEquals(1, dest.size());
            // Should be zh-Hans since it supports zh-Hani. Also zh-Hant, zh-Hanb supports zh-Hani.
            assertEquals(availableLocales.get(3), dest.get(0));
        }
    }
}