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