diff options
3 files changed, 264 insertions, 4 deletions
diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java index ac17cbe0bcfc..742173b9b5b3 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodUtils.java +++ b/core/java/com/android/internal/inputmethod/InputMethodUtils.java @@ -1302,4 +1302,131 @@ public class InputMethodUtils { return enabledInputMethodAndSubtypes; } } + + // For spell checker service manager. + // TODO: Should we have TextServicesUtils.java? + private static final Locale LOCALE_EN_US = new Locale("en", "US"); + private static final Locale LOCALE_EN_GB = new Locale("en", "GB"); + + /** + * Returns a list of {@link Locale} in the order of appropriateness for the default spell + * checker service. + * + * <p>If the system language is English, and the region is also explicitly specified in the + * system locale, the following fallback order will be applied.</p> + * <ul> + * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li> + * <li>(system-locale-language, system-locale-region)</li> + * <li>("en", "US")</li> + * <li>("en", "GB")</li> + * <li>("en")</li> + * </ul> + * + * <p>If the system language is English, but no region is specified in the system locale, + * the following fallback order will be applied.</p> + * <ul> + * <li>("en")</li> + * <li>("en", "US")</li> + * <li>("en", "GB")</li> + * </ul> + * + * <p>If the system language is not English, the following fallback order will be applied.</p> + * <ul> + * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li> + * <li>(system-locale-language, system-locale-region) (if exists)</li> + * <li>(system-locale-language) (if exists)</li> + * <li>("en", "US")</li> + * <li>("en", "GB")</li> + * <li>("en")</li> + * </ul> + * + * @param systemLocale the current system locale to be taken into consideration. + * @return a list of {@link Locale}. The first one is considered to be most appropriate. + */ + @VisibleForTesting + public static ArrayList<Locale> getSuitableLocalesForSpellChecker( + @Nullable final Locale systemLocale) { + final Locale systemLocaleLanguageCountryVariant; + final Locale systemLocaleLanguageCountry; + final Locale systemLocaleLanguage; + if (systemLocale != null) { + final String language = systemLocale.getLanguage(); + final boolean hasLanguage = !TextUtils.isEmpty(language); + final String country = systemLocale.getCountry(); + final boolean hasCountry = !TextUtils.isEmpty(country); + final String variant = systemLocale.getVariant(); + final boolean hasVariant = !TextUtils.isEmpty(variant); + if (hasLanguage && hasCountry && hasVariant) { + systemLocaleLanguageCountryVariant = new Locale(language, country, variant); + } else { + systemLocaleLanguageCountryVariant = null; + } + if (hasLanguage && hasCountry) { + systemLocaleLanguageCountry = new Locale(language, country); + } else { + systemLocaleLanguageCountry = null; + } + if (hasLanguage) { + systemLocaleLanguage = new Locale(language); + } else { + systemLocaleLanguage = null; + } + } else { + systemLocaleLanguageCountryVariant = null; + systemLocaleLanguageCountry = null; + systemLocaleLanguage = null; + } + + final ArrayList<Locale> locales = new ArrayList<>(); + if (systemLocaleLanguageCountryVariant != null) { + locales.add(systemLocaleLanguageCountryVariant); + } + + if (Locale.ENGLISH.equals(systemLocaleLanguage)) { + if (systemLocaleLanguageCountry != null) { + // If the system language is English, and the region is also explicitly specified, + // following fallback order will be applied. + // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null] + // - en_US [if systemLocaleLanguageCountry is non-null and not en_US] + // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB] + // - en + if (systemLocaleLanguageCountry != null) { + locales.add(systemLocaleLanguageCountry); + } + if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) { + locales.add(LOCALE_EN_US); + } + if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) { + locales.add(LOCALE_EN_GB); + } + locales.add(Locale.ENGLISH); + } else { + // If the system language is English, but no region is specified, following + // fallback order will be applied. + // - en + // - en_US + // - en_GB + locales.add(Locale.ENGLISH); + locales.add(LOCALE_EN_US); + locales.add(LOCALE_EN_GB); + } + } else { + // If the system language is not English, the fallback order will be + // - systemLocaleLanguageCountry [if non-null] + // - systemLocaleLanguage [if non-null] + // - en_US + // - en_GB + // - en + if (systemLocaleLanguageCountry != null) { + locales.add(systemLocaleLanguageCountry); + } + if (systemLocaleLanguage != null) { + locales.add(systemLocaleLanguage); + } + locales.add(LOCALE_EN_US); + locales.add(LOCALE_EN_GB); + locales.add(Locale.ENGLISH); + } + return locales; + } } diff --git a/core/tests/inputmethodtests/src/android/os/InputMethodTest.java b/core/tests/inputmethodtests/src/android/os/InputMethodTest.java index a50fb54973ab..31a4703c2662 100644 --- a/core/tests/inputmethodtests/src/android/os/InputMethodTest.java +++ b/core/tests/inputmethodtests/src/android/os/InputMethodTest.java @@ -51,11 +51,15 @@ public class InputMethodTest extends InstrumentationTestCase { private static final Locale LOCALE_FR = new Locale("fr"); private static final Locale LOCALE_FR_CA = new Locale("fr", "CA"); private static final Locale LOCALE_HI = new Locale("hi"); + private static final Locale LOCALE_JA = new Locale("ja"); private static final Locale LOCALE_JA_JP = new Locale("ja", "JP"); private static final Locale LOCALE_ZH_CN = new Locale("zh", "CN"); private static final Locale LOCALE_ZH_TW = new Locale("zh", "TW"); private static final Locale LOCALE_IN = new Locale("in"); private static final Locale LOCALE_ID = new Locale("id"); + private static final Locale LOCALE_TH = new Locale("ht"); + private static final Locale LOCALE_TH_TH = new Locale("ht", "TH"); + private static final Locale LOCALE_TH_TH_TH = new Locale("ht", "TH", "TH"); private static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; private static final String SUBTYPE_MODE_VOICE = "voice"; private static final String SUBTYPE_MODE_ANY = null; @@ -849,4 +853,99 @@ public class InputMethodTest extends InstrumentationTestCase { return preinstalledImes; } + + @SmallTest + public void testGetSuitableLocalesForSpellChecker() throws Exception { + { + final ArrayList<Locale> locales = + InputMethodUtils.getSuitableLocalesForSpellChecker(LOCALE_EN_US); + assertEquals(3, locales.size()); + assertEquals(LOCALE_EN_US, locales.get(0)); + assertEquals(LOCALE_EN_GB, locales.get(1)); + assertEquals(LOCALE_EN, locales.get(2)); + } + + { + final ArrayList<Locale> locales = + InputMethodUtils.getSuitableLocalesForSpellChecker(LOCALE_EN_GB); + assertEquals(3, locales.size()); + assertEquals(LOCALE_EN_GB, locales.get(0)); + assertEquals(LOCALE_EN_US, locales.get(1)); + assertEquals(LOCALE_EN, locales.get(2)); + } + + { + final ArrayList<Locale> locales = + InputMethodUtils.getSuitableLocalesForSpellChecker(LOCALE_EN); + assertEquals(3, locales.size()); + assertEquals(LOCALE_EN, locales.get(0)); + assertEquals(LOCALE_EN_US, locales.get(1)); + assertEquals(LOCALE_EN_GB, locales.get(2)); + } + + { + final ArrayList<Locale> locales = + InputMethodUtils.getSuitableLocalesForSpellChecker(LOCALE_EN_IN); + assertEquals(4, locales.size()); + assertEquals(LOCALE_EN_IN, locales.get(0)); + assertEquals(LOCALE_EN_US, locales.get(1)); + assertEquals(LOCALE_EN_GB, locales.get(2)); + assertEquals(LOCALE_EN, locales.get(3)); + } + + { + final ArrayList<Locale> locales = + InputMethodUtils.getSuitableLocalesForSpellChecker(LOCALE_JA_JP); + assertEquals(5, locales.size()); + assertEquals(LOCALE_JA_JP, locales.get(0)); + assertEquals(LOCALE_JA, locales.get(1)); + assertEquals(LOCALE_EN_US, locales.get(2)); + assertEquals(LOCALE_EN_GB, locales.get(3)); + assertEquals(Locale.ENGLISH, locales.get(4)); + } + + // Test 3-letter language code. + { + final ArrayList<Locale> locales = + InputMethodUtils.getSuitableLocalesForSpellChecker(LOCALE_FIL_PH); + assertEquals(5, locales.size()); + assertEquals(LOCALE_FIL_PH, locales.get(0)); + assertEquals(LOCALE_FIL, locales.get(1)); + assertEquals(LOCALE_EN_US, locales.get(2)); + assertEquals(LOCALE_EN_GB, locales.get(3)); + assertEquals(Locale.ENGLISH, locales.get(4)); + } + + // Test variant. + { + final ArrayList<Locale> locales = + InputMethodUtils.getSuitableLocalesForSpellChecker(LOCALE_TH_TH_TH); + assertEquals(6, locales.size()); + assertEquals(LOCALE_TH_TH_TH, locales.get(0)); + assertEquals(LOCALE_TH_TH, locales.get(1)); + assertEquals(LOCALE_TH, locales.get(2)); + assertEquals(LOCALE_EN_US, locales.get(3)); + assertEquals(LOCALE_EN_GB, locales.get(4)); + assertEquals(Locale.ENGLISH, locales.get(5)); + } + + // Test Locale extension. + { + final Locale localeWithoutVariant = LOCALE_JA_JP; + final Locale localeWithVariant = new Locale.Builder() + .setLocale(LOCALE_JA_JP) + .setExtension('x', "android") + .build(); + assertFalse(localeWithoutVariant.equals(localeWithVariant)); + + final ArrayList<Locale> locales = + InputMethodUtils.getSuitableLocalesForSpellChecker(localeWithVariant); + assertEquals(5, locales.size()); + assertEquals(LOCALE_JA_JP, locales.get(0)); + assertEquals(LOCALE_JA, locales.get(1)); + assertEquals(LOCALE_EN_US, locales.get(2)); + assertEquals(LOCALE_EN_GB, locales.get(3)); + assertEquals(Locale.ENGLISH, locales.get(4)); + } + } } diff --git a/services/core/java/com/android/server/TextServicesManagerService.java b/services/core/java/com/android/server/TextServicesManagerService.java index 5bce6ebac003..aace66c7745d 100644 --- a/services/core/java/com/android/server/TextServicesManagerService.java +++ b/services/core/java/com/android/server/TextServicesManagerService.java @@ -18,6 +18,7 @@ package com.android.server; import com.android.internal.annotations.GuardedBy; import com.android.internal.content.PackageMonitor; +import com.android.internal.inputmethod.InputMethodUtils; import com.android.internal.textservice.ISpellCheckerService; import com.android.internal.textservice.ISpellCheckerSession; import com.android.internal.textservice.ISpellCheckerSessionListener; @@ -61,9 +62,11 @@ import android.view.textservice.SpellCheckerSubtype; import java.io.FileDescriptor; import java.io.IOException; import java.io.PrintWriter; +import java.util.Arrays; import java.util.ArrayList; import java.util.HashMap; import java.util.List; +import java.util.Locale; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; @@ -141,7 +144,7 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { buildSpellCheckerMapLocked(mContext, mSpellCheckerList, mSpellCheckerMap, mSettings); SpellCheckerInfo sci = getCurrentSpellChecker(null); if (sci == null) { - sci = findAvailSpellCheckerLocked(null, null); + sci = findAvailSpellCheckerLocked(null); if (sci != null) { // Set the current spell checker if there is one or more spell checkers // available. In this case, "sci" is the first one in the available spell @@ -190,7 +193,7 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { change == PACKAGE_PERMANENT_CHANGE || change == PACKAGE_TEMPORARY_CHANGE // Package modified || isPackageModified(packageName)) { - sci = findAvailSpellCheckerLocked(null, packageName); + sci = findAvailSpellCheckerLocked(packageName); if (sci != null) { setCurrentSpellCheckerLocked(sci.getId()); } @@ -331,8 +334,7 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { mSpellCheckerBindGroups.clear(); } - // TODO: find an appropriate spell checker for specified locale - private SpellCheckerInfo findAvailSpellCheckerLocked(String locale, String prefPackage) { + private SpellCheckerInfo findAvailSpellCheckerLocked(String prefPackage) { final int spellCheckersCount = mSpellCheckerList.size(); if (spellCheckersCount == 0) { Slog.w(TAG, "no available spell checker services found"); @@ -349,6 +351,38 @@ public class TextServicesManagerService extends ITextServicesManager.Stub { } } } + + // Look up a spell checker based on the system locale. + // TODO: Still there is a room to improve in the following logic: e.g., check if the package + // is pre-installed or not. + final Locale systemLocal = mContext.getResources().getConfiguration().locale; + final ArrayList<Locale> suitableLocales = + InputMethodUtils.getSuitableLocalesForSpellChecker(systemLocal); + if (DBG) { + Slog.w(TAG, "findAvailSpellCheckerLocked suitableLocales=" + + Arrays.toString(suitableLocales.toArray(new Locale[suitableLocales.size()]))); + } + final int localeCount = suitableLocales.size(); + for (int localeIndex = 0; localeIndex < localeCount; ++localeIndex) { + final Locale locale = suitableLocales.get(localeIndex); + for (int spellCheckersIndex = 0; spellCheckersIndex < spellCheckersCount; + ++spellCheckersIndex) { + final SpellCheckerInfo info = mSpellCheckerList.get(spellCheckersIndex); + final int subtypeCount = info.getSubtypeCount(); + for (int subtypeIndex = 0; subtypeIndex < subtypeCount; ++subtypeIndex) { + final SpellCheckerSubtype subtype = info.getSubtypeAt(subtypeIndex); + final Locale subtypeLocale = InputMethodUtils.constructLocaleFromString( + subtype.getLocale()); + if (locale.equals(subtypeLocale)) { + // TODO: We may have more spell checkers that fall into this category. + // Ideally we should pick up the most suitable one instead of simply + // returning the first found one. + return info; + } + } + } + } + if (spellCheckersCount > 1) { Slog.w(TAG, "more than one spell checker service found, picking first"); } |