Home | History | Annotate | Download | only in inputmethod
      1 /*
      2  * Copyright (C) 2013 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.internal.inputmethod;
     18 
     19 import android.annotation.NonNull;
     20 import android.annotation.Nullable;
     21 import android.app.AppOpsManager;
     22 import android.content.ContentResolver;
     23 import android.content.Context;
     24 import android.content.pm.ApplicationInfo;
     25 import android.content.pm.IPackageManager;
     26 import android.content.pm.PackageManager;
     27 import android.content.res.Resources;
     28 import android.os.RemoteException;
     29 import android.provider.Settings;
     30 import android.provider.Settings.SettingNotFoundException;
     31 import android.text.TextUtils;
     32 import android.util.Pair;
     33 import android.util.Slog;
     34 import android.view.inputmethod.InputMethodInfo;
     35 import android.view.inputmethod.InputMethodSubtype;
     36 import android.view.textservice.SpellCheckerInfo;
     37 import android.view.textservice.TextServicesManager;
     38 
     39 import com.android.internal.annotations.VisibleForTesting;
     40 
     41 import java.util.ArrayList;
     42 import java.util.Arrays;
     43 import java.util.HashMap;
     44 import java.util.LinkedHashSet;
     45 import java.util.List;
     46 import java.util.Locale;
     47 
     48 /**
     49  * InputMethodManagerUtils contains some static methods that provides IME informations.
     50  * This methods are supposed to be used in both the framework and the Settings application.
     51  */
     52 public class InputMethodUtils {
     53     public static final boolean DEBUG = false;
     54     public static final int NOT_A_SUBTYPE_ID = -1;
     55     public static final String SUBTYPE_MODE_ANY = null;
     56     public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
     57     public static final String SUBTYPE_MODE_VOICE = "voice";
     58     private static final String TAG = "InputMethodUtils";
     59     private static final Locale ENGLISH_LOCALE = new Locale("en");
     60     private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
     61     private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
     62             "EnabledWhenDefaultIsNotAsciiCapable";
     63     private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
     64     /**
     65      * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs
     66      * that are mainly used until the system becomes ready. Note that {@link Locale} in this array
     67      * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH}
     68      * doesn't automatically match {@code Locale("en", "IN")}.
     69      */
     70     private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = {
     71         Locale.ENGLISH, // "en"
     72         Locale.US, // "en_US"
     73         Locale.UK, // "en_GB"
     74     };
     75 
     76     private InputMethodUtils() {
     77         // This utility class is not publicly instantiable.
     78     }
     79 
     80     // ----------------------------------------------------------------------
     81     // Utilities for debug
     82     public static String getStackTrace() {
     83         final StringBuilder sb = new StringBuilder();
     84         try {
     85             throw new RuntimeException();
     86         } catch (RuntimeException e) {
     87             final StackTraceElement[] frames = e.getStackTrace();
     88             // Start at 1 because the first frame is here and we don't care about it
     89             for (int j = 1; j < frames.length; ++j) {
     90                 sb.append(frames[j].toString() + "\n");
     91             }
     92         }
     93         return sb.toString();
     94     }
     95 
     96     public static String getApiCallStack() {
     97         String apiCallStack = "";
     98         try {
     99             throw new RuntimeException();
    100         } catch (RuntimeException e) {
    101             final StackTraceElement[] frames = e.getStackTrace();
    102             for (int j = 1; j < frames.length; ++j) {
    103                 final String tempCallStack = frames[j].toString();
    104                 if (TextUtils.isEmpty(apiCallStack)) {
    105                     // Overwrite apiCallStack if it's empty
    106                     apiCallStack = tempCallStack;
    107                 } else if (tempCallStack.indexOf("Transact(") < 0) {
    108                     // Overwrite apiCallStack if it's not a binder call
    109                     apiCallStack = tempCallStack;
    110                 } else {
    111                     break;
    112                 }
    113             }
    114         }
    115         return apiCallStack;
    116     }
    117     // ----------------------------------------------------------------------
    118 
    119     public static boolean isSystemIme(InputMethodInfo inputMethod) {
    120         return (inputMethod.getServiceInfo().applicationInfo.flags
    121                 & ApplicationInfo.FLAG_SYSTEM) != 0;
    122     }
    123 
    124     public static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi,
    125             final Context context, final boolean checkDefaultAttribute,
    126             @Nullable final Locale requiredLocale, final boolean checkCountry,
    127             final String requiredSubtypeMode) {
    128         if (!isSystemIme(imi)) {
    129             return false;
    130         }
    131         if (checkDefaultAttribute && !imi.isDefault(context)) {
    132             return false;
    133         }
    134         if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) {
    135             return false;
    136         }
    137         return true;
    138     }
    139 
    140     @Nullable
    141     public static Locale getFallbackLocaleForDefaultIme(final ArrayList<InputMethodInfo> imis,
    142             final Context context) {
    143         // At first, find the fallback locale from the IMEs that are declared as "default" in the
    144         // current locale.  Note that IME developers can declare an IME as "default" only for
    145         // some particular locales but "not default" for other locales.
    146         for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
    147             for (int i = 0; i < imis.size(); ++i) {
    148                 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
    149                         true /* checkDefaultAttribute */, fallbackLocale,
    150                         true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
    151                     return fallbackLocale;
    152                 }
    153             }
    154         }
    155         // If no fallback locale is found in the above condition, find fallback locales regardless
    156         // of the "default" attribute as a last resort.
    157         for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
    158             for (int i = 0; i < imis.size(); ++i) {
    159                 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
    160                         false /* checkDefaultAttribute */, fallbackLocale,
    161                         true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
    162                     return fallbackLocale;
    163                 }
    164             }
    165         }
    166         Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray()));
    167         return null;
    168     }
    169 
    170     private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(final InputMethodInfo imi,
    171             final Context context, final boolean checkDefaultAttribute) {
    172         if (!isSystemIme(imi)) {
    173             return false;
    174         }
    175         if (checkDefaultAttribute && !imi.isDefault(context)) {
    176             return false;
    177         }
    178         if (!imi.isAuxiliaryIme()) {
    179             return false;
    180         }
    181         final int subtypeCount = imi.getSubtypeCount();
    182         for (int i = 0; i < subtypeCount; ++i) {
    183             final InputMethodSubtype s = imi.getSubtypeAt(i);
    184             if (s.overridesImplicitlyEnabledSubtype()) {
    185                 return true;
    186             }
    187         }
    188         return false;
    189     }
    190 
    191     public static Locale getSystemLocaleFromContext(final Context context) {
    192         try {
    193             return context.getResources().getConfiguration().locale;
    194         } catch (Resources.NotFoundException ex) {
    195             return null;
    196         }
    197     }
    198 
    199     private static final class InputMethodListBuilder {
    200         // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration
    201         // order can have non-trivial effect in the call sites.
    202         @NonNull
    203         private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>();
    204 
    205         public InputMethodListBuilder fillImes(final ArrayList<InputMethodInfo> imis,
    206                 final Context context, final boolean checkDefaultAttribute,
    207                 @Nullable final Locale locale, final boolean checkCountry,
    208                 final String requiredSubtypeMode) {
    209             for (int i = 0; i < imis.size(); ++i) {
    210                 final InputMethodInfo imi = imis.get(i);
    211                 if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale,
    212                         checkCountry, requiredSubtypeMode)) {
    213                     mInputMethodSet.add(imi);
    214                 }
    215             }
    216             return this;
    217         }
    218 
    219         // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be
    220         // documented more clearly.
    221         public InputMethodListBuilder fillAuxiliaryImes(final ArrayList<InputMethodInfo> imis,
    222                 final Context context) {
    223             // If one or more auxiliary input methods are available, OK to stop populating the list.
    224             for (final InputMethodInfo imi : mInputMethodSet) {
    225                 if (imi.isAuxiliaryIme()) {
    226                     return this;
    227                 }
    228             }
    229             boolean added = false;
    230             for (int i = 0; i < imis.size(); ++i) {
    231                 final InputMethodInfo imi = imis.get(i);
    232                 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
    233                         true /* checkDefaultAttribute */)) {
    234                     mInputMethodSet.add(imi);
    235                     added = true;
    236                 }
    237             }
    238             if (added) {
    239                 return this;
    240             }
    241             for (int i = 0; i < imis.size(); ++i) {
    242                 final InputMethodInfo imi = imis.get(i);
    243                 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
    244                         false /* checkDefaultAttribute */)) {
    245                     mInputMethodSet.add(imi);
    246                 }
    247             }
    248             return this;
    249         }
    250 
    251         public boolean isEmpty() {
    252             return mInputMethodSet.isEmpty();
    253         }
    254 
    255         @NonNull
    256         public ArrayList<InputMethodInfo> build() {
    257             return new ArrayList<>(mInputMethodSet);
    258         }
    259     }
    260 
    261     private static InputMethodListBuilder getMinimumKeyboardSetWithoutSystemLocale(
    262             final ArrayList<InputMethodInfo> imis, final Context context,
    263             @Nullable final Locale fallbackLocale) {
    264         // Before the system becomes ready, we pick up at least one keyboard in the following order.
    265         // The first user (device owner) falls into this category.
    266         // 1. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
    267         // 2. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
    268         // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
    269         // 4. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
    270         // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
    271 
    272         final InputMethodListBuilder builder = new InputMethodListBuilder();
    273         builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
    274                 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    275         if (!builder.isEmpty()) {
    276             return builder;
    277         }
    278         builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
    279                 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    280         if (!builder.isEmpty()) {
    281             return builder;
    282         }
    283         builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
    284                 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    285         if (!builder.isEmpty()) {
    286             return builder;
    287         }
    288         builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
    289                 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    290         if (!builder.isEmpty()) {
    291             return builder;
    292         }
    293         Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
    294                 + " fallbackLocale=" + fallbackLocale);
    295         return builder;
    296     }
    297 
    298     private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale(
    299             final ArrayList<InputMethodInfo> imis, final Context context,
    300             @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) {
    301         // Once the system becomes ready, we pick up at least one keyboard in the following order.
    302         // Secondary users fall into this category in general.
    303         // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true
    304         // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false
    305         // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
    306         // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
    307         // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
    308         // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
    309         // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
    310 
    311         final InputMethodListBuilder builder = new InputMethodListBuilder();
    312         builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
    313                 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    314         if (!builder.isEmpty()) {
    315             return builder;
    316         }
    317         builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
    318                 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    319         if (!builder.isEmpty()) {
    320             return builder;
    321         }
    322         builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
    323                 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    324         if (!builder.isEmpty()) {
    325             return builder;
    326         }
    327         builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
    328                 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    329         if (!builder.isEmpty()) {
    330             return builder;
    331         }
    332         builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
    333                 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    334         if (!builder.isEmpty()) {
    335             return builder;
    336         }
    337         builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
    338                 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
    339         if (!builder.isEmpty()) {
    340             return builder;
    341         }
    342         Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
    343                 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale);
    344         return builder;
    345     }
    346 
    347     public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context,
    348             final boolean isSystemReady, final ArrayList<InputMethodInfo> imis) {
    349         final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
    350         if (!isSystemReady) {
    351             // When the system is not ready, the system locale is not stable and reliable. Hence
    352             // we will pick up IMEs that support software keyboard based on the fallback locale.
    353             // Also pick up suitable IMEs regardless of the software keyboard support.
    354             // (e.g. Voice IMEs)
    355             return getMinimumKeyboardSetWithoutSystemLocale(imis, context, fallbackLocale)
    356                     .fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
    357                             true /* checkCountry */, SUBTYPE_MODE_ANY)
    358                     .build();
    359         }
    360 
    361         // When the system is ready, we will primarily rely on the system locale, but also keep
    362         // relying on the fallback locale as a last resort.
    363         // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs),
    364         // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic"
    365         // subtype)
    366         final Locale systemLocale = getSystemLocaleFromContext(context);
    367         return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale)
    368                 .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
    369                         true /* checkCountry */, SUBTYPE_MODE_ANY)
    370                 .fillAuxiliaryImes(imis, context)
    371                 .build();
    372     }
    373 
    374     public static Locale constructLocaleFromString(String localeStr) {
    375         if (TextUtils.isEmpty(localeStr)) {
    376             return null;
    377         }
    378         // TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(languageTag)}.
    379         String[] localeParams = localeStr.split("_", 3);
    380         // The length of localeStr is guaranteed to always return a 1 <= value <= 3
    381         // because localeStr is not empty.
    382         if (localeParams.length == 1) {
    383             if (localeParams.length >= 1 && "tl".equals(localeParams[0])) {
    384                 // Convert a locale whose language is "tl" to one whose language is "fil".
    385                 // For example, "tl_PH" will get converted to "fil_PH".
    386                 // Versions of Android earlier than Lollipop did not support three letter language
    387                 // codes, and used "tl" (Tagalog) as the language string for "fil" (Filipino).
    388                 // On Lollipop and above, the current three letter version must be used.
    389                 localeParams[0] = "fil";
    390             }
    391             return new Locale(localeParams[0]);
    392         } else if (localeParams.length == 2) {
    393             return new Locale(localeParams[0], localeParams[1]);
    394         } else if (localeParams.length == 3) {
    395             return new Locale(localeParams[0], localeParams[1], localeParams[2]);
    396         }
    397         return null;
    398     }
    399 
    400     public static boolean containsSubtypeOf(final InputMethodInfo imi,
    401             @Nullable final Locale locale, final boolean checkCountry, final String mode) {
    402         if (locale == null) {
    403             return false;
    404         }
    405         final int N = imi.getSubtypeCount();
    406         for (int i = 0; i < N; ++i) {
    407             final InputMethodSubtype subtype = imi.getSubtypeAt(i);
    408             if (checkCountry) {
    409                 final Locale subtypeLocale = subtype.getLocaleObject();
    410                 if (subtypeLocale == null ||
    411                         !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
    412                         !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
    413                     continue;
    414                 }
    415             } else {
    416                 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
    417                         subtype.getLocale()));
    418                 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
    419                     continue;
    420                 }
    421             }
    422             if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
    423                     mode.equalsIgnoreCase(subtype.getMode())) {
    424                 return true;
    425             }
    426         }
    427         return false;
    428     }
    429 
    430     public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
    431         ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
    432         final int subtypeCount = imi.getSubtypeCount();
    433         for (int i = 0; i < subtypeCount; ++i) {
    434             subtypes.add(imi.getSubtypeAt(i));
    435         }
    436         return subtypes;
    437     }
    438 
    439     public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
    440             InputMethodInfo imi, String mode) {
    441         ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
    442         final int subtypeCount = imi.getSubtypeCount();
    443         for (int i = 0; i < subtypeCount; ++i) {
    444             final InputMethodSubtype subtype = imi.getSubtypeAt(i);
    445             if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
    446                 subtypes.add(subtype);
    447             }
    448         }
    449         return subtypes;
    450     }
    451 
    452     public static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
    453         if (enabledImes == null || enabledImes.isEmpty()) {
    454             return null;
    455         }
    456         // We'd prefer to fall back on a system IME, since that is safer.
    457         int i = enabledImes.size();
    458         int firstFoundSystemIme = -1;
    459         while (i > 0) {
    460             i--;
    461             final InputMethodInfo imi = enabledImes.get(i);
    462             if (imi.isAuxiliaryIme()) {
    463                 continue;
    464             }
    465             if (InputMethodUtils.isSystemIme(imi)
    466                     && containsSubtypeOf(imi, ENGLISH_LOCALE, false /* checkCountry */,
    467                             SUBTYPE_MODE_KEYBOARD)) {
    468                 return imi;
    469             }
    470             if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)) {
    471                 firstFoundSystemIme = i;
    472             }
    473         }
    474         return enabledImes.get(Math.max(firstFoundSystemIme, 0));
    475     }
    476 
    477     public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
    478         return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
    479     }
    480 
    481     public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
    482         if (imi != null) {
    483             final int subtypeCount = imi.getSubtypeCount();
    484             for (int i = 0; i < subtypeCount; ++i) {
    485                 InputMethodSubtype ims = imi.getSubtypeAt(i);
    486                 if (subtypeHashCode == ims.hashCode()) {
    487                     return i;
    488                 }
    489             }
    490         }
    491         return NOT_A_SUBTYPE_ID;
    492     }
    493 
    494     @VisibleForTesting
    495     public static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
    496             Resources res, InputMethodInfo imi) {
    497         final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
    498         final String systemLocale = res.getConfiguration().locale.toString();
    499         if (TextUtils.isEmpty(systemLocale)) return new ArrayList<InputMethodSubtype>();
    500         final String systemLanguage = res.getConfiguration().locale.getLanguage();
    501         final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap =
    502                 new HashMap<String, InputMethodSubtype>();
    503         final int N = subtypes.size();
    504         for (int i = 0; i < N; ++i) {
    505             // scan overriding implicitly enabled subtypes.
    506             InputMethodSubtype subtype = subtypes.get(i);
    507             if (subtype.overridesImplicitlyEnabledSubtype()) {
    508                 final String mode = subtype.getMode();
    509                 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
    510                     applicableModeAndSubtypesMap.put(mode, subtype);
    511                 }
    512             }
    513         }
    514         if (applicableModeAndSubtypesMap.size() > 0) {
    515             return new ArrayList<InputMethodSubtype>(applicableModeAndSubtypesMap.values());
    516         }
    517         for (int i = 0; i < N; ++i) {
    518             final InputMethodSubtype subtype = subtypes.get(i);
    519             final String locale = subtype.getLocale();
    520             final String mode = subtype.getMode();
    521             final String language = getLanguageFromLocaleString(locale);
    522             // When system locale starts with subtype's locale, that subtype will be applicable
    523             // for system locale. We need to make sure the languages are the same, to prevent
    524             // locales like "fil" (Filipino) being matched by "fi" (Finnish).
    525             //
    526             // For instance, it's clearly applicable for cases like system locale = en_US and
    527             // subtype = en, but it is not necessarily considered applicable for cases like system
    528             // locale = en and subtype = en_US.
    529             //
    530             // We just call systemLocale.startsWith(locale) in this function because there is no
    531             // need to find applicable subtypes aggressively unlike
    532             // findLastResortApplicableSubtypeLocked.
    533             //
    534             // TODO: This check is broken. It won't take scripts into account and doesn't
    535             // account for the mandatory conversions performed by Locale#toString.
    536             if (language.equals(systemLanguage) && systemLocale.startsWith(locale)) {
    537                 final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode);
    538                 // If more applicable subtypes are contained, skip.
    539                 if (applicableSubtype != null) {
    540                     if (systemLocale.equals(applicableSubtype.getLocale())) continue;
    541                     if (!systemLocale.equals(locale)) continue;
    542                 }
    543                 applicableModeAndSubtypesMap.put(mode, subtype);
    544             }
    545         }
    546         final InputMethodSubtype keyboardSubtype
    547                 = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD);
    548         final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<InputMethodSubtype>(
    549                 applicableModeAndSubtypesMap.values());
    550         if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
    551             for (int i = 0; i < N; ++i) {
    552                 final InputMethodSubtype subtype = subtypes.get(i);
    553                 final String mode = subtype.getMode();
    554                 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
    555                         TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
    556                     applicableSubtypes.add(subtype);
    557                 }
    558             }
    559         }
    560         if (keyboardSubtype == null) {
    561             InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
    562                     res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
    563             if (lastResortKeyboardSubtype != null) {
    564                 applicableSubtypes.add(lastResortKeyboardSubtype);
    565             }
    566         }
    567         return applicableSubtypes;
    568     }
    569 
    570     /**
    571      * Returns the language component of a given locale string.
    572      * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
    573      */
    574     public static String getLanguageFromLocaleString(String locale) {
    575         final int idx = locale.indexOf('_');
    576         if (idx < 0) {
    577             return locale;
    578         } else {
    579             return locale.substring(0, idx);
    580         }
    581     }
    582 
    583     /**
    584      * If there are no selected subtypes, tries finding the most applicable one according to the
    585      * given locale.
    586      * @param subtypes this function will search the most applicable subtype in subtypes
    587      * @param mode subtypes will be filtered by mode
    588      * @param locale subtypes will be filtered by locale
    589      * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
    590      * it will return the first subtype matched with mode
    591      * @return the most applicable subtypeId
    592      */
    593     public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
    594             Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
    595             boolean canIgnoreLocaleAsLastResort) {
    596         if (subtypes == null || subtypes.size() == 0) {
    597             return null;
    598         }
    599         if (TextUtils.isEmpty(locale)) {
    600             locale = res.getConfiguration().locale.toString();
    601         }
    602         final String language = getLanguageFromLocaleString(locale);
    603         boolean partialMatchFound = false;
    604         InputMethodSubtype applicableSubtype = null;
    605         InputMethodSubtype firstMatchedModeSubtype = null;
    606         final int N = subtypes.size();
    607         for (int i = 0; i < N; ++i) {
    608             InputMethodSubtype subtype = subtypes.get(i);
    609             final String subtypeLocale = subtype.getLocale();
    610             final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
    611             // An applicable subtype should match "mode". If mode is null, mode will be ignored,
    612             // and all subtypes with all modes can be candidates.
    613             if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
    614                 if (firstMatchedModeSubtype == null) {
    615                     firstMatchedModeSubtype = subtype;
    616                 }
    617                 if (locale.equals(subtypeLocale)) {
    618                     // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
    619                     applicableSubtype = subtype;
    620                     break;
    621                 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
    622                     // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
    623                     applicableSubtype = subtype;
    624                     partialMatchFound = true;
    625                 }
    626             }
    627         }
    628 
    629         if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
    630             return firstMatchedModeSubtype;
    631         }
    632 
    633         // The first subtype applicable to the system locale will be defined as the most applicable
    634         // subtype.
    635         if (DEBUG) {
    636             if (applicableSubtype != null) {
    637                 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
    638                         + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
    639             }
    640         }
    641         return applicableSubtype;
    642     }
    643 
    644     public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
    645         if (subtype == null) return true;
    646         return !subtype.isAuxiliary();
    647     }
    648 
    649     public static void setNonSelectedSystemImesDisabledUntilUsed(
    650             IPackageManager packageManager, List<InputMethodInfo> enabledImis,
    651             int userId, String callingPackage) {
    652         if (DEBUG) {
    653             Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
    654         }
    655         final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
    656                 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
    657         if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
    658             return;
    659         }
    660         // Only the current spell checker should be treated as an enabled one.
    661         final SpellCheckerInfo currentSpellChecker =
    662                 TextServicesManager.getInstance().getCurrentSpellChecker();
    663         for (final String packageName : systemImesDisabledUntilUsed) {
    664             if (DEBUG) {
    665                 Slog.d(TAG, "check " + packageName);
    666             }
    667             boolean enabledIme = false;
    668             for (int j = 0; j < enabledImis.size(); ++j) {
    669                 final InputMethodInfo imi = enabledImis.get(j);
    670                 if (packageName.equals(imi.getPackageName())) {
    671                     enabledIme = true;
    672                     break;
    673                 }
    674             }
    675             if (enabledIme) {
    676                 // enabled ime. skip
    677                 continue;
    678             }
    679             if (currentSpellChecker != null
    680                     && packageName.equals(currentSpellChecker.getPackageName())) {
    681                 // enabled spell checker. skip
    682                 if (DEBUG) {
    683                     Slog.d(TAG, packageName + " is the current spell checker. skip");
    684                 }
    685                 continue;
    686             }
    687             ApplicationInfo ai = null;
    688             try {
    689                 ai = packageManager.getApplicationInfo(packageName,
    690                         PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
    691             } catch (RemoteException e) {
    692                 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
    693                         + " userId=" + userId, e);
    694                 continue;
    695             }
    696             if (ai == null) {
    697                 // No app found for packageName
    698                 continue;
    699             }
    700             final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
    701             if (!isSystemPackage) {
    702                 continue;
    703             }
    704             setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
    705         }
    706     }
    707 
    708     private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
    709             int userId, String callingPackage) {
    710         final int state;
    711         try {
    712             state = packageManager.getApplicationEnabledSetting(packageName, userId);
    713         } catch (RemoteException e) {
    714             Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
    715                     + " userId=" + userId, e);
    716             return;
    717         }
    718         if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
    719                 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
    720             if (DEBUG) {
    721                 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
    722             }
    723             try {
    724                 packageManager.setApplicationEnabledSetting(packageName,
    725                         PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
    726                         0 /* newState */, userId, callingPackage);
    727             } catch (RemoteException e) {
    728                 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
    729                         + " userId=" + userId + " callingPackage=" + callingPackage, e);
    730                 return;
    731             }
    732         } else {
    733             if (DEBUG) {
    734                 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
    735             }
    736         }
    737     }
    738 
    739     public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
    740             InputMethodSubtype subtype) {
    741         final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
    742         return subtype != null
    743                 ? TextUtils.concat(subtype.getDisplayName(context,
    744                         imi.getPackageName(), imi.getServiceInfo().applicationInfo),
    745                                 (TextUtils.isEmpty(imiLabel) ?
    746                                         "" : " - " + imiLabel))
    747                 : imiLabel;
    748     }
    749 
    750     /**
    751      * Returns true if a package name belongs to a UID.
    752      *
    753      * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
    754      * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
    755      * @param uid the UID to be validated.
    756      * @param packageName the package name.
    757      * @return {@code true} if the package name belongs to the UID.
    758      */
    759     public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager,
    760             final int uid, final String packageName) {
    761         try {
    762             appOpsManager.checkPackage(uid, packageName);
    763             return true;
    764         } catch (SecurityException e) {
    765             return false;
    766         }
    767     }
    768 
    769     /**
    770      * Utility class for putting and getting settings for InputMethod
    771      * TODO: Move all putters and getters of settings to this class.
    772      */
    773     public static class InputMethodSettings {
    774         // The string for enabled input method is saved as follows:
    775         // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
    776         private static final char INPUT_METHOD_SEPARATER = ':';
    777         private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';';
    778         private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
    779                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER);
    780 
    781         private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
    782                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER);
    783 
    784         private final Resources mRes;
    785         private final ContentResolver mResolver;
    786         private final HashMap<String, InputMethodInfo> mMethodMap;
    787         private final ArrayList<InputMethodInfo> mMethodList;
    788 
    789         private String mEnabledInputMethodsStrCache;
    790         private int mCurrentUserId;
    791         private int[] mCurrentProfileIds = new int[0];
    792 
    793         private static void buildEnabledInputMethodsSettingString(
    794                 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
    795             builder.append(ime.first);
    796             // Inputmethod and subtypes are saved in the settings as follows:
    797             // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
    798             for (String subtypeId: ime.second) {
    799                 builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId);
    800             }
    801         }
    802 
    803         public static String buildInputMethodsSettingString(
    804                 List<Pair<String, ArrayList<String>>> allImeSettingsMap) {
    805             final StringBuilder b = new StringBuilder();
    806             boolean needsSeparator = false;
    807             for (Pair<String, ArrayList<String>> ime : allImeSettingsMap) {
    808                 if (needsSeparator) {
    809                     b.append(INPUT_METHOD_SEPARATER);
    810                 }
    811                 buildEnabledInputMethodsSettingString(b, ime);
    812                 needsSeparator = true;
    813             }
    814             return b.toString();
    815         }
    816 
    817         public static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
    818                 String enabledInputMethodsStr,
    819                 TextUtils.SimpleStringSplitter inputMethodSplitter,
    820                 TextUtils.SimpleStringSplitter subtypeSplitter) {
    821             ArrayList<Pair<String, ArrayList<String>>> imsList =
    822                     new ArrayList<Pair<String, ArrayList<String>>>();
    823             if (TextUtils.isEmpty(enabledInputMethodsStr)) {
    824                 return imsList;
    825             }
    826             inputMethodSplitter.setString(enabledInputMethodsStr);
    827             while (inputMethodSplitter.hasNext()) {
    828                 String nextImsStr = inputMethodSplitter.next();
    829                 subtypeSplitter.setString(nextImsStr);
    830                 if (subtypeSplitter.hasNext()) {
    831                     ArrayList<String> subtypeHashes = new ArrayList<String>();
    832                     // The first element is ime id.
    833                     String imeId = subtypeSplitter.next();
    834                     while (subtypeSplitter.hasNext()) {
    835                         subtypeHashes.add(subtypeSplitter.next());
    836                     }
    837                     imsList.add(new Pair<String, ArrayList<String>>(imeId, subtypeHashes));
    838                 }
    839             }
    840             return imsList;
    841         }
    842 
    843         public InputMethodSettings(
    844                 Resources res, ContentResolver resolver,
    845                 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
    846                 int userId) {
    847             setCurrentUserId(userId);
    848             mRes = res;
    849             mResolver = resolver;
    850             mMethodMap = methodMap;
    851             mMethodList = methodList;
    852         }
    853 
    854         public void setCurrentUserId(int userId) {
    855             if (DEBUG) {
    856                 Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId);
    857             }
    858             // IMMS settings are kept per user, so keep track of current user
    859             mCurrentUserId = userId;
    860         }
    861 
    862         public void setCurrentProfileIds(int[] currentProfileIds) {
    863             synchronized (this) {
    864                 mCurrentProfileIds = currentProfileIds;
    865             }
    866         }
    867 
    868         public boolean isCurrentProfile(int userId) {
    869             synchronized (this) {
    870                 if (userId == mCurrentUserId) return true;
    871                 for (int i = 0; i < mCurrentProfileIds.length; i++) {
    872                     if (userId == mCurrentProfileIds[i]) return true;
    873                 }
    874                 return false;
    875             }
    876         }
    877 
    878         public List<InputMethodInfo> getEnabledInputMethodListLocked() {
    879             return createEnabledInputMethodListLocked(
    880                     getEnabledInputMethodsAndSubtypeListLocked());
    881         }
    882 
    883         public List<Pair<InputMethodInfo, ArrayList<String>>>
    884                 getEnabledInputMethodAndSubtypeHashCodeListLocked() {
    885             return createEnabledInputMethodAndSubtypeHashCodeListLocked(
    886                     getEnabledInputMethodsAndSubtypeListLocked());
    887         }
    888 
    889         public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
    890                 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
    891             List<InputMethodSubtype> enabledSubtypes =
    892                     getEnabledInputMethodSubtypeListLocked(imi);
    893             if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
    894                 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
    895                         context.getResources(), imi);
    896             }
    897             return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
    898         }
    899 
    900         public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
    901                 InputMethodInfo imi) {
    902             List<Pair<String, ArrayList<String>>> imsList =
    903                     getEnabledInputMethodsAndSubtypeListLocked();
    904             ArrayList<InputMethodSubtype> enabledSubtypes =
    905                     new ArrayList<InputMethodSubtype>();
    906             if (imi != null) {
    907                 for (Pair<String, ArrayList<String>> imsPair : imsList) {
    908                     InputMethodInfo info = mMethodMap.get(imsPair.first);
    909                     if (info != null && info.getId().equals(imi.getId())) {
    910                         final int subtypeCount = info.getSubtypeCount();
    911                         for (int i = 0; i < subtypeCount; ++i) {
    912                             InputMethodSubtype ims = info.getSubtypeAt(i);
    913                             for (String s: imsPair.second) {
    914                                 if (String.valueOf(ims.hashCode()).equals(s)) {
    915                                     enabledSubtypes.add(ims);
    916                                 }
    917                             }
    918                         }
    919                         break;
    920                     }
    921                 }
    922             }
    923             return enabledSubtypes;
    924         }
    925 
    926         // At the initial boot, the settings for input methods are not set,
    927         // so we need to enable IME in that case.
    928         public void enableAllIMEsIfThereIsNoEnabledIME() {
    929             if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
    930                 StringBuilder sb = new StringBuilder();
    931                 final int N = mMethodList.size();
    932                 for (int i = 0; i < N; i++) {
    933                     InputMethodInfo imi = mMethodList.get(i);
    934                     Slog.i(TAG, "Adding: " + imi.getId());
    935                     if (i > 0) sb.append(':');
    936                     sb.append(imi.getId());
    937                 }
    938                 putEnabledInputMethodsStr(sb.toString());
    939             }
    940         }
    941 
    942         public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
    943             return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
    944                     mInputMethodSplitter,
    945                     mSubtypeSplitter);
    946         }
    947 
    948         public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
    949             if (reloadInputMethodStr) {
    950                 getEnabledInputMethodsStr();
    951             }
    952             if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
    953                 // Add in the newly enabled input method.
    954                 putEnabledInputMethodsStr(id);
    955             } else {
    956                 putEnabledInputMethodsStr(
    957                         mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATER + id);
    958             }
    959         }
    960 
    961         /**
    962          * Build and put a string of EnabledInputMethods with removing specified Id.
    963          * @return the specified id was removed or not.
    964          */
    965         public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
    966                 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
    967             boolean isRemoved = false;
    968             boolean needsAppendSeparator = false;
    969             for (Pair<String, ArrayList<String>> ims: imsList) {
    970                 String curId = ims.first;
    971                 if (curId.equals(id)) {
    972                     // We are disabling this input method, and it is
    973                     // currently enabled.  Skip it to remove from the
    974                     // new list.
    975                     isRemoved = true;
    976                 } else {
    977                     if (needsAppendSeparator) {
    978                         builder.append(INPUT_METHOD_SEPARATER);
    979                     } else {
    980                         needsAppendSeparator = true;
    981                     }
    982                     buildEnabledInputMethodsSettingString(builder, ims);
    983                 }
    984             }
    985             if (isRemoved) {
    986                 // Update the setting with the new list of input methods.
    987                 putEnabledInputMethodsStr(builder.toString());
    988             }
    989             return isRemoved;
    990         }
    991 
    992         private List<InputMethodInfo> createEnabledInputMethodListLocked(
    993                 List<Pair<String, ArrayList<String>>> imsList) {
    994             final ArrayList<InputMethodInfo> res = new ArrayList<InputMethodInfo>();
    995             for (Pair<String, ArrayList<String>> ims: imsList) {
    996                 InputMethodInfo info = mMethodMap.get(ims.first);
    997                 if (info != null) {
    998                     res.add(info);
    999                 }
   1000             }
   1001             return res;
   1002         }
   1003 
   1004         private List<Pair<InputMethodInfo, ArrayList<String>>>
   1005                 createEnabledInputMethodAndSubtypeHashCodeListLocked(
   1006                         List<Pair<String, ArrayList<String>>> imsList) {
   1007             final ArrayList<Pair<InputMethodInfo, ArrayList<String>>> res
   1008                     = new ArrayList<Pair<InputMethodInfo, ArrayList<String>>>();
   1009             for (Pair<String, ArrayList<String>> ims : imsList) {
   1010                 InputMethodInfo info = mMethodMap.get(ims.first);
   1011                 if (info != null) {
   1012                     res.add(new Pair<InputMethodInfo, ArrayList<String>>(info, ims.second));
   1013                 }
   1014             }
   1015             return res;
   1016         }
   1017 
   1018         private void putEnabledInputMethodsStr(String str) {
   1019             Settings.Secure.putStringForUser(
   1020                     mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId);
   1021             mEnabledInputMethodsStrCache = str;
   1022             if (DEBUG) {
   1023                 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
   1024             }
   1025         }
   1026 
   1027         public String getEnabledInputMethodsStr() {
   1028             mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser(
   1029                     mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId);
   1030             if (DEBUG) {
   1031                 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
   1032                         + ", " + mCurrentUserId);
   1033             }
   1034             return mEnabledInputMethodsStrCache;
   1035         }
   1036 
   1037         private void saveSubtypeHistory(
   1038                 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
   1039             StringBuilder builder = new StringBuilder();
   1040             boolean isImeAdded = false;
   1041             if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
   1042                 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append(
   1043                         newSubtypeId);
   1044                 isImeAdded = true;
   1045             }
   1046             for (Pair<String, String> ime: savedImes) {
   1047                 String imeId = ime.first;
   1048                 String subtypeId = ime.second;
   1049                 if (TextUtils.isEmpty(subtypeId)) {
   1050                     subtypeId = NOT_A_SUBTYPE_ID_STR;
   1051                 }
   1052                 if (isImeAdded) {
   1053                     builder.append(INPUT_METHOD_SEPARATER);
   1054                 } else {
   1055                     isImeAdded = true;
   1056                 }
   1057                 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append(
   1058                         subtypeId);
   1059             }
   1060             // Remove the last INPUT_METHOD_SEPARATER
   1061             putSubtypeHistoryStr(builder.toString());
   1062         }
   1063 
   1064         private void addSubtypeToHistory(String imeId, String subtypeId) {
   1065             List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
   1066             for (Pair<String, String> ime: subtypeHistory) {
   1067                 if (ime.first.equals(imeId)) {
   1068                     if (DEBUG) {
   1069                         Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
   1070                                 + ime.second);
   1071                     }
   1072                     // We should break here
   1073                     subtypeHistory.remove(ime);
   1074                     break;
   1075                 }
   1076             }
   1077             if (DEBUG) {
   1078                 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
   1079             }
   1080             saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
   1081         }
   1082 
   1083         private void putSubtypeHistoryStr(String str) {
   1084             if (DEBUG) {
   1085                 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
   1086             }
   1087             Settings.Secure.putStringForUser(
   1088                     mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId);
   1089         }
   1090 
   1091         public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
   1092             // Gets the first one from the history
   1093             return getLastSubtypeForInputMethodLockedInternal(null);
   1094         }
   1095 
   1096         public String getLastSubtypeForInputMethodLocked(String imeId) {
   1097             Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
   1098             if (ime != null) {
   1099                 return ime.second;
   1100             } else {
   1101                 return null;
   1102             }
   1103         }
   1104 
   1105         private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
   1106             List<Pair<String, ArrayList<String>>> enabledImes =
   1107                     getEnabledInputMethodsAndSubtypeListLocked();
   1108             List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
   1109             for (Pair<String, String> imeAndSubtype : subtypeHistory) {
   1110                 final String imeInTheHistory = imeAndSubtype.first;
   1111                 // If imeId is empty, returns the first IME and subtype in the history
   1112                 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
   1113                     final String subtypeInTheHistory = imeAndSubtype.second;
   1114                     final String subtypeHashCode =
   1115                             getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
   1116                                     enabledImes, imeInTheHistory, subtypeInTheHistory);
   1117                     if (!TextUtils.isEmpty(subtypeHashCode)) {
   1118                         if (DEBUG) {
   1119                             Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
   1120                         }
   1121                         return new Pair<String, String>(imeInTheHistory, subtypeHashCode);
   1122                     }
   1123                 }
   1124             }
   1125             if (DEBUG) {
   1126                 Slog.d(TAG, "No enabled IME found in the history");
   1127             }
   1128             return null;
   1129         }
   1130 
   1131         private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
   1132                 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
   1133             for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
   1134                 if (enabledIme.first.equals(imeId)) {
   1135                     final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
   1136                     final InputMethodInfo imi = mMethodMap.get(imeId);
   1137                     if (explicitlyEnabledSubtypes.size() == 0) {
   1138                         // If there are no explicitly enabled subtypes, applicable subtypes are
   1139                         // enabled implicitly.
   1140                         // If IME is enabled and no subtypes are enabled, applicable subtypes
   1141                         // are enabled implicitly, so needs to treat them to be enabled.
   1142                         if (imi != null && imi.getSubtypeCount() > 0) {
   1143                             List<InputMethodSubtype> implicitlySelectedSubtypes =
   1144                                     getImplicitlyApplicableSubtypesLocked(mRes, imi);
   1145                             if (implicitlySelectedSubtypes != null) {
   1146                                 final int N = implicitlySelectedSubtypes.size();
   1147                                 for (int i = 0; i < N; ++i) {
   1148                                     final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
   1149                                     if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
   1150                                         return subtypeHashCode;
   1151                                     }
   1152                                 }
   1153                             }
   1154                         }
   1155                     } else {
   1156                         for (String s: explicitlyEnabledSubtypes) {
   1157                             if (s.equals(subtypeHashCode)) {
   1158                                 // If both imeId and subtypeId are enabled, return subtypeId.
   1159                                 try {
   1160                                     final int hashCode = Integer.valueOf(subtypeHashCode);
   1161                                     // Check whether the subtype id is valid or not
   1162                                     if (isValidSubtypeId(imi, hashCode)) {
   1163                                         return s;
   1164                                     } else {
   1165                                         return NOT_A_SUBTYPE_ID_STR;
   1166                                     }
   1167                                 } catch (NumberFormatException e) {
   1168                                     return NOT_A_SUBTYPE_ID_STR;
   1169                                 }
   1170                             }
   1171                         }
   1172                     }
   1173                     // If imeId was enabled but subtypeId was disabled.
   1174                     return NOT_A_SUBTYPE_ID_STR;
   1175                 }
   1176             }
   1177             // If both imeId and subtypeId are disabled, return null
   1178             return null;
   1179         }
   1180 
   1181         private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
   1182             ArrayList<Pair<String, String>> imsList = new ArrayList<Pair<String, String>>();
   1183             final String subtypeHistoryStr = getSubtypeHistoryStr();
   1184             if (TextUtils.isEmpty(subtypeHistoryStr)) {
   1185                 return imsList;
   1186             }
   1187             mInputMethodSplitter.setString(subtypeHistoryStr);
   1188             while (mInputMethodSplitter.hasNext()) {
   1189                 String nextImsStr = mInputMethodSplitter.next();
   1190                 mSubtypeSplitter.setString(nextImsStr);
   1191                 if (mSubtypeSplitter.hasNext()) {
   1192                     String subtypeId = NOT_A_SUBTYPE_ID_STR;
   1193                     // The first element is ime id.
   1194                     String imeId = mSubtypeSplitter.next();
   1195                     while (mSubtypeSplitter.hasNext()) {
   1196                         subtypeId = mSubtypeSplitter.next();
   1197                         break;
   1198                     }
   1199                     imsList.add(new Pair<String, String>(imeId, subtypeId));
   1200                 }
   1201             }
   1202             return imsList;
   1203         }
   1204 
   1205         private String getSubtypeHistoryStr() {
   1206             if (DEBUG) {
   1207                 Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser(
   1208                         mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId));
   1209             }
   1210             return Settings.Secure.getStringForUser(
   1211                     mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId);
   1212         }
   1213 
   1214         public void putSelectedInputMethod(String imeId) {
   1215             if (DEBUG) {
   1216                 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
   1217                         + mCurrentUserId);
   1218             }
   1219             Settings.Secure.putStringForUser(
   1220                     mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId);
   1221         }
   1222 
   1223         public void putSelectedSubtype(int subtypeId) {
   1224             if (DEBUG) {
   1225                 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
   1226                         + mCurrentUserId);
   1227             }
   1228             Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
   1229                     subtypeId, mCurrentUserId);
   1230         }
   1231 
   1232         public String getDisabledSystemInputMethods() {
   1233             return Settings.Secure.getStringForUser(
   1234                     mResolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, mCurrentUserId);
   1235         }
   1236 
   1237         public String getSelectedInputMethod() {
   1238             if (DEBUG) {
   1239                 Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser(
   1240                         mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId)
   1241                         + ", " + mCurrentUserId);
   1242             }
   1243             return Settings.Secure.getStringForUser(
   1244                     mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId);
   1245         }
   1246 
   1247         public boolean isSubtypeSelected() {
   1248             return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
   1249         }
   1250 
   1251         private int getSelectedInputMethodSubtypeHashCode() {
   1252             try {
   1253                 return Settings.Secure.getIntForUser(
   1254                         mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId);
   1255             } catch (SettingNotFoundException e) {
   1256                 return NOT_A_SUBTYPE_ID;
   1257             }
   1258         }
   1259 
   1260         public boolean isShowImeWithHardKeyboardEnabled() {
   1261                 return Settings.Secure.getIntForUser(mResolver,
   1262                         Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0, mCurrentUserId) == 1;
   1263         }
   1264 
   1265         public void setShowImeWithHardKeyboard(boolean show) {
   1266             Settings.Secure.putIntForUser(mResolver, Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD,
   1267                     show ? 1 : 0, mCurrentUserId);
   1268         }
   1269 
   1270         public int getCurrentUserId() {
   1271             return mCurrentUserId;
   1272         }
   1273 
   1274         public int getSelectedInputMethodSubtypeId(String selectedImiId) {
   1275             final InputMethodInfo imi = mMethodMap.get(selectedImiId);
   1276             if (imi == null) {
   1277                 return NOT_A_SUBTYPE_ID;
   1278             }
   1279             final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
   1280             return getSubtypeIdFromHashCode(imi, subtypeHashCode);
   1281         }
   1282 
   1283         public void saveCurrentInputMethodAndSubtypeToHistory(
   1284                 String curMethodId, InputMethodSubtype currentSubtype) {
   1285             String subtypeId = NOT_A_SUBTYPE_ID_STR;
   1286             if (currentSubtype != null) {
   1287                 subtypeId = String.valueOf(currentSubtype.hashCode());
   1288             }
   1289             if (canAddToLastInputMethod(currentSubtype)) {
   1290                 addSubtypeToHistory(curMethodId, subtypeId);
   1291             }
   1292         }
   1293 
   1294         public HashMap<InputMethodInfo, List<InputMethodSubtype>>
   1295                 getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
   1296             HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
   1297                     new HashMap<InputMethodInfo, List<InputMethodSubtype>>();
   1298             for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
   1299                 enabledInputMethodAndSubtypes.put(
   1300                         imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
   1301             }
   1302             return enabledInputMethodAndSubtypes;
   1303         }
   1304     }
   1305 
   1306     // For spell checker service manager.
   1307     // TODO: Should we have TextServicesUtils.java?
   1308     private static final Locale LOCALE_EN_US = new Locale("en", "US");
   1309     private static final Locale LOCALE_EN_GB = new Locale("en", "GB");
   1310 
   1311     /**
   1312      * Returns a list of {@link Locale} in the order of appropriateness for the default spell
   1313      * checker service.
   1314      *
   1315      * <p>If the system language is English, and the region is also explicitly specified in the
   1316      * system locale, the following fallback order will be applied.</p>
   1317      * <ul>
   1318      * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
   1319      * <li>(system-locale-language, system-locale-region)</li>
   1320      * <li>("en", "US")</li>
   1321      * <li>("en", "GB")</li>
   1322      * <li>("en")</li>
   1323      * </ul>
   1324      *
   1325      * <p>If the system language is English, but no region is specified in the system locale,
   1326      * the following fallback order will be applied.</p>
   1327      * <ul>
   1328      * <li>("en")</li>
   1329      * <li>("en", "US")</li>
   1330      * <li>("en", "GB")</li>
   1331      * </ul>
   1332      *
   1333      * <p>If the system language is not English, the following fallback order will be applied.</p>
   1334      * <ul>
   1335      * <li>(system-locale-language, system-locale-region, system-locale-variant) (if exists)</li>
   1336      * <li>(system-locale-language, system-locale-region) (if exists)</li>
   1337      * <li>(system-locale-language) (if exists)</li>
   1338      * <li>("en", "US")</li>
   1339      * <li>("en", "GB")</li>
   1340      * <li>("en")</li>
   1341      * </ul>
   1342      *
   1343      * @param systemLocale the current system locale to be taken into consideration.
   1344      * @return a list of {@link Locale}. The first one is considered to be most appropriate.
   1345      */
   1346     @VisibleForTesting
   1347     public static ArrayList<Locale> getSuitableLocalesForSpellChecker(
   1348             @Nullable final Locale systemLocale) {
   1349         final Locale systemLocaleLanguageCountryVariant;
   1350         final Locale systemLocaleLanguageCountry;
   1351         final Locale systemLocaleLanguage;
   1352         if (systemLocale != null) {
   1353             final String language = systemLocale.getLanguage();
   1354             final boolean hasLanguage = !TextUtils.isEmpty(language);
   1355             final String country = systemLocale.getCountry();
   1356             final boolean hasCountry = !TextUtils.isEmpty(country);
   1357             final String variant = systemLocale.getVariant();
   1358             final boolean hasVariant = !TextUtils.isEmpty(variant);
   1359             if (hasLanguage && hasCountry && hasVariant) {
   1360                 systemLocaleLanguageCountryVariant = new Locale(language, country, variant);
   1361             } else {
   1362                 systemLocaleLanguageCountryVariant = null;
   1363             }
   1364             if (hasLanguage && hasCountry) {
   1365                 systemLocaleLanguageCountry = new Locale(language, country);
   1366             } else {
   1367                 systemLocaleLanguageCountry = null;
   1368             }
   1369             if (hasLanguage) {
   1370                 systemLocaleLanguage = new Locale(language);
   1371             } else {
   1372                 systemLocaleLanguage = null;
   1373             }
   1374         } else {
   1375             systemLocaleLanguageCountryVariant = null;
   1376             systemLocaleLanguageCountry = null;
   1377             systemLocaleLanguage = null;
   1378         }
   1379 
   1380         final ArrayList<Locale> locales = new ArrayList<>();
   1381         if (systemLocaleLanguageCountryVariant != null) {
   1382             locales.add(systemLocaleLanguageCountryVariant);
   1383         }
   1384 
   1385         if (Locale.ENGLISH.equals(systemLocaleLanguage)) {
   1386             if (systemLocaleLanguageCountry != null) {
   1387                 // If the system language is English, and the region is also explicitly specified,
   1388                 // following fallback order will be applied.
   1389                 // - systemLocaleLanguageCountry [if systemLocaleLanguageCountry is non-null]
   1390                 // - en_US [if systemLocaleLanguageCountry is non-null and not en_US]
   1391                 // - en_GB [if systemLocaleLanguageCountry is non-null and not en_GB]
   1392                 // - en
   1393                 if (systemLocaleLanguageCountry != null) {
   1394                     locales.add(systemLocaleLanguageCountry);
   1395                 }
   1396                 if (!LOCALE_EN_US.equals(systemLocaleLanguageCountry)) {
   1397                     locales.add(LOCALE_EN_US);
   1398                 }
   1399                 if (!LOCALE_EN_GB.equals(systemLocaleLanguageCountry)) {
   1400                     locales.add(LOCALE_EN_GB);
   1401                 }
   1402                 locales.add(Locale.ENGLISH);
   1403             } else {
   1404                 // If the system language is English, but no region is specified, following
   1405                 // fallback order will be applied.
   1406                 // - en
   1407                 // - en_US
   1408                 // - en_GB
   1409                 locales.add(Locale.ENGLISH);
   1410                 locales.add(LOCALE_EN_US);
   1411                 locales.add(LOCALE_EN_GB);
   1412             }
   1413         } else {
   1414             // If the system language is not English, the fallback order will be
   1415             // - systemLocaleLanguageCountry  [if non-null]
   1416             // - systemLocaleLanguage  [if non-null]
   1417             // - en_US
   1418             // - en_GB
   1419             // - en
   1420             if (systemLocaleLanguageCountry != null) {
   1421                 locales.add(systemLocaleLanguageCountry);
   1422             }
   1423             if (systemLocaleLanguage != null) {
   1424                 locales.add(systemLocaleLanguage);
   1425             }
   1426             locales.add(LOCALE_EN_US);
   1427             locales.add(LOCALE_EN_GB);
   1428             locales.add(Locale.ENGLISH);
   1429         }
   1430         return locales;
   1431     }
   1432 }
   1433