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.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.content.ContentResolver; 22 import android.content.Context; 23 import android.content.SharedPreferences; 24 import android.content.res.Configuration; 25 import android.icu.text.ListFormatter; 26 import android.provider.Settings; 27 import android.provider.Settings.SettingNotFoundException; 28 import android.support.v7.preference.Preference; 29 import android.support.v7.preference.PreferenceScreen; 30 import android.support.v7.preference.TwoStatePreference; 31 import android.text.TextUtils; 32 import android.util.Log; 33 import android.view.inputmethod.InputMethodInfo; 34 import android.view.inputmethod.InputMethodSubtype; 35 36 import com.android.internal.app.LocaleHelper; 37 import com.android.internal.inputmethod.InputMethodUtils; 38 import com.android.settings.SettingsPreferenceFragment; 39 40 import java.util.HashMap; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Locale; 44 import java.util.Map; 45 46 // TODO: Consolidate this with {@link InputMethodSettingValuesWrapper}. 47 class InputMethodAndSubtypeUtil { 48 49 private static final boolean DEBUG = false; 50 static final String TAG = "InputMethdAndSubtypeUtil"; 51 52 private static final char INPUT_METHOD_SEPARATER = ':'; 53 private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';'; 54 private static final int NOT_A_SUBTYPE_ID = -1; 55 56 private static final TextUtils.SimpleStringSplitter sStringInputMethodSplitter 57 = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER); 58 59 private static final TextUtils.SimpleStringSplitter sStringInputMethodSubtypeSplitter 60 = new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER); 61 62 // InputMethods and subtypes are saved in the settings as follows: 63 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 64 static String buildInputMethodsAndSubtypesString( 65 final HashMap<String, HashSet<String>> imeToSubtypesMap) { 66 final StringBuilder builder = new StringBuilder(); 67 for (final String imi : imeToSubtypesMap.keySet()) { 68 if (builder.length() > 0) { 69 builder.append(INPUT_METHOD_SEPARATER); 70 } 71 final HashSet<String> subtypeIdSet = imeToSubtypesMap.get(imi); 72 builder.append(imi); 73 for (final String subtypeId : subtypeIdSet) { 74 builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId); 75 } 76 } 77 return builder.toString(); 78 } 79 80 private static String buildInputMethodsString(final HashSet<String> imiList) { 81 final StringBuilder builder = new StringBuilder(); 82 for (final String imi : imiList) { 83 if (builder.length() > 0) { 84 builder.append(INPUT_METHOD_SEPARATER); 85 } 86 builder.append(imi); 87 } 88 return builder.toString(); 89 } 90 91 private static int getInputMethodSubtypeSelected(ContentResolver resolver) { 92 try { 93 return Settings.Secure.getInt(resolver, 94 Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE); 95 } catch (SettingNotFoundException e) { 96 return NOT_A_SUBTYPE_ID; 97 } 98 } 99 100 private static boolean isInputMethodSubtypeSelected(ContentResolver resolver) { 101 return getInputMethodSubtypeSelected(resolver) != NOT_A_SUBTYPE_ID; 102 } 103 104 private static void putSelectedInputMethodSubtype(ContentResolver resolver, int hashCode) { 105 Settings.Secure.putInt(resolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, hashCode); 106 } 107 108 // Needs to modify InputMethodManageService if you want to change the format of saved string. 109 private static HashMap<String, HashSet<String>> getEnabledInputMethodsAndSubtypeList( 110 ContentResolver resolver) { 111 final String enabledInputMethodsStr = Settings.Secure.getString( 112 resolver, Settings.Secure.ENABLED_INPUT_METHODS); 113 if (DEBUG) { 114 Log.d(TAG, "--- Load enabled input methods: " + enabledInputMethodsStr); 115 } 116 return parseInputMethodsAndSubtypesString(enabledInputMethodsStr); 117 } 118 119 static HashMap<String, HashSet<String>> parseInputMethodsAndSubtypesString( 120 final String inputMethodsAndSubtypesString) { 121 final HashMap<String, HashSet<String>> subtypesMap = new HashMap<>(); 122 if (TextUtils.isEmpty(inputMethodsAndSubtypesString)) { 123 return subtypesMap; 124 } 125 sStringInputMethodSplitter.setString(inputMethodsAndSubtypesString); 126 while (sStringInputMethodSplitter.hasNext()) { 127 final String nextImsStr = sStringInputMethodSplitter.next(); 128 sStringInputMethodSubtypeSplitter.setString(nextImsStr); 129 if (sStringInputMethodSubtypeSplitter.hasNext()) { 130 final HashSet<String> subtypeIdSet = new HashSet<>(); 131 // The first element is {@link InputMethodInfoId}. 132 final String imiId = sStringInputMethodSubtypeSplitter.next(); 133 while (sStringInputMethodSubtypeSplitter.hasNext()) { 134 subtypeIdSet.add(sStringInputMethodSubtypeSplitter.next()); 135 } 136 subtypesMap.put(imiId, subtypeIdSet); 137 } 138 } 139 return subtypesMap; 140 } 141 142 static void enableInputMethodSubtypesOf(final ContentResolver resolver, final String imiId, 143 final HashSet<String> enabledSubtypeIdSet) { 144 final HashMap<String, HashSet<String>> enabledImeAndSubtypeIdsMap = 145 getEnabledInputMethodsAndSubtypeList(resolver); 146 enabledImeAndSubtypeIdsMap.put(imiId, enabledSubtypeIdSet); 147 final String enabledImesAndSubtypesString = buildInputMethodsAndSubtypesString( 148 enabledImeAndSubtypeIdsMap); 149 Settings.Secure.putString(resolver, 150 Settings.Secure.ENABLED_INPUT_METHODS, enabledImesAndSubtypesString); 151 } 152 153 private static HashSet<String> getDisabledSystemIMEs(ContentResolver resolver) { 154 HashSet<String> set = new HashSet<>(); 155 String disabledIMEsStr = Settings.Secure.getString( 156 resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS); 157 if (TextUtils.isEmpty(disabledIMEsStr)) { 158 return set; 159 } 160 sStringInputMethodSplitter.setString(disabledIMEsStr); 161 while(sStringInputMethodSplitter.hasNext()) { 162 set.add(sStringInputMethodSplitter.next()); 163 } 164 return set; 165 } 166 167 static void saveInputMethodSubtypeList(SettingsPreferenceFragment context, 168 ContentResolver resolver, List<InputMethodInfo> inputMethodInfos, 169 boolean hasHardKeyboard) { 170 String currentInputMethodId = Settings.Secure.getString(resolver, 171 Settings.Secure.DEFAULT_INPUT_METHOD); 172 final int selectedInputMethodSubtype = getInputMethodSubtypeSelected(resolver); 173 final HashMap<String, HashSet<String>> enabledIMEsAndSubtypesMap = 174 getEnabledInputMethodsAndSubtypeList(resolver); 175 final HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver); 176 177 boolean needsToResetSelectedSubtype = false; 178 for (final InputMethodInfo imi : inputMethodInfos) { 179 final String imiId = imi.getId(); 180 final Preference pref = context.findPreference(imiId); 181 if (pref == null) { 182 continue; 183 } 184 // In the choose input method screen or in the subtype enabler screen, 185 // <code>pref</code> is an instance of TwoStatePreference. 186 final boolean isImeChecked = (pref instanceof TwoStatePreference) ? 187 ((TwoStatePreference) pref).isChecked() 188 : enabledIMEsAndSubtypesMap.containsKey(imiId); 189 final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId); 190 final boolean systemIme = InputMethodUtils.isSystemIme(imi); 191 if ((!hasHardKeyboard && InputMethodSettingValuesWrapper.getInstance( 192 context.getActivity()).isAlwaysCheckedIme(imi, context.getActivity())) 193 || isImeChecked) { 194 if (!enabledIMEsAndSubtypesMap.containsKey(imiId)) { 195 // imiId has just been enabled 196 enabledIMEsAndSubtypesMap.put(imiId, new HashSet<String>()); 197 } 198 final HashSet<String> subtypesSet = enabledIMEsAndSubtypesMap.get(imiId); 199 200 boolean subtypePrefFound = false; 201 final int subtypeCount = imi.getSubtypeCount(); 202 for (int i = 0; i < subtypeCount; ++i) { 203 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 204 final String subtypeHashCodeStr = String.valueOf(subtype.hashCode()); 205 final TwoStatePreference subtypePref = (TwoStatePreference) context 206 .findPreference(imiId + subtypeHashCodeStr); 207 // In the Configure input method screen which does not have subtype preferences. 208 if (subtypePref == null) { 209 continue; 210 } 211 if (!subtypePrefFound) { 212 // Once subtype preference is found, subtypeSet needs to be cleared. 213 // Because of system change, hashCode value could have been changed. 214 subtypesSet.clear(); 215 // If selected subtype preference is disabled, needs to reset. 216 needsToResetSelectedSubtype = true; 217 subtypePrefFound = true; 218 } 219 // Checking <code>subtypePref.isEnabled()</code> is insufficient to determine 220 // whether the user manually enabled this subtype or not. Implicitly-enabled 221 // subtypes are also checked just as an indicator to users. We also need to 222 // check <code>subtypePref.isEnabled()</code> so that only manually enabled 223 // subtypes can be saved here. 224 if (subtypePref.isEnabled() && 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 enabledIMEsAndSubtypesMap.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 final String enabledIMEsAndSubtypesString = buildInputMethodsAndSubtypesString( 266 enabledIMEsAndSubtypesMap); 267 final String disabledSystemIMEsString = buildInputMethodsString(disabledSystemIMEs); 268 if (DEBUG) { 269 Log.d(TAG, "--- Save enabled inputmethod settings. :" + enabledIMEsAndSubtypesString); 270 Log.d(TAG, "--- Save disabled system inputmethod settings. :" 271 + disabledSystemIMEsString); 272 Log.d(TAG, "--- Save default inputmethod settings. :" + currentInputMethodId); 273 Log.d(TAG, "--- Needs to reset the selected subtype :" + needsToResetSelectedSubtype); 274 Log.d(TAG, "--- Subtype is selected :" + isInputMethodSubtypeSelected(resolver)); 275 } 276 277 // Redefines SelectedSubtype when all subtypes are unchecked or there is no subtype 278 // selected. And if the selected subtype of the current input method was disabled, 279 // We should reset the selected input method's subtype. 280 if (needsToResetSelectedSubtype || !isInputMethodSubtypeSelected(resolver)) { 281 if (DEBUG) { 282 Log.d(TAG, "--- Reset inputmethod subtype because it's not defined."); 283 } 284 putSelectedInputMethodSubtype(resolver, NOT_A_SUBTYPE_ID); 285 } 286 287 Settings.Secure.putString(resolver, 288 Settings.Secure.ENABLED_INPUT_METHODS, enabledIMEsAndSubtypesString); 289 if (disabledSystemIMEsString.length() > 0) { 290 Settings.Secure.putString(resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, 291 disabledSystemIMEsString); 292 } 293 // If the current input method is unset, InputMethodManagerService will find the applicable 294 // IME from the history and the system locale. 295 Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD, 296 currentInputMethodId != null ? currentInputMethodId : ""); 297 } 298 299 static void loadInputMethodSubtypeList(final SettingsPreferenceFragment context, 300 final ContentResolver resolver, final List<InputMethodInfo> inputMethodInfos, 301 final Map<String, List<Preference>> inputMethodPrefsMap) { 302 final HashMap<String, HashSet<String>> enabledSubtypes = 303 getEnabledInputMethodsAndSubtypeList(resolver); 304 305 for (final InputMethodInfo imi : inputMethodInfos) { 306 final String imiId = imi.getId(); 307 final Preference pref = context.findPreference(imiId); 308 if (pref instanceof TwoStatePreference) { 309 final TwoStatePreference subtypePref = (TwoStatePreference) pref; 310 final boolean isEnabled = enabledSubtypes.containsKey(imiId); 311 subtypePref.setChecked(isEnabled); 312 if (inputMethodPrefsMap != null) { 313 for (final Preference childPref: inputMethodPrefsMap.get(imiId)) { 314 childPref.setEnabled(isEnabled); 315 } 316 } 317 setSubtypesPreferenceEnabled(context, inputMethodInfos, imiId, isEnabled); 318 } 319 } 320 updateSubtypesPreferenceChecked(context, inputMethodInfos, enabledSubtypes); 321 } 322 323 static void setSubtypesPreferenceEnabled(final SettingsPreferenceFragment context, 324 final List<InputMethodInfo> inputMethodProperties, final String id, 325 final boolean enabled) { 326 final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); 327 for (final InputMethodInfo imi : inputMethodProperties) { 328 if (id.equals(imi.getId())) { 329 final int subtypeCount = imi.getSubtypeCount(); 330 for (int i = 0; i < subtypeCount; ++i) { 331 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 332 final TwoStatePreference pref = (TwoStatePreference) preferenceScreen 333 .findPreference(id + subtype.hashCode()); 334 if (pref != null) { 335 pref.setEnabled(enabled); 336 } 337 } 338 } 339 } 340 } 341 342 private static void updateSubtypesPreferenceChecked(final SettingsPreferenceFragment context, 343 final List<InputMethodInfo> inputMethodProperties, 344 final HashMap<String, HashSet<String>> enabledSubtypes) { 345 final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); 346 for (final InputMethodInfo imi : inputMethodProperties) { 347 final String id = imi.getId(); 348 if (!enabledSubtypes.containsKey(id)) { 349 // There is no need to enable/disable subtypes of disabled IMEs. 350 continue; 351 } 352 final HashSet<String> enabledSubtypesSet = enabledSubtypes.get(id); 353 final int subtypeCount = imi.getSubtypeCount(); 354 for (int i = 0; i < subtypeCount; ++i) { 355 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 356 final String hashCode = String.valueOf(subtype.hashCode()); 357 if (DEBUG) { 358 Log.d(TAG, "--- Set checked state: " + "id" + ", " + hashCode + ", " 359 + enabledSubtypesSet.contains(hashCode)); 360 } 361 final TwoStatePreference pref = (TwoStatePreference) preferenceScreen 362 .findPreference(id + hashCode); 363 if (pref != null) { 364 pref.setChecked(enabledSubtypesSet.contains(hashCode)); 365 } 366 } 367 } 368 } 369 370 static void removeUnnecessaryNonPersistentPreference(final Preference pref) { 371 final String key = pref.getKey(); 372 if (pref.isPersistent() || key == null) { 373 return; 374 } 375 final SharedPreferences prefs = pref.getSharedPreferences(); 376 if (prefs != null && prefs.contains(key)) { 377 prefs.edit().remove(key).apply(); 378 } 379 } 380 381 @NonNull 382 static String getSubtypeLocaleNameAsSentence(@Nullable InputMethodSubtype subtype, 383 @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo) { 384 if (subtype == null) { 385 return ""; 386 } 387 final Locale locale = getDisplayLocale(context); 388 final CharSequence subtypeName = subtype.getDisplayName(context, 389 inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo() 390 .applicationInfo); 391 return LocaleHelper.toSentenceCase(subtypeName.toString(), locale); 392 } 393 394 @NonNull 395 static String getSubtypeLocaleNameListAsSentence( 396 @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context, 397 @NonNull final InputMethodInfo inputMethodInfo) { 398 if (subtypes.isEmpty()) { 399 return ""; 400 } 401 final Locale locale = getDisplayLocale(context); 402 final int subtypeCount = subtypes.size(); 403 final CharSequence[] subtypeNames = new CharSequence[subtypeCount]; 404 for (int i = 0; i < subtypeCount; i++) { 405 subtypeNames[i] = subtypes.get(i).getDisplayName(context, 406 inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo() 407 .applicationInfo); 408 } 409 return LocaleHelper.toSentenceCase( 410 ListFormatter.getInstance(locale).format(subtypeNames), locale); 411 } 412 413 @NonNull 414 private static Locale getDisplayLocale(@Nullable final Context context) { 415 if (context == null) { 416 return Locale.getDefault(); 417 } 418 if (context.getResources() == null) { 419 return Locale.getDefault(); 420 } 421 final Configuration configuration = context.getResources().getConfiguration(); 422 if (configuration == null) { 423 return Locale.getDefault(); 424 } 425 final Locale configurationLocale = configuration.getLocales().get(0); 426 if (configurationLocale == null) { 427 return Locale.getDefault(); 428 } 429 return configurationLocale; 430 } 431 } 432