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