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