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.UserIdInt;
22 import android.app.AppOpsManager;
23 import android.content.ContentResolver;
24 import android.content.Context;
25 import android.content.pm.ApplicationInfo;
26 import android.content.pm.IPackageManager;
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.RemoteException;
32 import android.os.UserHandle;
33 import android.os.UserManagerInternal;
34 import android.provider.Settings;
35 import android.text.TextUtils;
36 import android.util.ArrayMap;
37 import android.util.ArraySet;
38 import android.util.Pair;
39 import android.util.Printer;
40 import android.util.Slog;
41 import android.view.inputmethod.InputMethodInfo;
42 import android.view.inputmethod.InputMethodSubtype;
43 import android.view.textservice.SpellCheckerInfo;
44 
45 import com.android.internal.annotations.GuardedBy;
46 import com.android.internal.annotations.VisibleForTesting;
47 import com.android.internal.inputmethod.StartInputFlags;
48 import com.android.server.LocalServices;
49 import com.android.server.textservices.TextServicesManagerInternal;
50 
51 import java.io.PrintWriter;
52 import java.util.ArrayList;
53 import java.util.Arrays;
54 import java.util.LinkedHashSet;
55 import java.util.List;
56 import java.util.Locale;
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 
containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, boolean checkCountry, String mode)340     static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale,
341             boolean checkCountry, String mode) {
342         if (locale == null) {
343             return false;
344         }
345         final int N = imi.getSubtypeCount();
346         for (int i = 0; i < N; ++i) {
347             final InputMethodSubtype subtype = imi.getSubtypeAt(i);
348             if (checkCountry) {
349                 final Locale subtypeLocale = subtype.getLocaleObject();
350                 if (subtypeLocale == null ||
351                         !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage()) ||
352                         !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
353                     continue;
354                 }
355             } else {
356                 final Locale subtypeLocale = new Locale(getLanguageFromLocaleString(
357                         subtype.getLocale()));
358                 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
359                     continue;
360                 }
361             }
362             if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode) ||
363                     mode.equalsIgnoreCase(subtype.getMode())) {
364                 return true;
365             }
366         }
367         return false;
368     }
369 
getSubtypes(InputMethodInfo imi)370     static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
371         ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
372         final int subtypeCount = imi.getSubtypeCount();
373         for (int i = 0; i < subtypeCount; ++i) {
374             subtypes.add(imi.getSubtypeAt(i));
375         }
376         return subtypes;
377     }
378 
getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes)379     static InputMethodInfo getMostApplicableDefaultIME(List<InputMethodInfo> enabledImes) {
380         if (enabledImes == null || enabledImes.isEmpty()) {
381             return null;
382         }
383         // We'd prefer to fall back on a system IME, since that is safer.
384         int i = enabledImes.size();
385         int firstFoundSystemIme = -1;
386         while (i > 0) {
387             i--;
388             final InputMethodInfo imi = enabledImes.get(i);
389             if (imi.isAuxiliaryIme()) {
390                 continue;
391             }
392             if (imi.isSystem() && containsSubtypeOf(
393                     imi, ENGLISH_LOCALE, false /* checkCountry */, SUBTYPE_MODE_KEYBOARD)) {
394                 return imi;
395             }
396             if (firstFoundSystemIme < 0 && imi.isSystem()) {
397                 firstFoundSystemIme = i;
398             }
399         }
400         return enabledImes.get(Math.max(firstFoundSystemIme, 0));
401     }
402 
isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode)403     static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
404         return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
405     }
406 
getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode)407     static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
408         if (imi != null) {
409             final int subtypeCount = imi.getSubtypeCount();
410             for (int i = 0; i < subtypeCount; ++i) {
411                 InputMethodSubtype ims = imi.getSubtypeAt(i);
412                 if (subtypeHashCode == ims.hashCode()) {
413                     return i;
414                 }
415             }
416         }
417         return NOT_A_SUBTYPE_ID;
418     }
419 
420     private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
421             new LocaleUtils.LocaleExtractor<InputMethodSubtype>() {
422                 @Override
423                 public Locale get(InputMethodSubtype source) {
424                     return source != null ? source.getLocaleObject() : null;
425                 }
426             };
427 
428     @VisibleForTesting
getImplicitlyApplicableSubtypesLocked( Resources res, InputMethodInfo imi)429     static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
430             Resources res, InputMethodInfo imi) {
431         final LocaleList systemLocales = res.getConfiguration().getLocales();
432 
433         synchronized (sCacheLock) {
434             // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
435             // it does not check if subtypes are also identical.
436             if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
437                 return new ArrayList<>(sCachedResult);
438             }
439         }
440 
441         // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
442         // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
443         // LocaleList rather than Resource.
444         final ArrayList<InputMethodSubtype> result =
445                 getImplicitlyApplicableSubtypesLockedImpl(res, imi);
446         synchronized (sCacheLock) {
447             // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
448             sCachedSystemLocales = systemLocales;
449             sCachedInputMethodInfo = imi;
450             sCachedResult = new ArrayList<>(result);
451         }
452         return result;
453     }
454 
getImplicitlyApplicableSubtypesLockedImpl( Resources res, InputMethodInfo imi)455     private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
456             Resources res, InputMethodInfo imi) {
457         final List<InputMethodSubtype> subtypes = InputMethodUtils.getSubtypes(imi);
458         final LocaleList systemLocales = res.getConfiguration().getLocales();
459         final String systemLocale = systemLocales.get(0).toString();
460         if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
461         final int numSubtypes = subtypes.size();
462 
463         // Handle overridesImplicitlyEnabledSubtype mechanism.
464         final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>();
465         for (int i = 0; i < numSubtypes; ++i) {
466             // scan overriding implicitly enabled subtypes.
467             final InputMethodSubtype subtype = subtypes.get(i);
468             if (subtype.overridesImplicitlyEnabledSubtype()) {
469                 final String mode = subtype.getMode();
470                 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
471                     applicableModeAndSubtypesMap.put(mode, subtype);
472                 }
473             }
474         }
475         if (applicableModeAndSubtypesMap.size() > 0) {
476             return new ArrayList<>(applicableModeAndSubtypesMap.values());
477         }
478 
479         final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
480                 new ArrayMap<>();
481         final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
482 
483         for (int i = 0; i < numSubtypes; ++i) {
484             final InputMethodSubtype subtype = subtypes.get(i);
485             final String mode = subtype.getMode();
486             if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
487                 keyboardSubtypes.add(subtype);
488             } else {
489                 if (!nonKeyboardSubtypesMap.containsKey(mode)) {
490                     nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
491                 }
492                 nonKeyboardSubtypesMap.get(mode).add(subtype);
493             }
494         }
495 
496         final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
497         LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
498                 applicableSubtypes);
499 
500         if (!applicableSubtypes.isEmpty()) {
501             boolean hasAsciiCapableKeyboard = false;
502             final int numApplicationSubtypes = applicableSubtypes.size();
503             for (int i = 0; i < numApplicationSubtypes; ++i) {
504                 final InputMethodSubtype subtype = applicableSubtypes.get(i);
505                 if (subtype.isAsciiCapable()) {
506                     hasAsciiCapableKeyboard = true;
507                     break;
508                 }
509             }
510             if (!hasAsciiCapableKeyboard) {
511                 final int numKeyboardSubtypes = keyboardSubtypes.size();
512                 for (int i = 0; i < numKeyboardSubtypes; ++i) {
513                     final InputMethodSubtype subtype = keyboardSubtypes.get(i);
514                     final String mode = subtype.getMode();
515                     if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
516                             TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
517                         applicableSubtypes.add(subtype);
518                     }
519                 }
520             }
521         }
522 
523         if (applicableSubtypes.isEmpty()) {
524             InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
525                     res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
526             if (lastResortKeyboardSubtype != null) {
527                 applicableSubtypes.add(lastResortKeyboardSubtype);
528             }
529         }
530 
531         // For each non-keyboard mode, extract subtypes with system locales.
532         for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
533             LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
534                     applicableSubtypes);
535         }
536 
537         return applicableSubtypes;
538     }
539 
540     /**
541      * Returns the language component of a given locale string.
542      * TODO: Use {@link Locale#toLanguageTag()} and {@link Locale#forLanguageTag(String)}
543      */
getLanguageFromLocaleString(String locale)544     private static String getLanguageFromLocaleString(String locale) {
545         final int idx = locale.indexOf('_');
546         if (idx < 0) {
547             return locale;
548         } else {
549             return locale.substring(0, idx);
550         }
551     }
552 
553     /**
554      * If there are no selected subtypes, tries finding the most applicable one according to the
555      * given locale.
556      * @param subtypes this function will search the most applicable subtype in subtypes
557      * @param mode subtypes will be filtered by mode
558      * @param locale subtypes will be filtered by locale
559      * @param canIgnoreLocaleAsLastResort if this function can't find the most applicable subtype,
560      * it will return the first subtype matched with mode
561      * @return the most applicable subtypeId
562      */
findLastResortApplicableSubtypeLocked( Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, boolean canIgnoreLocaleAsLastResort)563     static InputMethodSubtype findLastResortApplicableSubtypeLocked(
564             Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
565             boolean canIgnoreLocaleAsLastResort) {
566         if (subtypes == null || subtypes.size() == 0) {
567             return null;
568         }
569         if (TextUtils.isEmpty(locale)) {
570             locale = res.getConfiguration().locale.toString();
571         }
572         final String language = getLanguageFromLocaleString(locale);
573         boolean partialMatchFound = false;
574         InputMethodSubtype applicableSubtype = null;
575         InputMethodSubtype firstMatchedModeSubtype = null;
576         final int N = subtypes.size();
577         for (int i = 0; i < N; ++i) {
578             InputMethodSubtype subtype = subtypes.get(i);
579             final String subtypeLocale = subtype.getLocale();
580             final String subtypeLanguage = getLanguageFromLocaleString(subtypeLocale);
581             // An applicable subtype should match "mode". If mode is null, mode will be ignored,
582             // and all subtypes with all modes can be candidates.
583             if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
584                 if (firstMatchedModeSubtype == null) {
585                     firstMatchedModeSubtype = subtype;
586                 }
587                 if (locale.equals(subtypeLocale)) {
588                     // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
589                     applicableSubtype = subtype;
590                     break;
591                 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
592                     // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
593                     applicableSubtype = subtype;
594                     partialMatchFound = true;
595                 }
596             }
597         }
598 
599         if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
600             return firstMatchedModeSubtype;
601         }
602 
603         // The first subtype applicable to the system locale will be defined as the most applicable
604         // subtype.
605         if (DEBUG) {
606             if (applicableSubtype != null) {
607                 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
608                         + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
609             }
610         }
611         return applicableSubtype;
612     }
613 
canAddToLastInputMethod(InputMethodSubtype subtype)614     static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
615         if (subtype == null) return true;
616         return !subtype.isAuxiliary();
617     }
618 
setNonSelectedSystemImesDisabledUntilUsed(IPackageManager packageManager, List<InputMethodInfo> enabledImis, @UserIdInt int userId, String callingPackage)619     static void setNonSelectedSystemImesDisabledUntilUsed(IPackageManager packageManager,
620             List<InputMethodInfo> enabledImis, @UserIdInt int userId, String callingPackage) {
621         if (DEBUG) {
622             Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
623         }
624         final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
625                 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
626         if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
627             return;
628         }
629         // Only the current spell checker should be treated as an enabled one.
630         final SpellCheckerInfo currentSpellChecker =
631                 TextServicesManagerInternal.get().getCurrentSpellCheckerForUser(userId);
632         for (final String packageName : systemImesDisabledUntilUsed) {
633             if (DEBUG) {
634                 Slog.d(TAG, "check " + packageName);
635             }
636             boolean enabledIme = false;
637             for (int j = 0; j < enabledImis.size(); ++j) {
638                 final InputMethodInfo imi = enabledImis.get(j);
639                 if (packageName.equals(imi.getPackageName())) {
640                     enabledIme = true;
641                     break;
642                 }
643             }
644             if (enabledIme) {
645                 // enabled ime. skip
646                 continue;
647             }
648             if (currentSpellChecker != null
649                     && packageName.equals(currentSpellChecker.getPackageName())) {
650                 // enabled spell checker. skip
651                 if (DEBUG) {
652                     Slog.d(TAG, packageName + " is the current spell checker. skip");
653                 }
654                 continue;
655             }
656             ApplicationInfo ai = null;
657             try {
658                 ai = packageManager.getApplicationInfo(packageName,
659                         PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS, userId);
660             } catch (RemoteException e) {
661                 Slog.w(TAG, "getApplicationInfo failed. packageName=" + packageName
662                         + " userId=" + userId, e);
663                 continue;
664             }
665             if (ai == null) {
666                 // No app found for packageName
667                 continue;
668             }
669             final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
670             if (!isSystemPackage) {
671                 continue;
672             }
673             setDisabledUntilUsed(packageManager, packageName, userId, callingPackage);
674         }
675     }
676 
setDisabledUntilUsed(IPackageManager packageManager, String packageName, int userId, String callingPackage)677     private static void setDisabledUntilUsed(IPackageManager packageManager, String packageName,
678             int userId, String callingPackage) {
679         final int state;
680         try {
681             state = packageManager.getApplicationEnabledSetting(packageName, userId);
682         } catch (RemoteException e) {
683             Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
684                     + " userId=" + userId, e);
685             return;
686         }
687         if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
688                 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
689             if (DEBUG) {
690                 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
691             }
692             try {
693                 packageManager.setApplicationEnabledSetting(packageName,
694                         PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
695                         0 /* newState */, userId, callingPackage);
696             } catch (RemoteException e) {
697                 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
698                         + " userId=" + userId + " callingPackage=" + callingPackage, e);
699                 return;
700             }
701         } else {
702             if (DEBUG) {
703                 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
704             }
705         }
706     }
707 
getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi, InputMethodSubtype subtype)708     static CharSequence getImeAndSubtypeDisplayName(Context context, InputMethodInfo imi,
709             InputMethodSubtype subtype) {
710         final CharSequence imiLabel = imi.loadLabel(context.getPackageManager());
711         return subtype != null
712                 ? TextUtils.concat(subtype.getDisplayName(context,
713                         imi.getPackageName(), imi.getServiceInfo().applicationInfo),
714                                 (TextUtils.isEmpty(imiLabel) ?
715                                         "" : " - " + imiLabel))
716                 : imiLabel;
717     }
718 
719     /**
720      * Returns true if a package name belongs to a UID.
721      *
722      * <p>This is a simple wrapper of {@link AppOpsManager#checkPackage(int, String)}.</p>
723      * @param appOpsManager the {@link AppOpsManager} object to be used for the validation.
724      * @param uid the UID to be validated.
725      * @param packageName the package name.
726      * @return {@code true} if the package name belongs to the UID.
727      */
checkIfPackageBelongsToUid(AppOpsManager appOpsManager, @UserIdInt int uid, String packageName)728     static boolean checkIfPackageBelongsToUid(AppOpsManager appOpsManager,
729             @UserIdInt int uid, String packageName) {
730         try {
731             appOpsManager.checkPackage(uid, packageName);
732             return true;
733         } catch (SecurityException e) {
734             return false;
735         }
736     }
737 
738     /**
739      * Utility class for putting and getting settings for InputMethod
740      * TODO: Move all putters and getters of settings to this class.
741      */
742     public static class InputMethodSettings {
743         private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
744                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
745 
746         private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
747                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
748 
749         private final Resources mRes;
750         private final ContentResolver mResolver;
751         private final ArrayMap<String, InputMethodInfo> mMethodMap;
752 
753         /**
754          * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}.
755          */
756         private final ArrayMap<String, String> mCopyOnWriteDataStore = new ArrayMap<>();
757 
758         private static final ArraySet<String> CLONE_TO_MANAGED_PROFILE = new ArraySet<>();
759         static {
760             Settings.Secure.getCloneToManagedProfileSettings(CLONE_TO_MANAGED_PROFILE);
761         }
762 
763         private static final UserManagerInternal sUserManagerInternal =
764                 LocalServices.getService(UserManagerInternal.class);
765 
766         private boolean mCopyOnWrite = false;
767         @NonNull
768         private String mEnabledInputMethodsStrCache = "";
769         @UserIdInt
770         private int mCurrentUserId;
771         private int[] mCurrentProfileIds = new int[0];
772 
buildEnabledInputMethodsSettingString( StringBuilder builder, Pair<String, ArrayList<String>> ime)773         private static void buildEnabledInputMethodsSettingString(
774                 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
775             builder.append(ime.first);
776             // Inputmethod and subtypes are saved in the settings as follows:
777             // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
778             for (String subtypeId: ime.second) {
779                 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
780             }
781         }
782 
buildInputMethodsAndSubtypeList( String enabledInputMethodsStr, TextUtils.SimpleStringSplitter inputMethodSplitter, TextUtils.SimpleStringSplitter subtypeSplitter)783         private static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
784                 String enabledInputMethodsStr,
785                 TextUtils.SimpleStringSplitter inputMethodSplitter,
786                 TextUtils.SimpleStringSplitter subtypeSplitter) {
787             ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
788             if (TextUtils.isEmpty(enabledInputMethodsStr)) {
789                 return imsList;
790             }
791             inputMethodSplitter.setString(enabledInputMethodsStr);
792             while (inputMethodSplitter.hasNext()) {
793                 String nextImsStr = inputMethodSplitter.next();
794                 subtypeSplitter.setString(nextImsStr);
795                 if (subtypeSplitter.hasNext()) {
796                     ArrayList<String> subtypeHashes = new ArrayList<>();
797                     // The first element is ime id.
798                     String imeId = subtypeSplitter.next();
799                     while (subtypeSplitter.hasNext()) {
800                         subtypeHashes.add(subtypeSplitter.next());
801                     }
802                     imsList.add(new Pair<>(imeId, subtypeHashes));
803                 }
804             }
805             return imsList;
806         }
807 
InputMethodSettings(Resources res, ContentResolver resolver, ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId, boolean copyOnWrite)808         InputMethodSettings(Resources res, ContentResolver resolver,
809                 ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId,
810                 boolean copyOnWrite) {
811             mRes = res;
812             mResolver = resolver;
813             mMethodMap = methodMap;
814             switchCurrentUser(userId, copyOnWrite);
815         }
816 
817         /**
818          * Must be called when the current user is changed.
819          *
820          * @param userId The user ID.
821          * @param copyOnWrite If {@code true}, for each settings key
822          * (e.g. {@link Settings.Secure#ACTION_INPUT_METHOD_SUBTYPE_SETTINGS}) we use the actual
823          * settings on the {@link Settings.Secure} until we do the first write operation.
824          */
switchCurrentUser(@serIdInt int userId, boolean copyOnWrite)825         void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) {
826             if (DEBUG) {
827                 Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
828             }
829             if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) {
830                 mCopyOnWriteDataStore.clear();
831                 mEnabledInputMethodsStrCache = "";
832                 // TODO: mCurrentProfileIds should be cleared here.
833             }
834             mCurrentUserId = userId;
835             mCopyOnWrite = copyOnWrite;
836             // TODO: mCurrentProfileIds should be updated here.
837         }
838 
putString(@onNull String key, @Nullable String str)839         private void putString(@NonNull String key, @Nullable String str) {
840             if (mCopyOnWrite) {
841                 mCopyOnWriteDataStore.put(key, str);
842             } else {
843                 final int userId = CLONE_TO_MANAGED_PROFILE.contains(key)
844                         ? sUserManagerInternal.getProfileParentId(mCurrentUserId) : mCurrentUserId;
845                 Settings.Secure.putStringForUser(mResolver, key, str, userId);
846             }
847         }
848 
849         @Nullable
getString(@onNull String key, @Nullable String defaultValue)850         private String getString(@NonNull String key, @Nullable String defaultValue) {
851             final String result;
852             if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
853                 result = mCopyOnWriteDataStore.get(key);
854             } else {
855                 result = Settings.Secure.getStringForUser(mResolver, key, mCurrentUserId);
856             }
857             return result != null ? result : defaultValue;
858         }
859 
putInt(String key, int value)860         private void putInt(String key, int value) {
861             if (mCopyOnWrite) {
862                 mCopyOnWriteDataStore.put(key, String.valueOf(value));
863             } else {
864                 final int userId = CLONE_TO_MANAGED_PROFILE.contains(key)
865                         ? sUserManagerInternal.getProfileParentId(mCurrentUserId) : mCurrentUserId;
866                 Settings.Secure.putIntForUser(mResolver, key, value, userId);
867             }
868         }
869 
getInt(String key, int defaultValue)870         private int getInt(String key, int defaultValue) {
871             if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
872                 final String result = mCopyOnWriteDataStore.get(key);
873                 return result != null ? Integer.parseInt(result) : defaultValue;
874             }
875             return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId);
876         }
877 
putBoolean(String key, boolean value)878         private void putBoolean(String key, boolean value) {
879             putInt(key, value ? 1 : 0);
880         }
881 
getBoolean(String key, boolean defaultValue)882         private boolean getBoolean(String key, boolean defaultValue) {
883             return getInt(key, defaultValue ? 1 : 0) == 1;
884         }
885 
setCurrentProfileIds(int[] currentProfileIds)886         public void setCurrentProfileIds(int[] currentProfileIds) {
887             synchronized (this) {
888                 mCurrentProfileIds = currentProfileIds;
889             }
890         }
891 
isCurrentProfile(int userId)892         public boolean isCurrentProfile(int userId) {
893             synchronized (this) {
894                 if (userId == mCurrentUserId) return true;
895                 for (int i = 0; i < mCurrentProfileIds.length; i++) {
896                     if (userId == mCurrentProfileIds[i]) return true;
897                 }
898                 return false;
899             }
900         }
901 
getEnabledInputMethodListLocked()902         ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
903             return createEnabledInputMethodListLocked(
904                     getEnabledInputMethodsAndSubtypeListLocked());
905         }
906 
getEnabledInputMethodSubtypeListLocked( Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes)907         List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
908                 Context context, InputMethodInfo imi, boolean allowsImplicitlySelectedSubtypes) {
909             List<InputMethodSubtype> enabledSubtypes =
910                     getEnabledInputMethodSubtypeListLocked(imi);
911             if (allowsImplicitlySelectedSubtypes && enabledSubtypes.isEmpty()) {
912                 enabledSubtypes = InputMethodUtils.getImplicitlyApplicableSubtypesLocked(
913                         context.getResources(), imi);
914             }
915             return InputMethodSubtype.sort(context, 0, imi, enabledSubtypes);
916         }
917 
getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi)918         List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) {
919             List<Pair<String, ArrayList<String>>> imsList =
920                     getEnabledInputMethodsAndSubtypeListLocked();
921             ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
922             if (imi != null) {
923                 for (Pair<String, ArrayList<String>> imsPair : imsList) {
924                     InputMethodInfo info = mMethodMap.get(imsPair.first);
925                     if (info != null && info.getId().equals(imi.getId())) {
926                         final int subtypeCount = info.getSubtypeCount();
927                         for (int i = 0; i < subtypeCount; ++i) {
928                             InputMethodSubtype ims = info.getSubtypeAt(i);
929                             for (String s: imsPair.second) {
930                                 if (String.valueOf(ims.hashCode()).equals(s)) {
931                                     enabledSubtypes.add(ims);
932                                 }
933                             }
934                         }
935                         break;
936                     }
937                 }
938             }
939             return enabledSubtypes;
940         }
941 
getEnabledInputMethodsAndSubtypeListLocked()942         List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
943             return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
944                     mInputMethodSplitter,
945                     mSubtypeSplitter);
946         }
947 
appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr)948         void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
949             if (reloadInputMethodStr) {
950                 getEnabledInputMethodsStr();
951             }
952             if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
953                 // Add in the newly enabled input method.
954                 putEnabledInputMethodsStr(id);
955             } else {
956                 putEnabledInputMethodsStr(
957                         mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
958             }
959         }
960 
961         /**
962          * Build and put a string of EnabledInputMethods with removing specified Id.
963          * @return the specified id was removed or not.
964          */
buildAndPutEnabledInputMethodsStrRemovingIdLocked( StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id)965         boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
966                 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
967             boolean isRemoved = false;
968             boolean needsAppendSeparator = false;
969             for (Pair<String, ArrayList<String>> ims: imsList) {
970                 String curId = ims.first;
971                 if (curId.equals(id)) {
972                     // We are disabling this input method, and it is
973                     // currently enabled.  Skip it to remove from the
974                     // new list.
975                     isRemoved = true;
976                 } else {
977                     if (needsAppendSeparator) {
978                         builder.append(INPUT_METHOD_SEPARATOR);
979                     } else {
980                         needsAppendSeparator = true;
981                     }
982                     buildEnabledInputMethodsSettingString(builder, ims);
983                 }
984             }
985             if (isRemoved) {
986                 // Update the setting with the new list of input methods.
987                 putEnabledInputMethodsStr(builder.toString());
988             }
989             return isRemoved;
990         }
991 
createEnabledInputMethodListLocked( List<Pair<String, ArrayList<String>>> imsList)992         private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
993                 List<Pair<String, ArrayList<String>>> imsList) {
994             final ArrayList<InputMethodInfo> res = new ArrayList<>();
995             for (Pair<String, ArrayList<String>> ims: imsList) {
996                 InputMethodInfo info = mMethodMap.get(ims.first);
997                 if (info != null && !info.isVrOnly()) {
998                     res.add(info);
999                 }
1000             }
1001             return res;
1002         }
1003 
putEnabledInputMethodsStr(@ullable String str)1004         void putEnabledInputMethodsStr(@Nullable String str) {
1005             if (DEBUG) {
1006                 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
1007             }
1008             if (TextUtils.isEmpty(str)) {
1009                 // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
1010                 // empty data scenario.
1011                 putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
1012             } else {
1013                 putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
1014             }
1015             // TODO: Update callers of putEnabledInputMethodsStr to make str @NonNull.
1016             mEnabledInputMethodsStrCache = (str != null ? str : "");
1017         }
1018 
1019         @NonNull
getEnabledInputMethodsStr()1020         String getEnabledInputMethodsStr() {
1021             mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
1022             if (DEBUG) {
1023                 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
1024                         + ", " + mCurrentUserId);
1025             }
1026             return mEnabledInputMethodsStrCache;
1027         }
1028 
saveSubtypeHistory( List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId)1029         private void saveSubtypeHistory(
1030                 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
1031             StringBuilder builder = new StringBuilder();
1032             boolean isImeAdded = false;
1033             if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
1034                 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
1035                         newSubtypeId);
1036                 isImeAdded = true;
1037             }
1038             for (Pair<String, String> ime: savedImes) {
1039                 String imeId = ime.first;
1040                 String subtypeId = ime.second;
1041                 if (TextUtils.isEmpty(subtypeId)) {
1042                     subtypeId = NOT_A_SUBTYPE_ID_STR;
1043                 }
1044                 if (isImeAdded) {
1045                     builder.append(INPUT_METHOD_SEPARATOR);
1046                 } else {
1047                     isImeAdded = true;
1048                 }
1049                 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
1050                         subtypeId);
1051             }
1052             // Remove the last INPUT_METHOD_SEPARATOR
1053             putSubtypeHistoryStr(builder.toString());
1054         }
1055 
addSubtypeToHistory(String imeId, String subtypeId)1056         private void addSubtypeToHistory(String imeId, String subtypeId) {
1057             List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1058             for (Pair<String, String> ime: subtypeHistory) {
1059                 if (ime.first.equals(imeId)) {
1060                     if (DEBUG) {
1061                         Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
1062                                 + ime.second);
1063                     }
1064                     // We should break here
1065                     subtypeHistory.remove(ime);
1066                     break;
1067                 }
1068             }
1069             if (DEBUG) {
1070                 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
1071             }
1072             saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
1073         }
1074 
putSubtypeHistoryStr(@onNull String str)1075         private void putSubtypeHistoryStr(@NonNull String str) {
1076             if (DEBUG) {
1077                 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
1078             }
1079             if (TextUtils.isEmpty(str)) {
1080                 // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
1081                 // data scenario.
1082                 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
1083             } else {
1084                 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
1085             }
1086         }
1087 
getLastInputMethodAndSubtypeLocked()1088         Pair<String, String> getLastInputMethodAndSubtypeLocked() {
1089             // Gets the first one from the history
1090             return getLastSubtypeForInputMethodLockedInternal(null);
1091         }
1092 
getLastSubtypeForInputMethodLocked(String imeId)1093         String getLastSubtypeForInputMethodLocked(String imeId) {
1094             Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
1095             if (ime != null) {
1096                 return ime.second;
1097             } else {
1098                 return null;
1099             }
1100         }
1101 
getLastSubtypeForInputMethodLockedInternal(String imeId)1102         private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
1103             List<Pair<String, ArrayList<String>>> enabledImes =
1104                     getEnabledInputMethodsAndSubtypeListLocked();
1105             List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
1106             for (Pair<String, String> imeAndSubtype : subtypeHistory) {
1107                 final String imeInTheHistory = imeAndSubtype.first;
1108                 // If imeId is empty, returns the first IME and subtype in the history
1109                 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
1110                     final String subtypeInTheHistory = imeAndSubtype.second;
1111                     final String subtypeHashCode =
1112                             getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
1113                                     enabledImes, imeInTheHistory, subtypeInTheHistory);
1114                     if (!TextUtils.isEmpty(subtypeHashCode)) {
1115                         if (DEBUG) {
1116                             Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
1117                         }
1118                         return new Pair<>(imeInTheHistory, subtypeHashCode);
1119                     }
1120                 }
1121             }
1122             if (DEBUG) {
1123                 Slog.d(TAG, "No enabled IME found in the history");
1124             }
1125             return null;
1126         }
1127 
getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode)1128         private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
1129                 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
1130             for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
1131                 if (enabledIme.first.equals(imeId)) {
1132                     final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
1133                     final InputMethodInfo imi = mMethodMap.get(imeId);
1134                     if (explicitlyEnabledSubtypes.size() == 0) {
1135                         // If there are no explicitly enabled subtypes, applicable subtypes are
1136                         // enabled implicitly.
1137                         // If IME is enabled and no subtypes are enabled, applicable subtypes
1138                         // are enabled implicitly, so needs to treat them to be enabled.
1139                         if (imi != null && imi.getSubtypeCount() > 0) {
1140                             List<InputMethodSubtype> implicitlySelectedSubtypes =
1141                                     getImplicitlyApplicableSubtypesLocked(mRes, imi);
1142                             if (implicitlySelectedSubtypes != null) {
1143                                 final int N = implicitlySelectedSubtypes.size();
1144                                 for (int i = 0; i < N; ++i) {
1145                                     final InputMethodSubtype st = implicitlySelectedSubtypes.get(i);
1146                                     if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
1147                                         return subtypeHashCode;
1148                                     }
1149                                 }
1150                             }
1151                         }
1152                     } else {
1153                         for (String s: explicitlyEnabledSubtypes) {
1154                             if (s.equals(subtypeHashCode)) {
1155                                 // If both imeId and subtypeId are enabled, return subtypeId.
1156                                 try {
1157                                     final int hashCode = Integer.parseInt(subtypeHashCode);
1158                                     // Check whether the subtype id is valid or not
1159                                     if (isValidSubtypeId(imi, hashCode)) {
1160                                         return s;
1161                                     } else {
1162                                         return NOT_A_SUBTYPE_ID_STR;
1163                                     }
1164                                 } catch (NumberFormatException e) {
1165                                     return NOT_A_SUBTYPE_ID_STR;
1166                                 }
1167                             }
1168                         }
1169                     }
1170                     // If imeId was enabled but subtypeId was disabled.
1171                     return NOT_A_SUBTYPE_ID_STR;
1172                 }
1173             }
1174             // If both imeId and subtypeId are disabled, return null
1175             return null;
1176         }
1177 
loadInputMethodAndSubtypeHistoryLocked()1178         private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
1179             ArrayList<Pair<String, String>> imsList = new ArrayList<>();
1180             final String subtypeHistoryStr = getSubtypeHistoryStr();
1181             if (TextUtils.isEmpty(subtypeHistoryStr)) {
1182                 return imsList;
1183             }
1184             mInputMethodSplitter.setString(subtypeHistoryStr);
1185             while (mInputMethodSplitter.hasNext()) {
1186                 String nextImsStr = mInputMethodSplitter.next();
1187                 mSubtypeSplitter.setString(nextImsStr);
1188                 if (mSubtypeSplitter.hasNext()) {
1189                     String subtypeId = NOT_A_SUBTYPE_ID_STR;
1190                     // The first element is ime id.
1191                     String imeId = mSubtypeSplitter.next();
1192                     while (mSubtypeSplitter.hasNext()) {
1193                         subtypeId = mSubtypeSplitter.next();
1194                         break;
1195                     }
1196                     imsList.add(new Pair<>(imeId, subtypeId));
1197                 }
1198             }
1199             return imsList;
1200         }
1201 
1202         @NonNull
getSubtypeHistoryStr()1203         private String getSubtypeHistoryStr() {
1204             final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
1205             if (DEBUG) {
1206                 Slog.d(TAG, "getSubtypeHistoryStr: " + history);
1207             }
1208             return history;
1209         }
1210 
putSelectedInputMethod(String imeId)1211         void putSelectedInputMethod(String imeId) {
1212             if (DEBUG) {
1213                 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
1214                         + mCurrentUserId);
1215             }
1216             putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
1217         }
1218 
putSelectedSubtype(int subtypeId)1219         void putSelectedSubtype(int subtypeId) {
1220             if (DEBUG) {
1221                 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
1222                         + mCurrentUserId);
1223             }
1224             putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
1225         }
1226 
1227         @Nullable
getSelectedInputMethod()1228         String getSelectedInputMethod() {
1229             final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
1230             if (DEBUG) {
1231                 Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
1232             }
1233             return imi;
1234         }
1235 
isSubtypeSelected()1236         boolean isSubtypeSelected() {
1237             return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
1238         }
1239 
getSelectedInputMethodSubtypeHashCode()1240         private int getSelectedInputMethodSubtypeHashCode() {
1241             return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
1242         }
1243 
isShowImeWithHardKeyboardEnabled()1244         boolean isShowImeWithHardKeyboardEnabled() {
1245             return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
1246         }
1247 
setShowImeWithHardKeyboard(boolean show)1248         void setShowImeWithHardKeyboard(boolean show) {
1249             putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
1250         }
1251 
1252         @UserIdInt
getCurrentUserId()1253         public int getCurrentUserId() {
1254             return mCurrentUserId;
1255         }
1256 
getSelectedInputMethodSubtypeId(String selectedImiId)1257         int getSelectedInputMethodSubtypeId(String selectedImiId) {
1258             final InputMethodInfo imi = mMethodMap.get(selectedImiId);
1259             if (imi == null) {
1260                 return NOT_A_SUBTYPE_ID;
1261             }
1262             final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
1263             return getSubtypeIdFromHashCode(imi, subtypeHashCode);
1264         }
1265 
saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, InputMethodSubtype currentSubtype)1266         void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId,
1267                 InputMethodSubtype currentSubtype) {
1268             String subtypeId = NOT_A_SUBTYPE_ID_STR;
1269             if (currentSubtype != null) {
1270                 subtypeId = String.valueOf(currentSubtype.hashCode());
1271             }
1272             if (canAddToLastInputMethod(currentSubtype)) {
1273                 addSubtypeToHistory(curMethodId, subtypeId);
1274             }
1275         }
1276 
dumpLocked(final Printer pw, final String prefix)1277         public void dumpLocked(final Printer pw, final String prefix) {
1278             pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
1279             pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds));
1280             pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite);
1281             pw.println(prefix + "mEnabledInputMethodsStrCache=" + mEnabledInputMethodsStrCache);
1282         }
1283     }
1284 
isSoftInputModeStateVisibleAllowed(int targetSdkVersion, @StartInputFlags int startInputFlags)1285     static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion,
1286             @StartInputFlags int startInputFlags) {
1287         if (targetSdkVersion < Build.VERSION_CODES.P) {
1288             // for compatibility.
1289             return true;
1290         }
1291         if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) == 0) {
1292             return false;
1293         }
1294         if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0) {
1295             return false;
1296         }
1297         return true;
1298     }
1299 
1300     /**
1301      * Converts a user ID, which can be a pseudo user ID such as {@link UserHandle#USER_ALL} to a
1302      * list of real user IDs.
1303      *
1304      * @param userIdToBeResolved A user ID. Two pseudo user ID {@link UserHandle#USER_CURRENT} and
1305      *                           {@link UserHandle#USER_ALL} are also supported
1306      * @param currentUserId A real user ID, which will be used when {@link UserHandle#USER_CURRENT}
1307      *                      is specified in {@code userIdToBeResolved}.
1308      * @param warningWriter A {@link PrintWriter} to output some debug messages. {@code null} if
1309      *                      no debug message is required.
1310      * @return An integer array that contain user IDs.
1311      */
resolveUserId(@serIdInt int userIdToBeResolved, @UserIdInt int currentUserId, @Nullable PrintWriter warningWriter)1312     static int[] resolveUserId(@UserIdInt int userIdToBeResolved,
1313             @UserIdInt int currentUserId, @Nullable PrintWriter warningWriter) {
1314         final UserManagerInternal userManagerInternal =
1315                 LocalServices.getService(UserManagerInternal.class);
1316 
1317         if (userIdToBeResolved == UserHandle.USER_ALL) {
1318             return userManagerInternal.getUserIds();
1319         }
1320 
1321         final int sourceUserId;
1322         if (userIdToBeResolved == UserHandle.USER_CURRENT) {
1323             sourceUserId = currentUserId;
1324         } else if (userIdToBeResolved < 0) {
1325             if (warningWriter != null) {
1326                 warningWriter.print("Pseudo user ID ");
1327                 warningWriter.print(userIdToBeResolved);
1328                 warningWriter.println(" is not supported.");
1329             }
1330             return new int[]{};
1331         } else if (userManagerInternal.exists(userIdToBeResolved)) {
1332             sourceUserId = userIdToBeResolved;
1333         } else {
1334             if (warningWriter != null) {
1335                 warningWriter.print("User #");
1336                 warningWriter.print(userIdToBeResolved);
1337                 warningWriter.println(" does not exit.");
1338             }
1339             return new int[]{};
1340         }
1341         return new int[]{sourceUserId};
1342     }
1343 }
1344