• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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 android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserHandleAware;
22 import android.annotation.UserIdInt;
23 import android.app.AppOpsManager;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.res.Resources;
29 import android.os.Build;
30 import android.os.LocaleList;
31 import android.os.UserHandle;
32 import android.provider.Settings;
33 import android.text.TextUtils;
34 import android.util.ArrayMap;
35 import android.util.ArraySet;
36 import android.util.Pair;
37 import android.util.Printer;
38 import android.util.Slog;
39 import android.view.inputmethod.InputMethodInfo;
40 import android.view.inputmethod.InputMethodSubtype;
41 import android.view.textservice.SpellCheckerInfo;
42 
43 import com.android.internal.annotations.GuardedBy;
44 import com.android.internal.annotations.VisibleForTesting;
45 import com.android.internal.inputmethod.StartInputFlags;
46 import com.android.server.LocalServices;
47 import com.android.server.pm.UserManagerInternal;
48 import com.android.server.textservices.TextServicesManagerInternal;
49 
50 import java.io.PrintWriter;
51 import java.util.ArrayList;
52 import java.util.Arrays;
53 import java.util.LinkedHashSet;
54 import java.util.List;
55 import java.util.Locale;
56 import java.util.function.Predicate;
57 
58 /**
59  * This class provides random static utility methods for {@link InputMethodManagerService} and its
60  * utility classes.
61  *
62  * <p>This class is intentionally package-private.  Utility methods here are tightly coupled with
63  * implementation details in {@link InputMethodManagerService}.  Hence this class is not suitable
64  * for other components to directly use.</p>
65  */
66 final class InputMethodUtils {
67     public static final boolean DEBUG = false;
68     static final int NOT_A_SUBTYPE_ID = -1;
69     private static final String SUBTYPE_MODE_ANY = null;
70     static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
71     private static final String TAG = "InputMethodUtils";
72     private static final Locale ENGLISH_LOCALE = new Locale("en");
73     private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
74     private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
75             "EnabledWhenDefaultIsNotAsciiCapable";
76 
77     // The string for enabled input method is saved as follows:
78     // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
79     private static final char INPUT_METHOD_SEPARATOR = ':';
80     private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
81     /**
82      * Used in {@link #getFallbackLocaleForDefaultIme(ArrayList, Context)} to find the fallback IMEs
83      * that are mainly used until the system becomes ready. Note that {@link Locale} in this array
84      * is checked with {@link Locale#equals(Object)}, which means that {@code Locale.ENGLISH}
85      * doesn't automatically match {@code Locale("en", "IN")}.
86      */
87     private static final Locale[] SEARCH_ORDER_OF_FALLBACK_LOCALES = {
88         Locale.ENGLISH, // "en"
89         Locale.US, // "en_US"
90         Locale.UK, // "en_GB"
91     };
92 
93     // A temporary workaround for the performance concerns in
94     // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo).
95     // TODO: Optimize all the critical paths including this one.
96     private static final Object sCacheLock = new Object();
97     @GuardedBy("sCacheLock")
98     private static LocaleList sCachedSystemLocales;
99     @GuardedBy("sCacheLock")
100     private static InputMethodInfo sCachedInputMethodInfo;
101     @GuardedBy("sCacheLock")
102     private static ArrayList<InputMethodSubtype> sCachedResult;
103 
InputMethodUtils()104     private InputMethodUtils() {
105         // This utility class is not publicly instantiable.
106     }
107 
108     // ----------------------------------------------------------------------
109     // Utilities for debug
getApiCallStack()110     static String getApiCallStack() {
111         String apiCallStack = "";
112         try {
113             throw new RuntimeException();
114         } catch (RuntimeException e) {
115             final StackTraceElement[] frames = e.getStackTrace();
116             for (int j = 1; j < frames.length; ++j) {
117                 final String tempCallStack = frames[j].toString();
118                 if (TextUtils.isEmpty(apiCallStack)) {
119                     // Overwrite apiCallStack if it's empty
120                     apiCallStack = tempCallStack;
121                 } else if (tempCallStack.indexOf("Transact(") < 0) {
122                     // Overwrite apiCallStack if it's not a binder call
123                     apiCallStack = tempCallStack;
124                 } else {
125                     break;
126                 }
127             }
128         }
129         return apiCallStack;
130     }
131     // ----------------------------------------------------------------------
132 
isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context, boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry, String requiredSubtypeMode)133     private static boolean isSystemImeThatHasSubtypeOf(InputMethodInfo imi, Context context,
134             boolean checkDefaultAttribute, @Nullable Locale requiredLocale, boolean checkCountry,
135             String requiredSubtypeMode) {
136         if (!imi.isSystem()) {
137             return false;
138         }
139         if (checkDefaultAttribute && !imi.isDefault(context)) {
140             return false;
141         }
142         if (!containsSubtypeOf(imi, requiredLocale, checkCountry, requiredSubtypeMode)) {
143             return false;
144         }
145         return true;
146     }
147 
148     @Nullable
getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis, Context context)149     private static Locale getFallbackLocaleForDefaultIme(ArrayList<InputMethodInfo> imis,
150             Context context) {
151         // At first, find the fallback locale from the IMEs that are declared as "default" in the
152         // current locale.  Note that IME developers can declare an IME as "default" only for
153         // some particular locales but "not default" for other locales.
154         for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
155             for (int i = 0; i < imis.size(); ++i) {
156                 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
157                         true /* checkDefaultAttribute */, fallbackLocale,
158                         true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
159                     return fallbackLocale;
160                 }
161             }
162         }
163         // If no fallback locale is found in the above condition, find fallback locales regardless
164         // of the "default" attribute as a last resort.
165         for (final Locale fallbackLocale : SEARCH_ORDER_OF_FALLBACK_LOCALES) {
166             for (int i = 0; i < imis.size(); ++i) {
167                 if (isSystemImeThatHasSubtypeOf(imis.get(i), context,
168                         false /* checkDefaultAttribute */, fallbackLocale,
169                         true /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
170                     return fallbackLocale;
171                 }
172             }
173         }
174         Slog.w(TAG, "Found no fallback locale. imis=" + Arrays.toString(imis.toArray()));
175         return null;
176     }
177 
isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi, Context context, boolean checkDefaultAttribute)178     private static boolean isSystemAuxilialyImeThatHasAutomaticSubtype(InputMethodInfo imi,
179             Context context, boolean checkDefaultAttribute) {
180         if (!imi.isSystem()) {
181             return false;
182         }
183         if (checkDefaultAttribute && !imi.isDefault(context)) {
184             return false;
185         }
186         if (!imi.isAuxiliaryIme()) {
187             return false;
188         }
189         final int subtypeCount = imi.getSubtypeCount();
190         for (int i = 0; i < subtypeCount; ++i) {
191             final InputMethodSubtype s = imi.getSubtypeAt(i);
192             if (s.overridesImplicitlyEnabledSubtype()) {
193                 return true;
194             }
195         }
196         return false;
197     }
198 
getSystemLocaleFromContext(Context context)199     private static Locale getSystemLocaleFromContext(Context context) {
200         try {
201             return context.getResources().getConfiguration().locale;
202         } catch (Resources.NotFoundException ex) {
203             return null;
204         }
205     }
206 
207     private static final class InputMethodListBuilder {
208         // Note: We use LinkedHashSet instead of android.util.ArraySet because the enumeration
209         // order can have non-trivial effect in the call sites.
210         @NonNull
211         private final LinkedHashSet<InputMethodInfo> mInputMethodSet = new LinkedHashSet<>();
212 
fillImes(ArrayList<InputMethodInfo> imis, Context context, boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry, String requiredSubtypeMode)213         InputMethodListBuilder fillImes(ArrayList<InputMethodInfo> imis, Context context,
214                 boolean checkDefaultAttribute, @Nullable Locale locale, boolean checkCountry,
215                 String requiredSubtypeMode) {
216             for (int i = 0; i < imis.size(); ++i) {
217                 final InputMethodInfo imi = imis.get(i);
218                 if (isSystemImeThatHasSubtypeOf(imi, context, checkDefaultAttribute, locale,
219                         checkCountry, requiredSubtypeMode)) {
220                     mInputMethodSet.add(imi);
221                 }
222             }
223             return this;
224         }
225 
226         // TODO: The behavior of InputMethodSubtype#overridesImplicitlyEnabledSubtype() should be
227         // documented more clearly.
fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context)228         InputMethodListBuilder fillAuxiliaryImes(ArrayList<InputMethodInfo> imis, Context context) {
229             // If one or more auxiliary input methods are available, OK to stop populating the list.
230             for (final InputMethodInfo imi : mInputMethodSet) {
231                 if (imi.isAuxiliaryIme()) {
232                     return this;
233                 }
234             }
235             boolean added = false;
236             for (int i = 0; i < imis.size(); ++i) {
237                 final InputMethodInfo imi = imis.get(i);
238                 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
239                         true /* checkDefaultAttribute */)) {
240                     mInputMethodSet.add(imi);
241                     added = true;
242                 }
243             }
244             if (added) {
245                 return this;
246             }
247             for (int i = 0; i < imis.size(); ++i) {
248                 final InputMethodInfo imi = imis.get(i);
249                 if (isSystemAuxilialyImeThatHasAutomaticSubtype(imi, context,
250                         false /* checkDefaultAttribute */)) {
251                     mInputMethodSet.add(imi);
252                 }
253             }
254             return this;
255         }
256 
isEmpty()257         public boolean isEmpty() {
258             return mInputMethodSet.isEmpty();
259         }
260 
261         @NonNull
build()262         public ArrayList<InputMethodInfo> build() {
263             return new ArrayList<>(mInputMethodSet);
264         }
265     }
266 
getMinimumKeyboardSetWithSystemLocale( ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale, @Nullable Locale fallbackLocale)267     private static InputMethodListBuilder getMinimumKeyboardSetWithSystemLocale(
268             ArrayList<InputMethodInfo> imis, Context context, @Nullable Locale systemLocale,
269             @Nullable Locale fallbackLocale) {
270         // Once the system becomes ready, we pick up at least one keyboard in the following order.
271         // Secondary users fall into this category in general.
272         // 1. checkDefaultAttribute: true, locale: systemLocale, checkCountry: true
273         // 2. checkDefaultAttribute: true, locale: systemLocale, checkCountry: false
274         // 3. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: true
275         // 4. checkDefaultAttribute: true, locale: fallbackLocale, checkCountry: false
276         // 5. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: true
277         // 6. checkDefaultAttribute: false, locale: fallbackLocale, checkCountry: false
278         // TODO: We should check isAsciiCapable instead of relying on fallbackLocale.
279 
280         final InputMethodListBuilder builder = new InputMethodListBuilder();
281         builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
282                 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
283         if (!builder.isEmpty()) {
284             return builder;
285         }
286         builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
287                 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
288         if (!builder.isEmpty()) {
289             return builder;
290         }
291         builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
292                 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
293         if (!builder.isEmpty()) {
294             return builder;
295         }
296         builder.fillImes(imis, context, true /* checkDefaultAttribute */, fallbackLocale,
297                 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
298         if (!builder.isEmpty()) {
299             return builder;
300         }
301         builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
302                 true /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
303         if (!builder.isEmpty()) {
304             return builder;
305         }
306         builder.fillImes(imis, context, false /* checkDefaultAttribute */, fallbackLocale,
307                 false /* checkCountry */, SUBTYPE_MODE_KEYBOARD);
308         if (!builder.isEmpty()) {
309             return builder;
310         }
311         Slog.w(TAG, "No software keyboard is found. imis=" + Arrays.toString(imis.toArray())
312                 + " systemLocale=" + systemLocale + " fallbackLocale=" + fallbackLocale);
313         return builder;
314     }
315 
getDefaultEnabledImes( Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum)316     static ArrayList<InputMethodInfo> getDefaultEnabledImes(
317             Context context, ArrayList<InputMethodInfo> imis, boolean onlyMinimum) {
318         final Locale fallbackLocale = getFallbackLocaleForDefaultIme(imis, context);
319         // We will primarily rely on the system locale, but also keep relying on the fallback locale
320         // as a last resort.
321         // Also pick up suitable IMEs regardless of the software keyboard support (e.g. Voice IMEs),
322         // then pick up suitable auxiliary IMEs when necessary (e.g. Voice IMEs with "automatic"
323         // subtype)
324         final Locale systemLocale = getSystemLocaleFromContext(context);
325         final InputMethodListBuilder builder =
326                 getMinimumKeyboardSetWithSystemLocale(imis, context, systemLocale, fallbackLocale);
327         if (!onlyMinimum) {
328             builder.fillImes(imis, context, true /* checkDefaultAttribute */, systemLocale,
329                     true /* checkCountry */, SUBTYPE_MODE_ANY)
330                     .fillAuxiliaryImes(imis, context);
331         }
332         return builder.build();
333     }
334 
getDefaultEnabledImes( Context context, ArrayList<InputMethodInfo> imis)335     static ArrayList<InputMethodInfo> getDefaultEnabledImes(
336             Context context, ArrayList<InputMethodInfo> imis) {
337         return getDefaultEnabledImes(context, imis, false /* onlyMinimum */);
338     }
339 
340     /**
341      * Chooses an eligible system voice IME from the given IMEs.
342      *
343      * @param methodMap Map from the IME ID to {@link InputMethodInfo}.
344      * @param systemSpeechRecognizerPackageName System speech recognizer configured by the system
345      *                                          config.
346      * @param currentDefaultVoiceImeId IME ID currently set to
347      *                                 {@link Settings.Secure#DEFAULT_VOICE_INPUT_METHOD}
348      * @return {@link InputMethodInfo} that is found in {@code methodMap} and most suitable for
349      *                                 the system voice IME.
350      */
351     @Nullable
chooseSystemVoiceIme( @onNull ArrayMap<String, InputMethodInfo> methodMap, @Nullable String systemSpeechRecognizerPackageName, @Nullable String currentDefaultVoiceImeId)352     static InputMethodInfo chooseSystemVoiceIme(
353             @NonNull ArrayMap<String, InputMethodInfo> methodMap,
354             @Nullable String systemSpeechRecognizerPackageName,
355             @Nullable String currentDefaultVoiceImeId) {
356         if (TextUtils.isEmpty(systemSpeechRecognizerPackageName)) {
357             return null;
358         }
359         final InputMethodInfo defaultVoiceIme = methodMap.get(currentDefaultVoiceImeId);
360         // If the config matches the package of the setting, use the current one.
361         if (defaultVoiceIme != null && defaultVoiceIme.isSystem()
362                 && defaultVoiceIme.getPackageName().equals(systemSpeechRecognizerPackageName)) {
363             return defaultVoiceIme;
364         }
365         InputMethodInfo firstMatchingIme = null;
366         final int methodCount = methodMap.size();
367         for (int i = 0; i < methodCount; ++i) {
368             final InputMethodInfo imi = methodMap.valueAt(i);
369             if (!imi.isSystem()) {
370                 continue;
371             }
372             if (!TextUtils.equals(imi.getPackageName(), systemSpeechRecognizerPackageName)) {
373                 continue;
374             }
375             if (firstMatchingIme != null) {
376                 Slog.e(TAG, "At most one InputMethodService can be published in "
377                         + "systemSpeechRecognizer: " + systemSpeechRecognizerPackageName
378                         + ". Ignoring all of them.");
379                 return null;
380             }
381             firstMatchingIme = imi;
382         }
383         return firstMatchingIme;
384     }
385 
containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, boolean checkCountry, String mode)386     static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale,
387             boolean checkCountry, String mode) {
388         if (locale == null) {
389             return false;
390         }
391         final int N = imi.getSubtypeCount();
392         for (int i = 0; i < N; ++i) {
393             final InputMethodSubtype subtype = imi.getSubtypeAt(i);
394             if (checkCountry) {
395                 final Locale subtypeLocale = subtype.getLocaleObject();
396                 if (subtypeLocale == null ||
397                         !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
398                         !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
399                     continue;
400                 }
401             } else {
402                 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
403                         subtype.getLocale()));
404                 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
405                     continue;
406                 }
407             }
408             if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
409                     mode.equalsIgnoreCase(subtype.getMode())) {
410                 return true;
411             }
412         }
413         return false;
414     }
415 
getSubtypes(InputMethodInfo imi)416     static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
417         ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
418         final int subtypeCount = imi.getSubtypeCount();
419         for (int i = 0; i < subtypeCount; ++i) {
420             subtypes.add(imi.getSubtypeAt(i));
421         }
422         return subtypes;
423     }
424 
getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes)425     static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
426         if (enabledImes == null || enabledImes.isEmpty()) {
427             return null;
428         }
429         // We'd prefer to fall back on a system IME, since that is safer.
430         int i = enabledImes.size();
431         int firstFoundSystemIme = -1;
432         while (i > 0) {
433             i--;
434             final InputMethodInfo imi = enabledImes.get(i);
435             if (imi.isAuxiliaryIme()) {
436                 continue;
437             }
438             if (imi.isSystem() && containsSubtypeOf(
439                     imi, ENGLISH_LOCALE, false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
440                 return imi;
441             }
442             if (firstFoundSystemIme < 0 && imi.isSystem()) {
443                 firstFoundSystemIme = i;
444             }
445         }
446         return enabledImes.get(Math.max(firstFoundSystemIme, 0));
447     }
448 
isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode)449     static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
450         return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
451     }
452 
getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode)453     static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
454         if (imi != null) {
455             final int subtypeCount = imi.getSubtypeCount();
456             for (int i = 0; i < subtypeCount; ++i) {
457                 InputMethodSubtype ims = imi.getSubtypeAt(i);
458                 if (subtypeHashCode == ims.hashCode()) {
459                     return i;
460                 }
461             }
462         }
463         return NOT_A_SUBTYPE_ID;
464     }
465 
466     private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
467             new LocaleUtils.LocaleExtractor<InputMethodSubtype>() {
468                 @Override
469                 public Locale get(InputMethodSubtype source) {
470                     return source != null ? source.getLocaleObject() : null;
471                 }
472             };
473 
474     @VisibleForTesting
getImplicitlyApplicableSubtypesLocked( Resources res, InputMethodInfo imi)475     static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
476             Resources res, InputMethodInfo imi) {
477         final LocaleList systemLocales = res.getConfiguration().getLocales();
478 
479         synchronized (sCacheLock) {
480             // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
481             // it does not check if subtypes are also identical.
482             if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
483                 return new ArrayList<>(sCachedResult);
484             }
485         }
486 
487         // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
488         // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
489         // LocaleList rather than Resource.
490         final ArrayList<InputMethodSubtype> result =
491                 getImplicitlyApplicableSubtypesLockedImpl(res, imi);
492         synchronized (sCacheLock) {
493             // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
494             sCachedSystemLocales = systemLocales;
495             sCachedInputMethodInfo = imi;
496             sCachedResult = new ArrayList<>(result);
497         }
498         return result;
499     }
500 
getImplicitlyApplicableSubtypesLockedImpl( Resources res, InputMethodInfo imi)501     private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
502             Resources res, InputMethodInfo imi) {
503         final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
504         final LocaleList systemLocales = res.getConfiguration().getLocales();
505         final String systemLocale = systemLocales.get(0).toString();
506         if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
507         final int numSubtypes = subtypes.size();
508 
509         // Handle overridesImplicitlyEnabledSubtype mechanism.
510         final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>();
511         for (int i = 0; i < numSubtypes; ++i) {
512             // scan overriding implicitly enabled subtypes.
513             final InputMethodSubtype subtype = subtypes.get(i);
514             if (subtype.overridesImplicitlyEnabledSubtype()) {
515                 final String mode = subtype.getMode();
516                 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
517                     applicableModeAndSubtypesMap.put(mode, subtype);
518                 }
519             }
520         }
521         if (applicableModeAndSubtypesMap.size() > 0) {
522             return new ArrayList<>(applicableModeAndSubtypesMap.values());
523         }
524 
525         final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
526                 new ArrayMap<>();
527         final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
528 
529         for (int i = 0; i < numSubtypes; ++i) {
530             final InputMethodSubtype subtype = subtypes.get(i);
531             final String mode = subtype.getMode();
532             if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
533                 keyboardSubtypes.add(subtype);
534             } else {
535                 if (!nonKeyboardSubtypesMap.containsKey(mode)) {
536                     nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
537                 }
538                 nonKeyboardSubtypesMap.get(mode).add(subtype);
539             }
540         }
541 
542         final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
543         LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
544                 applicableSubtypes);
545 
546         if (!applicableSubtypes.isEmpty()) {
547             boolean hasAsciiCapableKeyboard = false;
548             final int numApplicationSubtypes = applicableSubtypes.size();
549             for (int i = 0; i < numApplicationSubtypes; ++i) {
550                 final InputMethodSubtype subtype = applicableSubtypes.get(i);
551                 if (subtype.isAsciiCapable()) {
552                     hasAsciiCapableKeyboard = true;
553                     break;
554                 }
555             }
556             if (!hasAsciiCapableKeyboard) {
557                 final int numKeyboardSubtypes = keyboardSubtypes.size();
558                 for (int i = 0; i < numKeyboardSubtypes; ++i) {
559                     final InputMethodSubtype subtype = keyboardSubtypes.get(i);
560                     final String mode = subtype.getMode();
561                     if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
562                             TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
563                         applicableSubtypes.add(subtype);
564                     }
565                 }
566             }
567         }
568 
569         if (applicableSubtypes.isEmpty()) {
570             InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
571                     res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
572             if (lastResortKeyboardSubtype != null) {
573                 applicableSubtypes.add(lastResortKeyboardSubtype);
574             }
575         }
576 
577         // For each non-keyboard mode, extract subtypes with system locales.
578         for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
579             LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
580                     applicableSubtypes);
581         }
582 
583         return applicableSubtypes;
584     }
585 
586     /**
587      * Returns the language component of a given locale string.
588      * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
589      */
getLanguageFromLocaleString(String locale)590     private static String getLanguageFromLocaleString(String locale) {
591         final int idx = locale.indexOf('_');
592         if (idx < 0) {
593             return locale;
594         } else {
595             return locale.substring(0, idx);
596         }
597     }
598 
599     /**
600      * If there are no selected subtypes, tries finding the most applicable one according to the
601      * given locale.
602      * @param subtypes this function will search the most applicable subtype in subtypes
603      * @param mode subtypes will be filtered by mode
604      * @param locale subtypes will be filtered by locale
605      * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
606      * it will return the first subtype matched with mode
607      * @return the most applicable subtypeId
608      */
findLastResortApplicableSubtypeLocked( Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, boolean canIgnoreLocaleAsLastResort)609     static InputMethodSubtype findLastResortApplicableSubtypeLocked(
610             Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
611             boolean canIgnoreLocaleAsLastResort) {
612         if (subtypes == null || subtypes.size() == 0) {
613             return null;
614         }
615         if (TextUtils.isEmpty(locale)) {
616             locale = res.getConfiguration().locale.toString();
617         }
618         final String language = getLanguageFromLocaleString(locale);
619         boolean partialMatchFound = false;
620         InputMethodSubtype applicableSubtype = null;
621         InputMethodSubtype firstMatchedModeSubtype = null;
622         final int N = subtypes.size();
623         for (int i = 0; i < N; ++i) {
624             InputMethodSubtype subtype = subtypes.get(i);
625             final String subtypeLocale = subtype.getLocale();
626             final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
627             // An applicable subtype should match "mode". If mode is null, mode will be ignored,
628             // and all subtypes with all modes can be candidates.
629             if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
630                 if (firstMatchedModeSubtype == null) {
631                     firstMatchedModeSubtype = subtype;
632                 }
633                 if (locale.equals(subtypeLocale)) {
634                     // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
635                     applicableSubtype = subtype;
636                     break;
637                 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
638                     // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
639                     applicableSubtype = subtype;
640                     partialMatchFound = true;
641                 }
642             }
643         }
644 
645         if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
646             return firstMatchedModeSubtype;
647         }
648 
649         // The first subtype applicable to the system locale will be defined as the most applicable
650         // subtype.
651         if (DEBUG) {
652             if (applicableSubtype != null) {
653                 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
654                         + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
655             }
656         }
657         return applicableSubtype;
658     }
659 
canAddToLastInputMethod(InputMethodSubtype subtype)660     static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
661         if (subtype == null) return true;
662         return !subtype.isAuxiliary();
663     }
664 
665     @UserHandleAware
setNonSelectedSystemImesDisabledUntilUsed(PackageManager packageManagerForUser, List<InputMethodInfo> enabledImis)666     static void setNonSelectedSystemImesDisabledUntilUsed(PackageManager packageManagerForUser,
667             List<InputMethodInfo> enabledImis) {
668         if (DEBUG) {
669             Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
670         }
671         final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
672                 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
673         if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
674             return;
675         }
676         // Only the current spell checker should be treated as an enabled one.
677         final SpellCheckerInfo currentSpellChecker =
678                 TextServicesManagerInternal.get().getCurrentSpellCheckerForUser(
679                         packageManagerForUser.getUserId());
680         for (final String packageName : systemImesDisabledUntilUsed) {
681             if (DEBUG) {
682                 Slog.d(TAG, "check " + packageName);
683             }
684             boolean enabledIme = false;
685             for (int j = 0; j < enabledImis.size(); ++j) {
686                 final InputMethodInfo imi = enabledImis.get(j);
687                 if (packageName.equals(imi.getPackageName())) {
688                     enabledIme = true;
689                     break;
690                 }
691             }
692             if (enabledIme) {
693                 // enabled ime. skip
694                 continue;
695             }
696             if (currentSpellChecker != null
697                     && packageName.equals(currentSpellChecker.getPackageName())) {
698                 // enabled spell checker. skip
699                 if (DEBUG) {
700                     Slog.d(TAG, packageName + " is the current spell checker. skip");
701                 }
702                 continue;
703             }
704             ApplicationInfo ai = null;
705             try {
706                 ai = packageManagerForUser.getApplicationInfo(packageName,
707                         PackageManager.ApplicationInfoFlags.of(
708                                 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS));
709             } catch (PackageManager.NameNotFoundException e) {
710                 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
711                         + " userId=" + packageManagerForUser.getUserId(), e);
712                 continue;
713             }
714             if (ai == null) {
715                 // No app found for packageName
716                 continue;
717             }
718             final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
719             if (!isSystemPackage) {
720                 continue;
721             }
722             setDisabledUntilUsed(packageManagerForUser, packageName);
723         }
724     }
725 
setDisabledUntilUsed(PackageManager packageManagerForUser, String packageName)726     private static void setDisabledUntilUsed(PackageManager packageManagerForUser,
727             String packageName) {
728         final int state;
729         try {
730             state = packageManagerForUser.getApplicationEnabledSetting(packageName);
731         } catch (IllegalArgumentException e) {
732             Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
733                     + " userId=" + packageManagerForUser.getUserId(), e);
734             return;
735         }
736         if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
737                 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
738             if (DEBUG) {
739                 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
740             }
741             try {
742                 packageManagerForUser.setApplicationEnabledSetting(packageName,
743                         PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
744                         0 /* newState */);
745             } catch (IllegalArgumentException e) {
746                 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
747                         + " userId=" + packageManagerForUser.getUserId(), e);
748                 return;
749             }
750         } else {
751             if (DEBUG) {
752                 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
753             }
754         }
755     }
756 
getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi, InputMethodSubtype subtype)757     static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
758             InputMethodSubtype subtype) {
759         final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
760         return subtype != null
761                 ? TextUtils.concat(subtype.getDisplayName(context,
762                         imi.getPackageName(), imi.getServiceInfo().applicationInfo),
763                                 (TextUtils.isEmpty(imiLabel) ?
764                                         "" : " - " + imiLabel))
765                 : imiLabel;
766     }
767 
768     /**
769      * Returns true if a package name belongs to a UID.
770      *
771      * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
772      * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
773      * @param uid the UID to be validated.
774      * @param packageName the package name.
775      * @return {@code true} if the package name belongs to the UID.
776      */
checkIfPackageBelongsToUid(AppOpsManager appOpsManager, @UserIdInt int uid, String packageName)777     static boolean checkIfPackageBelongsToUid(AppOpsManager appOpsManager,
778             @UserIdInt int uid, String packageName) {
779         try {
780             appOpsManager.checkPackage(uid, packageName);
781             return true;
782         } catch (SecurityException e) {
783             return false;
784         }
785     }
786 
787     /**
788      * Utility class for putting and getting settings for InputMethod
789      * TODO: Move all putters and getters of settings to this class.
790      */
791     public static class InputMethodSettings {
792         private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
793                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
794 
795         private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
796                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
797 
798         private final Resources mRes;
799         private final ContentResolver mResolver;
800         private final ArrayMap<String, InputMethodInfo> mMethodMap;
801 
802         /**
803          * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}.
804          */
805         private final ArrayMap<String, String> mCopyOnWriteDataStore = new ArrayMap<>();
806 
807         private static final ArraySet<String> CLONE_TO_MANAGED_PROFILE = new ArraySet<>();
808         static {
809             Settings.Secure.getCloneToManagedProfileSettings(CLONE_TO_MANAGED_PROFILE);
810         }
811 
812         private static final UserManagerInternal sUserManagerInternal =
813                 LocalServices.getService(UserManagerInternal.class);
814 
815         private boolean mCopyOnWrite = false;
816         @NonNull
817         private String mEnabledInputMethodsStrCache = "";
818         @UserIdInt
819         private int mCurrentUserId;
820         private int[] mCurrentProfileIds = new int[0];
821 
buildEnabledInputMethodsSettingString( StringBuilder builder, Pair<String, ArrayList<String>> ime)822         private static void buildEnabledInputMethodsSettingString(
823                 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
824             builder.append(ime.first);
825             // Inputmethod and subtypes are saved in the settings as follows:
826             // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
827             for (String subtypeId: ime.second) {
828                 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
829             }
830         }
831 
buildInputMethodsAndSubtypeList( String enabledInputMethodsStr, TextUtils.SimpleStringSplitter inputMethodSplitter, TextUtils.SimpleStringSplitter subtypeSplitter)832         private static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
833                 String enabledInputMethodsStr,
834                 TextUtils.SimpleStringSplitter inputMethodSplitter,
835                 TextUtils.SimpleStringSplitter subtypeSplitter) {
836             ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
837             if (TextUtils.isEmpty(enabledInputMethodsStr)) {
838                 return imsList;
839             }
840             inputMethodSplitter.setString(enabledInputMethodsStr);
841             while (inputMethodSplitter.hasNext()) {
842                 String nextImsStr = inputMethodSplitter.next();
843                 subtypeSplitter.setString(nextImsStr);
844                 if (subtypeSplitter.hasNext()) {
845                     ArrayList<String> subtypeHashes = new ArrayList<>();
846                     // The first element is ime id.
847                     String imeId = subtypeSplitter.next();
848                     while (subtypeSplitter.hasNext()) {
849                         subtypeHashes.add(subtypeSplitter.next());
850                     }
851                     imsList.add(new Pair<>(imeId, subtypeHashes));
852                 }
853             }
854             return imsList;
855         }
856 
InputMethodSettings(Resources res, ContentResolver resolver, ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId, boolean copyOnWrite)857         InputMethodSettings(Resources res, ContentResolver resolver,
858                 ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId,
859                 boolean copyOnWrite) {
860             mRes = res;
861             mResolver = resolver;
862             mMethodMap = methodMap;
863             switchCurrentUser(userId, copyOnWrite);
864         }
865 
866         /**
867          * Must be called when the current user is changed.
868          *
869          * @param userId The user ID.
870          * @param copyOnWrite If {@code true}, for each settings key
871          * (e.g. {@link Settings.Secure#ACTION_INPUT_METHOD_SUBTYPE_SETTINGS}) we use the actual
872          * settings on the {@link Settings.Secure} until we do the first write operation.
873          */
switchCurrentUser(@serIdInt int userId, boolean copyOnWrite)874         void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) {
875             if (DEBUG) {
876                 Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
877             }
878             if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) {
879                 mCopyOnWriteDataStore.clear();
880                 mEnabledInputMethodsStrCache = "";
881                 // TODO: mCurrentProfileIds should be cleared here.
882             }
883             mCurrentUserId = userId;
884             mCopyOnWrite = copyOnWrite;
885             // TODO: mCurrentProfileIds should be updated here.
886         }
887 
putString(@onNull String key, @Nullable String str)888         private void putString(@NonNull String key, @Nullable String str) {
889             if (mCopyOnWrite) {
890                 mCopyOnWriteDataStore.put(key, str);
891             } else {
892                 final int userId = CLONE_TO_MANAGED_PROFILE.contains(key)
893                         ? sUserManagerInternal.getProfileParentId(mCurrentUserId) : mCurrentUserId;
894                 Settings.Secure.putStringForUser(mResolver, key, str, userId);
895             }
896         }
897 
898         @Nullable
getString(@onNull String key, @Nullable String defaultValue)899         private String getString(@NonNull String key, @Nullable String defaultValue) {
900             final String result;
901             if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
902                 result = mCopyOnWriteDataStore.get(key);
903             } else {
904                 result = Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId);
905             }
906             return result != null ? result : defaultValue;
907         }
908 
putInt(String key, int value)909         private void putInt(String key, int value) {
910             if (mCopyOnWrite) {
911                 mCopyOnWriteDataStore.put(key, String.valueOf(value));
912             } else {
913                 final int userId = CLONE_TO_MANAGED_PROFILE.contains(key)
914                         ? sUserManagerInternal.getProfileParentId(mCurrentUserId) : mCurrentUserId;
915                 Settings.Secure.putIntForUser(mResolver, key, value, userId);
916             }
917         }
918 
getInt(String key, int defaultValue)919         private int getInt(String key, int defaultValue) {
920             if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
921                 final String result = mCopyOnWriteDataStore.get(key);
922                 return result != null ? Integer.parseInt(result) : defaultValue;
923             }
924             return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId);
925         }
926 
putBoolean(String key, boolean value)927         private void putBoolean(String key, boolean value) {
928             putInt(key, value ? 1 : 0);
929         }
930 
getBoolean(String key, boolean defaultValue)931         private boolean getBoolean(String key, boolean defaultValue) {
932             return getInt(key, defaultValue ? 1 : 0) == 1;
933         }
934 
setCurrentProfileIds(int[] currentProfileIds)935         public void setCurrentProfileIds(int[] currentProfileIds) {
936             synchronized (this) {
937                 mCurrentProfileIds = currentProfileIds;
938             }
939         }
940 
isCurrentProfile(int userId)941         public boolean isCurrentProfile(int userId) {
942             synchronized (this) {
943                 if (userId == mCurrentUserId) return true;
944                 for (int i = 0; i < mCurrentProfileIds.length; i++) {
945                     if (userId == mCurrentProfileIds[i]) return true;
946                 }
947                 return false;
948             }
949         }
950 
getEnabledInputMethodListLocked()951         ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
952             return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */);
953         }
954 
955         @NonNull
getEnabledInputMethodListWithFilterLocked( @ullable Predicate<InputMethodInfo> matchingCondition)956         ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked(
957                 @Nullable Predicate<InputMethodInfo> matchingCondition) {
958             return createEnabledInputMethodListLocked(
959                     getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition);
960         }
961 
getEnabledInputMethodSubtypeListLocked( Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes)962         List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
963                 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
964             List<InputMethodSubtype> enabledSubtypes =
965                     getEnabledInputMethodSubtypeListLocked(imi);
966             if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
967                 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
968                         context.getResources(), imi);
969             }
970             return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
971         }
972 
getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi)973         List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) {
974             List<Pair<String, ArrayList<String>>> imsList =
975                     getEnabledInputMethodsAndSubtypeListLocked();
976             ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
977             if (imi != null) {
978                 for (Pair<String, ArrayList<String>> imsPair : imsList) {
979                     InputMethodInfo info = mMethodMap.get(imsPair.first);
980                     if (info != null && info.getId().equals(imi.getId())) {
981                         final int subtypeCount = info.getSubtypeCount();
982                         for (int i = 0; i < subtypeCount; ++i) {
983                             InputMethodSubtype ims = info.getSubtypeAt(i);
984                             for (String s: imsPair.second) {
985                                 if (String.valueOf(ims.hashCode()).equals(s)) {
986                                     enabledSubtypes.add(ims);
987                                 }
988                             }
989                         }
990                         break;
991                     }
992                 }
993             }
994             return enabledSubtypes;
995         }
996 
getEnabledInputMethodsAndSubtypeListLocked()997         List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
998             return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
999                     mInputMethodSplitter,
1000                     mSubtypeSplitter);
1001         }
1002 
appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr)1003         void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
1004             if (reloadInputMethodStr) {
1005                 getEnabledInputMethodsStr();
1006             }
1007             if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
1008                 // Add in the newly enabled input method.
1009                 putEnabledInputMethodsStr(id);
1010             } else {
1011                 putEnabledInputMethodsStr(
1012                         mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
1013             }
1014         }
1015 
1016         /**
1017          * Build and put a string of EnabledInputMethods with removing specified Id.
1018          * @return the specified id was removed or not.
1019          */
buildAndPutEnabledInputMethodsStrRemovingIdLocked( StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id)1020         boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
1021                 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
1022             boolean isRemoved = false;
1023             boolean needsAppendSeparator = false;
1024             for (Pair<String, ArrayList<String>> ims: imsList) {
1025                 String curId = ims.first;
1026                 if (curId.equals(id)) {
1027                     // We are disabling this input method, and it is
1028                     // currently enabled.  Skip it to remove from the
1029                     // new list.
1030                     isRemoved = true;
1031                 } else {
1032                     if (needsAppendSeparator) {
1033                         builder.append(INPUT_METHOD_SEPARATOR);
1034                     } else {
1035                         needsAppendSeparator = true;
1036                     }
1037                     buildEnabledInputMethodsSettingString(builder, ims);
1038                 }
1039             }
1040             if (isRemoved) {
1041                 // Update the setting with the new list of input methods.
1042                 putEnabledInputMethodsStr(builder.toString());
1043             }
1044             return isRemoved;
1045         }
1046 
createEnabledInputMethodListLocked( List<Pair<String, ArrayList<String>>> imsList, Predicate<InputMethodInfo> matchingCondition)1047         private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
1048                 List<Pair<String, ArrayList<String>>> imsList,
1049                 Predicate<InputMethodInfo> matchingCondition) {
1050             final ArrayList<InputMethodInfo> res = new ArrayList<>();
1051             for (Pair<String, ArrayList<String>> ims: imsList) {
1052                 InputMethodInfo info = mMethodMap.get(ims.first);
1053                 if (info != null && !info.isVrOnly()
1054                         && (matchingCondition == null || matchingCondition.test(info))) {
1055                     res.add(info);
1056                 }
1057             }
1058             return res;
1059         }
1060 
putEnabledInputMethodsStr(@ullable String str)1061         void putEnabledInputMethodsStr(@Nullable String str) {
1062             if (DEBUG) {
1063                 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
1064             }
1065             if (TextUtils.isEmpty(str)) {
1066                 // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
1067                 // empty data scenario.
1068                 putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
1069             } else {
1070                 putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
1071             }
1072             // TODO: Update callers of putEnabledInputMethodsStr to make str @NonNull.
1073             mEnabledInputMethodsStrCache = (str != null ? str : "");
1074         }
1075 
1076         @NonNull
getEnabledInputMethodsStr()1077         String getEnabledInputMethodsStr() {
1078             mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
1079             if (DEBUG) {
1080                 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
1081                         + ", " + mCurrentUserId);
1082             }
1083             return mEnabledInputMethodsStrCache;
1084         }
1085 
saveSubtypeHistory( List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId)1086         private void saveSubtypeHistory(
1087                 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
1088             StringBuilder builder = new StringBuilder();
1089             boolean isImeAdded = false;
1090             if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
1091                 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
1092                         newSubtypeId);
1093                 isImeAdded = true;
1094             }
1095             for (Pair<String, String> ime: savedImes) {
1096                 String imeId = ime.first;
1097                 String subtypeId = ime.second;
1098                 if (TextUtils.isEmpty(subtypeId)) {
1099                     subtypeId = NOT_A_SUBTYPE_ID_STR;
1100                 }
1101                 if (isImeAdded) {
1102                     builder.append(INPUT_METHOD_SEPARATOR);
1103                 } else {
1104                     isImeAdded = true;
1105                 }
1106                 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
1107                         subtypeId);
1108             }
1109             // Remove the last INPUT_METHOD_SEPARATOR
1110             putSubtypeHistoryStr(builder.toString());
1111         }
1112 
addSubtypeToHistory(String imeId, String subtypeId)1113         private void addSubtypeToHistory(String imeId, String subtypeId) {
1114             List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1115             for (Pair<String, String> ime: subtypeHistory) {
1116                 if (ime.first.equals(imeId)) {
1117                     if (DEBUG) {
1118                         Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
1119                                 + ime.second);
1120                     }
1121                     // We should break here
1122                     subtypeHistory.remove(ime);
1123                     break;
1124                 }
1125             }
1126             if (DEBUG) {
1127                 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
1128             }
1129             saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
1130         }
1131 
putSubtypeHistoryStr(@onNull String str)1132         private void putSubtypeHistoryStr(@NonNull String str) {
1133             if (DEBUG) {
1134                 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
1135             }
1136             if (TextUtils.isEmpty(str)) {
1137                 // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
1138                 // data scenario.
1139                 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
1140             } else {
1141                 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
1142             }
1143         }
1144 
getLastInputMethodAndSubtypeLocked()1145         Pair<String, String> getLastInputMethodAndSubtypeLocked() {
1146             // Gets the first one from the history
1147             return getLastSubtypeForInputMethodLockedInternal(null);
1148         }
1149 
getLastSubtypeForInputMethodLocked(String imeId)1150         String getLastSubtypeForInputMethodLocked(String imeId) {
1151             Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
1152             if (ime != null) {
1153                 return ime.second;
1154             } else {
1155                 return null;
1156             }
1157         }
1158 
getLastSubtypeForInputMethodLockedInternal(String imeId)1159         private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
1160             List<Pair<String, ArrayList<String>>> enabledImes =
1161                     getEnabledInputMethodsAndSubtypeListLocked();
1162             List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1163             for (Pair<String, String> imeAndSubtype : subtypeHistory) {
1164                 final String imeInTheHistory = imeAndSubtype.first;
1165                 // If imeId is empty, returns the first IME and subtype in the history
1166                 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
1167                     final String subtypeInTheHistory = imeAndSubtype.second;
1168                     final String subtypeHashCode =
1169                             getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
1170                                     enabledImes, imeInTheHistory, subtypeInTheHistory);
1171                     if (!TextUtils.isEmpty(subtypeHashCode)) {
1172                         if (DEBUG) {
1173                             Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
1174                         }
1175                         return new Pair<>(imeInTheHistory, subtypeHashCode);
1176                     }
1177                 }
1178             }
1179             if (DEBUG) {
1180                 Slog.d(TAG, "No enabled IME found in the history");
1181             }
1182             return null;
1183         }
1184 
getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode)1185         private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
1186                 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
1187             for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
1188                 if (enabledIme.first.equals(imeId)) {
1189                     final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
1190                     final InputMethodInfo imi = mMethodMap.get(imeId);
1191                     if (explicitlyEnabledSubtypes.size() == 0) {
1192                         // If there are no explicitly enabled subtypes, applicable subtypes are
1193                         // enabled implicitly.
1194                         // If IME is enabled and no subtypes are enabled, applicable subtypes
1195                         // are enabled implicitly, so needs to treat them to be enabled.
1196                         if (imi != null && imi.getSubtypeCount() > 0) {
1197                             List<InputMethodSubtype> implicitlySelectedSubtypes =
1198                                     getImplicitlyApplicableSubtypesLocked(mRes, imi);
1199                             if (implicitlySelectedSubtypes != null) {
1200                                 final int N = implicitlySelectedSubtypes.size();
1201                                 for (int i = 0; i < N; ++i) {
1202                                     final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
1203                                     if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
1204                                         return subtypeHashCode;
1205                                     }
1206                                 }
1207                             }
1208                         }
1209                     } else {
1210                         for (String s: explicitlyEnabledSubtypes) {
1211                             if (s.equals(subtypeHashCode)) {
1212                                 // If both imeId and subtypeId are enabled, return subtypeId.
1213                                 try {
1214                                     final int hashCode = Integer.parseInt(subtypeHashCode);
1215                                     // Check whether the subtype id is valid or not
1216                                     if (isValidSubtypeId(imi, hashCode)) {
1217                                         return s;
1218                                     } else {
1219                                         return NOT_A_SUBTYPE_ID_STR;
1220                                     }
1221                                 } catch (NumberFormatException e) {
1222                                     return NOT_A_SUBTYPE_ID_STR;
1223                                 }
1224                             }
1225                         }
1226                     }
1227                     // If imeId was enabled but subtypeId was disabled.
1228                     return NOT_A_SUBTYPE_ID_STR;
1229                 }
1230             }
1231             // If both imeId and subtypeId are disabled, return null
1232             return null;
1233         }
1234 
loadInputMethodAndSubtypeHistoryLocked()1235         private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
1236             ArrayList<Pair<String, String>> imsList = new ArrayList<>();
1237             final String subtypeHistoryStr = getSubtypeHistoryStr();
1238             if (TextUtils.isEmpty(subtypeHistoryStr)) {
1239                 return imsList;
1240             }
1241             mInputMethodSplitter.setString(subtypeHistoryStr);
1242             while (mInputMethodSplitter.hasNext()) {
1243                 String nextImsStr = mInputMethodSplitter.next();
1244                 mSubtypeSplitter.setString(nextImsStr);
1245                 if (mSubtypeSplitter.hasNext()) {
1246                     String subtypeId = NOT_A_SUBTYPE_ID_STR;
1247                     // The first element is ime id.
1248                     String imeId = mSubtypeSplitter.next();
1249                     while (mSubtypeSplitter.hasNext()) {
1250                         subtypeId = mSubtypeSplitter.next();
1251                         break;
1252                     }
1253                     imsList.add(new Pair<>(imeId, subtypeId));
1254                 }
1255             }
1256             return imsList;
1257         }
1258 
1259         @NonNull
getSubtypeHistoryStr()1260         private String getSubtypeHistoryStr() {
1261             final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
1262             if (DEBUG) {
1263                 Slog.d(TAG, "getSubtypeHistoryStr: " + history);
1264             }
1265             return history;
1266         }
1267 
putSelectedInputMethod(String imeId)1268         void putSelectedInputMethod(String imeId) {
1269             if (DEBUG) {
1270                 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
1271                         + mCurrentUserId);
1272             }
1273             putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
1274         }
1275 
putSelectedSubtype(int subtypeId)1276         void putSelectedSubtype(int subtypeId) {
1277             if (DEBUG) {
1278                 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
1279                         + mCurrentUserId);
1280             }
1281             putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
1282         }
1283 
1284         @Nullable
getSelectedInputMethod()1285         String getSelectedInputMethod() {
1286             final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
1287             if (DEBUG) {
1288                 Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
1289             }
1290             return imi;
1291         }
1292 
putDefaultVoiceInputMethod(String imeId)1293         void putDefaultVoiceInputMethod(String imeId) {
1294             if (DEBUG) {
1295                 Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId);
1296             }
1297             putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId);
1298         }
1299 
1300         @Nullable
getDefaultVoiceInputMethod()1301         String getDefaultVoiceInputMethod() {
1302             final String imi = getString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, null);
1303             if (DEBUG) {
1304                 Slog.d(TAG, "getDefaultVoiceInputMethodStr: " + imi);
1305             }
1306             return imi;
1307         }
1308 
isSubtypeSelected()1309         boolean isSubtypeSelected() {
1310             return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
1311         }
1312 
getSelectedInputMethodSubtypeHashCode()1313         private int getSelectedInputMethodSubtypeHashCode() {
1314             return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
1315         }
1316 
isShowImeWithHardKeyboardEnabled()1317         boolean isShowImeWithHardKeyboardEnabled() {
1318             return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
1319         }
1320 
setShowImeWithHardKeyboard(boolean show)1321         void setShowImeWithHardKeyboard(boolean show) {
1322             putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
1323         }
1324 
1325         @UserIdInt
getCurrentUserId()1326         public int getCurrentUserId() {
1327             return mCurrentUserId;
1328         }
1329 
getSelectedInputMethodSubtypeId(String selectedImiId)1330         int getSelectedInputMethodSubtypeId(String selectedImiId) {
1331             final InputMethodInfo imi = mMethodMap.get(selectedImiId);
1332             if (imi == null) {
1333                 return NOT_A_SUBTYPE_ID;
1334             }
1335             final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
1336             return getSubtypeIdFromHashCode(imi, subtypeHashCode);
1337         }
1338 
saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, InputMethodSubtype currentSubtype)1339         void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId,
1340                 InputMethodSubtype currentSubtype) {
1341             String subtypeId = NOT_A_SUBTYPE_ID_STR;
1342             if (currentSubtype != null) {
1343                 subtypeId = String.valueOf(currentSubtype.hashCode());
1344             }
1345             if (canAddToLastInputMethod(currentSubtype)) {
1346                 addSubtypeToHistory(curMethodId, subtypeId);
1347             }
1348         }
1349 
dumpLocked(final Printer pw, final String prefix)1350         public void dumpLocked(final Printer pw, final String prefix) {
1351             pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
1352             pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds));
1353             pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite);
1354             pw.println(prefix + "mEnabledInputMethodsStrCache=" + mEnabledInputMethodsStrCache);
1355         }
1356     }
1357 
isSoftInputModeStateVisibleAllowed(int targetSdkVersion, @StartInputFlags int startInputFlags)1358     static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion,
1359             @StartInputFlags int startInputFlags) {
1360         if (targetSdkVersion < Build.VERSION_CODES.P) {
1361             // for compatibility.
1362             return true;
1363         }
1364         if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) == 0) {
1365             return false;
1366         }
1367         if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0) {
1368             return false;
1369         }
1370         return true;
1371     }
1372 
1373     /**
1374      * Converts a user ID, which can be a pseudo user ID such as {@link UserHandle#USER_ALL} to a
1375      * list of real user IDs.
1376      *
1377      * @param userIdToBeResolved A user ID. Two pseudo user ID {@link UserHandle#USER_CURRENT} and
1378      *                           {@link UserHandle#USER_ALL} are also supported
1379      * @param currentUserId A real user ID, which will be used when {@link UserHandle#USER_CURRENT}
1380      *                      is specified in {@code userIdToBeResolved}.
1381      * @param warningWriter A {@link PrintWriter} to output some debug messages. {@code null} if
1382      *                      no debug message is required.
1383      * @return An integer array that contain user IDs.
1384      */
resolveUserId(@serIdInt int userIdToBeResolved, @UserIdInt int currentUserId, @Nullable PrintWriter warningWriter)1385     static int[] resolveUserId(@UserIdInt int userIdToBeResolved,
1386             @UserIdInt int currentUserId, @Nullable PrintWriter warningWriter) {
1387         final UserManagerInternal userManagerInternal =
1388                 LocalServices.getService(UserManagerInternal.class);
1389 
1390         if (userIdToBeResolved == UserHandle.USER_ALL) {
1391             return userManagerInternal.getUserIds();
1392         }
1393 
1394         final int sourceUserId;
1395         if (userIdToBeResolved == UserHandle.USER_CURRENT) {
1396             sourceUserId = currentUserId;
1397         } else if (userIdToBeResolved < 0) {
1398             if (warningWriter != null) {
1399                 warningWriter.print("Pseudo user ID ");
1400                 warningWriter.print(userIdToBeResolved);
1401                 warningWriter.println(" is not supported.");
1402             }
1403             return new int[]{};
1404         } else if (userManagerInternal.exists(userIdToBeResolved)) {
1405             sourceUserId = userIdToBeResolved;
1406         } else {
1407             if (warningWriter != null) {
1408                 warningWriter.print("User #");
1409                 warningWriter.print(userIdToBeResolved);
1410                 warningWriter.println(" does not exit.");
1411             }
1412             return new int[]{};
1413         }
1414         return new int[]{sourceUserId};
1415     }
1416 }
1417