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