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