InputMethodUtils.java revision 77cbcb6637e3733e9b80a93d37745f27ec4d4561
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.content.ContentResolver;
20import android.content.Context;
21import android.content.pm.ApplicationInfo;
22import android.content.pm.PackageManager;
23import android.content.pm.PackageManager.NameNotFoundException;
24import android.content.res.Resources;
25import android.provider.Settings;
26import android.provider.Settings.SettingNotFoundException;
27import android.text.TextUtils;
28import android.util.Pair;
29import android.util.Slog;
30import android.view.inputmethod.InputMethodInfo;
31import android.view.inputmethod.InputMethodSubtype;
32import android.view.textservice.SpellCheckerInfo;
33import android.view.textservice.TextServicesManager;
34
35import java.util.ArrayList;
36import java.util.HashMap;
37import java.util.List;
38import java.util.Locale;
39
40/**
41 * InputMethodManagerUtils contains some static methods that provides IME informations.
42 * This methods are supposed to be used in both the framework and the Settings application.
43 */
44public class InputMethodUtils {
45    public static final boolean DEBUG = false;
46    public static final int NOT_A_SUBTYPE_ID = -1;
47    public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
48    public static final String SUBTYPE_MODE_VOICE = "voice";
49    private static final String TAG = "InputMethodUtils";
50    private static final Locale ENGLISH_LOCALE = new Locale("en");
51    private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
52    private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
53            "EnabledWhenDefaultIsNotAsciiCapable";
54    private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
55
56    private InputMethodUtils() {
57        // This utility class is not publicly instantiable.
58    }
59
60    // ----------------------------------------------------------------------
61    // Utilities for debug
62    public static String getStackTrace() {
63        final StringBuilder sb = new StringBuilder();
64        try {
65            throw new RuntimeException();
66        } catch (RuntimeException e) {
67            final StackTraceElement[] frames = e.getStackTrace();
68            // Start at 1 because the first frame is here and we don't care about it
69            for (int j = 1; j < frames.length; ++j) {
70                sb.append(frames[j].toString() + "\n");
71            }
72        }
73        return sb.toString();
74    }
75
76    public static String getApiCallStack() {
77        String apiCallStack = "";
78        try {
79            throw new RuntimeException();
80        } catch (RuntimeException e) {
81            final StackTraceElement[] frames = e.getStackTrace();
82            for (int j = 1; j < frames.length; ++j) {
83                final String tempCallStack = frames[j].toString();
84                if (TextUtils.isEmpty(apiCallStack)) {
85                    // Overwrite apiCallStack if it's empty
86                    apiCallStack = tempCallStack;
87                } else if (tempCallStack.indexOf("Transact(") < 0) {
88                    // Overwrite apiCallStack if it's not a binder call
89                    apiCallStack = tempCallStack;
90                } else {
91                    break;
92                }
93            }
94        }
95        return apiCallStack;
96    }
97    // ----------------------------------------------------------------------
98
99    public static boolean isSystemIme(InputMethodInfo inputMethod) {
100        return (inputMethod.getServiceInfo().applicationInfo.flags
101                & ApplicationInfo.FLAG_SYSTEM) != 0;
102    }
103
104    public static boolean isSystemImeThatHasEnglishKeyboardSubtype(InputMethodInfo imi) {
105        if (!isSystemIme(imi)) {
106            return false;
107        }
108        return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage(), SUBTYPE_MODE_KEYBOARD);
109    }
110
111    private static boolean isSystemAuxilialyImeThatHashAutomaticSubtype(InputMethodInfo imi) {
112        if (!isSystemIme(imi)) {
113            return false;
114        }
115        if (!imi.isAuxiliaryIme()) {
116            return false;
117        }
118        final int subtypeCount = imi.getSubtypeCount();
119        for (int i = 0; i < subtypeCount; ++i) {
120            final InputMethodSubtype s = imi.getSubtypeAt(i);
121            if (s.overridesImplicitlyEnabledSubtype()) {
122                return true;
123            }
124        }
125        return false;
126    }
127
128    public static ArrayList<InputMethodInfo> getDefaultEnabledImes(
129            Context context, boolean isSystemReady, ArrayList<InputMethodInfo> imis) {
130        final ArrayList<InputMethodInfo> retval = new ArrayList<InputMethodInfo>();
131        boolean auxilialyImeAdded = false;
132        for (int i = 0; i < imis.size(); ++i) {
133            final InputMethodInfo imi = imis.get(i);
134            if (isDefaultEnabledIme(isSystemReady, imi, context)) {
135                retval.add(imi);
136                if (imi.isAuxiliaryIme()) {
137                    auxilialyImeAdded = true;
138                }
139            }
140        }
141        if (auxilialyImeAdded) {
142            return retval;
143        }
144        for (int i = 0; i < imis.size(); ++i) {
145            final InputMethodInfo imi = imis.get(i);
146            if (isSystemAuxilialyImeThatHashAutomaticSubtype(imi)) {
147                retval.add(imi);
148            }
149        }
150        return retval;
151    }
152
153    // TODO: Rename isSystemDefaultImeThatHasCurrentLanguageSubtype
154    public static boolean isValidSystemDefaultIme(
155            boolean isSystemReady, InputMethodInfo imi, Context context) {
156        if (!isSystemReady) {
157            return false;
158        }
159        if (!isSystemIme(imi)) {
160            return false;
161        }
162        if (imi.getIsDefaultResourceId() != 0) {
163            try {
164                if (imi.isDefault(context) && containsSubtypeOf(
165                        imi, context.getResources().getConfiguration().locale.getLanguage(),
166                        null /* mode */)) {
167                    return true;
168                }
169            } catch (Resources.NotFoundException ex) {
170            }
171        }
172        if (imi.getSubtypeCount() == 0) {
173            Slog.w(TAG, "Found no subtypes in a system IME: " + imi.getPackageName());
174        }
175        return false;
176    }
177
178    public static boolean isDefaultEnabledIme(
179            boolean isSystemReady, InputMethodInfo imi, Context context) {
180        return isValidSystemDefaultIme(isSystemReady, imi, context)
181                || isSystemImeThatHasEnglishKeyboardSubtype(imi);
182    }
183
184    public static boolean containsSubtypeOf(InputMethodInfo imi, String language, String mode) {
185        final int N = imi.getSubtypeCount();
186        for (int i = 0; i < N; ++i) {
187            if (!imi.getSubtypeAt(i).getLocale().startsWith(language)) {
188                continue;
189            }
190            if(!TextUtils.isEmpty(mode) && !imi.getSubtypeAt(i).getMode().equalsIgnoreCase(mode)) {
191                continue;
192            }
193            return true;
194        }
195        return false;
196    }
197
198    public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
199        ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
200        final int subtypeCount = imi.getSubtypeCount();
201        for (int i = 0; i < subtypeCount; ++i) {
202            subtypes.add(imi.getSubtypeAt(i));
203        }
204        return subtypes;
205    }
206
207    public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
208            InputMethodInfo imi, String mode) {
209        ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
210        final int subtypeCount = imi.getSubtypeCount();
211        for (int i = 0; i < subtypeCount; ++i) {
212            final InputMethodSubtype subtype = imi.getSubtypeAt(i);
213            if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
214                subtypes.add(subtype);
215            }
216        }
217        return subtypes;
218    }
219
220    public static InputMethodInfo getMostApplicableDefaultIME(
221            List<InputMethodInfo> enabledImes) {
222        if (enabledImes != null && enabledImes.size() > 0) {
223            // We'd prefer to fall back on a system IME, since that is safer.
224            int i = enabledImes.size();
225            int firstFoundSystemIme = -1;
226            while (i > 0) {
227                i--;
228                final InputMethodInfo imi = enabledImes.get(i);
229                if (InputMethodUtils.isSystemImeThatHasEnglishKeyboardSubtype(imi)
230                        && !imi.isAuxiliaryIme()) {
231                    return imi;
232                }
233                if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)
234                        && !imi.isAuxiliaryIme()) {
235                    firstFoundSystemIme = i;
236                }
237            }
238            return enabledImes.get(Math.max(firstFoundSystemIme, 0));
239        }
240        return null;
241    }
242
243    public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
244        return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
245    }
246
247    public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
248        if (imi != null) {
249            final int subtypeCount = imi.getSubtypeCount();
250            for (int i = 0; i < subtypeCount; ++i) {
251                InputMethodSubtype ims = imi.getSubtypeAt(i);
252                if (subtypeHashCode == ims.hashCode()) {
253                    return i;
254                }
255            }
256        }
257        return NOT_A_SUBTYPE_ID;
258    }
259
260    private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
261            Resources res, InputMethodInfo imi) {
262        final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
263        final String systemLocale = res.getConfiguration().locale.toString();
264        if (TextUtils.isEmpty(systemLocale)) return new ArrayList<InputMethodSubtype>();
265        final String systemLanguage = res.getConfiguration().locale.getLanguage();
266        final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap =
267                new HashMap<String, InputMethodSubtype>();
268        final int N = subtypes.size();
269        for (int i = 0; i < N; ++i) {
270            // scan overriding implicitly enabled subtypes.
271            InputMethodSubtype subtype = subtypes.get(i);
272            if (subtype.overridesImplicitlyEnabledSubtype()) {
273                final String mode = subtype.getMode();
274                if (!applicableModeAndSubtypesMap.containsKey(mode)) {
275                    applicableModeAndSubtypesMap.put(mode, subtype);
276                }
277            }
278        }
279        if (applicableModeAndSubtypesMap.size() > 0) {
280            return new ArrayList<InputMethodSubtype>(applicableModeAndSubtypesMap.values());
281        }
282        for (int i = 0; i < N; ++i) {
283            final InputMethodSubtype subtype = subtypes.get(i);
284            final String locale = subtype.getLocale();
285            final String mode = subtype.getMode();
286            final String language = getLanguageFromLocaleString(locale);
287            // When system locale starts with subtype's locale, that subtype will be applicable
288            // for system locale. We need to make sure the languages are the same, to prevent
289            // locales like "fil" (Filipino) being matched by "fi" (Finnish).
290            //
291            // For instance, it's clearly applicable for cases like system locale = en_US and
292            // subtype = en, but it is not necessarily considered applicable for cases like system
293            // locale = en and subtype = en_US.
294            //
295            // We just call systemLocale.startsWith(locale) in this function because there is no
296            // need to find applicable subtypes aggressively unlike
297            // findLastResortApplicableSubtypeLocked.
298            //
299            // TODO: This check is broken. It won't take scripts into account and doesn't
300            // account for the mandatory conversions performed by Locale#toString.
301            if (language.equals(systemLanguage) && systemLocale.startsWith(locale)) {
302                final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode);
303                // If more applicable subtypes are contained, skip.
304                if (applicableSubtype != null) {
305                    if (systemLocale.equals(applicableSubtype.getLocale())) continue;
306                    if (!systemLocale.equals(locale)) continue;
307                }
308                applicableModeAndSubtypesMap.put(mode, subtype);
309            }
310        }
311        final InputMethodSubtype keyboardSubtype
312                = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD);
313        final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<InputMethodSubtype>(
314                applicableModeAndSubtypesMap.values());
315        if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
316            for (int i = 0; i < N; ++i) {
317                final InputMethodSubtype subtype = subtypes.get(i);
318                final String mode = subtype.getMode();
319                if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
320                        TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
321                    applicableSubtypes.add(subtype);
322                }
323            }
324        }
325        if (keyboardSubtype == null) {
326            InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
327                    res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
328            if (lastResortKeyboardSubtype != null) {
329                applicableSubtypes.add(lastResortKeyboardSubtype);
330            }
331        }
332        return applicableSubtypes;
333    }
334
335    private static List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
336            Context context, InputMethodInfo imi, List<InputMethodSubtype> enabledSubtypes,
337            boolean allowsImplicitlySelectedSubtypes) {
338        if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
339            enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
340                    context.getResources(), imi);
341        }
342        return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
343    }
344
345    /**
346     * Returns the language component of a given locale string.
347     */
348    public static String getLanguageFromLocaleString(String locale) {
349        final int idx = locale.indexOf('_');
350        if (idx < 0) {
351            return locale;
352        } else {
353            return locale.substring(0, idx);
354        }
355    }
356
357    /**
358     * If there are no selected subtypes, tries finding the most applicable one according to the
359     * given locale.
360     * @param subtypes this function will search the most applicable subtype in subtypes
361     * @param mode subtypes will be filtered by mode
362     * @param locale subtypes will be filtered by locale
363     * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
364     * it will return the first subtype matched with mode
365     * @return the most applicable subtypeId
366     */
367    public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
368            Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
369            boolean canIgnoreLocaleAsLastResort) {
370        if (subtypes == null || subtypes.size() == 0) {
371            return null;
372        }
373        if (TextUtils.isEmpty(locale)) {
374            locale = res.getConfiguration().locale.toString();
375        }
376        final String language = getLanguageFromLocaleString(locale);
377        boolean partialMatchFound = false;
378        InputMethodSubtype applicableSubtype = null;
379        InputMethodSubtype firstMatchedModeSubtype = null;
380        final int N = subtypes.size();
381        for (int i = 0; i < N; ++i) {
382            InputMethodSubtype subtype = subtypes.get(i);
383            final String subtypeLocale = subtype.getLocale();
384            final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
385            // An applicable subtype should match "mode". If mode is null, mode will be ignored,
386            // and all subtypes with all modes can be candidates.
387            if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
388                if (firstMatchedModeSubtype == null) {
389                    firstMatchedModeSubtype = subtype;
390                }
391                if (locale.equals(subtypeLocale)) {
392                    // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
393                    applicableSubtype = subtype;
394                    break;
395                } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
396                    // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
397                    applicableSubtype = subtype;
398                    partialMatchFound = true;
399                }
400            }
401        }
402
403        if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
404            return firstMatchedModeSubtype;
405        }
406
407        // The first subtype applicable to the system locale will be defined as the most applicable
408        // subtype.
409        if (DEBUG) {
410            if (applicableSubtype != null) {
411                Slog.d(TAG, "Applicable InputMethodSubtype was found: "
412                        + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
413            }
414        }
415        return applicableSubtype;
416    }
417
418    public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
419        if (subtype == null) return true;
420        return !subtype.isAuxiliary();
421    }
422
423    public static void setNonSelectedSystemImesDisabledUntilUsed(
424            PackageManager packageManager, List<InputMethodInfo> enabledImis) {
425        if (DEBUG) {
426            Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
427        }
428        final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
429                com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
430        if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
431            return;
432        }
433        // Only the current spell checker should be treated as an enabled one.
434        final SpellCheckerInfo currentSpellChecker =
435                TextServicesManager.getInstance().getCurrentSpellChecker();
436        for (final String packageName : systemImesDisabledUntilUsed) {
437            if (DEBUG) {
438                Slog.d(TAG, "check " + packageName);
439            }
440            boolean enabledIme = false;
441            for (int j = 0; j < enabledImis.size(); ++j) {
442                final InputMethodInfo imi = enabledImis.get(j);
443                if (packageName.equals(imi.getPackageName())) {
444                    enabledIme = true;
445                    break;
446                }
447            }
448            if (enabledIme) {
449                // enabled ime. skip
450                continue;
451            }
452            if (currentSpellChecker != null
453                    && packageName.equals(currentSpellChecker.getPackageName())) {
454                // enabled spell checker. skip
455                if (DEBUG) {
456                    Slog.d(TAG, packageName + " is the current spell checker. skip");
457                }
458                continue;
459            }
460            ApplicationInfo ai = null;
461            try {
462                ai = packageManager.getApplicationInfo(packageName,
463                        PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS);
464            } catch (NameNotFoundException e) {
465                Slog.w(TAG, "NameNotFoundException: " + packageName, e);
466            }
467            if (ai == null) {
468                // No app found for packageName
469                continue;
470            }
471            final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
472            if (!isSystemPackage) {
473                continue;
474            }
475            setDisabledUntilUsed(packageManager, packageName);
476        }
477    }
478
479    private static void setDisabledUntilUsed(PackageManager packageManager, String packageName) {
480        final int state = packageManager.getApplicationEnabledSetting(packageName);
481        if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
482                || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
483            if (DEBUG) {
484                Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
485            }
486            packageManager.setApplicationEnabledSetting(packageName,
487                    PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, 0);
488        } else {
489            if (DEBUG) {
490                Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
491            }
492        }
493    }
494
495    public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
496            InputMethodSubtype subtype) {
497        final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
498        return subtype != null
499                ? TextUtils.concat(subtype.getDisplayName(context,
500                        imi.getPackageName(), imi.getServiceInfo().applicationInfo),
501                                (TextUtils.isEmpty(imiLabel) ?
502                                        "" : " - " + imiLabel))
503                : imiLabel;
504    }
505
506    /**
507     * Utility class for putting and getting settings for InputMethod
508     * TODO: Move all putters and getters of settings to this class.
509     */
510    public static class InputMethodSettings {
511        // The string for enabled input method is saved as follows:
512        // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
513        private static final char INPUT_METHOD_SEPARATER = ':';
514        private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';';
515        private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
516                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER);
517
518        private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
519                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER);
520
521        private final Resources mRes;
522        private final ContentResolver mResolver;
523        private final HashMap<String, InputMethodInfo> mMethodMap;
524        private final ArrayList<InputMethodInfo> mMethodList;
525
526        private String mEnabledInputMethodsStrCache;
527        private int mCurrentUserId;
528        private int[] mCurrentProfileIds = new int[0];
529
530        private static void buildEnabledInputMethodsSettingString(
531                StringBuilder builder, Pair<String, ArrayList<String>> pair) {
532            String id = pair.first;
533            ArrayList<String> subtypes = pair.second;
534            builder.append(id);
535            // Inputmethod and subtypes are saved in the settings as follows:
536            // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
537            for (String subtypeId: subtypes) {
538                builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId);
539            }
540        }
541
542        public InputMethodSettings(
543                Resources res, ContentResolver resolver,
544                HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
545                int userId) {
546            setCurrentUserId(userId);
547            mRes = res;
548            mResolver = resolver;
549            mMethodMap = methodMap;
550            mMethodList = methodList;
551        }
552
553        public void setCurrentUserId(int userId) {
554            if (DEBUG) {
555                Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId);
556            }
557            // IMMS settings are kept per user, so keep track of current user
558            mCurrentUserId = userId;
559        }
560
561        public void setCurrentProfileIds(int[] currentProfileIds) {
562            synchronized (this) {
563                mCurrentProfileIds = currentProfileIds;
564            }
565        }
566
567        public boolean isCurrentProfile(int userId) {
568            synchronized (this) {
569                if (userId == mCurrentUserId) return true;
570                for (int i = 0; i < mCurrentProfileIds.length; i++) {
571                    if (userId == mCurrentProfileIds[i]) return true;
572                }
573                return false;
574            }
575        }
576
577        public List<InputMethodInfo> getEnabledInputMethodListLocked() {
578            return createEnabledInputMethodListLocked(
579                    getEnabledInputMethodsAndSubtypeListLocked());
580        }
581
582        public List<Pair<InputMethodInfo, ArrayList<String>>>
583                getEnabledInputMethodAndSubtypeHashCodeListLocked() {
584            return createEnabledInputMethodAndSubtypeHashCodeListLocked(
585                    getEnabledInputMethodsAndSubtypeListLocked());
586        }
587
588        public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
589                Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
590            List<InputMethodSubtype> enabledSubtypes =
591                    getEnabledInputMethodSubtypeListLocked(imi);
592            if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
593                enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
594                        context.getResources(), imi);
595            }
596            return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
597        }
598
599        public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
600                InputMethodInfo imi) {
601            List<Pair<String, ArrayList<String>>> imsList =
602                    getEnabledInputMethodsAndSubtypeListLocked();
603            ArrayList<InputMethodSubtype> enabledSubtypes =
604                    new ArrayList<InputMethodSubtype>();
605            if (imi != null) {
606                for (Pair<String, ArrayList<String>> imsPair : imsList) {
607                    InputMethodInfo info = mMethodMap.get(imsPair.first);
608                    if (info != null && info.getId().equals(imi.getId())) {
609                        final int subtypeCount = info.getSubtypeCount();
610                        for (int i = 0; i < subtypeCount; ++i) {
611                            InputMethodSubtype ims = info.getSubtypeAt(i);
612                            for (String s: imsPair.second) {
613                                if (String.valueOf(ims.hashCode()).equals(s)) {
614                                    enabledSubtypes.add(ims);
615                                }
616                            }
617                        }
618                        break;
619                    }
620                }
621            }
622            return enabledSubtypes;
623        }
624
625        // At the initial boot, the settings for input methods are not set,
626        // so we need to enable IME in that case.
627        public void enableAllIMEsIfThereIsNoEnabledIME() {
628            if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
629                StringBuilder sb = new StringBuilder();
630                final int N = mMethodList.size();
631                for (int i = 0; i < N; i++) {
632                    InputMethodInfo imi = mMethodList.get(i);
633                    Slog.i(TAG, "Adding: " + imi.getId());
634                    if (i > 0) sb.append(':');
635                    sb.append(imi.getId());
636                }
637                putEnabledInputMethodsStr(sb.toString());
638            }
639        }
640
641        public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
642            ArrayList<Pair<String, ArrayList<String>>> imsList
643                    = new ArrayList<Pair<String, ArrayList<String>>>();
644            final String enabledInputMethodsStr = getEnabledInputMethodsStr();
645            if (TextUtils.isEmpty(enabledInputMethodsStr)) {
646                return imsList;
647            }
648            mInputMethodSplitter.setString(enabledInputMethodsStr);
649            while (mInputMethodSplitter.hasNext()) {
650                String nextImsStr = mInputMethodSplitter.next();
651                mSubtypeSplitter.setString(nextImsStr);
652                if (mSubtypeSplitter.hasNext()) {
653                    ArrayList<String> subtypeHashes = new ArrayList<String>();
654                    // The first element is ime id.
655                    String imeId = mSubtypeSplitter.next();
656                    while (mSubtypeSplitter.hasNext()) {
657                        subtypeHashes.add(mSubtypeSplitter.next());
658                    }
659                    imsList.add(new Pair<String, ArrayList<String>>(imeId, subtypeHashes));
660                }
661            }
662            return imsList;
663        }
664
665        public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
666            if (reloadInputMethodStr) {
667                getEnabledInputMethodsStr();
668            }
669            if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
670                // Add in the newly enabled input method.
671                putEnabledInputMethodsStr(id);
672            } else {
673                putEnabledInputMethodsStr(
674                        mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATER + id);
675            }
676        }
677
678        /**
679         * Build and put a string of EnabledInputMethods with removing specified Id.
680         * @return the specified id was removed or not.
681         */
682        public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
683                StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
684            boolean isRemoved = false;
685            boolean needsAppendSeparator = false;
686            for (Pair<String, ArrayList<String>> ims: imsList) {
687                String curId = ims.first;
688                if (curId.equals(id)) {
689                    // We are disabling this input method, and it is
690                    // currently enabled.  Skip it to remove from the
691                    // new list.
692                    isRemoved = true;
693                } else {
694                    if (needsAppendSeparator) {
695                        builder.append(INPUT_METHOD_SEPARATER);
696                    } else {
697                        needsAppendSeparator = true;
698                    }
699                    buildEnabledInputMethodsSettingString(builder, ims);
700                }
701            }
702            if (isRemoved) {
703                // Update the setting with the new list of input methods.
704                putEnabledInputMethodsStr(builder.toString());
705            }
706            return isRemoved;
707        }
708
709        private List<InputMethodInfo> createEnabledInputMethodListLocked(
710                List<Pair<String, ArrayList<String>>> imsList) {
711            final ArrayList<InputMethodInfo> res = new ArrayList<InputMethodInfo>();
712            for (Pair<String, ArrayList<String>> ims: imsList) {
713                InputMethodInfo info = mMethodMap.get(ims.first);
714                if (info != null) {
715                    res.add(info);
716                }
717            }
718            return res;
719        }
720
721        private List<Pair<InputMethodInfo, ArrayList<String>>>
722                createEnabledInputMethodAndSubtypeHashCodeListLocked(
723                        List<Pair<String, ArrayList<String>>> imsList) {
724            final ArrayList<Pair<InputMethodInfo, ArrayList<String>>> res
725                    = new ArrayList<Pair<InputMethodInfo, ArrayList<String>>>();
726            for (Pair<String, ArrayList<String>> ims : imsList) {
727                InputMethodInfo info = mMethodMap.get(ims.first);
728                if (info != null) {
729                    res.add(new Pair<InputMethodInfo, ArrayList<String>>(info, ims.second));
730                }
731            }
732            return res;
733        }
734
735        private void putEnabledInputMethodsStr(String str) {
736            Settings.Secure.putStringForUser(
737                    mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId);
738            mEnabledInputMethodsStrCache = str;
739            if (DEBUG) {
740                Slog.d(TAG, "putEnabledInputMethodStr: " + str);
741            }
742        }
743
744        public String getEnabledInputMethodsStr() {
745            mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser(
746                    mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId);
747            if (DEBUG) {
748                Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
749                        + ", " + mCurrentUserId);
750            }
751            return mEnabledInputMethodsStrCache;
752        }
753
754        private void saveSubtypeHistory(
755                List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
756            StringBuilder builder = new StringBuilder();
757            boolean isImeAdded = false;
758            if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
759                builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append(
760                        newSubtypeId);
761                isImeAdded = true;
762            }
763            for (Pair<String, String> ime: savedImes) {
764                String imeId = ime.first;
765                String subtypeId = ime.second;
766                if (TextUtils.isEmpty(subtypeId)) {
767                    subtypeId = NOT_A_SUBTYPE_ID_STR;
768                }
769                if (isImeAdded) {
770                    builder.append(INPUT_METHOD_SEPARATER);
771                } else {
772                    isImeAdded = true;
773                }
774                builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append(
775                        subtypeId);
776            }
777            // Remove the last INPUT_METHOD_SEPARATER
778            putSubtypeHistoryStr(builder.toString());
779        }
780
781        private void addSubtypeToHistory(String imeId, String subtypeId) {
782            List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
783            for (Pair<String, String> ime: subtypeHistory) {
784                if (ime.first.equals(imeId)) {
785                    if (DEBUG) {
786                        Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
787                                + ime.second);
788                    }
789                    // We should break here
790                    subtypeHistory.remove(ime);
791                    break;
792                }
793            }
794            if (DEBUG) {
795                Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
796            }
797            saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
798        }
799
800        private void putSubtypeHistoryStr(String str) {
801            if (DEBUG) {
802                Slog.d(TAG, "putSubtypeHistoryStr: " + str);
803            }
804            Settings.Secure.putStringForUser(
805                    mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId);
806        }
807
808        public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
809            // Gets the first one from the history
810            return getLastSubtypeForInputMethodLockedInternal(null);
811        }
812
813        public String getLastSubtypeForInputMethodLocked(String imeId) {
814            Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
815            if (ime != null) {
816                return ime.second;
817            } else {
818                return null;
819            }
820        }
821
822        private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
823            List<Pair<String, ArrayList<String>>> enabledImes =
824                    getEnabledInputMethodsAndSubtypeListLocked();
825            List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
826            for (Pair<String, String> imeAndSubtype : subtypeHistory) {
827                final String imeInTheHistory = imeAndSubtype.first;
828                // If imeId is empty, returns the first IME and subtype in the history
829                if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
830                    final String subtypeInTheHistory = imeAndSubtype.second;
831                    final String subtypeHashCode =
832                            getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
833                                    enabledImes, imeInTheHistory, subtypeInTheHistory);
834                    if (!TextUtils.isEmpty(subtypeHashCode)) {
835                        if (DEBUG) {
836                            Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
837                        }
838                        return new Pair<String, String>(imeInTheHistory, subtypeHashCode);
839                    }
840                }
841            }
842            if (DEBUG) {
843                Slog.d(TAG, "No enabled IME found in the history");
844            }
845            return null;
846        }
847
848        private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
849                ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
850            for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
851                if (enabledIme.first.equals(imeId)) {
852                    final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
853                    final InputMethodInfo imi = mMethodMap.get(imeId);
854                    if (explicitlyEnabledSubtypes.size() == 0) {
855                        // If there are no explicitly enabled subtypes, applicable subtypes are
856                        // enabled implicitly.
857                        // If IME is enabled and no subtypes are enabled, applicable subtypes
858                        // are enabled implicitly, so needs to treat them to be enabled.
859                        if (imi != null && imi.getSubtypeCount() > 0) {
860                            List<InputMethodSubtype> implicitlySelectedSubtypes =
861                                    getImplicitlyApplicableSubtypesLocked(mRes, imi);
862                            if (implicitlySelectedSubtypes != null) {
863                                final int N = implicitlySelectedSubtypes.size();
864                                for (int i = 0; i < N; ++i) {
865                                    final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
866                                    if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
867                                        return subtypeHashCode;
868                                    }
869                                }
870                            }
871                        }
872                    } else {
873                        for (String s: explicitlyEnabledSubtypes) {
874                            if (s.equals(subtypeHashCode)) {
875                                // If both imeId and subtypeId are enabled, return subtypeId.
876                                try {
877                                    final int hashCode = Integer.valueOf(subtypeHashCode);
878                                    // Check whether the subtype id is valid or not
879                                    if (isValidSubtypeId(imi, hashCode)) {
880                                        return s;
881                                    } else {
882                                        return NOT_A_SUBTYPE_ID_STR;
883                                    }
884                                } catch (NumberFormatException e) {
885                                    return NOT_A_SUBTYPE_ID_STR;
886                                }
887                            }
888                        }
889                    }
890                    // If imeId was enabled but subtypeId was disabled.
891                    return NOT_A_SUBTYPE_ID_STR;
892                }
893            }
894            // If both imeId and subtypeId are disabled, return null
895            return null;
896        }
897
898        private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
899            ArrayList<Pair<String, String>> imsList = new ArrayList<Pair<String, String>>();
900            final String subtypeHistoryStr = getSubtypeHistoryStr();
901            if (TextUtils.isEmpty(subtypeHistoryStr)) {
902                return imsList;
903            }
904            mInputMethodSplitter.setString(subtypeHistoryStr);
905            while (mInputMethodSplitter.hasNext()) {
906                String nextImsStr = mInputMethodSplitter.next();
907                mSubtypeSplitter.setString(nextImsStr);
908                if (mSubtypeSplitter.hasNext()) {
909                    String subtypeId = NOT_A_SUBTYPE_ID_STR;
910                    // The first element is ime id.
911                    String imeId = mSubtypeSplitter.next();
912                    while (mSubtypeSplitter.hasNext()) {
913                        subtypeId = mSubtypeSplitter.next();
914                        break;
915                    }
916                    imsList.add(new Pair<String, String>(imeId, subtypeId));
917                }
918            }
919            return imsList;
920        }
921
922        private String getSubtypeHistoryStr() {
923            if (DEBUG) {
924                Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser(
925                        mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId));
926            }
927            return Settings.Secure.getStringForUser(
928                    mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId);
929        }
930
931        public void putSelectedInputMethod(String imeId) {
932            if (DEBUG) {
933                Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
934                        + mCurrentUserId);
935            }
936            Settings.Secure.putStringForUser(
937                    mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId);
938        }
939
940        public void putSelectedSubtype(int subtypeId) {
941            if (DEBUG) {
942                Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
943                        + mCurrentUserId);
944            }
945            Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
946                    subtypeId, mCurrentUserId);
947        }
948
949        public String getDisabledSystemInputMethods() {
950            return Settings.Secure.getStringForUser(
951                    mResolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, mCurrentUserId);
952        }
953
954        public String getSelectedInputMethod() {
955            if (DEBUG) {
956                Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser(
957                        mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId)
958                        + ", " + mCurrentUserId);
959            }
960            return Settings.Secure.getStringForUser(
961                    mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId);
962        }
963
964        public boolean isSubtypeSelected() {
965            return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
966        }
967
968        private int getSelectedInputMethodSubtypeHashCode() {
969            try {
970                return Settings.Secure.getIntForUser(
971                        mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId);
972            } catch (SettingNotFoundException e) {
973                return NOT_A_SUBTYPE_ID;
974            }
975        }
976
977        public int getCurrentUserId() {
978            return mCurrentUserId;
979        }
980
981        public int getSelectedInputMethodSubtypeId(String selectedImiId) {
982            final InputMethodInfo imi = mMethodMap.get(selectedImiId);
983            if (imi == null) {
984                return NOT_A_SUBTYPE_ID;
985            }
986            final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
987            return getSubtypeIdFromHashCode(imi, subtypeHashCode);
988        }
989
990        public void saveCurrentInputMethodAndSubtypeToHistory(
991                String curMethodId, InputMethodSubtype currentSubtype) {
992            String subtypeId = NOT_A_SUBTYPE_ID_STR;
993            if (currentSubtype != null) {
994                subtypeId = String.valueOf(currentSubtype.hashCode());
995            }
996            if (canAddToLastInputMethod(currentSubtype)) {
997                addSubtypeToHistory(curMethodId, subtypeId);
998            }
999        }
1000
1001        public HashMap<InputMethodInfo, List<InputMethodSubtype>>
1002                getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) {
1003            HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes =
1004                    new HashMap<InputMethodInfo, List<InputMethodSubtype>>();
1005            for (InputMethodInfo imi: getEnabledInputMethodListLocked()) {
1006                enabledInputMethodAndSubtypes.put(
1007                        imi, getEnabledInputMethodSubtypeListLocked(context, imi, true));
1008            }
1009            return enabledInputMethodAndSubtypes;
1010        }
1011    }
1012}
1013