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