1/* 2 * Copyright (C) 2017 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 17package com.android.settingslib.inputmethod; 18 19import android.annotation.NonNull; 20import android.annotation.Nullable; 21import android.content.ContentResolver; 22import android.content.Context; 23import android.content.SharedPreferences; 24import android.content.res.Configuration; 25import android.icu.text.ListFormatter; 26import android.provider.Settings; 27import android.provider.Settings.SettingNotFoundException; 28import android.support.v14.preference.PreferenceFragment; 29import android.support.v7.preference.Preference; 30import android.support.v7.preference.PreferenceScreen; 31import android.support.v7.preference.TwoStatePreference; 32import android.text.TextUtils; 33import android.util.Log; 34import android.view.inputmethod.InputMethodInfo; 35import android.view.inputmethod.InputMethodSubtype; 36 37import com.android.internal.app.LocaleHelper; 38import com.android.internal.inputmethod.InputMethodUtils; 39 40import java.util.HashMap; 41import java.util.HashSet; 42import java.util.List; 43import java.util.Locale; 44import java.util.Map; 45 46// TODO: Consolidate this with {@link InputMethodSettingValuesWrapper}. 47public class InputMethodAndSubtypeUtil { 48 49 private static final boolean DEBUG = false; 50 private static final String TAG = "InputMethdAndSubtypeUtl"; 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 private 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 private 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 private static HashSet<String> getDisabledSystemIMEs(ContentResolver resolver) { 143 HashSet<String> set = new HashSet<>(); 144 String disabledIMEsStr = Settings.Secure.getString( 145 resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS); 146 if (TextUtils.isEmpty(disabledIMEsStr)) { 147 return set; 148 } 149 sStringInputMethodSplitter.setString(disabledIMEsStr); 150 while(sStringInputMethodSplitter.hasNext()) { 151 set.add(sStringInputMethodSplitter.next()); 152 } 153 return set; 154 } 155 156 public static void saveInputMethodSubtypeList(PreferenceFragment context, 157 ContentResolver resolver, List<InputMethodInfo> inputMethodInfos, 158 boolean hasHardKeyboard) { 159 String currentInputMethodId = Settings.Secure.getString(resolver, 160 Settings.Secure.DEFAULT_INPUT_METHOD); 161 final int selectedInputMethodSubtype = getInputMethodSubtypeSelected(resolver); 162 final HashMap<String, HashSet<String>> enabledIMEsAndSubtypesMap = 163 getEnabledInputMethodsAndSubtypeList(resolver); 164 final HashSet<String> disabledSystemIMEs = getDisabledSystemIMEs(resolver); 165 166 boolean needsToResetSelectedSubtype = false; 167 for (final InputMethodInfo imi : inputMethodInfos) { 168 final String imiId = imi.getId(); 169 final Preference pref = context.findPreference(imiId); 170 if (pref == null) { 171 continue; 172 } 173 // In the choose input method screen or in the subtype enabler screen, 174 // <code>pref</code> is an instance of TwoStatePreference. 175 final boolean isImeChecked = (pref instanceof TwoStatePreference) ? 176 ((TwoStatePreference) pref).isChecked() 177 : enabledIMEsAndSubtypesMap.containsKey(imiId); 178 final boolean isCurrentInputMethod = imiId.equals(currentInputMethodId); 179 final boolean systemIme = InputMethodUtils.isSystemIme(imi); 180 if ((!hasHardKeyboard && InputMethodSettingValuesWrapper.getInstance( 181 context.getActivity()).isAlwaysCheckedIme(imi, context.getActivity())) 182 || isImeChecked) { 183 if (!enabledIMEsAndSubtypesMap.containsKey(imiId)) { 184 // imiId has just been enabled 185 enabledIMEsAndSubtypesMap.put(imiId, new HashSet<>()); 186 } 187 final HashSet<String> subtypesSet = enabledIMEsAndSubtypesMap.get(imiId); 188 189 boolean subtypePrefFound = false; 190 final int subtypeCount = imi.getSubtypeCount(); 191 for (int i = 0; i < subtypeCount; ++i) { 192 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 193 final String subtypeHashCodeStr = String.valueOf(subtype.hashCode()); 194 final TwoStatePreference subtypePref = (TwoStatePreference) context 195 .findPreference(imiId + subtypeHashCodeStr); 196 // In the Configure input method screen which does not have subtype preferences. 197 if (subtypePref == null) { 198 continue; 199 } 200 if (!subtypePrefFound) { 201 // Once subtype preference is found, subtypeSet needs to be cleared. 202 // Because of system change, hashCode value could have been changed. 203 subtypesSet.clear(); 204 // If selected subtype preference is disabled, needs to reset. 205 needsToResetSelectedSubtype = true; 206 subtypePrefFound = true; 207 } 208 // Checking <code>subtypePref.isEnabled()</code> is insufficient to determine 209 // whether the user manually enabled this subtype or not. Implicitly-enabled 210 // subtypes are also checked just as an indicator to users. We also need to 211 // check <code>subtypePref.isEnabled()</code> so that only manually enabled 212 // subtypes can be saved here. 213 if (subtypePref.isEnabled() && subtypePref.isChecked()) { 214 subtypesSet.add(subtypeHashCodeStr); 215 if (isCurrentInputMethod) { 216 if (selectedInputMethodSubtype == subtype.hashCode()) { 217 // Selected subtype is still enabled, there is no need to reset 218 // selected subtype. 219 needsToResetSelectedSubtype = false; 220 } 221 } 222 } else { 223 subtypesSet.remove(subtypeHashCodeStr); 224 } 225 } 226 } else { 227 enabledIMEsAndSubtypesMap.remove(imiId); 228 if (isCurrentInputMethod) { 229 // We are processing the current input method, but found that it's not enabled. 230 // This means that the current input method has been uninstalled. 231 // If currentInputMethod is already uninstalled, InputMethodManagerService will 232 // find the applicable IME from the history and the system locale. 233 if (DEBUG) { 234 Log.d(TAG, "Current IME was uninstalled or disabled."); 235 } 236 currentInputMethodId = null; 237 } 238 } 239 // If it's a disabled system ime, add it to the disabled list so that it 240 // doesn't get enabled automatically on any changes to the package list 241 if (systemIme && hasHardKeyboard) { 242 if (disabledSystemIMEs.contains(imiId)) { 243 if (isImeChecked) { 244 disabledSystemIMEs.remove(imiId); 245 } 246 } else { 247 if (!isImeChecked) { 248 disabledSystemIMEs.add(imiId); 249 } 250 } 251 } 252 } 253 254 final String enabledIMEsAndSubtypesString = buildInputMethodsAndSubtypesString( 255 enabledIMEsAndSubtypesMap); 256 final String disabledSystemIMEsString = buildInputMethodsString(disabledSystemIMEs); 257 if (DEBUG) { 258 Log.d(TAG, "--- Save enabled inputmethod settings. :" + enabledIMEsAndSubtypesString); 259 Log.d(TAG, "--- Save disabled system inputmethod settings. :" 260 + disabledSystemIMEsString); 261 Log.d(TAG, "--- Save default inputmethod settings. :" + currentInputMethodId); 262 Log.d(TAG, "--- Needs to reset the selected subtype :" + needsToResetSelectedSubtype); 263 Log.d(TAG, "--- Subtype is selected :" + isInputMethodSubtypeSelected(resolver)); 264 } 265 266 // Redefines SelectedSubtype when all subtypes are unchecked or there is no subtype 267 // selected. And if the selected subtype of the current input method was disabled, 268 // We should reset the selected input method's subtype. 269 if (needsToResetSelectedSubtype || !isInputMethodSubtypeSelected(resolver)) { 270 if (DEBUG) { 271 Log.d(TAG, "--- Reset inputmethod subtype because it's not defined."); 272 } 273 putSelectedInputMethodSubtype(resolver, NOT_A_SUBTYPE_ID); 274 } 275 276 Settings.Secure.putString(resolver, 277 Settings.Secure.ENABLED_INPUT_METHODS, enabledIMEsAndSubtypesString); 278 if (disabledSystemIMEsString.length() > 0) { 279 Settings.Secure.putString(resolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, 280 disabledSystemIMEsString); 281 } 282 // If the current input method is unset, InputMethodManagerService will find the applicable 283 // IME from the history and the system locale. 284 Settings.Secure.putString(resolver, Settings.Secure.DEFAULT_INPUT_METHOD, 285 currentInputMethodId != null ? currentInputMethodId : ""); 286 } 287 288 public static void loadInputMethodSubtypeList(final PreferenceFragment context, 289 final ContentResolver resolver, final List<InputMethodInfo> inputMethodInfos, 290 final Map<String, List<Preference>> inputMethodPrefsMap) { 291 final HashMap<String, HashSet<String>> enabledSubtypes = 292 getEnabledInputMethodsAndSubtypeList(resolver); 293 294 for (final InputMethodInfo imi : inputMethodInfos) { 295 final String imiId = imi.getId(); 296 final Preference pref = context.findPreference(imiId); 297 if (pref instanceof TwoStatePreference) { 298 final TwoStatePreference subtypePref = (TwoStatePreference) pref; 299 final boolean isEnabled = enabledSubtypes.containsKey(imiId); 300 subtypePref.setChecked(isEnabled); 301 if (inputMethodPrefsMap != null) { 302 for (final Preference childPref: inputMethodPrefsMap.get(imiId)) { 303 childPref.setEnabled(isEnabled); 304 } 305 } 306 setSubtypesPreferenceEnabled(context, inputMethodInfos, imiId, isEnabled); 307 } 308 } 309 updateSubtypesPreferenceChecked(context, inputMethodInfos, enabledSubtypes); 310 } 311 312 private static void setSubtypesPreferenceEnabled(final PreferenceFragment context, 313 final List<InputMethodInfo> inputMethodProperties, final String id, 314 final boolean enabled) { 315 final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); 316 for (final InputMethodInfo imi : inputMethodProperties) { 317 if (id.equals(imi.getId())) { 318 final int subtypeCount = imi.getSubtypeCount(); 319 for (int i = 0; i < subtypeCount; ++i) { 320 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 321 final TwoStatePreference pref = (TwoStatePreference) preferenceScreen 322 .findPreference(id + subtype.hashCode()); 323 if (pref != null) { 324 pref.setEnabled(enabled); 325 } 326 } 327 } 328 } 329 } 330 331 private static void updateSubtypesPreferenceChecked(final PreferenceFragment context, 332 final List<InputMethodInfo> inputMethodProperties, 333 final HashMap<String, HashSet<String>> enabledSubtypes) { 334 final PreferenceScreen preferenceScreen = context.getPreferenceScreen(); 335 for (final InputMethodInfo imi : inputMethodProperties) { 336 final String id = imi.getId(); 337 if (!enabledSubtypes.containsKey(id)) { 338 // There is no need to enable/disable subtypes of disabled IMEs. 339 continue; 340 } 341 final HashSet<String> enabledSubtypesSet = enabledSubtypes.get(id); 342 final int subtypeCount = imi.getSubtypeCount(); 343 for (int i = 0; i < subtypeCount; ++i) { 344 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 345 final String hashCode = String.valueOf(subtype.hashCode()); 346 if (DEBUG) { 347 Log.d(TAG, "--- Set checked state: " + "id" + ", " + hashCode + ", " 348 + enabledSubtypesSet.contains(hashCode)); 349 } 350 final TwoStatePreference pref = (TwoStatePreference) preferenceScreen 351 .findPreference(id + hashCode); 352 if (pref != null) { 353 pref.setChecked(enabledSubtypesSet.contains(hashCode)); 354 } 355 } 356 } 357 } 358 359 public static void removeUnnecessaryNonPersistentPreference(final Preference pref) { 360 final String key = pref.getKey(); 361 if (pref.isPersistent() || key == null) { 362 return; 363 } 364 final SharedPreferences prefs = pref.getSharedPreferences(); 365 if (prefs != null && prefs.contains(key)) { 366 prefs.edit().remove(key).apply(); 367 } 368 } 369 370 @NonNull 371 public static String getSubtypeLocaleNameAsSentence(@Nullable InputMethodSubtype subtype, 372 @NonNull final Context context, @NonNull final InputMethodInfo inputMethodInfo) { 373 if (subtype == null) { 374 return ""; 375 } 376 final Locale locale = getDisplayLocale(context); 377 final CharSequence subtypeName = subtype.getDisplayName(context, 378 inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo() 379 .applicationInfo); 380 return LocaleHelper.toSentenceCase(subtypeName.toString(), locale); 381 } 382 383 @NonNull 384 public static String getSubtypeLocaleNameListAsSentence( 385 @NonNull final List<InputMethodSubtype> subtypes, @NonNull final Context context, 386 @NonNull final InputMethodInfo inputMethodInfo) { 387 if (subtypes.isEmpty()) { 388 return ""; 389 } 390 final Locale locale = getDisplayLocale(context); 391 final int subtypeCount = subtypes.size(); 392 final CharSequence[] subtypeNames = new CharSequence[subtypeCount]; 393 for (int i = 0; i < subtypeCount; i++) { 394 subtypeNames[i] = subtypes.get(i).getDisplayName(context, 395 inputMethodInfo.getPackageName(), inputMethodInfo.getServiceInfo() 396 .applicationInfo); 397 } 398 return LocaleHelper.toSentenceCase( 399 ListFormatter.getInstance(locale).format((Object[]) subtypeNames), locale); 400 } 401 402 @NonNull 403 private static Locale getDisplayLocale(@Nullable final Context context) { 404 if (context == null) { 405 return Locale.getDefault(); 406 } 407 if (context.getResources() == null) { 408 return Locale.getDefault(); 409 } 410 final Configuration configuration = context.getResources().getConfiguration(); 411 if (configuration == null) { 412 return Locale.getDefault(); 413 } 414 final Locale configurationLocale = configuration.getLocales().get(0); 415 if (configurationLocale == null) { 416 return Locale.getDefault(); 417 } 418 return configurationLocale; 419 } 420} 421