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.os.Parcel; 26 import android.text.TextUtils; 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(List, 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(List<InputMethodInfo> imis, Context context, boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, String requiredSubtypeMode)67 InputMethodListBuilder fillImes(List<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 fillAuxiliaryImes(List<InputMethodInfo> imis, Context context)80 InputMethodListBuilder fillAuxiliaryImes(List<InputMethodInfo> imis, Context context) { 81 // If one or more auxiliary input methods are available, OK to stop populating the list. 82 for (final InputMethodInfo imi : mInputMethodSet) { 83 if (imi.isAuxiliaryIme()) { 84 return this; 85 } 86 } 87 boolean added = false; 88 for (int i = 0; i < imis.size(); ++i) { 89 final InputMethodInfo imi = imis.get(i); 90 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, 91 true /* checkDefaultAttribute */)) { 92 mInputMethodSet.add(imi); 93 added = true; 94 } 95 } 96 if (added) { 97 return this; 98 } 99 for (int i = 0; i < imis.size(); ++i) { 100 final InputMethodInfo imi = imis.get(i); 101 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, 102 false /* checkDefaultAttribute */)) { 103 mInputMethodSet.add(imi); 104 } 105 } 106 return this; 107 108 } 109 isEmpty()110 public boolean isEmpty() { 111 return mInputMethodSet.isEmpty(); 112 } 113 114 @NonNull build()115 public ArrayList<InputMethodInfo> build() { 116 return new ArrayList<>(mInputMethodSet); 117 } 118 } 119 getMinimumKeyboardSetWithSystemLocale( List<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, @Nullable Locale fallbackLocale)120 private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( 121 List<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, 122 @Nullable Locale fallbackLocale) { 123 // Once the system becomes ready, we pick up at least one keyboard in the following order. 124 // Secondary users fall into this category in general. 125 // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true 126 // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false 127 // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true 128 // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false 129 // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true 130 // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false 131 // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. 132 133 final InputMethodListBuilder builder = new InputMethodListBuilder(); 134 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 135 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 136 if (!builder.isEmpty()) { 137 return builder; 138 } 139 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 140 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 141 if (!builder.isEmpty()) { 142 return builder; 143 } 144 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 145 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 146 if (!builder.isEmpty()) { 147 return builder; 148 } 149 builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, 150 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 151 if (!builder.isEmpty()) { 152 return builder; 153 } 154 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, 155 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 156 if (!builder.isEmpty()) { 157 return builder; 158 } 159 builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, 160 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); 161 if (!builder.isEmpty()) { 162 return builder; 163 } 164 Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) 165 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); 166 return builder; 167 } 168 getDefaultEnabledImes( Context context, List<InputMethodInfo> imis, boolean onlyMinimum)169 static ArrayList<InputMethodInfo> getDefaultEnabledImes( 170 Context context, List<InputMethodInfo> imis, boolean onlyMinimum) { 171 final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); 172 // We will primarily rely on the system locale, but also keep relying on the fallback locale 173 // as a last resort. 174 // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), 175 // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" 176 // subtype) 177 final Locale systemLocale = LocaleUtils.getSystemLocaleFromContext(context); 178 final InputMethodListBuilder builder = 179 getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale); 180 if (!onlyMinimum) { 181 builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, 182 true /* checkCountry */, SUBTYPE_MODE_ANY) 183 .fillAuxiliaryImes(imis, context); 184 } 185 return builder.build(); 186 } 187 getDefaultEnabledImes( Context context, List<InputMethodInfo> imis)188 static ArrayList<InputMethodInfo> getDefaultEnabledImes( 189 Context context, List<InputMethodInfo> imis) { 190 return getDefaultEnabledImes(context, imis, false /* onlyMinimum */); 191 } 192 193 /** 194 * Chooses an eligible system voice IME from the given IMEs. 195 * 196 * @param methodMap Map from the IME ID to {@link InputMethodInfo}. 197 * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system 198 * config. 199 * @param currentDefaultVoiceImeId the default voice IME id, which may be {@code null} or 200 * the value assigned for 201 * {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD} 202 * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for 203 * the system voice IME. 204 */ 205 @Nullable chooseSystemVoiceIme( @onNull InputMethodMap methodMap, @Nullable String systemSpeechRecognizerPackageName, @Nullable String currentDefaultVoiceImeId)206 static InputMethodInfo chooseSystemVoiceIme( 207 @NonNull InputMethodMap methodMap, 208 @Nullable String systemSpeechRecognizerPackageName, 209 @Nullable String currentDefaultVoiceImeId) { 210 if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { 211 return null; 212 } 213 final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId); 214 // If the config matches the package of the setting, use the current one. 215 if (defaultVoiceIme != null && defaultVoiceIme.isSystem() 216 && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) { 217 return defaultVoiceIme; 218 } 219 InputMethodInfo firstMatchingIme = null; 220 final int methodCount = methodMap.size(); 221 for (int i = 0; i < methodCount; ++i) { 222 final InputMethodInfo imi = methodMap.valueAt(i); 223 if (!imi.isSystem()) { 224 continue; 225 } 226 if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) { 227 continue; 228 } 229 if (firstMatchingIme != null) { 230 Slog.e(TAG, "At most one InputMethodService can be published in " 231 + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName 232 + ". Ignoring all of them."); 233 return null; 234 } 235 firstMatchingIme = imi; 236 } 237 return firstMatchingIme; 238 } 239 getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes)240 static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) { 241 if (enabledImes == null || enabledImes.isEmpty()) { 242 return null; 243 } 244 // We'd prefer to fall back on a system IME, since that is safer. 245 int i = enabledImes.size(); 246 int firstFoundSystemIme = -1; 247 while (i > 0) { 248 i--; 249 final InputMethodInfo imi = enabledImes.get(i); 250 if (imi.isAuxiliaryIme()) { 251 continue; 252 } 253 if (imi.isSystem() && SubtypeUtils.containsSubtypeOf(imi, ENGLISH_LOCALE, 254 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { 255 return imi; 256 } 257 if (firstFoundSystemIme < 0 && imi.isSystem()) { 258 firstFoundSystemIme = i; 259 } 260 } 261 return enabledImes.get(Math.max(firstFoundSystemIme, 0)); 262 } 263 isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, Context context, boolean checkDefaultAttribute)264 private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, 265 Context context, boolean checkDefaultAttribute) { 266 if (!imi.isSystem()) { 267 return false; 268 } 269 if (checkDefaultAttribute && !imi.isDefault(context)) { 270 return false; 271 } 272 if (!imi.isAuxiliaryIme()) { 273 return false; 274 } 275 final int subtypeCount = imi.getSubtypeCount(); 276 for (int i = 0; i < subtypeCount; ++i) { 277 final InputMethodSubtype s = imi.getSubtypeAt(i); 278 if (s.overridesImplicitlyEnabledSubtype()) { 279 return true; 280 } 281 } 282 return false; 283 } 284 285 @Nullable getFallbackLocaleForDefaultIme(List<InputMethodInfo> imis, Context context)286 private static Locale getFallbackLocaleForDefaultIme(List<InputMethodInfo> imis, 287 Context context) { 288 // At first, find the fallback locale from the IMEs that are declared as "default" in the 289 // current locale. Note that IME developers can declare an IME as "default" only for 290 // some particular locales but "not default" for other locales. 291 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { 292 for (int i = 0; i < imis.size(); ++i) { 293 if (isSystemImeThatHasSubtypeOf(imis.get(i), context, 294 true /* checkDefaultAttribute */, fallbackLocale, 295 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { 296 return fallbackLocale; 297 } 298 } 299 } 300 // If no fallback locale is found in the above condition, find fallback locales regardless 301 // of the "default" attribute as a last resort. 302 for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { 303 for (int i = 0; i < imis.size(); ++i) { 304 if (isSystemImeThatHasSubtypeOf(imis.get(i), context, 305 false /* checkDefaultAttribute */, fallbackLocale, 306 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { 307 return fallbackLocale; 308 } 309 } 310 } 311 Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); 312 return null; 313 } 314 isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, String requiredSubtypeMode)315 private static boolean isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, 316 boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, 317 String requiredSubtypeMode) { 318 if (!imi.isSystem()) { 319 return false; 320 } 321 if (checkDefaultAttribute && !imi.isDefault(context)) { 322 return false; 323 } 324 return SubtypeUtils.containsSubtypeOf(imi, requiredLocale, checkCountry, 325 requiredSubtypeMode); 326 } 327 328 /** 329 * Marshals the given {@link InputMethodInfo} into a byte array. 330 * 331 * @param imi {@link InputMethodInfo} to be marshalled 332 * @return a byte array where the given {@link InputMethodInfo} is marshalled 333 */ 334 @NonNull marshal(@onNull InputMethodInfo imi)335 static byte[] marshal(@NonNull InputMethodInfo imi) { 336 Parcel parcel = null; 337 try { 338 parcel = Parcel.obtain(); 339 parcel.writeTypedObject(imi, 0); 340 return parcel.marshall(); 341 } finally { 342 if (parcel != null) { 343 parcel.recycle(); 344 } 345 } 346 } 347 } 348