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