InputMethodUtils.java revision fd7adedebf88427162a3ce27fcc9cfd3893c869d
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.res.Resources;
24import android.provider.Settings;
25import android.provider.Settings.SettingNotFoundException;
26import android.text.TextUtils;
27import android.util.Pair;
28import android.util.Slog;
29import android.view.inputmethod.InputMethodInfo;
30import android.view.inputmethod.InputMethodSubtype;
31
32import java.util.ArrayList;
33import java.util.HashMap;
34import java.util.List;
35import java.util.Locale;
36
37/**
38 * InputMethodManagerUtils contains some static methods that provides IME informations.
39 * This methods are supposed to be used in both the framework and the Settings application.
40 */
41public class InputMethodUtils {
42    public static final boolean DEBUG = false;
43    public static final int NOT_A_SUBTYPE_ID = -1;
44    public static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
45    public static final String SUBTYPE_MODE_VOICE = "voice";
46    private static final String TAG = "InputMethodUtils";
47    private static final Locale ENGLISH_LOCALE = new Locale("en");
48    private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
49    private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
50            "EnabledWhenDefaultIsNotAsciiCapable";
51    private static final String TAG_ASCII_CAPABLE = "AsciiCapable";
52
53    private InputMethodUtils() {
54        // This utility class is not publicly instantiable.
55    }
56
57    public static boolean isSystemIme(InputMethodInfo inputMethod) {
58        return (inputMethod.getServiceInfo().applicationInfo.flags
59                & ApplicationInfo.FLAG_SYSTEM) != 0;
60    }
61
62    public static boolean isSystemImeThatHasEnglishSubtype(InputMethodInfo imi) {
63        if (!isSystemIme(imi)) {
64            return false;
65        }
66        return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage());
67    }
68
69    // TODO: Rename isSystemDefaultImeThatHasCurrentLanguageSubtype
70    public static boolean isValidSystemDefaultIme(
71            boolean isSystemReady, InputMethodInfo imi, Context context) {
72        if (!isSystemReady) {
73            return false;
74        }
75        if (!isSystemIme(imi)) {
76            return false;
77        }
78        if (imi.getIsDefaultResourceId() != 0) {
79            try {
80                Resources res = context.createPackageContext(
81                        imi.getPackageName(), 0).getResources();
82                if (res.getBoolean(imi.getIsDefaultResourceId())
83                        && containsSubtypeOf(imi, context.getResources().getConfiguration().
84                                locale.getLanguage())) {
85                    return true;
86                }
87            } catch (PackageManager.NameNotFoundException ex) {
88            } catch (Resources.NotFoundException ex) {
89            }
90        }
91        if (imi.getSubtypeCount() == 0) {
92            Slog.w(TAG, "Found no subtypes in a system IME: " + imi.getPackageName());
93        }
94        return false;
95    }
96
97    public static boolean isDefaultEnabledIme(
98            boolean isSystemReady, InputMethodInfo imi, Context context) {
99        return isValidSystemDefaultIme(isSystemReady, imi, context)
100                || isSystemImeThatHasEnglishSubtype(imi);
101    }
102
103    private static boolean containsSubtypeOf(InputMethodInfo imi, String language) {
104        final int N = imi.getSubtypeCount();
105        for (int i = 0; i < N; ++i) {
106            if (imi.getSubtypeAt(i).getLocale().startsWith(language)) {
107                return true;
108            }
109        }
110        return false;
111    }
112
113    public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
114        ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
115        final int subtypeCount = imi.getSubtypeCount();
116        for (int i = 0; i < subtypeCount; ++i) {
117            subtypes.add(imi.getSubtypeAt(i));
118        }
119        return subtypes;
120    }
121
122    public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes(
123            InputMethodInfo imi, String mode) {
124        ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>();
125        final int subtypeCount = imi.getSubtypeCount();
126        for (int i = 0; i < subtypeCount; ++i) {
127            final InputMethodSubtype subtype = imi.getSubtypeAt(i);
128            if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) {
129                subtypes.add(subtype);
130            }
131        }
132        return subtypes;
133    }
134
135    public static InputMethodInfo getMostApplicableDefaultIME(
136            List<InputMethodInfo> enabledImes) {
137        if (enabledImes != null && enabledImes.size() > 0) {
138            // We'd prefer to fall back on a system IME, since that is safer.
139            int i = enabledImes.size();
140            int firstFoundSystemIme = -1;
141            while (i > 0) {
142                i--;
143                final InputMethodInfo imi = enabledImes.get(i);
144                if (InputMethodUtils.isSystemImeThatHasEnglishSubtype(imi)
145                        && !imi.isAuxiliaryIme()) {
146                    return imi;
147                }
148                if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi)
149                        && !imi.isAuxiliaryIme()) {
150                    firstFoundSystemIme = i;
151                }
152            }
153            return enabledImes.get(Math.max(firstFoundSystemIme, 0));
154        }
155        return null;
156    }
157
158    public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
159        return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
160    }
161
162    public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
163        if (imi != null) {
164            final int subtypeCount = imi.getSubtypeCount();
165            for (int i = 0; i < subtypeCount; ++i) {
166                InputMethodSubtype ims = imi.getSubtypeAt(i);
167                if (subtypeHashCode == ims.hashCode()) {
168                    return i;
169                }
170            }
171        }
172        return NOT_A_SUBTYPE_ID;
173    }
174
175    private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
176            Resources res, InputMethodInfo imi) {
177        final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
178        final String systemLocale = res.getConfiguration().locale.toString();
179        if (TextUtils.isEmpty(systemLocale)) return new ArrayList<InputMethodSubtype>();
180        final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap =
181                new HashMap<String, InputMethodSubtype>();
182        final int N = subtypes.size();
183        for (int i = 0; i < N; ++i) {
184            // scan overriding implicitly enabled subtypes.
185            InputMethodSubtype subtype = subtypes.get(i);
186            if (subtype.overridesImplicitlyEnabledSubtype()) {
187                final String mode = subtype.getMode();
188                if (!applicableModeAndSubtypesMap.containsKey(mode)) {
189                    applicableModeAndSubtypesMap.put(mode, subtype);
190                }
191            }
192        }
193        if (applicableModeAndSubtypesMap.size() > 0) {
194            return new ArrayList<InputMethodSubtype>(applicableModeAndSubtypesMap.values());
195        }
196        for (int i = 0; i < N; ++i) {
197            final InputMethodSubtype subtype = subtypes.get(i);
198            final String locale = subtype.getLocale();
199            final String mode = subtype.getMode();
200            // When system locale starts with subtype's locale, that subtype will be applicable
201            // for system locale
202            // For instance, it's clearly applicable for cases like system locale = en_US and
203            // subtype = en, but it is not necessarily considered applicable for cases like system
204            // locale = en and subtype = en_US.
205            // We just call systemLocale.startsWith(locale) in this function because there is no
206            // need to find applicable subtypes aggressively unlike
207            // findLastResortApplicableSubtypeLocked.
208            if (systemLocale.startsWith(locale)) {
209                final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode);
210                // If more applicable subtypes are contained, skip.
211                if (applicableSubtype != null) {
212                    if (systemLocale.equals(applicableSubtype.getLocale())) continue;
213                    if (!systemLocale.equals(locale)) continue;
214                }
215                applicableModeAndSubtypesMap.put(mode, subtype);
216            }
217        }
218        final InputMethodSubtype keyboardSubtype
219                = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD);
220        final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<InputMethodSubtype>(
221                applicableModeAndSubtypesMap.values());
222        if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) {
223            for (int i = 0; i < N; ++i) {
224                final InputMethodSubtype subtype = subtypes.get(i);
225                final String mode = subtype.getMode();
226                if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
227                        TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
228                    applicableSubtypes.add(subtype);
229                }
230            }
231        }
232        if (keyboardSubtype == null) {
233            InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
234                    res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
235            if (lastResortKeyboardSubtype != null) {
236                applicableSubtypes.add(lastResortKeyboardSubtype);
237            }
238        }
239        return applicableSubtypes;
240    }
241
242    private static List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
243            Context context, InputMethodInfo imi, List<InputMethodSubtype> enabledSubtypes,
244            boolean allowsImplicitlySelectedSubtypes) {
245        if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
246            enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
247                    context.getResources(), imi);
248        }
249        return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
250    }
251
252    /**
253     * If there are no selected subtypes, tries finding the most applicable one according to the
254     * given locale.
255     * @param subtypes this function will search the most applicable subtype in subtypes
256     * @param mode subtypes will be filtered by mode
257     * @param locale subtypes will be filtered by locale
258     * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
259     * it will return the first subtype matched with mode
260     * @return the most applicable subtypeId
261     */
262    public static InputMethodSubtype findLastResortApplicableSubtypeLocked(
263            Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
264            boolean canIgnoreLocaleAsLastResort) {
265        if (subtypes == null || subtypes.size() == 0) {
266            return null;
267        }
268        if (TextUtils.isEmpty(locale)) {
269            locale = res.getConfiguration().locale.toString();
270        }
271        final String language = locale.substring(0, 2);
272        boolean partialMatchFound = false;
273        InputMethodSubtype applicableSubtype = null;
274        InputMethodSubtype firstMatchedModeSubtype = null;
275        final int N = subtypes.size();
276        for (int i = 0; i < N; ++i) {
277            InputMethodSubtype subtype = subtypes.get(i);
278            final String subtypeLocale = subtype.getLocale();
279            // An applicable subtype should match "mode". If mode is null, mode will be ignored,
280            // and all subtypes with all modes can be candidates.
281            if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
282                if (firstMatchedModeSubtype == null) {
283                    firstMatchedModeSubtype = subtype;
284                }
285                if (locale.equals(subtypeLocale)) {
286                    // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
287                    applicableSubtype = subtype;
288                    break;
289                } else if (!partialMatchFound && subtypeLocale.startsWith(language)) {
290                    // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
291                    applicableSubtype = subtype;
292                    partialMatchFound = true;
293                }
294            }
295        }
296
297        if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
298            return firstMatchedModeSubtype;
299        }
300
301        // The first subtype applicable to the system locale will be defined as the most applicable
302        // subtype.
303        if (DEBUG) {
304            if (applicableSubtype != null) {
305                Slog.d(TAG, "Applicable InputMethodSubtype was found: "
306                        + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
307            }
308        }
309        return applicableSubtype;
310    }
311
312    public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
313        if (subtype == null) return true;
314        return !subtype.isAuxiliary();
315    }
316
317    /**
318     * Utility class for putting and getting settings for InputMethod
319     * TODO: Move all putters and getters of settings to this class.
320     */
321    public static class InputMethodSettings {
322        // The string for enabled input method is saved as follows:
323        // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
324        private static final char INPUT_METHOD_SEPARATER = ':';
325        private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';';
326        private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
327                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER);
328
329        private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
330                new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER);
331
332        private final Resources mRes;
333        private final ContentResolver mResolver;
334        private final HashMap<String, InputMethodInfo> mMethodMap;
335        private final ArrayList<InputMethodInfo> mMethodList;
336
337        private String mEnabledInputMethodsStrCache;
338        private int mCurrentUserId;
339
340        private static void buildEnabledInputMethodsSettingString(
341                StringBuilder builder, Pair<String, ArrayList<String>> pair) {
342            String id = pair.first;
343            ArrayList<String> subtypes = pair.second;
344            builder.append(id);
345            // Inputmethod and subtypes are saved in the settings as follows:
346            // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
347            for (String subtypeId: subtypes) {
348                builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId);
349            }
350        }
351
352        public InputMethodSettings(
353                Resources res, ContentResolver resolver,
354                HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList,
355                int userId) {
356            setCurrentUserId(userId);
357            mRes = res;
358            mResolver = resolver;
359            mMethodMap = methodMap;
360            mMethodList = methodList;
361        }
362
363        public void setCurrentUserId(int userId) {
364            if (DEBUG) {
365                Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to "
366                        + userId + ", new ime = " + getSelectedInputMethod());
367            }
368            // IMMS settings are kept per user, so keep track of current user
369            mCurrentUserId = userId;
370        }
371
372        public List<InputMethodInfo> getEnabledInputMethodListLocked() {
373            return createEnabledInputMethodListLocked(
374                    getEnabledInputMethodsAndSubtypeListLocked());
375        }
376
377        public List<Pair<InputMethodInfo, ArrayList<String>>>
378                getEnabledInputMethodAndSubtypeHashCodeListLocked() {
379            return createEnabledInputMethodAndSubtypeHashCodeListLocked(
380                    getEnabledInputMethodsAndSubtypeListLocked());
381        }
382
383        public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
384                Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
385            List<InputMethodSubtype> enabledSubtypes =
386                    getEnabledInputMethodSubtypeListLocked(imi);
387            if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
388                enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
389                        context.getResources(), imi);
390            }
391            return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
392        }
393
394        private List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
395                InputMethodInfo imi) {
396            List<Pair<String, ArrayList<String>>> imsList =
397                    getEnabledInputMethodsAndSubtypeListLocked();
398            ArrayList<InputMethodSubtype> enabledSubtypes =
399                    new ArrayList<InputMethodSubtype>();
400            if (imi != null) {
401                for (Pair<String, ArrayList<String>> imsPair : imsList) {
402                    InputMethodInfo info = mMethodMap.get(imsPair.first);
403                    if (info != null && info.getId().equals(imi.getId())) {
404                        final int subtypeCount = info.getSubtypeCount();
405                        for (int i = 0; i < subtypeCount; ++i) {
406                            InputMethodSubtype ims = info.getSubtypeAt(i);
407                            for (String s: imsPair.second) {
408                                if (String.valueOf(ims.hashCode()).equals(s)) {
409                                    enabledSubtypes.add(ims);
410                                }
411                            }
412                        }
413                        break;
414                    }
415                }
416            }
417            return enabledSubtypes;
418        }
419
420        // At the initial boot, the settings for input methods are not set,
421        // so we need to enable IME in that case.
422        public void enableAllIMEsIfThereIsNoEnabledIME() {
423            if (TextUtils.isEmpty(getEnabledInputMethodsStr())) {
424                StringBuilder sb = new StringBuilder();
425                final int N = mMethodList.size();
426                for (int i = 0; i < N; i++) {
427                    InputMethodInfo imi = mMethodList.get(i);
428                    Slog.i(TAG, "Adding: " + imi.getId());
429                    if (i > 0) sb.append(':');
430                    sb.append(imi.getId());
431                }
432                putEnabledInputMethodsStr(sb.toString());
433            }
434        }
435
436        public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
437            ArrayList<Pair<String, ArrayList<String>>> imsList
438                    = new ArrayList<Pair<String, ArrayList<String>>>();
439            final String enabledInputMethodsStr = getEnabledInputMethodsStr();
440            if (TextUtils.isEmpty(enabledInputMethodsStr)) {
441                return imsList;
442            }
443            mInputMethodSplitter.setString(enabledInputMethodsStr);
444            while (mInputMethodSplitter.hasNext()) {
445                String nextImsStr = mInputMethodSplitter.next();
446                mSubtypeSplitter.setString(nextImsStr);
447                if (mSubtypeSplitter.hasNext()) {
448                    ArrayList<String> subtypeHashes = new ArrayList<String>();
449                    // The first element is ime id.
450                    String imeId = mSubtypeSplitter.next();
451                    while (mSubtypeSplitter.hasNext()) {
452                        subtypeHashes.add(mSubtypeSplitter.next());
453                    }
454                    imsList.add(new Pair<String, ArrayList<String>>(imeId, subtypeHashes));
455                }
456            }
457            return imsList;
458        }
459
460        public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
461            if (reloadInputMethodStr) {
462                getEnabledInputMethodsStr();
463            }
464            if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
465                // Add in the newly enabled input method.
466                putEnabledInputMethodsStr(id);
467            } else {
468                putEnabledInputMethodsStr(
469                        mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATER + id);
470            }
471        }
472
473        /**
474         * Build and put a string of EnabledInputMethods with removing specified Id.
475         * @return the specified id was removed or not.
476         */
477        public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
478                StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
479            boolean isRemoved = false;
480            boolean needsAppendSeparator = false;
481            for (Pair<String, ArrayList<String>> ims: imsList) {
482                String curId = ims.first;
483                if (curId.equals(id)) {
484                    // We are disabling this input method, and it is
485                    // currently enabled.  Skip it to remove from the
486                    // new list.
487                    isRemoved = true;
488                } else {
489                    if (needsAppendSeparator) {
490                        builder.append(INPUT_METHOD_SEPARATER);
491                    } else {
492                        needsAppendSeparator = true;
493                    }
494                    buildEnabledInputMethodsSettingString(builder, ims);
495                }
496            }
497            if (isRemoved) {
498                // Update the setting with the new list of input methods.
499                putEnabledInputMethodsStr(builder.toString());
500            }
501            return isRemoved;
502        }
503
504        private List<InputMethodInfo> createEnabledInputMethodListLocked(
505                List<Pair<String, ArrayList<String>>> imsList) {
506            final ArrayList<InputMethodInfo> res = new ArrayList<InputMethodInfo>();
507            for (Pair<String, ArrayList<String>> ims: imsList) {
508                InputMethodInfo info = mMethodMap.get(ims.first);
509                if (info != null) {
510                    res.add(info);
511                }
512            }
513            return res;
514        }
515
516        private List<Pair<InputMethodInfo, ArrayList<String>>>
517                createEnabledInputMethodAndSubtypeHashCodeListLocked(
518                        List<Pair<String, ArrayList<String>>> imsList) {
519            final ArrayList<Pair<InputMethodInfo, ArrayList<String>>> res
520                    = new ArrayList<Pair<InputMethodInfo, ArrayList<String>>>();
521            for (Pair<String, ArrayList<String>> ims : imsList) {
522                InputMethodInfo info = mMethodMap.get(ims.first);
523                if (info != null) {
524                    res.add(new Pair<InputMethodInfo, ArrayList<String>>(info, ims.second));
525                }
526            }
527            return res;
528        }
529
530        private void putEnabledInputMethodsStr(String str) {
531            Settings.Secure.putStringForUser(
532                    mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId);
533            mEnabledInputMethodsStrCache = str;
534            if (DEBUG) {
535                Slog.d(TAG, "putEnabledInputMethodStr: " + str);
536            }
537        }
538
539        public String getEnabledInputMethodsStr() {
540            mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser(
541                    mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId);
542            if (DEBUG) {
543                Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
544                        + ", " + mCurrentUserId);
545            }
546            return mEnabledInputMethodsStrCache;
547        }
548
549        private void saveSubtypeHistory(
550                List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
551            StringBuilder builder = new StringBuilder();
552            boolean isImeAdded = false;
553            if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
554                builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append(
555                        newSubtypeId);
556                isImeAdded = true;
557            }
558            for (Pair<String, String> ime: savedImes) {
559                String imeId = ime.first;
560                String subtypeId = ime.second;
561                if (TextUtils.isEmpty(subtypeId)) {
562                    subtypeId = NOT_A_SUBTYPE_ID_STR;
563                }
564                if (isImeAdded) {
565                    builder.append(INPUT_METHOD_SEPARATER);
566                } else {
567                    isImeAdded = true;
568                }
569                builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append(
570                        subtypeId);
571            }
572            // Remove the last INPUT_METHOD_SEPARATER
573            putSubtypeHistoryStr(builder.toString());
574        }
575
576        private void addSubtypeToHistory(String imeId, String subtypeId) {
577            List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
578            for (Pair<String, String> ime: subtypeHistory) {
579                if (ime.first.equals(imeId)) {
580                    if (DEBUG) {
581                        Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
582                                + ime.second);
583                    }
584                    // We should break here
585                    subtypeHistory.remove(ime);
586                    break;
587                }
588            }
589            if (DEBUG) {
590                Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
591            }
592            saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
593        }
594
595        private void putSubtypeHistoryStr(String str) {
596            if (DEBUG) {
597                Slog.d(TAG, "putSubtypeHistoryStr: " + str);
598            }
599            Settings.Secure.putStringForUser(
600                    mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId);
601        }
602
603        public Pair<String, String> getLastInputMethodAndSubtypeLocked() {
604            // Gets the first one from the history
605            return getLastSubtypeForInputMethodLockedInternal(null);
606        }
607
608        public String getLastSubtypeForInputMethodLocked(String imeId) {
609            Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
610            if (ime != null) {
611                return ime.second;
612            } else {
613                return null;
614            }
615        }
616
617        private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
618            List<Pair<String, ArrayList<String>>> enabledImes =
619                    getEnabledInputMethodsAndSubtypeListLocked();
620            List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
621            for (Pair<String, String> imeAndSubtype : subtypeHistory) {
622                final String imeInTheHistory = imeAndSubtype.first;
623                // If imeId is empty, returns the first IME and subtype in the history
624                if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
625                    final String subtypeInTheHistory = imeAndSubtype.second;
626                    final String subtypeHashCode =
627                            getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
628                                    enabledImes, imeInTheHistory, subtypeInTheHistory);
629                    if (!TextUtils.isEmpty(subtypeHashCode)) {
630                        if (DEBUG) {
631                            Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
632                        }
633                        return new Pair<String, String>(imeInTheHistory, subtypeHashCode);
634                    }
635                }
636            }
637            if (DEBUG) {
638                Slog.d(TAG, "No enabled IME found in the history");
639            }
640            return null;
641        }
642
643        private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
644                ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
645            for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
646                if (enabledIme.first.equals(imeId)) {
647                    final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
648                    final InputMethodInfo imi = mMethodMap.get(imeId);
649                    if (explicitlyEnabledSubtypes.size() == 0) {
650                        // If there are no explicitly enabled subtypes, applicable subtypes are
651                        // enabled implicitly.
652                        // If IME is enabled and no subtypes are enabled, applicable subtypes
653                        // are enabled implicitly, so needs to treat them to be enabled.
654                        if (imi != null && imi.getSubtypeCount() > 0) {
655                            List<InputMethodSubtype> implicitlySelectedSubtypes =
656                                    getImplicitlyApplicableSubtypesLocked(mRes, imi);
657                            if (implicitlySelectedSubtypes != null) {
658                                final int N = implicitlySelectedSubtypes.size();
659                                for (int i = 0; i < N; ++i) {
660                                    final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
661                                    if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
662                                        return subtypeHashCode;
663                                    }
664                                }
665                            }
666                        }
667                    } else {
668                        for (String s: explicitlyEnabledSubtypes) {
669                            if (s.equals(subtypeHashCode)) {
670                                // If both imeId and subtypeId are enabled, return subtypeId.
671                                try {
672                                    final int hashCode = Integer.valueOf(subtypeHashCode);
673                                    // Check whether the subtype id is valid or not
674                                    if (isValidSubtypeId(imi, hashCode)) {
675                                        return s;
676                                    } else {
677                                        return NOT_A_SUBTYPE_ID_STR;
678                                    }
679                                } catch (NumberFormatException e) {
680                                    return NOT_A_SUBTYPE_ID_STR;
681                                }
682                            }
683                        }
684                    }
685                    // If imeId was enabled but subtypeId was disabled.
686                    return NOT_A_SUBTYPE_ID_STR;
687                }
688            }
689            // If both imeId and subtypeId are disabled, return null
690            return null;
691        }
692
693        private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
694            ArrayList<Pair<String, String>> imsList = new ArrayList<Pair<String, String>>();
695            final String subtypeHistoryStr = getSubtypeHistoryStr();
696            if (TextUtils.isEmpty(subtypeHistoryStr)) {
697                return imsList;
698            }
699            mInputMethodSplitter.setString(subtypeHistoryStr);
700            while (mInputMethodSplitter.hasNext()) {
701                String nextImsStr = mInputMethodSplitter.next();
702                mSubtypeSplitter.setString(nextImsStr);
703                if (mSubtypeSplitter.hasNext()) {
704                    String subtypeId = NOT_A_SUBTYPE_ID_STR;
705                    // The first element is ime id.
706                    String imeId = mSubtypeSplitter.next();
707                    while (mSubtypeSplitter.hasNext()) {
708                        subtypeId = mSubtypeSplitter.next();
709                        break;
710                    }
711                    imsList.add(new Pair<String, String>(imeId, subtypeId));
712                }
713            }
714            return imsList;
715        }
716
717        private String getSubtypeHistoryStr() {
718            if (DEBUG) {
719                Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser(
720                        mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId));
721            }
722            return Settings.Secure.getStringForUser(
723                    mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId);
724        }
725
726        public void putSelectedInputMethod(String imeId) {
727            if (DEBUG) {
728                Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
729                        + mCurrentUserId);
730            }
731            Settings.Secure.putStringForUser(
732                    mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId);
733        }
734
735        public void putSelectedSubtype(int subtypeId) {
736            if (DEBUG) {
737                Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
738                        + mCurrentUserId);
739            }
740            Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE,
741                    subtypeId, mCurrentUserId);
742        }
743
744        public String getDisabledSystemInputMethods() {
745            return Settings.Secure.getStringForUser(
746                    mResolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, mCurrentUserId);
747        }
748
749        public String getSelectedInputMethod() {
750            if (DEBUG) {
751                Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser(
752                        mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId)
753                        + ", " + mCurrentUserId);
754            }
755            return Settings.Secure.getStringForUser(
756                    mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId);
757        }
758
759        public boolean isSubtypeSelected() {
760            return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
761        }
762
763        private int getSelectedInputMethodSubtypeHashCode() {
764            try {
765                return Settings.Secure.getIntForUser(
766                        mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId);
767            } catch (SettingNotFoundException e) {
768                return NOT_A_SUBTYPE_ID;
769            }
770        }
771
772        public int getCurrentUserId() {
773            return mCurrentUserId;
774        }
775
776        public int getSelectedInputMethodSubtypeId(String selectedImiId) {
777            final InputMethodInfo imi = mMethodMap.get(selectedImiId);
778            if (imi == null) {
779                return NOT_A_SUBTYPE_ID;
780            }
781            final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
782            return getSubtypeIdFromHashCode(imi, subtypeHashCode);
783        }
784
785        public void saveCurrentInputMethodAndSubtypeToHistory(
786                String curMethodId, InputMethodSubtype currentSubtype) {
787            String subtypeId = NOT_A_SUBTYPE_ID_STR;
788            if (currentSubtype != null) {
789                subtypeId = String.valueOf(currentSubtype.hashCode());
790            }
791            if (canAddToLastInputMethod(currentSubtype)) {
792                addSubtypeToHistory(curMethodId, subtypeId);
793            }
794        }
795    }
796}
797