1 /* 2 * Copyright (C) 2022 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.server.inputmethod; 18 19 import static com.android.server.inputmethod.SubtypeUtils.SUBTYPE_MODE_ANY; 20 import static com.android.server.inputmethod.SubtypeUtils.SUBTYPE_MODE_KEYBOARD; 21 22 import android.annotation.NonNull; 23 import android.annotation.Nullable; 24 import android.content.Context; 25 import android.text.TextUtils; 26 import android.util.ArrayMap; 27 import android.util.Slog; 28 import android.view.inputmethod.InputMethodInfo; 29 import android.view.inputmethod.InputMethodSubtype; 30 31 import java.util.ArrayList; 32 import java.util.Arrays; 33 import java.util.LinkedHashSet; 34 import java.util.List; 35 import java.util.Locale; 36 37 /** 38 * This class provides utility methods to generate or filter {@link InputMethodInfo} for 39 * {@link InputMethodManagerService}. 40 * 41 * <p>This class is intentionally package-private. Utility methods here are tightly coupled with 42 * implementation details in {@link InputMethodManagerService}. Hence this class is not suitable 43 * for other components to directly use.</p> 44 */ 45 final class InputMethodInfoUtils { 46 private static final String TAG = "InputMethodInfoUtils"; 47 48 /** 49 * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs 50 * that are mainly used until the system becomes ready. Note that {@link Locale} in this array 51 * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH} 52 * doesn't automatically match {@code Locale("en", "IN")}. 53 */ 54 private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = { 55 Locale.ENGLISH, // "en" 56 Locale.US, // "en_US" 57 Locale.UK, // "en_GB" 58 }; 59 private static final Locale ENGLISH_LOCALE = new Locale("en"); 60 61 private static final class InputMethodListBuilder { 62 // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration 63 // order can have non-trivial effect in the call sites. 64 @NonNull 65 private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>(); 66 fillImes(ArrayList<InputMethodInfo> imis, Context context, boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, String requiredSubtypeMode)67 InputMethodListBuilder fillImes(ArrayList<InputMethodInfo> imis, Context context, 68 boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, 69 String requiredSubtypeMode) { 70 for (int i = 0; i < imis.size(); ++i) { 71 final InputMethodInfo imi = imis.get(i); 72 if (isSystemImeThatHasSubtypeOf(imi, context, 73 checkDefaultAttribute, locale, checkCountry, requiredSubtypeMode)) { 74 mInputMethodSet.add(imi); 75 } 76 } 77 return this; 78 } 79 80 // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be 81 // documented more clearly. fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context)82 InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) { 83 // If one or more auxiliary input methods are available, OK to stop populating the list. 84 for (final InputMethodInfo imi : mInputMethodSet) { 85 if (imi.isAuxiliaryIme()) { 86 return this; 87 } 88 } 89 boolean added = false; 90 for (int i = 0; i < imis.size(); ++i) { 91 final InputMethodInfo imi = imis.get(i); 92 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, 93 true /* checkDefaultAttribute */)) { 94 mInputMethodSet.add(imi); 95 added = true; 96 } 97 } 98 if (added) { 99 return this; 100 } 101 for (int i = 0; i < imis.size(); ++i) { 102 final InputMethodInfo imi = imis.get(i); 103 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, 104 false /* checkDefaultAttribute */)) { 105 mInputMethodSet.add(imi); 106 } 107 } 108 return this; 109 110 } 111 isEmpty()112 public boolean isEmpty() { 113 return mInputMethodSet.isEmpty(); 114 } 115 116 @NonNull build()117 public ArrayList<InputMethodInfo> build() { 118 return new ArrayList<>(mInputMethodSet); 119 } 120 } 121 getMinimumKeyboardSetWithSystemLocale( ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, @Nullable Locale fallbackLocale)122 private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( 123 ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, 124 @Nullable Locale fallbackLocale) { 125 // Once the system becomes ready, we pick up at least one keyboard in the following order. 126 // Secondary users fall into this category in general. 127 // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true 128 // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false 129 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true 130 // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false 131 // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true 132 // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false 133 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. 134 135 final InputMethodListBuilder builder = new InputMethodListBuilder(); 136 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 137 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 138 if (!builder.isEmpty()) { 139 return builder; 140 } 141 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 142 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 143 if (!builder.isEmpty()) { 144 return builder; 145 } 146 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 147 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 148 if (!builder.isEmpty()) { 149 return builder; 150 } 151 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 152 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 153 if (!builder.isEmpty()) { 154 return builder; 155 } 156 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, 157 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 158 if (!builder.isEmpty()) { 159 return builder; 160 } 161 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, 162 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 163 if (!builder.isEmpty()) { 164 return builder; 165 } 166 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) 167 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); 168 return builder; 169 } 170 getDefaultEnabledImes( Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum)171 static ArrayList<InputMethodInfo> getDefaultEnabledImes( 172 Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum) { 173 final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); 174 // We will primarily rely on the system locale, but also keep relying on the fallback locale 175 // as a last resort. 176 // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), 177 // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" 178 // subtype) 179 final Locale systemLocale = LocaleUtils.getSystemLocaleFromContext(context); 180 final InputMethodListBuilder builder = 181 getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale); 182 if (!onlyMinimum) { 183 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 184 true /* checkCountry */, SUBTYPE_MODE_ANY) 185 .fillAuxiliaryImes(imis, context); 186 } 187 return builder.build(); 188 } 189 getDefaultEnabledImes( Context context, ArrayList<InputMethodInfo> imis)190 static ArrayList<InputMethodInfo> getDefaultEnabledImes( 191 Context context, ArrayList<InputMethodInfo> imis) { 192 return getDefaultEnabledImes(context, imis, false /* onlyMinimum */); 193 } 194 195 /** 196 * Chooses an eligible system voice IME from the given IMEs. 197 * 198 * @param methodMap Map from the IME ID to {@link InputMethodInfo}. 199 * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system 200 * config. 201 * @param currentDefaultVoiceImeId the default voice IME id, which may be {@code null} or 202 * the value assigned for 203 * {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD} 204 * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for 205 * the system voice IME. 206 */ 207 @Nullable chooseSystemVoiceIme( @onNull ArrayMap<String, InputMethodInfo> methodMap, @Nullable String systemSpeechRecognizerPackageName, @Nullable String currentDefaultVoiceImeId)208 static InputMethodInfo chooseSystemVoiceIme( 209 @NonNull ArrayMap<String, InputMethodInfo> methodMap, 210 @Nullable String systemSpeechRecognizerPackageName, 211 @Nullable String currentDefaultVoiceImeId) { 212 if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { 213 return null; 214 } 215 final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId); 216 // If the config matches the package of the setting, use the current one. 217 if (defaultVoiceIme != null && defaultVoiceIme.isSystem() 218 && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) { 219 return defaultVoiceIme; 220 } 221 InputMethodInfo firstMatchingIme = null; 222 final int methodCount = methodMap.size(); 223 for (int i = 0; i < methodCount; ++i) { 224 final InputMethodInfo imi = methodMap.valueAt(i); 225 if (!imi.isSystem()) { 226 continue; 227 } 228 if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) { 229 continue; 230 } 231 if (firstMatchingIme != null) { 232 Slog.e(TAG, "At most one InputMethodService can be published in " 233 + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName 234 + ". Ignoring all of them."); 235 return null; 236 } 237 firstMatchingIme = imi; 238 } 239 return firstMatchingIme; 240 } 241 getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes)242 static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) { 243 if (enabledImes == null || enabledImes.isEmpty()) { 244 return null; 245 } 246 // We'd prefer to fall back on a system IME, since that is safer. 247 int i = enabledImes.size(); 248 int firstFoundSystemIme = -1; 249 while (i > 0) { 250 i--; 251 final InputMethodInfo imi = enabledImes.get(i); 252 if (imi.isAuxiliaryIme()) { 253 continue; 254 } 255 if (imi.isSystem() && SubtypeUtils.containsSubtypeOf(imi, ENGLISH_LOCALE, 256 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { 257 return imi; 258 } 259 if (firstFoundSystemIme < 0 && imi.isSystem()) { 260 firstFoundSystemIme = i; 261 } 262 } 263 return enabledImes.get(Math.max(firstFoundSystemIme, 0)); 264 } 265 isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, Context context, boolean checkDefaultAttribute)266 private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, 267 Context context, boolean checkDefaultAttribute) { 268 if (!imi.isSystem()) { 269 return false; 270 } 271 if (checkDefaultAttribute && !imi.isDefault(context)) { 272 return false; 273 } 274 if (!imi.isAuxiliaryIme()) { 275 return false; 276 } 277 final int subtypeCount = imi.getSubtypeCount(); 278 for (int i = 0; i < subtypeCount; ++i) { 279 final InputMethodSubtype s = imi.getSubtypeAt(i); 280 if (s.overridesImplicitlyEnabledSubtype()) { 281 return true; 282 } 283 } 284 return false; 285 } 286 287 @Nullable getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis, Context context)288 private static Locale getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis, 289 Context context) { 290 // At first, find the fallback locale from the IMEs that are declared as "default" in the 291 // current locale. Note that IME developers can declare an IME as "default" only for 292 // some particular locales but "not default" for other locales. 293 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { 294 for (int i = 0; i < imis.size(); ++i) { 295 if (isSystemImeThatHasSubtypeOf(imis.get(i), context, 296 true /* checkDefaultAttribute */, fallbackLocale, 297 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { 298 return fallbackLocale; 299 } 300 } 301 } 302 // If no fallback locale is found in the above condition, find fallback locales regardless 303 // of the "default" attribute as a last resort. 304 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { 305 for (int i = 0; i < imis.size(); ++i) { 306 if (isSystemImeThatHasSubtypeOf(imis.get(i), context, 307 false /* checkDefaultAttribute */, fallbackLocale, 308 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { 309 return fallbackLocale; 310 } 311 } 312 } 313 Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); 314 return null; 315 } 316 isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, String requiredSubtypeMode)317 private static boolean isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, 318 boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, 319 String requiredSubtypeMode) { 320 if (!imi.isSystem()) { 321 return false; 322 } 323 if (checkDefaultAttribute && !imi.isDefault(context)) { 324 return false; 325 } 326 return SubtypeUtils.containsSubtypeOf(imi, requiredLocale, checkCountry, 327 requiredSubtypeMode); 328 } 329 } 330