Home | History | Annotate | Download | only in inputmethod
      1 /*
      2  * Copyright (C) 2010 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package com.android.settings.inputmethod;
     18 
     19 import com.android.settings.SettingsPreferenceFragment;
     20 
     21 import android.content.ContentResolver;
     22 import android.content.Context;
     23 import android.content.pm.ApplicationInfo;
     24 import android.content.pm.PackageManager;
     25 import android.content.res.Resources;
     26 import android.preference.CheckBoxPreference;
     27 import android.preference.Preference;
     28 import android.preference.PreferenceScreen;
     29 import android.provider.Settings;
     30 import android.provider.Settings.SettingNotFoundException;
     31 import android.text.TextUtils;
     32 import android.util.Log;
     33 import android.view.inputmethod.InputMethodInfo;
     34 import android.view.inputmethod.InputMethodManager;
     35 import android.view.inputmethod.InputMethodSubtype;
     36 
     37 import java.util.HashMap;
     38 import java.util.HashSet;
     39 import java.util.List;
     40 import java.util.Locale;
     41 import java.util.Map;
     42 
     43 public class InputMethodAndSubtypeUtil {
     44 
     45     private static final boolean DEBUG = false;
     46     static final String TAG = "InputMethdAndSubtypeUtil";
     47 
     48     private static final char INPUT_METHOD_SEPARATER = ':';
     49     private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';';
     50     private static final int NOT_A_SUBTYPE_ID = -1;
     51     private static final Locale ENGLISH_LOCALE = new Locale("en");
     52 
     53     private static final TextUtils.SimpleStringSplitter sStringInputMethodSplitter
     54             = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER);
     55 
     56     private static final TextUtils.SimpleStringSplitter sStringInputMethodSubtypeSplitter
     57             = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER);
     58 
     59     private static void buildEnabledInputMethodsString(
     60             StringBuilder builder, String imi, HashSet<String> subtypes) {
     61         builder.append(imi);
     62         // Inputmethod and subtypes are saved in the settings as follows:
     63         // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
     64         for (String subtypeId: subtypes) {
     65             builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId);
     66         }
     67     }
     68 
     69     public static void buildInputMethodsAndSubtypesString(
     70             StringBuilder builder, HashMap<String, HashSet<String>> imsList) {
     71         boolean needsAppendSeparator = false;
     72         for (String imi: imsList.keySet()) {
     73             if (needsAppendSeparator) {
     74                 builder.append(INPUT_METHOD_SEPARATER);
     75             } else {
     76                 needsAppendSeparator = true;
     77             }
     78             buildEnabledInputMethodsString(builder, imi, imsList.get(imi));
     79         }
     80     }
     81 
     82     public static void buildDisabledSystemInputMethods(
     83             StringBuilder builder, HashSet<String> imes) {
     84         boolean needsAppendSeparator = false;
     85         for (String ime: imes) {
     86             if (needsAppendSeparator) {
     87                 builder.append(INPUT_METHOD_SEPARATER);
     88             } else {
     89                 needsAppendSeparator = true;
     90             }
     91             builder.append(ime);
     92         }
     93     }
     94 
     95     private static int getInputMethodSubtypeSelected(ContentResolver resolver) {
     96         try {
     97             return Settings.Secure.getInt(resolver,
     98                     Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE);
     99         } catch (SettingNotFoundException e) {
    100             return NOT_A_SUBTYPE_ID;
    101         }
    102     }
    103 
    104     private static boolean isInputMethodSubtypeSelected(ContentResolver resolver) {
    105         return getInputMethodSubtypeSelected(resolver) != NOT_A_SUBTYPE_ID;
    106     }
    107 
    108     private static void putSelectedInputMethodSubtype(ContentResolver resolver, int hashCode) {
    109         Settings.Secure.putInt(resolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, hashCode);
    110     }
    111 
    112     // Needs to modify InputMethodManageService if you want to change the format of saved string.
    113     private static HashMap<String, HashSet<String>> getEnabledInputMethodsAndSubtypeList(
    114             ContentResolver resolver) {
    115         final String enabledInputMethodsStr = Settings.Secure.getString(
    116                 resolver, Settings.Secure.ENABLED_INPUT_METHODS);
    117         HashMap<String, HashSet<String>> imsList
    118                 = new HashMap<String, HashSet<String>>();
    119         if (DEBUG) {
    120             Log.d(TAG, "--- Load enabled input methods: " + enabledInputMethodsStr);
    121         }
    122 
    123         if (TextUtils.isEmpty(enabledInputMethodsStr)) {
    124             return imsList;
    125         }
    126         sStringInputMethodSplitter.setString(enabledInputMethodsStr);
    127         while (sStringInputMethodSplitter.hasNext()) {
    128             String nextImsStr = sStringInputMethodSplitter.next();
    129             sStringInputMethodSubtypeSplitter.setString(nextImsStr);
    130             if (sStringInputMethodSubtypeSplitter.hasNext()) {
    131                 HashSet<String> subtypeHashes = new HashSet<String>();
    132                 // The first element is ime id.
    133                 String imeId = sStringInputMethodSubtypeSplitter.next();
    134                 while (sStringInputMethodSubtypeSplitter.hasNext()) {
    135                     subtypeHashes.add(sStringInputMethodSubtypeSplitter.next());
    136                 }
    137                 imsList.put(imeId, subtypeHashes);
    138             }
    139         }
    140         return imsList;
    141     }
    142 
    143     private static HashSet<String> getDisabledSystemIMEs(ContentResolver resolver) {
    144         HashSet<String> set = new HashSet<String>();
    145         String disabledIMEsStr = Settings.Secure.getString(
    146                 resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS);
    147         if (TextUtils.isEmpty(disabledIMEsStr)) {
    148             return set;
    149         }
    150         sStringInputMethodSplitter.setString(disabledIMEsStr);
    151         while(sStringInputMethodSplitter.hasNext()) {
    152             set.add(sStringInputMethodSplitter.next());
    153         }
    154         return set;
    155     }
    156 
    157     public static CharSequence getCurrentInputMethodName(Context context, ContentResolver resolver,
    158             InputMethodManager imm, List<InputMethodInfo> imis, PackageManager pm) {
    159         if (resolver == null || imis == null) return null;
    160         final String currentInputMethodId = Settings.Secure.getString(resolver,
    161                 Settings.Secure.DEFAULT_INPUT_METHOD);
    162         if (TextUtils.isEmpty(currentInputMethodId)) return null;
    163         for (InputMethodInfo imi : imis) {
    164             if (currentInputMethodId.equals(imi.getId())) {
    165                 final InputMethodSubtype subtype = imm.getCurrentInputMethodSubtype();
    166                 final CharSequence imiLabel = imi.loadLabel(pm);
    167                 final CharSequence summary = subtype != null
    168                         ? TextUtils.concat(subtype.getDisplayName(context,
    169                                     imi.getPackageName(), imi.getServiceInfo().applicationInfo),
    170                                             (TextUtils.isEmpty(imiLabel) ?
    171                                                     "" : " - " + imiLabel))
    172                         : imiLabel;
    173                 return summary;
    174             }
    175         }
    176         return null;
    177     }
    178 
    179     public static void saveInputMethodSubtypeList(SettingsPreferenceFragment context,
    180             ContentResolver resolver, List<InputMethodInfo> inputMethodInfos,
    181             boolean hasHardKeyboard) {
    182         String currentInputMethodId = Settings.Secure.getString(resolver,
    183                 Settings.Secure.DEFAULT_INPUT_METHOD);
    184         final int selectedInputMethodSubtype = getInputMethodSubtypeSelected(resolver);
    185         HashMap<String, HashSet<String>> enabledIMEAndSubtypesMap =
    186                 getEnabledInputMethodsAndSubtypeList(resolver);
    187         HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver);
    188 
    189         final int imiCount = inputMethodInfos.size();
    190         boolean needsToResetSelectedSubtype = false;
    191         for (InputMethodInfo imi : inputMethodInfos) {
    192             final String imiId = imi.getId();
    193             Preference pref = context.findPreference(imiId);
    194             if (pref == null) continue;
    195             // In the Configure input method screen or in the subtype enabler screen.
    196             // pref is instance of CheckBoxPreference in the Configure input method screen.
    197             final boolean isImeChecked = (pref instanceof CheckBoxPreference) ?
    198                     ((CheckBoxPreference) pref).isChecked()
    199                     : enabledIMEAndSubtypesMap.containsKey(imiId);
    200             final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId);
    201             final boolean systemIme = isSystemIme(imi);
    202             if ((!hasHardKeyboard && isAlwaysCheckedIme(imi, context.getActivity(), imiCount))
    203                     || isImeChecked) {
    204                 if (!enabledIMEAndSubtypesMap.containsKey(imiId)) {
    205                     // imiId has just been enabled
    206                     enabledIMEAndSubtypesMap.put(imiId, new HashSet<String>());
    207                 }
    208                 HashSet<String> subtypesSet = enabledIMEAndSubtypesMap.get(imiId);
    209 
    210                 boolean subtypePrefFound = false;
    211                 final int subtypeCount = imi.getSubtypeCount();
    212                 for (int i = 0; i < subtypeCount; ++i) {
    213                     InputMethodSubtype subtype = imi.getSubtypeAt(i);
    214                     final String subtypeHashCodeStr = String.valueOf(subtype.hashCode());
    215                     CheckBoxPreference subtypePref = (CheckBoxPreference) context.findPreference(
    216                             imiId + subtypeHashCodeStr);
    217                     // In the Configure input method screen which does not have subtype preferences.
    218                     if (subtypePref == null) continue;
    219                     if (!subtypePrefFound) {
    220                         // Once subtype checkbox is found, subtypeSet needs to be cleared.
    221                         // Because of system change, hashCode value could have been changed.
    222                         subtypesSet.clear();
    223                         // If selected subtype preference is disabled, needs to reset.
    224                         needsToResetSelectedSubtype = true;
    225                         subtypePrefFound = true;
    226                     }
    227                     if (subtypePref.isChecked()) {
    228                         subtypesSet.add(subtypeHashCodeStr);
    229                         if (isCurrentInputMethod) {
    230                             if (selectedInputMethodSubtype == subtype.hashCode()) {
    231                                 // Selected subtype is still enabled, there is no need to reset
    232                                 // selected subtype.
    233                                 needsToResetSelectedSubtype = false;
    234                             }
    235                         }
    236                     } else {
    237                         subtypesSet.remove(subtypeHashCodeStr);
    238                     }
    239                 }
    240             } else {
    241                 enabledIMEAndSubtypesMap.remove(imiId);
    242                 if (isCurrentInputMethod) {
    243                     // We are processing the current input method, but found that it's not enabled.
    244                     // This means that the current input method has been uninstalled.
    245                     // If currentInputMethod is already uninstalled, InputMethodManagerService will
    246                     // find the applicable IME from the history and the system locale.
    247                     if (DEBUG) {
    248                         Log.d(TAG, "Current IME was uninstalled or disabled.");
    249                     }
    250                     currentInputMethodId = null;
    251                 }
    252             }
    253             // If it's a disabled system ime, add it to the disabled list so that it
    254             // doesn't get enabled automatically on any changes to the package list
    255             if (systemIme && hasHardKeyboard) {
    256                 if (disabledSystemIMEs.contains(imiId)) {
    257                     if (isImeChecked) {
    258                         disabledSystemIMEs.remove(imiId);
    259                     }
    260                 } else {
    261                     if (!isImeChecked) {
    262                         disabledSystemIMEs.add(imiId);
    263                     }
    264                 }
    265             }
    266         }
    267 
    268         StringBuilder builder = new StringBuilder();
    269         buildInputMethodsAndSubtypesString(builder, enabledIMEAndSubtypesMap);
    270         StringBuilder disabledSysImesBuilder = new StringBuilder();
    271         buildDisabledSystemInputMethods(disabledSysImesBuilder, disabledSystemIMEs);
    272         if (DEBUG) {
    273             Log.d(TAG, "--- Save enabled inputmethod settings. :" + builder.toString());
    274             Log.d(TAG, "--- Save disable system inputmethod settings. :"
    275                     + disabledSysImesBuilder.toString());
    276             Log.d(TAG, "--- Save default inputmethod settings. :" + currentInputMethodId);
    277             Log.d(TAG, "--- Needs to reset the selected subtype :" + needsToResetSelectedSubtype);
    278             Log.d(TAG, "--- Subtype is selected :" + isInputMethodSubtypeSelected(resolver));
    279         }
    280 
    281         // Redefines SelectedSubtype when all subtypes are unchecked or there is no subtype
    282         // selected. And if the selected subtype of the current input method was disabled,
    283         // We should reset the selected input method's subtype.
    284         if (needsToResetSelectedSubtype || !isInputMethodSubtypeSelected(resolver)) {
    285             if (DEBUG) {
    286                 Log.d(TAG, "--- Reset inputmethod subtype because it's not defined.");
    287             }
    288             putSelectedInputMethodSubtype(resolver, NOT_A_SUBTYPE_ID);
    289         }
    290 
    291         Settings.Secure.putString(resolver,
    292                 Settings.Secure.ENABLED_INPUT_METHODS, builder.toString());
    293         if (disabledSysImesBuilder.length() > 0) {
    294             Settings.Secure.putString(resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS,
    295                     disabledSysImesBuilder.toString());
    296         }
    297         // If the current input method is unset, InputMethodManagerService will find the applicable
    298         // IME from the history and the system locale.
    299         Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD,
    300                 currentInputMethodId != null ? currentInputMethodId : "");
    301     }
    302 
    303     public static void loadInputMethodSubtypeList(
    304             SettingsPreferenceFragment context, ContentResolver resolver,
    305             List<InputMethodInfo> inputMethodInfos,
    306             final Map<String, List<Preference>> inputMethodPrefsMap) {
    307         HashMap<String, HashSet<String>> enabledSubtypes =
    308                 getEnabledInputMethodsAndSubtypeList(resolver);
    309 
    310         for (InputMethodInfo imi : inputMethodInfos) {
    311             final String imiId = imi.getId();
    312             Preference pref = context.findPreference(imiId);
    313             if (pref != null && pref instanceof CheckBoxPreference) {
    314                 CheckBoxPreference checkBoxPreference = (CheckBoxPreference) pref;
    315                 boolean isEnabled = enabledSubtypes.containsKey(imiId);
    316                 checkBoxPreference.setChecked(isEnabled);
    317                 if (inputMethodPrefsMap != null) {
    318                     for (Preference childPref: inputMethodPrefsMap.get(imiId)) {
    319                         childPref.setEnabled(isEnabled);
    320                     }
    321                 }
    322                 setSubtypesPreferenceEnabled(context, inputMethodInfos, imiId, isEnabled);
    323             }
    324         }
    325         updateSubtypesPreferenceChecked(context, inputMethodInfos, enabledSubtypes);
    326     }
    327 
    328     public static void setSubtypesPreferenceEnabled(SettingsPreferenceFragment context,
    329             List<InputMethodInfo> inputMethodProperties, String id, boolean enabled) {
    330         PreferenceScreen preferenceScreen = context.getPreferenceScreen();
    331         for (InputMethodInfo imi : inputMethodProperties) {
    332             if (id.equals(imi.getId())) {
    333                 final int subtypeCount = imi.getSubtypeCount();
    334                 for (int i = 0; i < subtypeCount; ++i) {
    335                     InputMethodSubtype subtype = imi.getSubtypeAt(i);
    336                     CheckBoxPreference pref = (CheckBoxPreference) preferenceScreen.findPreference(
    337                             id + subtype.hashCode());
    338                     if (pref != null) {
    339                         pref.setEnabled(enabled);
    340                     }
    341                 }
    342             }
    343         }
    344     }
    345 
    346     public static void updateSubtypesPreferenceChecked(SettingsPreferenceFragment context,
    347             List<InputMethodInfo> inputMethodProperties,
    348             HashMap<String, HashSet<String>> enabledSubtypes) {
    349         PreferenceScreen preferenceScreen = context.getPreferenceScreen();
    350         for (InputMethodInfo imi : inputMethodProperties) {
    351             String id = imi.getId();
    352             if (!enabledSubtypes.containsKey(id)) break;
    353             final HashSet<String> enabledSubtypesSet = enabledSubtypes.get(id);
    354             final int subtypeCount = imi.getSubtypeCount();
    355             for (int i = 0; i < subtypeCount; ++i) {
    356                 InputMethodSubtype subtype = imi.getSubtypeAt(i);
    357                 String hashCode = String.valueOf(subtype.hashCode());
    358                 if (DEBUG) {
    359                     Log.d(TAG, "--- Set checked state: " + "id" + ", " + hashCode + ", "
    360                             + enabledSubtypesSet.contains(hashCode));
    361                 }
    362                 CheckBoxPreference pref = (CheckBoxPreference) preferenceScreen.findPreference(
    363                         id + hashCode);
    364                 if (pref != null) {
    365                     pref.setChecked(enabledSubtypesSet.contains(hashCode));
    366                 }
    367             }
    368         }
    369     }
    370 
    371     public static boolean isSystemIme(InputMethodInfo property) {
    372         return (property.getServiceInfo().applicationInfo.flags
    373                 & ApplicationInfo.FLAG_SYSTEM) != 0;
    374     }
    375 
    376     public static boolean isAuxiliaryIme(InputMethodInfo imi) {
    377         return imi.isAuxiliaryIme();
    378     }
    379 
    380     public static boolean isAlwaysCheckedIme(InputMethodInfo imi, Context context, int imiCount) {
    381         if (imiCount <= 1) {
    382             return true;
    383         }
    384         if (!isSystemIme(imi)) {
    385             return false;
    386         }
    387         if (isAuxiliaryIme(imi)) {
    388             return false;
    389         }
    390         if (isValidDefaultIme(imi, context)) {
    391             return true;
    392         }
    393         return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage());
    394     }
    395 
    396     private static boolean isValidDefaultIme(InputMethodInfo imi, Context context) {
    397         if (imi.getIsDefaultResourceId() != 0) {
    398             try {
    399                 Resources res = context.createPackageContext(
    400                         imi.getPackageName(), 0).getResources();
    401                 if (res.getBoolean(imi.getIsDefaultResourceId())
    402                         && containsSubtypeOf(imi, context.getResources().getConfiguration().
    403                                 locale.getLanguage())) {
    404                     return true;
    405                 }
    406             } catch (PackageManager.NameNotFoundException ex) {
    407             } catch (Resources.NotFoundException ex) {
    408             }
    409         }
    410         return false;
    411     }
    412 
    413     private static boolean containsSubtypeOf(InputMethodInfo imi, String language) {
    414         final int N = imi.getSubtypeCount();
    415         for (int i = 0; i < N; ++i) {
    416             if (imi.getSubtypeAt(i).getLocale().startsWith(language)) {
    417                 return true;
    418             }
    419         }
    420         return false;
    421     }
    422 }
    423