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.annotation.NonNull; 20 import android.annotation.Nullable; 21 import android.app.AppOpsManager; 22 import android.content.ContentResolver; 23 import android.content.Context; 24 import android.content.pm.ApplicationInfo; 25 import android.content.pm.PackageManager; 26 import android.content.pm.PackageManager.NameNotFoundException; 27 import android.content.res.Resources; 28 import android.provider.Settings; 29 import android.provider.Settings.SettingNotFoundException; 30 import android.text.TextUtils; 31 import android.util.Pair; 32 import android.util.Slog; 33 import android.view.inputmethod.InputMethodInfo; 34 import android.view.inputmethod.InputMethodSubtype; 35 import android.view.textservice.SpellCheckerInfo; 36 import android.view.textservice.TextServicesManager; 37 38 import java.util.ArrayList; 39 import java.util.Arrays; 40 import java.util.HashMap; 41 import java.util.LinkedHashSet; 42 import java.util.List; 43 import java.util.Locale; 44 45 /** 46 * InputMethodManagerUtils contains some static methods that provides IME informations. 47 * This methods are supposed to be used in both the framework and the Settings application. 48 */ 49 public class InputMethodUtils { 50 public static final boolean DEBUG = false; 51 public static final int NOT_A_SUBTYPE_ID = -1; 52 public static final String SUBTYPE_MODE_ANY = null; 53 public static final String SUBTYPE_MODE_KEYBOARD = "keyboard"; 54 public static final String SUBTYPE_MODE_VOICE = "voice"; 55 private static final String TAG = "InputMethodUtils"; 56 private static final Locale ENGLISH_LOCALE = new Locale("en"); 57 private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID); 58 private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE = 59 "EnabledWhenDefaultIsNotAsciiCapable"; 60 private static final String TAG_ASCII_CAPABLE = "AsciiCapable"; 61 /** 62 * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs 63 * that are mainly used until the system becomes ready. Note that {@link Locale} in this array 64 * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH} 65 * doesn't automatically match {@code Locale("en", "IN")}. 66 */ 67 private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = { 68 Locale.ENGLISH, // "en" 69 Locale.US, // "en_US" 70 Locale.UK, // "en_GB" 71 }; 72 73 private InputMethodUtils() { 74 // This utility class is not publicly instantiable. 75 } 76 77 // ---------------------------------------------------------------------- 78 // Utilities for debug 79 public static String getStackTrace() { 80 final StringBuilder sb = new StringBuilder(); 81 try { 82 throw new RuntimeException(); 83 } catch (RuntimeException e) { 84 final StackTraceElement[] frames = e.getStackTrace(); 85 // Start at 1 because the first frame is here and we don't care about it 86 for (int j = 1; j < frames.length; ++j) { 87 sb.append(frames[j].toString() + "\n"); 88 } 89 } 90 return sb.toString(); 91 } 92 93 public static String getApiCallStack() { 94 String apiCallStack = ""; 95 try { 96 throw new RuntimeException(); 97 } catch (RuntimeException e) { 98 final StackTraceElement[] frames = e.getStackTrace(); 99 for (int j = 1; j < frames.length; ++j) { 100 final String tempCallStack = frames[j].toString(); 101 if (TextUtils.isEmpty(apiCallStack)) { 102 // Overwrite apiCallStack if it's empty 103 apiCallStack = tempCallStack; 104 } else if (tempCallStack.indexOf("Transact(") < 0) { 105 // Overwrite apiCallStack if it's not a binder call 106 apiCallStack = tempCallStack; 107 } else { 108 break; 109 } 110 } 111 } 112 return apiCallStack; 113 } 114 // ---------------------------------------------------------------------- 115 116 public static boolean isSystemIme(InputMethodInfo inputMethod) { 117 return (inputMethod.getServiceInfo().applicationInfo.flags 118 & ApplicationInfo.FLAG_SYSTEM) != 0; 119 } 120 121 /** 122 * @deprecated Use {@link #isSystemImeThatHasSubtypeOf(InputMethodInfo, Context, boolean, 123 * Locale, boolean, String)} instead. 124 */ 125 @Deprecated 126 public static boolean isSystemImeThatHasEnglishKeyboardSubtype(InputMethodInfo imi) { 127 if (!isSystemIme(imi)) { 128 return false; 129 } 130 return containsSubtypeOf(imi, ENGLISH_LOCALE.getLanguage(), SUBTYPE_MODE_KEYBOARD); 131 } 132 133 private static boolean isSystemImeThatHasSubtypeOf(final InputMethodInfo imi, 134 final Context context, final boolean checkDefaultAttribute, 135 @Nullable final Locale requiredLocale, final boolean checkCountry, 136 final String requiredSubtypeMode) { 137 if (!isSystemIme(imi)) { 138 return false; 139 } 140 if (checkDefaultAttribute && !imi.isDefault(context)) { 141 return false; 142 } 143 if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) { 144 return false; 145 } 146 return true; 147 } 148 149 @Nullable 150 public static Locale getFallbackLocaleForDefaultIme(final ArrayList<InputMethodInfo> imis, 151 final Context context) { 152 // At first, find the fallback locale from the IMEs that are declared as "default" in the 153 // current locale. Note that IME developers can declare an IME as "default" only for 154 // some particular locales but "not default" for other locales. 155 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { 156 for (int i = 0; i < imis.size(); ++i) { 157 if (isSystemImeThatHasSubtypeOf(imis.get(i), context, 158 true /* checkDefaultAttribute */, fallbackLocale, 159 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { 160 return fallbackLocale; 161 } 162 } 163 } 164 // If no fallback locale is found in the above condition, find fallback locales regardless 165 // of the "default" attribute as a last resort. 166 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { 167 for (int i = 0; i < imis.size(); ++i) { 168 if (isSystemImeThatHasSubtypeOf(imis.get(i), context, 169 false /* checkDefaultAttribute */, fallbackLocale, 170 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { 171 return fallbackLocale; 172 } 173 } 174 } 175 Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); 176 return null; 177 } 178 179 private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(final InputMethodInfo imi, 180 final Context context, final boolean checkDefaultAttribute) { 181 if (!isSystemIme(imi)) { 182 return false; 183 } 184 if (checkDefaultAttribute && !imi.isDefault(context)) { 185 return false; 186 } 187 if (!imi.isAuxiliaryIme()) { 188 return false; 189 } 190 final int subtypeCount = imi.getSubtypeCount(); 191 for (int i = 0; i < subtypeCount; ++i) { 192 final InputMethodSubtype s = imi.getSubtypeAt(i); 193 if (s.overridesImplicitlyEnabledSubtype()) { 194 return true; 195 } 196 } 197 return false; 198 } 199 200 public static Locale getSystemLocaleFromContext(final Context context) { 201 try { 202 return context.getResources().getConfiguration().locale; 203 } catch (Resources.NotFoundException ex) { 204 return null; 205 } 206 } 207 208 private static final class InputMethodListBuilder { 209 // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration 210 // order can have non-trivial effect in the call sites. 211 @NonNull 212 private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>(); 213 214 public InputMethodListBuilder fillImes(final ArrayList<InputMethodInfo> imis, 215 final Context context, final boolean checkDefaultAttribute, 216 @Nullable final Locale locale, final boolean checkCountry, 217 final String requiredSubtypeMode) { 218 for (int i = 0; i < imis.size(); ++i) { 219 final InputMethodInfo imi = imis.get(i); 220 if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale, 221 checkCountry, requiredSubtypeMode)) { 222 mInputMethodSet.add(imi); 223 } 224 } 225 return this; 226 } 227 228 // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be 229 // documented more clearly. 230 public InputMethodListBuilder fillAuxiliaryImes(final ArrayList<InputMethodInfo> imis, 231 final Context context) { 232 // If one or more auxiliary input methods are available, OK to stop populating the list. 233 for (final InputMethodInfo imi : mInputMethodSet) { 234 if (imi.isAuxiliaryIme()) { 235 return this; 236 } 237 } 238 boolean added = false; 239 for (int i = 0; i < imis.size(); ++i) { 240 final InputMethodInfo imi = imis.get(i); 241 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, 242 true /* checkDefaultAttribute */)) { 243 mInputMethodSet.add(imi); 244 added = true; 245 } 246 } 247 if (added) { 248 return this; 249 } 250 for (int i = 0; i < imis.size(); ++i) { 251 final InputMethodInfo imi = imis.get(i); 252 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, 253 false /* checkDefaultAttribute */)) { 254 mInputMethodSet.add(imi); 255 } 256 } 257 return this; 258 } 259 260 public boolean isEmpty() { 261 return mInputMethodSet.isEmpty(); 262 } 263 264 @NonNull 265 public ArrayList<InputMethodInfo> build() { 266 return new ArrayList<>(mInputMethodSet); 267 } 268 } 269 270 private static InputMethodListBuilder getMinimumKeyboardSetWithoutSystemLocale( 271 final ArrayList<InputMethodInfo> imis, final Context context, 272 @Nullable final Locale fallbackLocale) { 273 // Before the system becomes ready, we pick up at least one keyboard in the following order. 274 // The first user (device owner) falls into this category. 275 // 1. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true 276 // 2. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true 277 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false 278 // 4. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false 279 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. 280 281 final InputMethodListBuilder builder = new InputMethodListBuilder(); 282 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 283 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 284 if (!builder.isEmpty()) { 285 return builder; 286 } 287 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, 288 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 289 if (!builder.isEmpty()) { 290 return builder; 291 } 292 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 293 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 294 if (!builder.isEmpty()) { 295 return builder; 296 } 297 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, 298 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 299 if (!builder.isEmpty()) { 300 return builder; 301 } 302 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) 303 + " fallbackLocale=" + fallbackLocale); 304 return builder; 305 } 306 307 private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( 308 final ArrayList<InputMethodInfo> imis, final Context context, 309 @Nullable final Locale systemLocale, @Nullable final Locale fallbackLocale) { 310 // Once the system becomes ready, we pick up at least one keyboard in the following order. 311 // Secondary users fall into this category in general. 312 // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true 313 // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false 314 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true 315 // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false 316 // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true 317 // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false 318 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. 319 320 final InputMethodListBuilder builder = new InputMethodListBuilder(); 321 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 322 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 323 if (!builder.isEmpty()) { 324 return builder; 325 } 326 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 327 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 328 if (!builder.isEmpty()) { 329 return builder; 330 } 331 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 332 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 333 if (!builder.isEmpty()) { 334 return builder; 335 } 336 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 337 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 338 if (!builder.isEmpty()) { 339 return builder; 340 } 341 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, 342 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 343 if (!builder.isEmpty()) { 344 return builder; 345 } 346 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, 347 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 348 if (!builder.isEmpty()) { 349 return builder; 350 } 351 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) 352 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); 353 return builder; 354 } 355 356 public static ArrayList<InputMethodInfo> getDefaultEnabledImes(final Context context, 357 final boolean isSystemReady, final ArrayList<InputMethodInfo> imis) { 358 final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); 359 if (!isSystemReady) { 360 // When the system is not ready, the system locale is not stable and reliable. Hence 361 // we will pick up IMEs that support software keyboard based on the fallback locale. 362 // Also pick up suitable IMEs regardless of the software keyboard support. 363 // (e.g. Voice IMEs) 364 return getMinimumKeyboardSetWithoutSystemLocale(imis, context, fallbackLocale) 365 .fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 366 true /* checkCountry */, SUBTYPE_MODE_ANY) 367 .build(); 368 } 369 370 // When the system is ready, we will primarily rely on the system locale, but also keep 371 // relying on the fallback locale as a last resort. 372 // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), 373 // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" 374 // subtype) 375 final Locale systemLocale = getSystemLocaleFromContext(context); 376 return getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale) 377 .fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 378 true /* checkCountry */, SUBTYPE_MODE_ANY) 379 .fillAuxiliaryImes(imis, context) 380 .build(); 381 } 382 383 /** 384 * @deprecated Use {@link #isSystemImeThatHasSubtypeOf(InputMethodInfo, Context, boolean, 385 * Locale, boolean, String)} instead. 386 */ 387 @Deprecated 388 public static boolean isValidSystemDefaultIme( 389 boolean isSystemReady, InputMethodInfo imi, Context context) { 390 if (!isSystemReady) { 391 return false; 392 } 393 if (!isSystemIme(imi)) { 394 return false; 395 } 396 if (imi.getIsDefaultResourceId() != 0) { 397 try { 398 if (imi.isDefault(context) && containsSubtypeOf( 399 imi, context.getResources().getConfiguration().locale.getLanguage(), 400 SUBTYPE_MODE_ANY)) { 401 return true; 402 } 403 } catch (Resources.NotFoundException ex) { 404 } 405 } 406 if (imi.getSubtypeCount() == 0) { 407 Slog.w(TAG, "Found no subtypes in a system IME: " + imi.getPackageName()); 408 } 409 return false; 410 } 411 412 public static boolean containsSubtypeOf(final InputMethodInfo imi, 413 @Nullable final Locale locale, final boolean checkCountry, final String mode) { 414 if (locale == null) { 415 return false; 416 } 417 final int N = imi.getSubtypeCount(); 418 for (int i = 0; i < N; ++i) { 419 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 420 if (checkCountry) { 421 // TODO: Use {@link Locale#toLanguageTag()} and 422 // {@link Locale#forLanguageTag(languageTag)} instead. 423 if (!TextUtils.equals(subtype.getLocale(), locale.toString())) { 424 continue; 425 } 426 } else { 427 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString( 428 subtype.getLocale())); 429 if (!subtypeLocale.getLanguage().equals(locale.getLanguage())) { 430 continue; 431 } 432 } 433 if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) || 434 mode.equalsIgnoreCase(subtype.getMode())) { 435 return true; 436 } 437 } 438 return false; 439 } 440 441 /** 442 * @deprecated Use {@link #containsSubtypeOf(InputMethodInfo, Locale, boolean, String)} instead. 443 */ 444 @Deprecated 445 public static boolean containsSubtypeOf(InputMethodInfo imi, String language, String mode) { 446 final int N = imi.getSubtypeCount(); 447 for (int i = 0; i < N; ++i) { 448 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 449 if (!subtype.getLocale().startsWith(language)) { 450 continue; 451 } 452 if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) || 453 mode.equalsIgnoreCase(subtype.getMode())) { 454 return true; 455 } 456 } 457 return false; 458 } 459 460 public static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) { 461 ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 462 final int subtypeCount = imi.getSubtypeCount(); 463 for (int i = 0; i < subtypeCount; ++i) { 464 subtypes.add(imi.getSubtypeAt(i)); 465 } 466 return subtypes; 467 } 468 469 public static ArrayList<InputMethodSubtype> getOverridingImplicitlyEnabledSubtypes( 470 InputMethodInfo imi, String mode) { 471 ArrayList<InputMethodSubtype> subtypes = new ArrayList<InputMethodSubtype>(); 472 final int subtypeCount = imi.getSubtypeCount(); 473 for (int i = 0; i < subtypeCount; ++i) { 474 final InputMethodSubtype subtype = imi.getSubtypeAt(i); 475 if (subtype.overridesImplicitlyEnabledSubtype() && subtype.getMode().equals(mode)) { 476 subtypes.add(subtype); 477 } 478 } 479 return subtypes; 480 } 481 482 public static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) { 483 if (enabledImes == null || enabledImes.isEmpty()) { 484 return null; 485 } 486 // We'd prefer to fall back on a system IME, since that is safer. 487 int i = enabledImes.size(); 488 int firstFoundSystemIme = -1; 489 while (i > 0) { 490 i--; 491 final InputMethodInfo imi = enabledImes.get(i); 492 if (InputMethodUtils.isSystemImeThatHasEnglishKeyboardSubtype(imi) 493 && !imi.isAuxiliaryIme()) { 494 return imi; 495 } 496 if (firstFoundSystemIme < 0 && InputMethodUtils.isSystemIme(imi) 497 && !imi.isAuxiliaryIme()) { 498 firstFoundSystemIme = i; 499 } 500 } 501 return enabledImes.get(Math.max(firstFoundSystemIme, 0)); 502 } 503 504 public static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) { 505 return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID; 506 } 507 508 public static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) { 509 if (imi != null) { 510 final int subtypeCount = imi.getSubtypeCount(); 511 for (int i = 0; i < subtypeCount; ++i) { 512 InputMethodSubtype ims = imi.getSubtypeAt(i); 513 if (subtypeHashCode == ims.hashCode()) { 514 return i; 515 } 516 } 517 } 518 return NOT_A_SUBTYPE_ID; 519 } 520 521 private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked( 522 Resources res, InputMethodInfo imi) { 523 final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi); 524 final String systemLocale = res.getConfiguration().locale.toString(); 525 if (TextUtils.isEmpty(systemLocale)) return new ArrayList<InputMethodSubtype>(); 526 final String systemLanguage = res.getConfiguration().locale.getLanguage(); 527 final HashMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = 528 new HashMap<String, InputMethodSubtype>(); 529 final int N = subtypes.size(); 530 for (int i = 0; i < N; ++i) { 531 // scan overriding implicitly enabled subtypes. 532 InputMethodSubtype subtype = subtypes.get(i); 533 if (subtype.overridesImplicitlyEnabledSubtype()) { 534 final String mode = subtype.getMode(); 535 if (!applicableModeAndSubtypesMap.containsKey(mode)) { 536 applicableModeAndSubtypesMap.put(mode, subtype); 537 } 538 } 539 } 540 if (applicableModeAndSubtypesMap.size() > 0) { 541 return new ArrayList<InputMethodSubtype>(applicableModeAndSubtypesMap.values()); 542 } 543 for (int i = 0; i < N; ++i) { 544 final InputMethodSubtype subtype = subtypes.get(i); 545 final String locale = subtype.getLocale(); 546 final String mode = subtype.getMode(); 547 final String language = getLanguageFromLocaleString(locale); 548 // When system locale starts with subtype's locale, that subtype will be applicable 549 // for system locale. We need to make sure the languages are the same, to prevent 550 // locales like "fil" (Filipino) being matched by "fi" (Finnish). 551 // 552 // For instance, it's clearly applicable for cases like system locale = en_US and 553 // subtype = en, but it is not necessarily considered applicable for cases like system 554 // locale = en and subtype = en_US. 555 // 556 // We just call systemLocale.startsWith(locale) in this function because there is no 557 // need to find applicable subtypes aggressively unlike 558 // findLastResortApplicableSubtypeLocked. 559 // 560 // TODO: This check is broken. It won't take scripts into account and doesn't 561 // account for the mandatory conversions performed by Locale#toString. 562 if (language.equals(systemLanguage) && systemLocale.startsWith(locale)) { 563 final InputMethodSubtype applicableSubtype = applicableModeAndSubtypesMap.get(mode); 564 // If more applicable subtypes are contained, skip. 565 if (applicableSubtype != null) { 566 if (systemLocale.equals(applicableSubtype.getLocale())) continue; 567 if (!systemLocale.equals(locale)) continue; 568 } 569 applicableModeAndSubtypesMap.put(mode, subtype); 570 } 571 } 572 final InputMethodSubtype keyboardSubtype 573 = applicableModeAndSubtypesMap.get(SUBTYPE_MODE_KEYBOARD); 574 final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<InputMethodSubtype>( 575 applicableModeAndSubtypesMap.values()); 576 if (keyboardSubtype != null && !keyboardSubtype.containsExtraValueKey(TAG_ASCII_CAPABLE)) { 577 for (int i = 0; i < N; ++i) { 578 final InputMethodSubtype subtype = subtypes.get(i); 579 final String mode = subtype.getMode(); 580 if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey( 581 TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) { 582 applicableSubtypes.add(subtype); 583 } 584 } 585 } 586 if (keyboardSubtype == null) { 587 InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked( 588 res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true); 589 if (lastResortKeyboardSubtype != null) { 590 applicableSubtypes.add(lastResortKeyboardSubtype); 591 } 592 } 593 return applicableSubtypes; 594 } 595 596 /** 597 * Returns the language component of a given locale string. 598 * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)} 599 */ 600 public static String getLanguageFromLocaleString(String locale) { 601 final int idx = locale.indexOf('_'); 602 if (idx < 0) { 603 return locale; 604 } else { 605 return locale.substring(0, idx); 606 } 607 } 608 609 /** 610 * If there are no selected subtypes, tries finding the most applicable one according to the 611 * given locale. 612 * @param subtypes this function will search the most applicable subtype in subtypes 613 * @param mode subtypes will be filtered by mode 614 * @param locale subtypes will be filtered by locale 615 * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype, 616 * it will return the first subtype matched with mode 617 * @return the most applicable subtypeId 618 */ 619 public static InputMethodSubtype findLastResortApplicableSubtypeLocked( 620 Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, 621 boolean canIgnoreLocaleAsLastResort) { 622 if (subtypes == null || subtypes.size() == 0) { 623 return null; 624 } 625 if (TextUtils.isEmpty(locale)) { 626 locale = res.getConfiguration().locale.toString(); 627 } 628 final String language = getLanguageFromLocaleString(locale); 629 boolean partialMatchFound = false; 630 InputMethodSubtype applicableSubtype = null; 631 InputMethodSubtype firstMatchedModeSubtype = null; 632 final int N = subtypes.size(); 633 for (int i = 0; i < N; ++i) { 634 InputMethodSubtype subtype = subtypes.get(i); 635 final String subtypeLocale = subtype.getLocale(); 636 final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale); 637 // An applicable subtype should match "mode". If mode is null, mode will be ignored, 638 // and all subtypes with all modes can be candidates. 639 if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) { 640 if (firstMatchedModeSubtype == null) { 641 firstMatchedModeSubtype = subtype; 642 } 643 if (locale.equals(subtypeLocale)) { 644 // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US") 645 applicableSubtype = subtype; 646 break; 647 } else if (!partialMatchFound && language.equals(subtypeLanguage)) { 648 // Partial match (e.g. system locale is "en_US" and subtype locale is "en") 649 applicableSubtype = subtype; 650 partialMatchFound = true; 651 } 652 } 653 } 654 655 if (applicableSubtype == null && canIgnoreLocaleAsLastResort) { 656 return firstMatchedModeSubtype; 657 } 658 659 // The first subtype applicable to the system locale will be defined as the most applicable 660 // subtype. 661 if (DEBUG) { 662 if (applicableSubtype != null) { 663 Slog.d(TAG, "Applicable InputMethodSubtype was found: " 664 + applicableSubtype.getMode() + "," + applicableSubtype.getLocale()); 665 } 666 } 667 return applicableSubtype; 668 } 669 670 public static boolean canAddToLastInputMethod(InputMethodSubtype subtype) { 671 if (subtype == null) return true; 672 return !subtype.isAuxiliary(); 673 } 674 675 public static void setNonSelectedSystemImesDisabledUntilUsed( 676 PackageManager packageManager, List<InputMethodInfo> enabledImis) { 677 if (DEBUG) { 678 Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed"); 679 } 680 final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray( 681 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes); 682 if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) { 683 return; 684 } 685 // Only the current spell checker should be treated as an enabled one. 686 final SpellCheckerInfo currentSpellChecker = 687 TextServicesManager.getInstance().getCurrentSpellChecker(); 688 for (final String packageName : systemImesDisabledUntilUsed) { 689 if (DEBUG) { 690 Slog.d(TAG, "check " + packageName); 691 } 692 boolean enabledIme = false; 693 for (int j = 0; j < enabledImis.size(); ++j) { 694 final InputMethodInfo imi = enabledImis.get(j); 695 if (packageName.equals(imi.getPackageName())) { 696 enabledIme = true; 697 break; 698 } 699 } 700 if (enabledIme) { 701 // enabled ime. skip 702 continue; 703 } 704 if (currentSpellChecker != null 705 && packageName.equals(currentSpellChecker.getPackageName())) { 706 // enabled spell checker. skip 707 if (DEBUG) { 708 Slog.d(TAG, packageName + " is the current spell checker. skip"); 709 } 710 continue; 711 } 712 ApplicationInfo ai = null; 713 try { 714 ai = packageManager.getApplicationInfo(packageName, 715 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS); 716 } catch (NameNotFoundException e) { 717 Slog.w(TAG, "NameNotFoundException: " + packageName, e); 718 } 719 if (ai == null) { 720 // No app found for packageName 721 continue; 722 } 723 final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0; 724 if (!isSystemPackage) { 725 continue; 726 } 727 setDisabledUntilUsed(packageManager, packageName); 728 } 729 } 730 731 private static void setDisabledUntilUsed(PackageManager packageManager, String packageName) { 732 final int state = packageManager.getApplicationEnabledSetting(packageName); 733 if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT 734 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) { 735 if (DEBUG) { 736 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED"); 737 } 738 packageManager.setApplicationEnabledSetting(packageName, 739 PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED, 0); 740 } else { 741 if (DEBUG) { 742 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED"); 743 } 744 } 745 } 746 747 public static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi, 748 InputMethodSubtype subtype) { 749 final CharSequence imiLabel = imi.loadLabel(context.getPackageManager()); 750 return subtype != null 751 ? TextUtils.concat(subtype.getDisplayName(context, 752 imi.getPackageName(), imi.getServiceInfo().applicationInfo), 753 (TextUtils.isEmpty(imiLabel) ? 754 "" : " - " + imiLabel)) 755 : imiLabel; 756 } 757 758 /** 759 * Returns true if a package name belongs to a UID. 760 * 761 * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p> 762 * @param appOpsManager the {@link AppOpsManager} object to be used for the validation. 763 * @param uid the UID to be validated. 764 * @param packageName the package name. 765 * @return {@code true} if the package name belongs to the UID. 766 */ 767 public static boolean checkIfPackageBelongsToUid(final AppOpsManager appOpsManager, 768 final int uid, final String packageName) { 769 try { 770 appOpsManager.checkPackage(uid, packageName); 771 return true; 772 } catch (SecurityException e) { 773 return false; 774 } 775 } 776 777 /** 778 * Utility class for putting and getting settings for InputMethod 779 * TODO: Move all putters and getters of settings to this class. 780 */ 781 public static class InputMethodSettings { 782 // The string for enabled input method is saved as follows: 783 // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0") 784 private static final char INPUT_METHOD_SEPARATER = ':'; 785 private static final char INPUT_METHOD_SUBTYPE_SEPARATER = ';'; 786 private final TextUtils.SimpleStringSplitter mInputMethodSplitter = 787 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATER); 788 789 private final TextUtils.SimpleStringSplitter mSubtypeSplitter = 790 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATER); 791 792 private final Resources mRes; 793 private final ContentResolver mResolver; 794 private final HashMap<String, InputMethodInfo> mMethodMap; 795 private final ArrayList<InputMethodInfo> mMethodList; 796 797 private String mEnabledInputMethodsStrCache; 798 private int mCurrentUserId; 799 private int[] mCurrentProfileIds = new int[0]; 800 801 private static void buildEnabledInputMethodsSettingString( 802 StringBuilder builder, Pair<String, ArrayList<String>> pair) { 803 String id = pair.first; 804 ArrayList<String> subtypes = pair.second; 805 builder.append(id); 806 // Inputmethod and subtypes are saved in the settings as follows: 807 // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1 808 for (String subtypeId: subtypes) { 809 builder.append(INPUT_METHOD_SUBTYPE_SEPARATER).append(subtypeId); 810 } 811 } 812 813 public InputMethodSettings( 814 Resources res, ContentResolver resolver, 815 HashMap<String, InputMethodInfo> methodMap, ArrayList<InputMethodInfo> methodList, 816 int userId) { 817 setCurrentUserId(userId); 818 mRes = res; 819 mResolver = resolver; 820 mMethodMap = methodMap; 821 mMethodList = methodList; 822 } 823 824 public void setCurrentUserId(int userId) { 825 if (DEBUG) { 826 Slog.d(TAG, "--- Swtich the current user from " + mCurrentUserId + " to " + userId); 827 } 828 // IMMS settings are kept per user, so keep track of current user 829 mCurrentUserId = userId; 830 } 831 832 public void setCurrentProfileIds(int[] currentProfileIds) { 833 synchronized (this) { 834 mCurrentProfileIds = currentProfileIds; 835 } 836 } 837 838 public boolean isCurrentProfile(int userId) { 839 synchronized (this) { 840 if (userId == mCurrentUserId) return true; 841 for (int i = 0; i < mCurrentProfileIds.length; i++) { 842 if (userId == mCurrentProfileIds[i]) return true; 843 } 844 return false; 845 } 846 } 847 848 public List<InputMethodInfo> getEnabledInputMethodListLocked() { 849 return createEnabledInputMethodListLocked( 850 getEnabledInputMethodsAndSubtypeListLocked()); 851 } 852 853 public List<Pair<InputMethodInfo, ArrayList<String>>> 854 getEnabledInputMethodAndSubtypeHashCodeListLocked() { 855 return createEnabledInputMethodAndSubtypeHashCodeListLocked( 856 getEnabledInputMethodsAndSubtypeListLocked()); 857 } 858 859 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( 860 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) { 861 List<InputMethodSubtype> enabledSubtypes = 862 getEnabledInputMethodSubtypeListLocked(imi); 863 if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) { 864 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked( 865 context.getResources(), imi); 866 } 867 return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes); 868 } 869 870 public List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked( 871 InputMethodInfo imi) { 872 List<Pair<String, ArrayList<String>>> imsList = 873 getEnabledInputMethodsAndSubtypeListLocked(); 874 ArrayList<InputMethodSubtype> enabledSubtypes = 875 new ArrayList<InputMethodSubtype>(); 876 if (imi != null) { 877 for (Pair<String, ArrayList<String>> imsPair : imsList) { 878 InputMethodInfo info = mMethodMap.get(imsPair.first); 879 if (info != null && info.getId().equals(imi.getId())) { 880 final int subtypeCount = info.getSubtypeCount(); 881 for (int i = 0; i < subtypeCount; ++i) { 882 InputMethodSubtype ims = info.getSubtypeAt(i); 883 for (String s: imsPair.second) { 884 if (String.valueOf(ims.hashCode()).equals(s)) { 885 enabledSubtypes.add(ims); 886 } 887 } 888 } 889 break; 890 } 891 } 892 } 893 return enabledSubtypes; 894 } 895 896 // At the initial boot, the settings for input methods are not set, 897 // so we need to enable IME in that case. 898 public void enableAllIMEsIfThereIsNoEnabledIME() { 899 if (TextUtils.isEmpty(getEnabledInputMethodsStr())) { 900 StringBuilder sb = new StringBuilder(); 901 final int N = mMethodList.size(); 902 for (int i = 0; i < N; i++) { 903 InputMethodInfo imi = mMethodList.get(i); 904 Slog.i(TAG, "Adding: " + imi.getId()); 905 if (i > 0) sb.append(':'); 906 sb.append(imi.getId()); 907 } 908 putEnabledInputMethodsStr(sb.toString()); 909 } 910 } 911 912 public List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() { 913 ArrayList<Pair<String, ArrayList<String>>> imsList 914 = new ArrayList<Pair<String, ArrayList<String>>>(); 915 final String enabledInputMethodsStr = getEnabledInputMethodsStr(); 916 if (TextUtils.isEmpty(enabledInputMethodsStr)) { 917 return imsList; 918 } 919 mInputMethodSplitter.setString(enabledInputMethodsStr); 920 while (mInputMethodSplitter.hasNext()) { 921 String nextImsStr = mInputMethodSplitter.next(); 922 mSubtypeSplitter.setString(nextImsStr); 923 if (mSubtypeSplitter.hasNext()) { 924 ArrayList<String> subtypeHashes = new ArrayList<String>(); 925 // The first element is ime id. 926 String imeId = mSubtypeSplitter.next(); 927 while (mSubtypeSplitter.hasNext()) { 928 subtypeHashes.add(mSubtypeSplitter.next()); 929 } 930 imsList.add(new Pair<String, ArrayList<String>>(imeId, subtypeHashes)); 931 } 932 } 933 return imsList; 934 } 935 936 public void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) { 937 if (reloadInputMethodStr) { 938 getEnabledInputMethodsStr(); 939 } 940 if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) { 941 // Add in the newly enabled input method. 942 putEnabledInputMethodsStr(id); 943 } else { 944 putEnabledInputMethodsStr( 945 mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATER + id); 946 } 947 } 948 949 /** 950 * Build and put a string of EnabledInputMethods with removing specified Id. 951 * @return the specified id was removed or not. 952 */ 953 public boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked( 954 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) { 955 boolean isRemoved = false; 956 boolean needsAppendSeparator = false; 957 for (Pair<String, ArrayList<String>> ims: imsList) { 958 String curId = ims.first; 959 if (curId.equals(id)) { 960 // We are disabling this input method, and it is 961 // currently enabled. Skip it to remove from the 962 // new list. 963 isRemoved = true; 964 } else { 965 if (needsAppendSeparator) { 966 builder.append(INPUT_METHOD_SEPARATER); 967 } else { 968 needsAppendSeparator = true; 969 } 970 buildEnabledInputMethodsSettingString(builder, ims); 971 } 972 } 973 if (isRemoved) { 974 // Update the setting with the new list of input methods. 975 putEnabledInputMethodsStr(builder.toString()); 976 } 977 return isRemoved; 978 } 979 980 private List<InputMethodInfo> createEnabledInputMethodListLocked( 981 List<Pair<String, ArrayList<String>>> imsList) { 982 final ArrayList<InputMethodInfo> res = new ArrayList<InputMethodInfo>(); 983 for (Pair<String, ArrayList<String>> ims: imsList) { 984 InputMethodInfo info = mMethodMap.get(ims.first); 985 if (info != null) { 986 res.add(info); 987 } 988 } 989 return res; 990 } 991 992 private List<Pair<InputMethodInfo, ArrayList<String>>> 993 createEnabledInputMethodAndSubtypeHashCodeListLocked( 994 List<Pair<String, ArrayList<String>>> imsList) { 995 final ArrayList<Pair<InputMethodInfo, ArrayList<String>>> res 996 = new ArrayList<Pair<InputMethodInfo, ArrayList<String>>>(); 997 for (Pair<String, ArrayList<String>> ims : imsList) { 998 InputMethodInfo info = mMethodMap.get(ims.first); 999 if (info != null) { 1000 res.add(new Pair<InputMethodInfo, ArrayList<String>>(info, ims.second)); 1001 } 1002 } 1003 return res; 1004 } 1005 1006 private void putEnabledInputMethodsStr(String str) { 1007 Settings.Secure.putStringForUser( 1008 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, str, mCurrentUserId); 1009 mEnabledInputMethodsStrCache = str; 1010 if (DEBUG) { 1011 Slog.d(TAG, "putEnabledInputMethodStr: " + str); 1012 } 1013 } 1014 1015 public String getEnabledInputMethodsStr() { 1016 mEnabledInputMethodsStrCache = Settings.Secure.getStringForUser( 1017 mResolver, Settings.Secure.ENABLED_INPUT_METHODS, mCurrentUserId); 1018 if (DEBUG) { 1019 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache 1020 + ", " + mCurrentUserId); 1021 } 1022 return mEnabledInputMethodsStrCache; 1023 } 1024 1025 private void saveSubtypeHistory( 1026 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) { 1027 StringBuilder builder = new StringBuilder(); 1028 boolean isImeAdded = false; 1029 if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) { 1030 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append( 1031 newSubtypeId); 1032 isImeAdded = true; 1033 } 1034 for (Pair<String, String> ime: savedImes) { 1035 String imeId = ime.first; 1036 String subtypeId = ime.second; 1037 if (TextUtils.isEmpty(subtypeId)) { 1038 subtypeId = NOT_A_SUBTYPE_ID_STR; 1039 } 1040 if (isImeAdded) { 1041 builder.append(INPUT_METHOD_SEPARATER); 1042 } else { 1043 isImeAdded = true; 1044 } 1045 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATER).append( 1046 subtypeId); 1047 } 1048 // Remove the last INPUT_METHOD_SEPARATER 1049 putSubtypeHistoryStr(builder.toString()); 1050 } 1051 1052 private void addSubtypeToHistory(String imeId, String subtypeId) { 1053 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); 1054 for (Pair<String, String> ime: subtypeHistory) { 1055 if (ime.first.equals(imeId)) { 1056 if (DEBUG) { 1057 Slog.v(TAG, "Subtype found in the history: " + imeId + ", " 1058 + ime.second); 1059 } 1060 // We should break here 1061 subtypeHistory.remove(ime); 1062 break; 1063 } 1064 } 1065 if (DEBUG) { 1066 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId); 1067 } 1068 saveSubtypeHistory(subtypeHistory, imeId, subtypeId); 1069 } 1070 1071 private void putSubtypeHistoryStr(String str) { 1072 if (DEBUG) { 1073 Slog.d(TAG, "putSubtypeHistoryStr: " + str); 1074 } 1075 Settings.Secure.putStringForUser( 1076 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str, mCurrentUserId); 1077 } 1078 1079 public Pair<String, String> getLastInputMethodAndSubtypeLocked() { 1080 // Gets the first one from the history 1081 return getLastSubtypeForInputMethodLockedInternal(null); 1082 } 1083 1084 public String getLastSubtypeForInputMethodLocked(String imeId) { 1085 Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId); 1086 if (ime != null) { 1087 return ime.second; 1088 } else { 1089 return null; 1090 } 1091 } 1092 1093 private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) { 1094 List<Pair<String, ArrayList<String>>> enabledImes = 1095 getEnabledInputMethodsAndSubtypeListLocked(); 1096 List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked(); 1097 for (Pair<String, String> imeAndSubtype : subtypeHistory) { 1098 final String imeInTheHistory = imeAndSubtype.first; 1099 // If imeId is empty, returns the first IME and subtype in the history 1100 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) { 1101 final String subtypeInTheHistory = imeAndSubtype.second; 1102 final String subtypeHashCode = 1103 getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked( 1104 enabledImes, imeInTheHistory, subtypeInTheHistory); 1105 if (!TextUtils.isEmpty(subtypeHashCode)) { 1106 if (DEBUG) { 1107 Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode); 1108 } 1109 return new Pair<String, String>(imeInTheHistory, subtypeHashCode); 1110 } 1111 } 1112 } 1113 if (DEBUG) { 1114 Slog.d(TAG, "No enabled IME found in the history"); 1115 } 1116 return null; 1117 } 1118 1119 private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, 1120 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) { 1121 for (Pair<String, ArrayList<String>> enabledIme: enabledImes) { 1122 if (enabledIme.first.equals(imeId)) { 1123 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second; 1124 final InputMethodInfo imi = mMethodMap.get(imeId); 1125 if (explicitlyEnabledSubtypes.size() == 0) { 1126 // If there are no explicitly enabled subtypes, applicable subtypes are 1127 // enabled implicitly. 1128 // If IME is enabled and no subtypes are enabled, applicable subtypes 1129 // are enabled implicitly, so needs to treat them to be enabled. 1130 if (imi != null && imi.getSubtypeCount() > 0) { 1131 List<InputMethodSubtype> implicitlySelectedSubtypes = 1132 getImplicitlyApplicableSubtypesLocked(mRes, imi); 1133 if (implicitlySelectedSubtypes != null) { 1134 final int N = implicitlySelectedSubtypes.size(); 1135 for (int i = 0; i < N; ++i) { 1136 final InputMethodSubtype st = implicitlySelectedSubtypes.get(i); 1137 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) { 1138 return subtypeHashCode; 1139 } 1140 } 1141 } 1142 } 1143 } else { 1144 for (String s: explicitlyEnabledSubtypes) { 1145 if (s.equals(subtypeHashCode)) { 1146 // If both imeId and subtypeId are enabled, return subtypeId. 1147 try { 1148 final int hashCode = Integer.valueOf(subtypeHashCode); 1149 // Check whether the subtype id is valid or not 1150 if (isValidSubtypeId(imi, hashCode)) { 1151 return s; 1152 } else { 1153 return NOT_A_SUBTYPE_ID_STR; 1154 } 1155 } catch (NumberFormatException e) { 1156 return NOT_A_SUBTYPE_ID_STR; 1157 } 1158 } 1159 } 1160 } 1161 // If imeId was enabled but subtypeId was disabled. 1162 return NOT_A_SUBTYPE_ID_STR; 1163 } 1164 } 1165 // If both imeId and subtypeId are disabled, return null 1166 return null; 1167 } 1168 1169 private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() { 1170 ArrayList<Pair<String, String>> imsList = new ArrayList<Pair<String, String>>(); 1171 final String subtypeHistoryStr = getSubtypeHistoryStr(); 1172 if (TextUtils.isEmpty(subtypeHistoryStr)) { 1173 return imsList; 1174 } 1175 mInputMethodSplitter.setString(subtypeHistoryStr); 1176 while (mInputMethodSplitter.hasNext()) { 1177 String nextImsStr = mInputMethodSplitter.next(); 1178 mSubtypeSplitter.setString(nextImsStr); 1179 if (mSubtypeSplitter.hasNext()) { 1180 String subtypeId = NOT_A_SUBTYPE_ID_STR; 1181 // The first element is ime id. 1182 String imeId = mSubtypeSplitter.next(); 1183 while (mSubtypeSplitter.hasNext()) { 1184 subtypeId = mSubtypeSplitter.next(); 1185 break; 1186 } 1187 imsList.add(new Pair<String, String>(imeId, subtypeId)); 1188 } 1189 } 1190 return imsList; 1191 } 1192 1193 private String getSubtypeHistoryStr() { 1194 if (DEBUG) { 1195 Slog.d(TAG, "getSubtypeHistoryStr: " + Settings.Secure.getStringForUser( 1196 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId)); 1197 } 1198 return Settings.Secure.getStringForUser( 1199 mResolver, Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, mCurrentUserId); 1200 } 1201 1202 public void putSelectedInputMethod(String imeId) { 1203 if (DEBUG) { 1204 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " 1205 + mCurrentUserId); 1206 } 1207 Settings.Secure.putStringForUser( 1208 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, imeId, mCurrentUserId); 1209 } 1210 1211 public void putSelectedSubtype(int subtypeId) { 1212 if (DEBUG) { 1213 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " 1214 + mCurrentUserId); 1215 } 1216 Settings.Secure.putIntForUser(mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, 1217 subtypeId, mCurrentUserId); 1218 } 1219 1220 public String getDisabledSystemInputMethods() { 1221 return Settings.Secure.getStringForUser( 1222 mResolver, Settings.Secure.DISABLED_SYSTEM_INPUT_METHODS, mCurrentUserId); 1223 } 1224 1225 public String getSelectedInputMethod() { 1226 if (DEBUG) { 1227 Slog.d(TAG, "getSelectedInputMethodStr: " + Settings.Secure.getStringForUser( 1228 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId) 1229 + ", " + mCurrentUserId); 1230 } 1231 return Settings.Secure.getStringForUser( 1232 mResolver, Settings.Secure.DEFAULT_INPUT_METHOD, mCurrentUserId); 1233 } 1234 1235 public boolean isSubtypeSelected() { 1236 return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID; 1237 } 1238 1239 private int getSelectedInputMethodSubtypeHashCode() { 1240 try { 1241 return Settings.Secure.getIntForUser( 1242 mResolver, Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, mCurrentUserId); 1243 } catch (SettingNotFoundException e) { 1244 return NOT_A_SUBTYPE_ID; 1245 } 1246 } 1247 1248 public boolean isShowImeWithHardKeyboardEnabled() { 1249 return Settings.Secure.getIntForUser(mResolver, 1250 Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 0, mCurrentUserId) == 1; 1251 } 1252 1253 public void setShowImeWithHardKeyboard(boolean show) { 1254 Settings.Secure.putIntForUser(mResolver, Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, 1255 show ? 1 : 0, mCurrentUserId); 1256 } 1257 1258 public int getCurrentUserId() { 1259 return mCurrentUserId; 1260 } 1261 1262 public int getSelectedInputMethodSubtypeId(String selectedImiId) { 1263 final InputMethodInfo imi = mMethodMap.get(selectedImiId); 1264 if (imi == null) { 1265 return NOT_A_SUBTYPE_ID; 1266 } 1267 final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode(); 1268 return getSubtypeIdFromHashCode(imi, subtypeHashCode); 1269 } 1270 1271 public void saveCurrentInputMethodAndSubtypeToHistory( 1272 String curMethodId, InputMethodSubtype currentSubtype) { 1273 String subtypeId = NOT_A_SUBTYPE_ID_STR; 1274 if (currentSubtype != null) { 1275 subtypeId = String.valueOf(currentSubtype.hashCode()); 1276 } 1277 if (canAddToLastInputMethod(currentSubtype)) { 1278 addSubtypeToHistory(curMethodId, subtypeId); 1279 } 1280 } 1281 1282 public HashMap<InputMethodInfo, List<InputMethodSubtype>> 1283 getExplicitlyOrImplicitlyEnabledInputMethodsAndSubtypeListLocked(Context context) { 1284 HashMap<InputMethodInfo, List<InputMethodSubtype>> enabledInputMethodAndSubtypes = 1285 new HashMap<InputMethodInfo, List<InputMethodSubtype>>(); 1286 for (InputMethodInfo imi: getEnabledInputMethodListLocked()) { 1287 enabledInputMethodAndSubtypes.put( 1288 imi, getEnabledInputMethodSubtypeListLocked(context, imi, true)); 1289 } 1290 return enabledInputMethodAndSubtypes; 1291 } 1292 } 1293 } 1294