| /* |
| * Copyright (C) 2022 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.server.inputmethod; |
| |
| import static com.android.server.inputmethod.SubtypeUtils.SUBTYPE_MODE_ANY; |
| import static com.android.server.inputmethod.SubtypeUtils.SUBTYPE_MODE_KEYBOARD; |
| |
| import android.annotation.NonNull; |
| import android.annotation.Nullable; |
| import android.content.Context; |
| import android.text.TextUtils; |
| import android.util.ArrayMap; |
| import android.util.Slog; |
| import android.view.inputmethod.InputMethodInfo; |
| import android.view.inputmethod.InputMethodSubtype; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.LinkedHashSet; |
| import java.util.List; |
| import java.util.Locale; |
| |
| /** |
| * This class provides utility methods to generate or filter {@link InputMethodInfo} for |
| * {@link InputMethodManagerService}. |
| * |
| * <p>This class is intentionally package-private. Utility methods here are tightly coupled with |
| * implementation details in {@link InputMethodManagerService}. Hence this class is not suitable |
| * for other components to directly use.</p> |
| */ |
| final class InputMethodInfoUtils { |
| private static final String TAG = "InputMethodInfoUtils"; |
| |
| /** |
| * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs |
| * that are mainly used until the system becomes ready. Note that {@link Locale} in this array |
| * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH} |
| * doesn't automatically match {@code Locale("en", "IN")}. |
| */ |
| private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = { |
| Locale.ENGLISH, // "en" |
| Locale.US, // "en_US" |
| Locale.UK, // "en_GB" |
| }; |
| private static final Locale ENGLISH_LOCALE = new Locale("en"); |
| |
| private static final class InputMethodListBuilder { |
| // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration |
| // order can have non-trivial effect in the call sites. |
| @NonNull |
| private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>(); |
| |
| InputMethodListBuilder fillImes(ArrayList<InputMethodInfo> imis, Context context, |
| boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, |
| String requiredSubtypeMode) { |
| for (int i = 0; i < imis.size(); ++i) { |
| final InputMethodInfo imi = imis.get(i); |
| if (isSystemImeThatHasSubtypeOf(imi, context, |
| checkDefaultAttribute, locale, checkCountry, requiredSubtypeMode)) { |
| mInputMethodSet.add(imi); |
| } |
| } |
| return this; |
| } |
| |
| // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be |
| // documented more clearly. |
| InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) { |
| // If one or more auxiliary input methods are available, OK to stop populating the list. |
| for (final InputMethodInfo imi : mInputMethodSet) { |
| if (imi.isAuxiliaryIme()) { |
| return this; |
| } |
| } |
| boolean added = false; |
| for (int i = 0; i < imis.size(); ++i) { |
| final InputMethodInfo imi = imis.get(i); |
| if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, |
| true /* checkDefaultAttribute */)) { |
| mInputMethodSet.add(imi); |
| added = true; |
| } |
| } |
| if (added) { |
| return this; |
| } |
| for (int i = 0; i < imis.size(); ++i) { |
| final InputMethodInfo imi = imis.get(i); |
| if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context, |
| false /* checkDefaultAttribute */)) { |
| mInputMethodSet.add(imi); |
| } |
| } |
| return this; |
| |
| } |
| |
| public boolean isEmpty() { |
| return mInputMethodSet.isEmpty(); |
| } |
| |
| @NonNull |
| public ArrayList<InputMethodInfo> build() { |
| return new ArrayList<>(mInputMethodSet); |
| } |
| } |
| |
| private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale( |
| ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, |
| @Nullable Locale fallbackLocale) { |
| // Once the system becomes ready, we pick up at least one keyboard in the following order. |
| // Secondary users fall into this category in general. |
| // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true |
| // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false |
| // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true |
| // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false |
| // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true |
| // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false |
| // TODO: We should check isAsciiCapable instead of relying on fallbackLocale. |
| |
| final InputMethodListBuilder builder = new InputMethodListBuilder(); |
| builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, |
| true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); |
| if (!builder.isEmpty()) { |
| return builder; |
| } |
| builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, |
| false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); |
| if (!builder.isEmpty()) { |
| return builder; |
| } |
| builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, |
| true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); |
| if (!builder.isEmpty()) { |
| return builder; |
| } |
| builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale, |
| false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); |
| if (!builder.isEmpty()) { |
| return builder; |
| } |
| builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, |
| true /* checkCountry */, SUBTYPE_MODE_KEYBOARD); |
| if (!builder.isEmpty()) { |
| return builder; |
| } |
| builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale, |
| false /* checkCountry */, SUBTYPE_MODE_KEYBOARD); |
| if (!builder.isEmpty()) { |
| return builder; |
| } |
| Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray()) |
| + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale); |
| return builder; |
| } |
| |
| static ArrayList<InputMethodInfo> getDefaultEnabledImes( |
| Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum) { |
| final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context); |
| // We will primarily rely on the system locale, but also keep relying on the fallback locale |
| // as a last resort. |
| // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs), |
| // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic" |
| // subtype) |
| final Locale systemLocale = LocaleUtils.getSystemLocaleFromContext(context); |
| final InputMethodListBuilder builder = |
| getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale); |
| if (!onlyMinimum) { |
| builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale, |
| true /* checkCountry */, SUBTYPE_MODE_ANY) |
| .fillAuxiliaryImes(imis, context); |
| } |
| return builder.build(); |
| } |
| |
| static ArrayList<InputMethodInfo> getDefaultEnabledImes( |
| Context context, ArrayList<InputMethodInfo> imis) { |
| return getDefaultEnabledImes(context, imis, false /* onlyMinimum */); |
| } |
| |
| /** |
| * Chooses an eligible system voice IME from the given IMEs. |
| * |
| * @param methodMap Map from the IME ID to {@link InputMethodInfo}. |
| * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system |
| * config. |
| * @param currentDefaultVoiceImeId the default voice IME id, which may be {@code null} or |
| * the value assigned for |
| * {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD} |
| * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for |
| * the system voice IME. |
| */ |
| @Nullable |
| static InputMethodInfo chooseSystemVoiceIme( |
| @NonNull ArrayMap<String, InputMethodInfo> methodMap, |
| @Nullable String systemSpeechRecognizerPackageName, |
| @Nullable String currentDefaultVoiceImeId) { |
| if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) { |
| return null; |
| } |
| final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId); |
| // If the config matches the package of the setting, use the current one. |
| if (defaultVoiceIme != null && defaultVoiceIme.isSystem() |
| && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) { |
| return defaultVoiceIme; |
| } |
| InputMethodInfo firstMatchingIme = null; |
| final int methodCount = methodMap.size(); |
| for (int i = 0; i < methodCount; ++i) { |
| final InputMethodInfo imi = methodMap.valueAt(i); |
| if (!imi.isSystem()) { |
| continue; |
| } |
| if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) { |
| continue; |
| } |
| if (firstMatchingIme != null) { |
| Slog.e(TAG, "At most one InputMethodService can be published in " |
| + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName |
| + ". Ignoring all of them."); |
| return null; |
| } |
| firstMatchingIme = imi; |
| } |
| return firstMatchingIme; |
| } |
| |
| static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) { |
| if (enabledImes == null || enabledImes.isEmpty()) { |
| return null; |
| } |
| // We'd prefer to fall back on a system IME, since that is safer. |
| int i = enabledImes.size(); |
| int firstFoundSystemIme = -1; |
| while (i > 0) { |
| i--; |
| final InputMethodInfo imi = enabledImes.get(i); |
| if (imi.isAuxiliaryIme()) { |
| continue; |
| } |
| if (imi.isSystem() && SubtypeUtils.containsSubtypeOf(imi, ENGLISH_LOCALE, |
| false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { |
| return imi; |
| } |
| if (firstFoundSystemIme < 0 && imi.isSystem()) { |
| firstFoundSystemIme = i; |
| } |
| } |
| return enabledImes.get(Math.max(firstFoundSystemIme, 0)); |
| } |
| |
| private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, |
| Context context, boolean checkDefaultAttribute) { |
| if (!imi.isSystem()) { |
| return false; |
| } |
| if (checkDefaultAttribute && !imi.isDefault(context)) { |
| return false; |
| } |
| if (!imi.isAuxiliaryIme()) { |
| return false; |
| } |
| final int subtypeCount = imi.getSubtypeCount(); |
| for (int i = 0; i < subtypeCount; ++i) { |
| final InputMethodSubtype s = imi.getSubtypeAt(i); |
| if (s.overridesImplicitlyEnabledSubtype()) { |
| return true; |
| } |
| } |
| return false; |
| } |
| |
| @Nullable |
| private static Locale getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis, |
| Context context) { |
| // At first, find the fallback locale from the IMEs that are declared as "default" in the |
| // current locale. Note that IME developers can declare an IME as "default" only for |
| // some particular locales but "not default" for other locales. |
| for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { |
| for (int i = 0; i < imis.size(); ++i) { |
| if (isSystemImeThatHasSubtypeOf(imis.get(i), context, |
| true /* checkDefaultAttribute */, fallbackLocale, |
| true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { |
| return fallbackLocale; |
| } |
| } |
| } |
| // If no fallback locale is found in the above condition, find fallback locales regardless |
| // of the "default" attribute as a last resort. |
| for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) { |
| for (int i = 0; i < imis.size(); ++i) { |
| if (isSystemImeThatHasSubtypeOf(imis.get(i), context, |
| false /* checkDefaultAttribute */, fallbackLocale, |
| true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) { |
| return fallbackLocale; |
| } |
| } |
| } |
| Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray())); |
| return null; |
| } |
| |
| private static boolean isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, |
| boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, |
| String requiredSubtypeMode) { |
| if (!imi.isSystem()) { |
| return false; |
| } |
| if (checkDefaultAttribute && !imi.isDefault(context)) { |
| return false; |
| } |
| return SubtypeUtils.containsSubtypeOf(imi, requiredLocale, checkCountry, |
| requiredSubtypeMode); |
| } |
| } |