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