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