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