From fc843713bc3c5558a7288c02aabe2474264e4ca3 Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Wed, 24 Feb 2016 00:27:34 -0800 Subject: [PATCH 1/3] Mechanical refactoring in InputMethodUtilsTest. This is a mechanical refactoring in InputMethodUtilsTest that changes nothing. Bug: 27129703 Change-Id: I21d3c5cc4cc3018fc844c18362035ebdc656dec1 --- .../inputmethod/InputMethodUtilsTest.java | 53 ++++++------------- 1 file changed, 17 insertions(+), 36 deletions(-) diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodUtilsTest.java b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodUtilsTest.java index f962a4334933c..380d3b44d8ea5 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodUtilsTest.java +++ b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodUtilsTest.java @@ -21,6 +21,7 @@ import android.content.pm.ApplicationInfo; import android.content.pm.ResolveInfo; import android.content.pm.ServiceInfo; import android.content.res.Configuration; +import android.content.res.Resources; import android.os.Parcel; import android.test.InstrumentationTestCase; import android.test.suitebuilder.annotation.SmallTest; @@ -233,9 +234,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_EN_US)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_EN_US), imi); assertEquals(1, result.size()); verifyEquality(autoSubtype, result.get(0)); } @@ -257,9 +256,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_EN_US)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_EN_US), imi); verifyEquality(nonAutoEnUS, result.get(0)); } @@ -279,9 +276,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_EN_GB)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_EN_GB), imi); assertEquals(1, result.size()); verifyEquality(nonAutoEnGB, result.get(0)); } @@ -303,9 +298,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_FR)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_FR), imi); assertEquals(1, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); } @@ -323,9 +316,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_FR_CA)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_FR_CA), imi); assertEquals(1, result.size()); verifyEquality(nonAutoFrCA, result.get(0)); } @@ -344,9 +335,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_JA_JP)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_JA_JP), imi); assertEquals(3, result.size()); verifyEquality(nonAutoJa, result.get(0)); verifyEquality(nonAutoEnabledWhenDefaultIsNotAsciiCalableSubtype, result.get(1)); @@ -364,9 +353,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_FIL_PH)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_FIL_PH), imi); assertEquals(1, result.size()); verifyEquality(nonAutoFil, result.get(0)); } @@ -384,9 +371,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_FI)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_FI), imi); assertEquals(1, result.size()); verifyEquality(nonAutoJa, result.get(0)); } @@ -402,9 +387,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_IN)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); } @@ -418,9 +401,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_ID)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoIn, result.get(0)); } @@ -434,9 +415,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_IN)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_IN), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); } @@ -450,9 +429,7 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { subtypes); final ArrayList result = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( - createTargetContextWithLocales(new LocaleList(LOCALE_ID)) - .getResources(), - imi); + getResourcesForLocales(LOCALE_ID), imi); assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); } @@ -638,6 +615,10 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { .createConfigurationContext(resourceConfiguration); } + private Resources getResourcesForLocales(Locale... locales) { + return createTargetContextWithLocales(new LocaleList(locales)).getResources(); + } + private String[] getPackageNames(final ArrayList imis) { final String[] packageNames = new String[imis.size()]; for (int i = 0; i < imis.size(); ++i) { From 102ff0726dad764df741e41766d78fcfb829184a Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Wed, 24 Feb 2016 18:25:16 -0800 Subject: [PATCH 2/3] Add a utility method to filter locales. This is a preparation CL to take secondary system locales into account in InputMethodUtils#getImplicitlyApplicableSubtypesLocked(). Suppose the following situation: available subtypes: en-US, en-IN, and en-GB, fr, fr-CA, fr-CH, fr (QWERTZ) system locales: en-GB, en-US, fr-MC Basically we want to have at most one subtype for each language appears in system locales. Hence the goal of this utility method is to filter the above available subtypes into en-GB and fr. In other word, we do not want to enable both en-GB and en-US subtypes in this scenario. This CL introduces LocaleUtils#filterByLanguage() for this purpose, with some unit tests. Note that that method is not used in production yet. Bug: 27129703 Change-Id: I315cf3722a06e00bdbfac284c4949578da8fe78d --- .../internal/inputmethod/LocaleUtils.java | 142 +++++++++++++ .../internal/inputmethod/LocaleUtilsTest.java | 194 ++++++++++++++++++ 2 files changed, 336 insertions(+) create mode 100644 core/java/com/android/internal/inputmethod/LocaleUtils.java create mode 100644 core/tests/coretests/src/com/android/internal/inputmethod/LocaleUtilsTest.java diff --git a/core/java/com/android/internal/inputmethod/LocaleUtils.java b/core/java/com/android/internal/inputmethod/LocaleUtils.java new file mode 100644 index 0000000000000..99bb4cbea14ad --- /dev/null +++ b/core/java/com/android/internal/inputmethod/LocaleUtils.java @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2016 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 com.android.internal.inputmethod; + +import com.android.internal.annotations.VisibleForTesting; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.TextUtils; +import android.util.LocaleList; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Objects; + +public final class LocaleUtils { + + @VisibleForTesting + public interface LocaleExtractor { + @Nullable + Locale get(@Nullable T source); + } + + @Nullable + private static String getLanguage(@Nullable Locale locale) { + if (locale == null) { + return null; + } + return locale.getLanguage(); + } + + /** + * Filters the given items based on language preferences. + * + *

For each language found in {@code preferredLanguages}, this method tries to copy at most + * one best-match item from {@code source} to {@code dest}. For example, if + * {@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} as follows. + *

    + *
  1. The first instance in {@code sources} that exactly matches {@code "en-GB"}
  2. + *
  3. The first instance in {@code sources} that exactly matches {@code "en-AU"}
  4. + *
  5. The first instance in {@code sources} that exactly matches {@code "en-IN"}
  6. + *
  7. The first instance in {@code sources} that partially matches {@code "en"}
  8. + *
+ *

Then this method iterates the same algorithm for Japanese then French.

+ * + * @param sources Source items to be filtered. + * @param extractor Type converter from the source items to {@link Locale} object. + * @param preferredLanguages Ordered list of locales with which the input items will be + * filtered. + * @param dest Destination into which the filtered items will be added. + * @param Type of the data items. + */ + @VisibleForTesting + public static void filterByLanguage( + @NonNull List sources, + @NonNull LocaleExtractor extractor, + @NonNull LocaleList preferredLanguages, + @NonNull ArrayList dest) { + 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; + } + } + } + } + + + 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; + } + } + + // 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; + } + } + + 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 diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/LocaleUtilsTest.java b/core/tests/coretests/src/com/android/internal/inputmethod/LocaleUtilsTest.java new file mode 100644 index 0000000000000..b9c2da75f55c8 --- /dev/null +++ b/core/tests/coretests/src/com/android/internal/inputmethod/LocaleUtilsTest.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2016 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 com.android.internal.inputmethod; + +import android.test.InstrumentationTestCase; +import android.test.suitebuilder.annotation.SmallTest; +import android.util.LocaleList; + +import java.util.ArrayList; +import java.util.Locale; + +public class LocaleUtilsTest extends InstrumentationTestCase { + + private static final LocaleUtils.LocaleExtractor sIdentityMapper = + new LocaleUtils.LocaleExtractor() { + @Override + public Locale get(Locale source) { + return source; + } + }; + + @SmallTest + public void testFilterByLanguageEmptyLanguageList() throws Exception { + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(Locale.forLanguageTag("en-US")); + availableLocales.add(Locale.forLanguageTag("fr-CA")); + availableLocales.add(Locale.forLanguageTag("in")); + availableLocales.add(Locale.forLanguageTag("ja")); + availableLocales.add(Locale.forLanguageTag("fil")); + + final LocaleList preferredLocales = LocaleList.getEmptyLocaleList(); + + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(0, dest.size()); + } + + @SmallTest + public void testFilterByLanguageEmptySource() throws Exception { + final ArrayList availableLocales = new ArrayList<>(); + + final LocaleList preferredLocales = LocaleList.forLanguageTags("fr,en-US,ja-JP"); + + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(0, dest.size()); + } + + @SmallTest + public void testFilterByLanguageNullAvailableLocales() throws Exception { + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(null); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(0, dest.size()); + } + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(null); + availableLocales.add(null); + availableLocales.add(null); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(0, dest.size()); + } + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(null); + availableLocales.add(Locale.forLanguageTag("en-US")); + availableLocales.add(null); + availableLocales.add(null); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(1, dest.size()); + assertEquals(availableLocales.get(1), dest.get(0)); // "en-US" + } + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(null); + availableLocales.add(Locale.forLanguageTag("en")); + availableLocales.add(null); + availableLocales.add(null); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(1, dest.size()); + assertEquals(availableLocales.get(1), dest.get(0)); // "en" + } + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(null); + availableLocales.add(Locale.forLanguageTag("ja-JP")); + availableLocales.add(null); + availableLocales.add(null); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(0, dest.size()); + } + } + + @SmallTest + public void testFilterByLanguage() throws Exception { + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(Locale.forLanguageTag("en-US")); + availableLocales.add(Locale.forLanguageTag("fr-CA")); + availableLocales.add(Locale.forLanguageTag("in")); + availableLocales.add(Locale.forLanguageTag("ja")); + availableLocales.add(Locale.forLanguageTag("fil")); + + final LocaleList preferredLocales = LocaleList.forLanguageTags("fr,en-US,ja-JP"); + + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(3, dest.size()); + assertEquals(availableLocales.get(1), dest.get(0)); // "fr-CA" + assertEquals(availableLocales.get(0), dest.get(1)); // "en-US" + assertEquals(availableLocales.get(3), dest.get(2)); // "ja" + } + + @SmallTest + public void testFilterByLanguageTheSameLanguage() throws Exception { + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(Locale.forLanguageTag("fr-CA")); + availableLocales.add(Locale.forLanguageTag("en-US")); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(1, dest.size()); + assertEquals(availableLocales.get(1), dest.get(0)); // "en-US" + } + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(Locale.forLanguageTag("fr-CA")); + availableLocales.add(Locale.forLanguageTag("en")); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(1, dest.size()); + assertEquals(availableLocales.get(1), dest.get(0)); // "en" + } + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(Locale.forLanguageTag("fr-CA")); + availableLocales.add(Locale.forLanguageTag("en-CA")); + availableLocales.add(Locale.forLanguageTag("en-IN")); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(1, dest.size()); + assertEquals(availableLocales.get(2), dest.get(0)); // "en-IN" + } + { + final LocaleList preferredLocales = + LocaleList.forLanguageTags("en-AU,en-GB,en-US,en-IN"); + final ArrayList availableLocales = new ArrayList<>(); + availableLocales.add(Locale.forLanguageTag("fr-CA")); + availableLocales.add(Locale.forLanguageTag("en-CA")); + availableLocales.add(Locale.forLanguageTag("en-NZ")); + availableLocales.add(Locale.forLanguageTag("en-BZ")); + final ArrayList dest = new ArrayList<>(); + LocaleUtils.filterByLanguage(availableLocales, sIdentityMapper, preferredLocales, dest); + assertEquals(1, dest.size()); + assertEquals(availableLocales.get(1), dest.get(0)); // "en-CA" + } + } +} From e985c240e3feb62ea38d5b4e386be083ca0f215b Mon Sep 17 00:00:00 2001 From: Yohei Yukawa Date: Wed, 24 Feb 2016 18:27:04 -0800 Subject: [PATCH 3/3] Use LocaleList for implicitly enabled subtypes. There are two major changes in this CL: 1. Now IMMS resets its internal state whenever the system locale list is changed, rather than just checking the primary system locale. 2. For software keyboard subtypes, InputMethodUtils#getImplicitlyApplicableSubtypesLocked() now takes the entire system locale list into account when determining what subtypes should be enabled by default when the user does not explicitly enable one or more subtypes. Bug: 27129703 Change-Id: Iaf179d60c12b9a98b4f097e2449471c4184e049b --- .../inputmethod/InputMethodUtils.java | 95 +++++++++++-------- .../inputmethod/InputMethodUtilsTest.java | 33 +++++++ .../server/InputMethodManagerService.java | 14 +-- 3 files changed, 97 insertions(+), 45 deletions(-) diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java index 62e149afab7f1..4e48e45d018ec 100644 --- a/core/java/com/android/internal/inputmethod/InputMethodUtils.java +++ b/core/java/com/android/internal/inputmethod/InputMethodUtils.java @@ -32,6 +32,7 @@ import android.text.TextUtils; import android.text.TextUtils.SimpleStringSplitter; import android.util.ArrayMap; import android.util.ArraySet; +import android.util.LocaleList; import android.util.Pair; import android.util.Printer; import android.util.Slog; @@ -486,18 +487,29 @@ public class InputMethodUtils { return NOT_A_SUBTYPE_ID; } + private static final LocaleUtils.LocaleExtractor sSubtypeToLocale = + new LocaleUtils.LocaleExtractor() { + @Override + public Locale get(InputMethodSubtype source) { + return source != null ? source.getLocaleObject() : null; + } + }; + @VisibleForTesting public static ArrayList getImplicitlyApplicableSubtypesLocked( Resources res, InputMethodInfo imi) { final List subtypes = InputMethodUtils.getSubtypes(imi); - final String systemLocale = res.getConfiguration().locale.toString(); + final LocaleList systemLocales = res.getConfiguration().getLocales(); + final String systemLocale = systemLocales.get(0).toString(); if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>(); - final String systemLanguage = res.getConfiguration().locale.getLanguage(); + final int numSubtypes = subtypes.size(); + + // Handle overridesImplicitlyEnabledSubtype mechanism. + final String systemLanguage = systemLocales.get(0).getLanguage(); final HashMap applicableModeAndSubtypesMap = new HashMap<>(); - final int N = subtypes.size(); - for (int i = 0; i < N; ++i) { + for (int i = 0; i < numSubtypes; ++i) { // scan overriding implicitly enabled subtypes. - InputMethodSubtype subtype = subtypes.get(i); + final InputMethodSubtype subtype = subtypes.get(i); if (subtype.overridesImplicitlyEnabledSubtype()) { final String mode = subtype.getMode(); if (!applicableModeAndSubtypesMap.containsKey(mode)) { @@ -508,42 +520,46 @@ public class InputMethodUtils { if (applicableModeAndSubtypesMap.size() > 0) { return new ArrayList<>(applicableModeAndSubtypesMap.values()); } - for (int i = 0; i < N; ++i) { + + final ArrayList keyboardSubtypes = new ArrayList<>(); + for (int i = 0; i < numSubtypes; ++i) { final InputMethodSubtype subtype = subtypes.get(i); - final String locale = subtype.getLocale(); - final String mode = subtype.getMode(); - final String language = getLanguageFromLocaleString(locale); - // When system locale starts with subtype's locale, that subtype will be applicable - // for system locale. We need to make sure the languages are the same, to prevent - // locales like "fil" (Filipino) being matched by "fi" (Finnish). - // - // For instance, it's clearly applicable for cases like system locale = en_US and - // subtype = en, but it is not necessarily considered applicable for cases like system - // locale = en and subtype = en_US. - // - // We just call systemLocale.startsWith(locale) in this function because there is no - // need to find applicable subtypes aggressively unlike - // findLastResortApplicableSubtypeLocked. - // - // TODO: This check is broken. It won't take scripts into account and doesn't - // account for the mandatory conversions performed by Locale#toString. - if (language.equals(systemLanguage) && systemLocale.startsWith(locale)) { - final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode); - // If more applicable subtypes are contained, skip. - if (applicableSubtype != null) { - if (systemLocale.equals(applicableSubtype.getLocale())) continue; - if (!systemLocale.equals(locale)) continue; + if (TextUtils.equals(SUBTYPE_MODE_KEYBOARD, subtype.getMode())) { + keyboardSubtypes.add(subtype); + } else { + final Locale locale = subtype.getLocaleObject(); + final String mode = subtype.getMode(); + // TODO: Take secondary system locales into consideration. + if (locale != null && locale.equals(systemLanguage)) { + final InputMethodSubtype applicableSubtype = + applicableModeAndSubtypesMap.get(mode); + // If more applicable subtypes are contained, skip. + if (applicableSubtype != null) { + if (systemLocale.equals(applicableSubtype.getLocaleObject())) continue; + if (!systemLocale.equals(locale)) continue; + } + applicableModeAndSubtypesMap.put(mode, subtype); } - applicableModeAndSubtypesMap.put(mode, subtype); } } - final InputMethodSubtype keyboardSubtype - = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD); - final ArrayList applicableSubtypes = new ArrayList<>( - applicableModeAndSubtypesMap.values()); - if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) { - for (int i = 0; i < N; ++i) { - final InputMethodSubtype subtype = subtypes.get(i); + + final ArrayList applicableSubtypes = new ArrayList<>(); + LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales, + applicableSubtypes); + + boolean hasAsciiCapableKeyboard = false; + final int numApplicationSubtypes = applicableSubtypes.size(); + for (int i = 0; i < numApplicationSubtypes; ++i) { + final InputMethodSubtype subtype = applicableSubtypes.get(i); + if (subtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) { + hasAsciiCapableKeyboard = true; + break; + } + } + if (!hasAsciiCapableKeyboard) { + final int numKeyboardSubtypes = keyboardSubtypes.size(); + for (int i = 0; i < numKeyboardSubtypes; ++i) { + final InputMethodSubtype subtype = keyboardSubtypes.get(i); final String mode = subtype.getMode(); if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey( TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) { @@ -551,13 +567,16 @@ public class InputMethodUtils { } } } - if (keyboardSubtype == null) { + + if (applicableSubtypes.isEmpty()) { InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); if (lastResortKeyboardSubtype != null) { applicableSubtypes.add(lastResortKeyboardSubtype); } } + + applicableSubtypes.addAll(applicableModeAndSubtypesMap.values()); return applicableSubtypes; } diff --git a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodUtilsTest.java b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodUtilsTest.java index 380d3b44d8ea5..ac020e48355ac 100644 --- a/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodUtilsTest.java +++ b/core/tests/coretests/src/com/android/internal/inputmethod/InputMethodUtilsTest.java @@ -37,6 +37,10 @@ import java.util.List; import java.util.Locale; import java.util.Objects; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.isIn; +import static org.hamcrest.Matchers.not; + public class InputMethodUtilsTest extends InstrumentationTestCase { private static final boolean IS_AUX = true; private static final boolean IS_DEFAULT = true; @@ -187,6 +191,9 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { final InputMethodSubtype nonAutoEnGB = createDummyInputMethodSubtype("en_GB", SUBTYPE_MODE_KEYBOARD, !IS_AUX, !IS_OVERRIDES_IMPLICITLY_ENABLED_SUBTYPE, IS_ASCII_CAPABLE, IS_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE); + final InputMethodSubtype nonAutoEnIN = createDummyInputMethodSubtype("en_IN", + SUBTYPE_MODE_KEYBOARD, !IS_AUX, !IS_OVERRIDES_IMPLICITLY_ENABLED_SUBTYPE, + IS_ASCII_CAPABLE, IS_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE); final InputMethodSubtype nonAutoFrCA = createDummyInputMethodSubtype("fr_CA", SUBTYPE_MODE_KEYBOARD, !IS_AUX, !IS_OVERRIDES_IMPLICITLY_ENABLED_SUBTYPE, IS_ASCII_CAPABLE, IS_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE); @@ -433,6 +440,32 @@ public class InputMethodUtilsTest extends InstrumentationTestCase { assertEquals(1, result.size()); verifyEquality(nonAutoId, result.get(0)); } + + // If there is no automatic subtype (overridesImplicitlyEnabledSubtype:true) and the system + // provides multiple locales, we try to enable multiple subtypes. + { + final ArrayList subtypes = new ArrayList<>(); + subtypes.add(nonAutoEnUS); + subtypes.add(nonAutoFrCA); + subtypes.add(nonAutoIn); + subtypes.add(nonAutoJa); + subtypes.add(nonAutoFil); + subtypes.add(nonAutoEnabledWhenDefaultIsNotAsciiCalableSubtype); + subtypes.add(nonAutoEnabledWhenDefaultIsNotAsciiCalableSubtype2); + final InputMethodInfo imi = createDummyInputMethodInfo( + "com.android.apps.inputmethod.latin", + "com.android.apps.inputmethod.latin", "DummyLatinIme", !IS_AUX, IS_DEFAULT, + subtypes); + final ArrayList result = + InputMethodUtils.getImplicitlyApplicableSubtypesLocked( + getResourcesForLocales(LOCALE_FR, LOCALE_EN_US, LOCALE_JA_JP), imi); + assertThat(nonAutoFrCA, isIn(result)); + assertThat(nonAutoEnUS, isIn(result)); + assertThat(nonAutoJa, isIn(result)); + assertThat(nonAutoIn, not(isIn(result))); + assertThat(nonAutoEnabledWhenDefaultIsNotAsciiCalableSubtype, not(isIn(result))); + assertThat(nonAutoEnabledWhenDefaultIsNotAsciiCalableSubtype, not(isIn(result))); + } } @SmallTest diff --git a/services/core/java/com/android/server/InputMethodManagerService.java b/services/core/java/com/android/server/InputMethodManagerService.java index 5ba8bd555237f..63c9822d82c8c 100644 --- a/services/core/java/com/android/server/InputMethodManagerService.java +++ b/services/core/java/com/android/server/InputMethodManagerService.java @@ -95,6 +95,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.AtomicFile; import android.util.EventLog; +import android.util.LocaleList; import android.util.LruCache; import android.util.Pair; import android.util.PrintWriterPrinter; @@ -135,7 +136,6 @@ import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Locale; /** * This class provides a system service that manages input methods. @@ -446,7 +446,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub private View mSwitchingDialogTitleView; private InputMethodInfo[] mIms; private int[] mSubtypeIds; - private Locale mLastSystemLocale; + private LocaleList mLastSystemLocales; private boolean mShowImeWithHardKeyboard; private boolean mAccessibilityRequestingNoSoftKeyboard; private final MyPackageMonitor mMyPackageMonitor = new MyPackageMonitor(); @@ -949,15 +949,15 @@ public class InputMethodManagerService extends IInputMethodManager.Stub // not system ready return; } - final Locale newLocale = mRes.getConfiguration().locale; + final LocaleList newLocales = mRes.getConfiguration().getLocales(); if (!updateOnlyWhenLocaleChanged - || (newLocale != null && !newLocale.equals(mLastSystemLocale))) { + || (newLocales != null && !newLocales.equals(mLastSystemLocales))) { if (!updateOnlyWhenLocaleChanged) { hideCurrentInputLocked(0, null); resetCurrentMethodAndClient(InputMethodClient.UNBIND_REASON_RESET_IME); } if (DEBUG) { - Slog.i(TAG, "Locale has been changed to " + newLocale); + Slog.i(TAG, "LocaleList has been changed to " + newLocales); } buildInputMethodListLocked(resetDefaultEnabledIme); if (!updateOnlyWhenLocaleChanged) { @@ -972,7 +972,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub resetDefaultImeLocked(mContext); } updateFromSettingsLocked(true); - mLastSystemLocale = newLocale; + mLastSystemLocales = newLocales; if (!updateOnlyWhenLocaleChanged) { try { startInputInnerLocked(); @@ -1079,7 +1079,7 @@ public class InputMethodManagerService extends IInputMethodManager.Stub mSettings.getEnabledInputMethodListLocked(), mSettings.getCurrentUserId(), mContext.getBasePackageName()); } - mLastSystemLocale = mRes.getConfiguration().locale; + mLastSystemLocales = mRes.getConfiguration().getLocales(); try { startInputInnerLocked(); } catch (RuntimeException e) {