1 /* 2 * Copyright (C) 2013 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.internal.inputmethod; 18 19 import android.content.ContentResolver; 20 import android.content.Context; 21 import android.content.pm.ApplicationInfo; 22 import android.content.pm.PackageManager; 23 import android.content.pm.PackageManager.NameNotFoundException; 24 import android.content.res.Resources; 25 import android.provider.Settings; 26 import android.provider.Settings.SettingNotFoundException; 27 import android.text.TextUtils; 28 import android.util.Pair; 29 import android.util.Slog; 30 import android.view.inputmethod.InputMethodInfo; 31 import android.view.inputmethod.InputMethodSubtype; 32 import android.view.textservice.SpellCheckerInfo; 33 import android.view.textservice.TextServicesManager; 34 35 import java.util.ArrayList; 36 import java.util.HashMap; 37 import java.util.List; 38 import java.util.Locale; 39 40 /** 41 * InputMethodManagerUtils contains some static methods that provides IME informations. 42 * This methods are supposed to be used in both the framework and the Settings application. 43 */ 44 public class InputMethodUtils { 45 public static final boolean DEBUG = false; 46 public static final int NOT_A_SUBTYPE_ID = -1; 47 public static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; 48 public static final String SUBTYPE_MODE_VOICE = "voice"; 49 private static final String TAG = "InputMethodUtils"; 50 private static final Locale ENGLISH_LOCALE = new Locale("en"); 51 private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID); 52 private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = 53 "EnabledWhenDefaultIsNotAsciiCapable"; 54 private static final String TAG_ASCII_CAPABLE = "AsciiCapable"; 55 56 private InputMethodUtils() { 57 // This utility class is not publicly instantiable. 58 } 59 60 // ---------------------------------------------------------------------- 61 // Utilities for debug 62 public static String getStackTrace() { 63 final StringBuilder sb = new StringBuilder(); 64 try { 65 throw new RuntimeException(); 66 } catch (RuntimeException e) { 67 final StackTraceElement[] frames = e.getStackTrace(); 68 // Start at 1 because the first frame is here and we don't care about it 69 for (int j = 1; j < frames.length; ++j) { 70 sb.append(frames[j].toString() + "\n"); 71 } 72 } 73 return sb.toString(); 74 } 75 76 public static String getApiCallStack() { 77 String apiCallStack = ""; 78 try { 79 throw new RuntimeException(); 80 } catch (RuntimeException e) { 81 final StackTraceElement[] frames = e.getStackTrace(); 82 for (int j = 1; j < frames.length; ++j) { 83 final String tempCallStack = frames[j].toString(); 84 if (TextUtils.isEmpty(apiCallStack)) { 85 // Overwrite apiCallStack if it's empty 86 apiCallStack = tempCallStack; 87 } else if (tempCallStack.indexOf("Transact(") < 0) { 88 // Overwrite apiCallStack if it's not a binder call 89 apiCallStack = tempCallStack; 90 } else { 91 break; 92 } 93 } 94 } 95 return apiCallStack; 96 } 97 // ---------------------------------------------------------------------- 98 99 public static boolean isSystemIme(InputMethodInfo inputMethod) { 100 return (inputMethod.getServiceInfo().applicationInfo.flags 101 & ApplicationInfo.FLAG_SYSTEM) != 0; 102 } 103 104 public static boolean isSystemImeThatHasEnglishKeyboardSubtype(InputMethodInfo imi) { 105 if (!isSystemIme(imi)) { 106 return false; 107 } 108 return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage(), SUBTYPE_MODE_KEYBOARD); 109 } 110 111 private static boolean isSystemAuxilialyImeThatHashAutomaticSubtype(InputMethodInfo imi) { 112 if (!isSystemIme(imi)) { 113 return false; 114 } 115 if (!imi.isAuxiliaryIme()) { 116 return false; 117 } 118 final int subtypeCount = imi.getSubtypeCount(); 119 for (int i = 0; i < subtypeCount; ++i) { 120 final InputMethodSubtype s = imi.getSubtypeAt(i); 121 if (s.overridesImplicitlyEnabledSubtype()) { 122 return true; 123 } 124 } 125 return false; 126 } 127 128 public static ArrayList<InputMethodInfo> getDefaultEnabledImes( 129 Context context, boolean isSystemReady, ArrayList<InputMethodInfo> imis) { 130 final ArrayList<InputMethodInfo> retval = new ArrayList<InputMethodInfo>(); 131 boolean auxilialyImeAdded = false; 132 for (int i = 0; i < imis.size(); ++i) { 133 final InputMethodInfo imi = imis.get(i); 134 if (isDefaultEnabledIme(isSystemReady, imi, context)) { 135 retval.add(imi); 136 if (imi.isAuxiliaryIme()) { 137 auxilialyImeAdded = true; 138 } 139 } 140 } 141 if (auxilialyImeAdded) { 142 return retval; 143 } 144 for (int i = 0; i < imis.size(); ++i) { 145 final InputMethodInfo imi = imis.get(i); 146 if (isSystemAuxilialyImeThatHashAutomaticSubtype(imi)) { 147 retval.add(imi); 148 } 149 } 150 return retval; 151 } 152 153 // TODO: Rename isSystemDefaultImeThatHasCurrentLanguageSubtype 154 public static boolean isValidSystemDefaultIme( 155 boolean isSystemReady, InputMethodInfo imi, Context context) { 156 if (!isSystemReady) { 157 return false; 158 } 159 if (!isSystemIme(imi)) { 160 return false; 161 } 162 if (imi.getIsDefaultResourceId() != 0) { 163 try { 164 if (imi.isDefault(context) && containsSubtypeOf( 165 imi, context.getResources().getConfiguration().locale.getLanguage(), 166 null /* mode */)) { 167 return true; 168 } 169 } catch (Resources.NotFoundException ex) { 170 } 171 } 172 if (imi.getSubtypeCount() == 0) { 173 Slog.w(TAG, "Found no subtypes in a system IME: " + imi.getPackageName()); 174 } 175 return false; 176 } 177 178 public static boolean isDefaultEnabledIme( 179 boolean isSystemReady, InputMethodInfo imi, Context context) { 180 return isValidSystemDefaultIme(isSystemReady, imi, context) 181 || isSystemImeThatHasEnglishKeyboardSubtype(imi); 182 } 183 184 public static boolean containsSubtypeOf(InputMethodInfo imi, String language, String mode) { 185 final int N = imi.getSubtypeCount(); 186 for (int i = 0; i < N; ++i) { 187 if (!imi.getSubtypeAt(i).getLocale().startsWith(language)) { 188 continue; 189 } 190 if(!TextUtils.isEmpty(mode) && !imi.getSubtypeAt(i).getMode().equalsIgnoreCase(mode)) { 191 continue; 192 } 193 return true; 194 } 195 return false; 196 } 197 198 public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) { 199 ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 200 final int subtypeCount = imi.getSubtypeCount(); 201 for (int i = 0; i < subtypeCount; ++i) { 202 subtypes.add(imi.getSubtypeAt(i)); 203 } 204 return subtypes; 205 } 206 207 public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes( 208 InputMethodInfo imi, String mode) { 209 ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 210 final int subtypeCount = imi.getSubtypeCount(); 211 for (int i = 0; i < subtypeCount; ++i) { 212 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 213 if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) { 214 subtypes.add(subtype); 215 } 216 } 217 return subtypes; 218 } 219 220 public static InputMethodInfo getMostApplicableDefaultIME( 221 List<InputMethodInfo> enabledImes) { 222 if (enabledImes != null && enabledImes.size() > 0) { 223 // We'd prefer to fall back on a system IME, since that is safer. 224 int i = enabledImes.size(); 225 int firstFoundSystemIme = -1; 226 while (i > 0) { 227 i--; 228 final InputMethodInfo imi = enabledImes.get(i); 229 if (InputMethodUtils.isSystemImeThatHasEnglishKeyboardSubtype(imi) 230 && !imi.isAuxiliaryIme()) { 231 return imi; 232 } 233 if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi) 234 && !imi.isAuxiliaryIme()) { 235 firstFoundSystemIme = i; 236 } 237 } 238 return enabledImes.get(Math.max(firstFoundSystemIme, 0)); 239 } 240 return null; 241 } 242 243 public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) { 244 return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID; 245 } 246 247 public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) { 248 if (imi != null) { 249 final int subtypeCount = imi.getSubtypeCount(); 250 for (int i = 0; i < subtypeCount; ++i) { 251 InputMethodSubtype ims = imi.getSubtypeAt(i); 252 if (subtypeHashCode == ims.hashCode()) { 253 return i; 254 } 255 } 256 } 257 return NOT_A_SUBTYPE_ID; 258 } 259 260 private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( 261 Resources res, InputMethodInfo imi) { 262 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi); 263 final String systemLocale = res.getConfiguration().locale.toString(); 264 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<InputMethodSubtype>(); 265 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = 266 new HashMap<String, InputMethodSubtype>(); 267 final int N = subtypes.size(); 268 for (int i = 0; i < N; ++i) { 269 // scan overriding implicitly enabled subtypes. 270 InputMethodSubtype subtype = subtypes.get(i); 271 if (subtype.overridesImplicitlyEnabledSubtype()) { 272 final String mode = subtype.getMode(); 273 if (!applicableModeAndSubtypesMap.containsKey(mode)) { 274 applicableModeAndSubtypesMap.put(mode, subtype); 275 } 276 } 277 } 278 if (applicableModeAndSubtypesMap.size() > 0) { 279 return new ArrayList<InputMethodSubtype>(applicableModeAndSubtypesMap.values()); 280 } 281 for (int i = 0; i < N; ++i) { 282 final InputMethodSubtype subtype = subtypes.get(i); 283 final String locale = subtype.getLocale(); 284 final String mode = subtype.getMode(); 285 // When system locale starts with subtype's locale, that subtype will be applicable 286 // for system locale 287 // For instance, it's clearly applicable for cases like system locale = en_US and 288 // subtype = en, but it is not necessarily considered applicable for cases like system 289 // locale = en and subtype = en_US. 290 // We just call systemLocale.startsWith(locale) in this function because there is no 291 // need to find applicable subtypes aggressively unlike 292 // findLastResortApplicableSubtypeLocked. 293 if (systemLocale.startsWith(locale)) { 294 final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode); 295 // If more applicable subtypes are contained, skip. 296 if (applicableSubtype != null) { 297 if (systemLocale.equals(applicableSubtype.getLocale())) continue; 298 if (!systemLocale.equals(locale)) continue; 299 } 300 applicableModeAndSubtypesMap.put(mode, subtype); 301 } 302 } 303 final InputMethodSubtype keyboardSubtype 304 = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD); 305 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<InputMethodSubtype>( 306 applicableModeAndSubtypesMap.values()); 307 if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) { 308 for (int i = 0; i < N; ++i) { 309 final InputMethodSubtype subtype = subtypes.get(i); 310 final String mode = subtype.getMode(); 311 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey( 312 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) { 313 applicableSubtypes.add(subtype); 314 } 315 } 316 } 317 if (keyboardSubtype == null) { 318 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( 319 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); 320 if (lastResortKeyboardSubtype != null) { 321 applicableSubtypes.add(lastResortKeyboardSubtype); 322 } 323 } 324 return applicableSubtypes; 325 } 326 327 private static List<InputMethodSubtype> getEnabledInputMethodSubtypeList( 328 Context context, InputMethodInfo imi, List<InputMethodSubtype> enabledSubtypes, 329 boolean allowsImplicitlySelectedSubtypes) { 330 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) { 331 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( 332 context.getResources(), imi); 333 } 334 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes); 335 } 336 337 /** 338 * If there are no selected subtypes, tries finding the most applicable one according to the 339 * given locale. 340 * @param subtypes this function will search the most applicable subtype in subtypes 341 * @param mode subtypes will be filtered by mode 342 * @param locale subtypes will be filtered by locale 343 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype, 344 * it will return the first subtype matched with mode 345 * @return the most applicable subtypeId 346 */ 347 public static InputMethodSubtype findLastResortApplicableSubtypeLocked( 348 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, 349 boolean canIgnoreLocaleAsLastResort) { 350 if (subtypes == null || subtypes.size() == 0) { 351 return null; 352 } 353 if (TextUtils.isEmpty(locale)) { 354 locale = res.getConfiguration().locale.toString(); 355 } 356 final String language = locale.substring(0, 2); 357 boolean partialMatchFound = false; 358 InputMethodSubtype applicableSubtype = null; 359 InputMethodSubtype firstMatchedModeSubtype = null; 360 final int N = subtypes.size(); 361 for (int i = 0; i < N; ++i) { 362 InputMethodSubtype subtype = subtypes.get(i); 363 final String subtypeLocale = subtype.getLocale(); 364 // An applicable subtype should match "mode". If mode is null, mode will be ignored, 365 // and all subtypes with all modes can be candidates. 366 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) { 367 if (firstMatchedModeSubtype == null) { 368 firstMatchedModeSubtype = subtype; 369 } 370 if (locale.equals(subtypeLocale)) { 371 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US") 372 applicableSubtype = subtype; 373 break; 374 } else if (!partialMatchFound && subtypeLocale.startsWith(language)) { 375 // Partial match (e.g. system locale is "en_US" and subtype locale is "en") 376 applicableSubtype = subtype; 377 partialMatchFound = true; 378 } 379 } 380 } 381 382 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) { 383 return firstMatchedModeSubtype; 384 } 385 386 // The first subtype applicable to the system locale will be defined as the most applicable 387 // subtype. 388 if (DEBUG) { 389 if (applicableSubtype != null) { 390 Slog.d(TAG, "Applicable InputMethodSubtype was found: " 391 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale()); 392 } 393 } 394 return applicableSubtype; 395 } 396 397 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) { 398 if (subtype == null) return true; 399 return !subtype.isAuxiliary(); 400 } 401 402 public static void setNonSelectedSystemImesDisabledUntilUsed( 403 PackageManager packageManager, List<InputMethodInfo> enabledImis) { 404 if (DEBUG) { 405 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed"); 406 } 407 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray( 408 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes); 409 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) { 410 return; 411 } 412 // Only the current spell checker should be treated as an enabled one. 413 final SpellCheckerInfo currentSpellChecker = 414 TextServicesManager.getInstance().getCurrentSpellChecker(); 415 for (final String packageName : systemImesDisabledUntilUsed) { 416 if (DEBUG) { 417 Slog.d(TAG, "check " + packageName); 418 } 419 boolean enabledIme = false; 420 for (int j = 0; j < enabledImis.size(); ++j) { 421 final InputMethodInfo imi = enabledImis.get(j); 422 if (packageName.equals(imi.getPackageName())) { 423 enabledIme = true; 424 break; 425 } 426 } 427 if (enabledIme) { 428 // enabled ime. skip 429 continue; 430 } 431 if (currentSpellChecker != null 432 && packageName.equals(currentSpellChecker.getPackageName())) { 433 // enabled spell checker. skip 434 if (DEBUG) { 435 Slog.d(TAG, packageName + " is the current spell checker. skip"); 436 } 437 continue; 438 } 439 ApplicationInfo ai = null; 440 try { 441 ai = packageManager.getApplicationInfo(packageName, 442 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS); 443 } catch (NameNotFoundException e) { 444 Slog.w(TAG, "NameNotFoundException: " + packageName, e); 445 } 446 if (ai == null) { 447 // No app found for packageName 448 continue; 449 } 450 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 451 if (!isSystemPackage) { 452 continue; 453 } 454 setDisabledUntilUsed(packageManager, packageName); 455 } 456 } 457 458 private static void setDisabledUntilUsed(PackageManager packageManager, String packageName) { 459 final int state = packageManager.getApplicationEnabledSetting(packageName); 460 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT 461 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { 462 if (DEBUG) { 463 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED"); 464 } 465 packageManager.setApplicationEnabledSetting(packageName, 466 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, 0); 467 } else { 468 if (DEBUG) { 469 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED"); 470 } 471 } 472 } 473 474 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi, 475 InputMethodSubtype subtype) { 476 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager()); 477 return subtype != null 478 ? TextUtils.concat(subtype.getDisplayName(context, 479 imi.getPackageName(), imi.getServiceInfo().applicationInfo), 480 (TextUtils.isEmpty(imiLabel) ? 481 "" : " - " + imiLabel)) 482 : imiLabel; 483 } 484 485 /** 486 * Utility class for putting and getting settings for InputMethod 487 * TODO: Move all putters and getters of settings to this class. 488 */ 489 public static class InputMethodSettings { 490 // The string for enabled input method is saved as follows: 491 // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0") 492 private static final char INPUT_METHOD_SEPARATER = ':'; 493 private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';'; 494 private final TextUtils.SimpleStringSplitter mInputMethodSplitter = 495 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER); 496 497 private final TextUtils.SimpleStringSplitter mSubtypeSplitter = 498 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER); 499 500 private final Resources mRes; 501 private final ContentResolver mResolver; 502 private final HashMap<String, InputMethodInfo> mMethodMap; 503 private final ArrayList<InputMethodInfo> mMethodList; 504 505 private String mEnabledInputMethodsStrCache; 506 private int mCurrentUserId; 507 508 private static void buildEnabledInputMethodsSettingString( 509 StringBuilder builder, Pair<String, ArrayList<String>> pair) { 510 String id = pair.first; 511 ArrayList<String> subtypes = pair.second; 512 builder.append(id); 513 // Inputmethod and subtypes are saved in the settings as follows: 514 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 515 for (String subtypeId: subtypes) { 516 builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId); 517 } 518 } 519 520 public InputMethodSettings( 521 Resources res, ContentResolver resolver, 522 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, 523 int userId) { 524 setCurrentUserId(userId); 525 mRes = res; 526 mResolver = resolver; 527 mMethodMap = methodMap; 528 mMethodList = methodList; 529 } 530 531 public void setCurrentUserId(int userId) { 532 if (DEBUG) { 533 Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId); 534 } 535 // IMMS settings are kept per user, so keep track of current user 536 mCurrentUserId = userId; 537 } 538 539 public List<InputMethodInfo> getEnabledInputMethodListLocked() { 540 return createEnabledInputMethodListLocked( 541 getEnabledInputMethodsAndSubtypeListLocked()); 542 } 543 544 public List<Pair<InputMethodInfo, ArrayList<String>>> 545 getEnabledInputMethodAndSubtypeHashCodeListLocked() { 546 return createEnabledInputMethodAndSubtypeHashCodeListLocked( 547 getEnabledInputMethodsAndSubtypeListLocked()); 548 } 549 550 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( 551 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) { 552 List<InputMethodSubtype> enabledSubtypes = 553 getEnabledInputMethodSubtypeListLocked(imi); 554 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) { 555 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( 556 context.getResources(), imi); 557 } 558 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes); 559 } 560 561 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( 562 InputMethodInfo imi) { 563 List<Pair<String, ArrayList<String>>> imsList = 564 getEnabledInputMethodsAndSubtypeListLocked(); 565 ArrayList<InputMethodSubtype> enabledSubtypes = 566 new ArrayList<InputMethodSubtype>(); 567 if (imi != null) { 568 for (Pair<String, ArrayList<String>> imsPair : imsList) { 569 InputMethodInfo info = mMethodMap.get(imsPair.first); 570 if (info != null && info.getId().equals(imi.getId())) { 571 final int subtypeCount = info.getSubtypeCount(); 572 for (int i = 0; i < subtypeCount; ++i) { 573 InputMethodSubtype ims = info.getSubtypeAt(i); 574 for (String s: imsPair.second) { 575 if (String.valueOf(ims.hashCode()).equals(s)) { 576 enabledSubtypes.add(ims); 577 } 578 } 579 } 580 break; 581 } 582 } 583 } 584 return enabledSubtypes; 585 } 586 587 // At the initial boot, the settings for input methods are not set, 588 // so we need to enable IME in that case. 589 public void enableAllIMEsIfThereIsNoEnabledIME() { 590 if (TextUtils.isEmpty(getEnabledInputMethodsStr())) { 591 StringBuilder sb = new StringBuilder(); 592 final int N = mMethodList.size(); 593 for (int i = 0; i < N; i++) { 594 InputMethodInfo imi = mMethodList.get(i); 595 Slog.i(TAG, "Adding: " + imi.getId()); 596 if (i > 0) sb.append(':'); 597 sb.append(imi.getId()); 598 } 599 putEnabledInputMethodsStr(sb.toString()); 600 } 601 } 602 603 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { 604 ArrayList<Pair<String, ArrayList<String>>> imsList 605 = new ArrayList<Pair<String, ArrayList<String>>>(); 606 final String enabledInputMethodsStr = getEnabledInputMethodsStr(); 607 if (TextUtils.isEmpty(enabledInputMethodsStr)) { 608 return imsList; 609 } 610 mInputMethodSplitter.setString(enabledInputMethodsStr); 611 while (mInputMethodSplitter.hasNext()) { 612 String nextImsStr = mInputMethodSplitter.next(); 613 mSubtypeSplitter.setString(nextImsStr); 614 if (mSubtypeSplitter.hasNext()) { 615 ArrayList<String> subtypeHashes = new ArrayList<String>(); 616 // The first element is ime id. 617 String imeId = mSubtypeSplitter.next(); 618 while (mSubtypeSplitter.hasNext()) { 619 subtypeHashes.add(mSubtypeSplitter.next()); 620 } 621 imsList.add(new Pair<String, ArrayList<String>>(imeId, subtypeHashes)); 622 } 623 } 624 return imsList; 625 } 626 627 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) { 628 if (reloadInputMethodStr) { 629 getEnabledInputMethodsStr(); 630 } 631 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) { 632 // Add in the newly enabled input method. 633 putEnabledInputMethodsStr(id); 634 } else { 635 putEnabledInputMethodsStr( 636 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATER + id); 637 } 638 } 639 640 /** 641 * Build and put a string of EnabledInputMethods with removing specified Id. 642 * @return the specified id was removed or not. 643 */ 644 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked( 645 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) { 646 boolean isRemoved = false; 647 boolean needsAppendSeparator = false; 648 for (Pair<String, ArrayList<String>> ims: imsList) { 649 String curId = ims.first; 650 if (curId.equals(id)) { 651 // We are disabling this input method, and it is 652 // currently enabled. Skip it to remove from the 653 // new list. 654 isRemoved = true; 655 } else { 656 if (needsAppendSeparator) { 657 builder.append(INPUT_METHOD_SEPARATER); 658 } else { 659 needsAppendSeparator = true; 660 } 661 buildEnabledInputMethodsSettingString(builder, ims); 662 } 663 } 664 if (isRemoved) { 665 // Update the setting with the new list of input methods. 666 putEnabledInputMethodsStr(builder.toString()); 667 } 668 return isRemoved; 669 } 670 671 private List<InputMethodInfo> createEnabledInputMethodListLocked( 672 List<Pair<String, ArrayList<String>>> imsList) { 673 final ArrayList<InputMethodInfo> res = new ArrayList<InputMethodInfo>(); 674 for (Pair<String, ArrayList<String>> ims: imsList) { 675 InputMethodInfo info = mMethodMap.get(ims.first); 676 if (info != null) { 677 res.add(info); 678 } 679 } 680 return res; 681 } 682 683 private List<Pair<InputMethodInfo, ArrayList<String>>> 684 createEnabledInputMethodAndSubtypeHashCodeListLocked( 685 List<Pair<String, ArrayList<String>>> imsList) { 686 final ArrayList<Pair<InputMethodInfo, ArrayList<String>>> res 687 = new ArrayList<Pair<InputMethodInfo, ArrayList<String>>>(); 688 for (Pair<String, ArrayList<String>> ims : imsList) { 689 InputMethodInfo info = mMethodMap.get(ims.first); 690 if (info != null) { 691 res.add(new Pair<InputMethodInfo, ArrayList<String>>(info, ims.second)); 692 } 693 } 694 return res; 695 } 696 697 private void putEnabledInputMethodsStr(String str) { 698 Settings.Secure.putStringForUser( 699 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId); 700 mEnabledInputMethodsStrCache = str; 701 if (DEBUG) { 702 Slog.d(TAG, "putEnabledInputMethodStr: " + str); 703 } 704 } 705 706 public String getEnabledInputMethodsStr() { 707 mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser( 708 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId); 709 if (DEBUG) { 710 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache 711 + ", " + mCurrentUserId); 712 } 713 return mEnabledInputMethodsStrCache; 714 } 715 716 private void saveSubtypeHistory( 717 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) { 718 StringBuilder builder = new StringBuilder(); 719 boolean isImeAdded = false; 720 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) { 721 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append( 722 newSubtypeId); 723 isImeAdded = true; 724 } 725 for (Pair<String, String> ime: savedImes) { 726 String imeId = ime.first; 727 String subtypeId = ime.second; 728 if (TextUtils.isEmpty(subtypeId)) { 729 subtypeId = NOT_A_SUBTYPE_ID_STR; 730 } 731 if (isImeAdded) { 732 builder.append(INPUT_METHOD_SEPARATER); 733 } else { 734 isImeAdded = true; 735 } 736 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append( 737 subtypeId); 738 } 739 // Remove the last INPUT_METHOD_SEPARATER 740 putSubtypeHistoryStr(builder.toString()); 741 } 742 743 private void addSubtypeToHistory(String imeId, String subtypeId) { 744 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); 745 for (Pair<String, String> ime: subtypeHistory) { 746 if (ime.first.equals(imeId)) { 747 if (DEBUG) { 748 Slog.v(TAG, "Subtype found in the history: " + imeId + ", " 749 + ime.second); 750 } 751 // We should break here 752 subtypeHistory.remove(ime); 753 break; 754 } 755 } 756 if (DEBUG) { 757 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId); 758 } 759 saveSubtypeHistory(subtypeHistory, imeId, subtypeId); 760 } 761 762 private void putSubtypeHistoryStr(String str) { 763 if (DEBUG) { 764 Slog.d(TAG, "putSubtypeHistoryStr: " + str); 765 } 766 Settings.Secure.putStringForUser( 767 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId); 768 } 769 770 public Pair<String, String> getLastInputMethodAndSubtypeLocked() { 771 // Gets the first one from the history 772 return getLastSubtypeForInputMethodLockedInternal(null); 773 } 774 775 public String getLastSubtypeForInputMethodLocked(String imeId) { 776 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId); 777 if (ime != null) { 778 return ime.second; 779 } else { 780 return null; 781 } 782 } 783 784 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) { 785 List<Pair<String, ArrayList<String>>> enabledImes = 786 getEnabledInputMethodsAndSubtypeListLocked(); 787 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); 788 for (Pair<String, String> imeAndSubtype : subtypeHistory) { 789 final String imeInTheHistory = imeAndSubtype.first; 790 // If imeId is empty, returns the first IME and subtype in the history 791 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) { 792 final String subtypeInTheHistory = imeAndSubtype.second; 793 final String subtypeHashCode = 794 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked( 795 enabledImes, imeInTheHistory, subtypeInTheHistory); 796 if (!TextUtils.isEmpty(subtypeHashCode)) { 797 if (DEBUG) { 798 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode); 799 } 800 return new Pair<String, String>(imeInTheHistory, subtypeHashCode); 801 } 802 } 803 } 804 if (DEBUG) { 805 Slog.d(TAG, "No enabled IME found in the history"); 806 } 807 return null; 808 } 809 810 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, 811 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { 812 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) { 813 if (enabledIme.first.equals(imeId)) { 814 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second; 815 final InputMethodInfo imi = mMethodMap.get(imeId); 816 if (explicitlyEnabledSubtypes.size() == 0) { 817 // If there are no explicitly enabled subtypes, applicable subtypes are 818 // enabled implicitly. 819 // If IME is enabled and no subtypes are enabled, applicable subtypes 820 // are enabled implicitly, so needs to treat them to be enabled. 821 if (imi != null && imi.getSubtypeCount() > 0) { 822 List<InputMethodSubtype> implicitlySelectedSubtypes = 823 getImplicitlyApplicableSubtypesLocked(mRes, imi); 824 if (implicitlySelectedSubtypes != null) { 825 final int N = implicitlySelectedSubtypes.size(); 826 for (int i = 0; i < N; ++i) { 827 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i); 828 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) { 829 return subtypeHashCode; 830 } 831 } 832 } 833 } 834 } else { 835 for (String s: explicitlyEnabledSubtypes) { 836 if (s.equals(subtypeHashCode)) { 837 // If both imeId and subtypeId are enabled, return subtypeId. 838 try { 839 final int hashCode = Integer.valueOf(subtypeHashCode); 840 // Check whether the subtype id is valid or not 841 if (isValidSubtypeId(imi, hashCode)) { 842 return s; 843 } else { 844 return NOT_A_SUBTYPE_ID_STR; 845 } 846 } catch (NumberFormatException e) { 847 return NOT_A_SUBTYPE_ID_STR; 848 } 849 } 850 } 851 } 852 // If imeId was enabled but subtypeId was disabled. 853 return NOT_A_SUBTYPE_ID_STR; 854 } 855 } 856 // If both imeId and subtypeId are disabled, return null 857 return null; 858 } 859 860 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() { 861 ArrayList<Pair<String, String>> imsList = new ArrayList<Pair<String, String>>(); 862 final String subtypeHistoryStr = getSubtypeHistoryStr(); 863 if (TextUtils.isEmpty(subtypeHistoryStr)) { 864 return imsList; 865 } 866 mInputMethodSplitter.setString(subtypeHistoryStr); 867 while (mInputMethodSplitter.hasNext()) { 868 String nextImsStr = mInputMethodSplitter.next(); 869 mSubtypeSplitter.setString(nextImsStr); 870 if (mSubtypeSplitter.hasNext()) { 871 String subtypeId = NOT_A_SUBTYPE_ID_STR; 872 // The first element is ime id. 873 String imeId = mSubtypeSplitter.next(); 874 while (mSubtypeSplitter.hasNext()) { 875 subtypeId = mSubtypeSplitter.next(); 876 break; 877 } 878 imsList.add(new Pair<String, String>(imeId, subtypeId)); 879 } 880 } 881 return imsList; 882 } 883 884 private String getSubtypeHistoryStr() { 885 if (DEBUG) { 886 Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser( 887 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId)); 888 } 889 return Settings.Secure.getStringForUser( 890 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId); 891 } 892 893 public void putSelectedInputMethod(String imeId) { 894 if (DEBUG) { 895 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " 896 + mCurrentUserId); 897 } 898 Settings.Secure.putStringForUser( 899 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId); 900 } 901 902 public void putSelectedSubtype(int subtypeId) { 903 if (DEBUG) { 904 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " 905 + mCurrentUserId); 906 } 907 Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, 908 subtypeId, mCurrentUserId); 909 } 910 911 public String getDisabledSystemInputMethods() { 912 return Settings.Secure.getStringForUser( 913 mResolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, mCurrentUserId); 914 } 915 916 public String getSelectedInputMethod() { 917 if (DEBUG) { 918 Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser( 919 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId) 920 + ", " + mCurrentUserId); 921 } 922 return Settings.Secure.getStringForUser( 923 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId); 924 } 925 926 public boolean isSubtypeSelected() { 927 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID; 928 } 929 930 private int getSelectedInputMethodSubtypeHashCode() { 931 try { 932 return Settings.Secure.getIntForUser( 933 mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId); 934 } catch (SettingNotFoundException e) { 935 return NOT_A_SUBTYPE_ID; 936 } 937 } 938 939 public int getCurrentUserId() { 940 return mCurrentUserId; 941 } 942 943 public int getSelectedInputMethodSubtypeId(String selectedImiId) { 944 final InputMethodInfo imi = mMethodMap.get(selectedImiId); 945 if (imi == null) { 946 return NOT_A_SUBTYPE_ID; 947 } 948 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); 949 return getSubtypeIdFromHashCode(imi, subtypeHashCode); 950 } 951 952 public void saveCurrentInputMethodAndSubtypeToHistory( 953 String curMethodId, InputMethodSubtype currentSubtype) { 954 String subtypeId = NOT_A_SUBTYPE_ID_STR; 955 if (currentSubtype != null) { 956 subtypeId = String.valueOf(currentSubtype.hashCode()); 957 } 958 if (canAddToLastInputMethod(currentSubtype)) { 959 addSubtypeToHistory(curMethodId, subtypeId); 960 } 961 } 962 } 963 } 964