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
diff --git a/core/java/com/android/internal/inputmethod/InputMethodUtils.java b/core/java/com/android/internal/inputmethod/InputMethodUtils.java
index 62e149a..4e48e45 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.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 @@
         return NOT_A_SUBTYPE_ID;
     }
 
+    private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
+            new LocaleUtils.LocaleExtractor<InputMethodSubtype>() {
+                @Override
+                public Locale get(InputMethodSubtype source) {
+                    return source != null ? source.getLocaleObject() : null;
+                }
+            };
+
     @VisibleForTesting
     public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
             Resources res, InputMethodInfo imi) {
         final List<InputMethodSubtype> 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<String, InputMethodSubtype> 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 @@
         if (applicableModeAndSubtypesMap.size() > 0) {
             return new ArrayList<>(applicableModeAndSubtypesMap.values());
         }
-        for (int i = 0; i < N; ++i) {
+
+        final ArrayList<InputMethodSubtype> 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<InputMethodSubtype> 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<InputMethodSubtype> 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 @@
                 }
             }
         }
-        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 380d3b4..ac020e4 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.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 @@
         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 @@
             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<InputMethodSubtype> 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<InputMethodSubtype> 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 5ba8bd5..63c9822 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.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.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 @@
     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 @@
             // 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 @@
                 resetDefaultImeLocked(mContext);
             }
             updateFromSettingsLocked(true);
-            mLastSystemLocale = newLocale;
+            mLastSystemLocales = newLocales;
             if (!updateOnlyWhenLocaleChanged) {
                 try {
                     startInputInnerLocked();
@@ -1079,7 +1079,7 @@
                             mSettings.getEnabledInputMethodListLocked(),
                             mSettings.getCurrentUserId(), mContext.getBasePackageName());
                 }
-                mLastSystemLocale = mRes.getConfiguration().locale;
+                mLastSystemLocales = mRes.getConfiguration().getLocales();
                 try {
                     startInputInnerLocked();
                 } catch (RuntimeException e) {