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