1 /*
2  * Copyright (C) 2024 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.AnyThread;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.annotation.UserIdInt;
23 import android.content.pm.PackageManagerInternal;
24 import android.os.LocaleList;
25 import android.provider.Settings;
26 import android.text.TextUtils;
27 import android.util.IntArray;
28 import android.util.Pair;
29 import android.util.Printer;
30 import android.util.Slog;
31 import android.view.inputmethod.InputMethodInfo;
32 import android.view.inputmethod.InputMethodSubtype;
33 
34 import com.android.internal.annotations.VisibleForTesting;
35 
36 import java.util.ArrayList;
37 import java.util.List;
38 import java.util.function.Predicate;
39 
40 /**
41  * Utility class for putting and getting settings for InputMethod.
42  *
43  * <p>This is used in two ways:</p>
44  * <ul>
45  *     <li>Singleton instance in {@link InputMethodManagerService}, which is updated on
46  *     user-switch to follow the current user.</li>
47  *     <li>On-demand instances when we need settings for non-current users.</li>
48  * </ul>
49  */
50 final class InputMethodSettings {
51     public static final boolean DEBUG = false;
52     private static final String TAG = "InputMethodSettings";
53 
54     /**
55      * An integer code that represents "no subtype" when a subtype hashcode is used.
56      *
57      * <p>Due to historical confusions with {@link InputMethodUtils#NOT_A_SUBTYPE_ID}, we have
58      * used {@code -1} here. We cannot change this value as it's already saved into secure settings.
59      * </p>
60      */
61     static final int INVALID_SUBTYPE_HASHCODE = -1;
62     /**
63      * A string code that represents "no subtype" when a subtype hashcode is used.
64      *
65      * <p>Due to historical confusions with {@link InputMethodUtils#NOT_A_SUBTYPE_ID}, we have
66      * used {@code "-1"} here. We cannot change this value as it's already saved into secure
67      * settings.</p>
68      */
69     private static final String INVALID_SUBTYPE_HASHCODE_STR =
70             String.valueOf(INVALID_SUBTYPE_HASHCODE);
71     private static final char INPUT_METHOD_SEPARATOR = InputMethodUtils.INPUT_METHOD_SEPARATOR;
72     private static final char INPUT_METHOD_SUBTYPE_SEPARATOR =
73             InputMethodUtils.INPUT_METHOD_SUBTYPE_SEPARATOR;
74 
75     private final InputMethodMap mMethodMap;
76     private final List<InputMethodInfo> mMethodList;
77 
78     @UserIdInt
79     private final int mUserId;
80 
buildEnabledInputMethodsSettingString( StringBuilder builder, Pair<String, ArrayList<String>> ime)81     private static void buildEnabledInputMethodsSettingString(
82             StringBuilder builder, Pair<String, ArrayList<String>> ime) {
83         builder.append(ime.first);
84         // Inputmethod and subtypes are saved in the settings as follows:
85         // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
86         for (int i = 0; i < ime.second.size(); ++i) {
87             final String subtypeId = ime.second.get(i);
88             builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
89         }
90     }
91 
createEmptyMap(@serIdInt int userId)92     static InputMethodSettings createEmptyMap(@UserIdInt int userId) {
93         return new InputMethodSettings(InputMethodMap.emptyMap(), userId);
94     }
95 
create(InputMethodMap methodMap, @UserIdInt int userId)96     static InputMethodSettings create(InputMethodMap methodMap, @UserIdInt int userId) {
97         return new InputMethodSettings(methodMap, userId);
98     }
99 
InputMethodSettings(InputMethodMap methodMap, @UserIdInt int userId)100     private InputMethodSettings(InputMethodMap methodMap, @UserIdInt int userId) {
101         mMethodMap = methodMap;
102         mMethodList = methodMap.values();
103         mUserId = userId;
104     }
105 
106     @AnyThread
107     @NonNull
getMethodMap()108     InputMethodMap getMethodMap() {
109         return mMethodMap;
110     }
111 
112     @AnyThread
113     @NonNull
getMethodList()114     List<InputMethodInfo> getMethodList() {
115         return mMethodList;
116     }
117 
putString(@onNull String key, @Nullable String str)118     private void putString(@NonNull String key, @Nullable String str) {
119         SecureSettingsWrapper.putString(key, str, mUserId);
120     }
121 
122     @Nullable
getString(@onNull String key, @Nullable String defaultValue)123     private String getString(@NonNull String key, @Nullable String defaultValue) {
124         return SecureSettingsWrapper.getString(key, defaultValue, mUserId);
125     }
126 
putInt(String key, int value)127     private void putInt(String key, int value) {
128         SecureSettingsWrapper.putInt(key, value, mUserId);
129     }
130 
getInt(String key, int defaultValue)131     private int getInt(String key, int defaultValue) {
132         return SecureSettingsWrapper.getInt(key, defaultValue, mUserId);
133     }
134 
getEnabledInputMethodList()135     ArrayList<InputMethodInfo> getEnabledInputMethodList() {
136         return getEnabledInputMethodListWithFilter(null /* matchingCondition */);
137     }
138 
139     @NonNull
getEnabledInputMethodListWithFilter( @ullable Predicate<InputMethodInfo> matchingCondition)140     ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilter(
141             @Nullable Predicate<InputMethodInfo> matchingCondition) {
142         return createEnabledInputMethodList(
143                 getEnabledInputMethodsAndSubtypeList(), matchingCondition);
144     }
145 
getEnabledInputMethodSubtypeList( InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes)146     List<InputMethodSubtype> getEnabledInputMethodSubtypeList(
147             InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) {
148         List<InputMethodSubtype> enabledSubtypes =
149                 getEnabledInputMethodSubtypeList(imi);
150         if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) {
151             enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypes(
152                     SystemLocaleWrapper.get(mUserId), imi);
153         }
154         return InputMethodSubtype.sort(imi, enabledSubtypes);
155     }
156 
getEnabledInputMethodSubtypeList(InputMethodInfo imi)157     List<InputMethodSubtype> getEnabledInputMethodSubtypeList(InputMethodInfo imi) {
158         List<Pair<String, ArrayList<String>>> imsList =
159                 getEnabledInputMethodsAndSubtypeList();
160         ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
161         if (imi != null) {
162             for (int i = 0; i < imsList.size(); ++i) {
163                 final Pair<String, ArrayList<String>> imsPair = imsList.get(i);
164                 final InputMethodInfo info = mMethodMap.get(imsPair.first);
165                 if (info != null && info.getId().equals(imi.getId())) {
166                     final int subtypeCount = info.getSubtypeCount();
167                     for (int j = 0; j < subtypeCount; ++j) {
168                         final InputMethodSubtype ims = info.getSubtypeAt(j);
169                         for (int k = 0; k < imsPair.second.size(); ++k) {
170                             final String s = imsPair.second.get(k);
171                             if (String.valueOf(ims.hashCode()).equals(s)) {
172                                 enabledSubtypes.add(ims);
173                             }
174                         }
175                     }
176                     break;
177                 }
178             }
179         }
180         return enabledSubtypes;
181     }
182 
getEnabledInputMethodsAndSubtypeList()183     List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeList() {
184         final String enabledInputMethodsStr = getEnabledInputMethodsStr();
185         final TextUtils.SimpleStringSplitter inputMethodSplitter =
186                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
187         final TextUtils.SimpleStringSplitter subtypeSplitter =
188                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
189         final ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
190         if (TextUtils.isEmpty(enabledInputMethodsStr)) {
191             return imsList;
192         }
193         inputMethodSplitter.setString(enabledInputMethodsStr);
194         while (inputMethodSplitter.hasNext()) {
195             String nextImsStr = inputMethodSplitter.next();
196             subtypeSplitter.setString(nextImsStr);
197             if (subtypeSplitter.hasNext()) {
198                 ArrayList<String> subtypeHashes = new ArrayList<>();
199                 // The first element is ime id.
200                 String imeId = subtypeSplitter.next();
201                 while (subtypeSplitter.hasNext()) {
202                     subtypeHashes.add(subtypeSplitter.next());
203                 }
204                 imsList.add(new Pair<>(imeId, subtypeHashes));
205             }
206         }
207         return imsList;
208     }
209 
210     /**
211      * Build and put a string of EnabledInputMethods with removing specified Id.
212      *
213      * @return the specified id was removed or not.
214      */
buildAndPutEnabledInputMethodsStrRemovingId( StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id)215     boolean buildAndPutEnabledInputMethodsStrRemovingId(
216             StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
217         boolean isRemoved = false;
218         boolean needsAppendSeparator = false;
219         for (int i = 0; i < imsList.size(); ++i) {
220             final Pair<String, ArrayList<String>> ims = imsList.get(i);
221             final String curId = ims.first;
222             if (curId.equals(id)) {
223                 // We are disabling this input method, and it is
224                 // currently enabled.  Skip it to remove from the
225                 // new list.
226                 isRemoved = true;
227             } else {
228                 if (needsAppendSeparator) {
229                     builder.append(INPUT_METHOD_SEPARATOR);
230                 } else {
231                     needsAppendSeparator = true;
232                 }
233                 buildEnabledInputMethodsSettingString(builder, ims);
234             }
235         }
236         if (isRemoved) {
237             // Update the setting with the new list of input methods.
238             putEnabledInputMethodsStr(builder.toString());
239         }
240         return isRemoved;
241     }
242 
createEnabledInputMethodList( List<Pair<String, ArrayList<String>>> imsList, Predicate<InputMethodInfo> matchingCondition)243     private ArrayList<InputMethodInfo> createEnabledInputMethodList(
244             List<Pair<String, ArrayList<String>>> imsList,
245             Predicate<InputMethodInfo> matchingCondition) {
246         final ArrayList<InputMethodInfo> res = new ArrayList<>();
247         for (int i = 0; i < imsList.size(); ++i) {
248             final Pair<String, ArrayList<String>> ims = imsList.get(i);
249             final InputMethodInfo info = mMethodMap.get(ims.first);
250             if (info != null && !info.isVrOnly()
251                     && (matchingCondition == null || matchingCondition.test(info))) {
252                 res.add(info);
253             }
254         }
255         return res;
256     }
257 
putEnabledInputMethodsStr(@ullable String str)258     void putEnabledInputMethodsStr(@Nullable String str) {
259         if (DEBUG) {
260             Slog.d(TAG, "putEnabledInputMethodStr: " + str);
261         }
262         if (TextUtils.isEmpty(str)) {
263             // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
264             // empty data scenario.
265             putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
266         } else {
267             putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
268         }
269     }
270 
271     @NonNull
getEnabledInputMethodsStr()272     String getEnabledInputMethodsStr() {
273         return getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
274     }
275 
saveSubtypeHistory( List<Pair<String, String>> savedImes, String newImeId, String newSubtypeHashCodeStr)276     private void saveSubtypeHistory(
277             List<Pair<String, String>> savedImes, String newImeId, String newSubtypeHashCodeStr) {
278         final StringBuilder builder = new StringBuilder();
279         boolean isImeAdded = false;
280         if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeHashCodeStr)) {
281             builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
282                     newSubtypeHashCodeStr);
283             isImeAdded = true;
284         }
285         for (int i = 0; i < savedImes.size(); ++i) {
286             final Pair<String, String> ime = savedImes.get(i);
287             final String imeId = ime.first;
288             String subtypeHashCodeStr = ime.second;
289             if (TextUtils.isEmpty(subtypeHashCodeStr)) {
290                 subtypeHashCodeStr = INVALID_SUBTYPE_HASHCODE_STR;
291             }
292             if (isImeAdded) {
293                 builder.append(INPUT_METHOD_SEPARATOR);
294             } else {
295                 isImeAdded = true;
296             }
297             builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeHashCodeStr);
298         }
299         // Remove the last INPUT_METHOD_SEPARATOR
300         putSubtypeHistoryStr(builder.toString());
301     }
302 
addSubtypeToHistory(String imeId, String subtypeHashCodeStr)303     private void addSubtypeToHistory(String imeId, String subtypeHashCodeStr) {
304         final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory();
305         for (int i = 0; i < subtypeHistory.size(); ++i) {
306             final Pair<String, String> ime = subtypeHistory.get(i);
307             if (ime.first.equals(imeId)) {
308                 if (DEBUG) {
309                     Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
310                             + ime.second);
311                 }
312                 // We should break here
313                 subtypeHistory.remove(ime);
314                 break;
315             }
316         }
317         if (DEBUG) {
318             Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeHashCodeStr);
319         }
320         saveSubtypeHistory(subtypeHistory, imeId, subtypeHashCodeStr);
321     }
322 
putSubtypeHistoryStr(@onNull String str)323     private void putSubtypeHistoryStr(@NonNull String str) {
324         if (DEBUG) {
325             Slog.d(TAG, "putSubtypeHistoryStr: " + str);
326         }
327         if (TextUtils.isEmpty(str)) {
328             // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
329             // data scenario.
330             putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
331         } else {
332             putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
333         }
334     }
335 
getLastInputMethodAndSubtype()336     Pair<String, String> getLastInputMethodAndSubtype() {
337         // Gets the first one from the history
338         return getLastSubtypeForInputMethodInternal(null);
339     }
340 
341     @Nullable
getLastInputMethodSubtype()342     InputMethodSubtype getLastInputMethodSubtype() {
343         final Pair<String, String> lastIme = getLastInputMethodAndSubtype();
344         // TODO: Handle the case of the last IME with no subtypes
345         if (lastIme == null || TextUtils.isEmpty(lastIme.first)
346                 || TextUtils.isEmpty(lastIme.second)) {
347             return null;
348         }
349         final InputMethodInfo lastImi = mMethodMap.get(lastIme.first);
350         if (lastImi == null) return null;
351         try {
352             final int lastSubtypeHash = Integer.parseInt(lastIme.second);
353             final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi,
354                     lastSubtypeHash);
355             if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) {
356                 return null;
357             }
358             return lastImi.getSubtypeAt(lastSubtypeId);
359         } catch (NumberFormatException e) {
360             return null;
361         }
362     }
363 
getLastSubtypeForInputMethod(String imeId)364     String getLastSubtypeForInputMethod(String imeId) {
365         Pair<String, String> ime = getLastSubtypeForInputMethodInternal(imeId);
366         if (ime != null) {
367             return ime.second;
368         } else {
369             return null;
370         }
371     }
372 
getLastSubtypeForInputMethodInternal(String imeId)373     private Pair<String, String> getLastSubtypeForInputMethodInternal(String imeId) {
374         final List<Pair<String, ArrayList<String>>> enabledImes =
375                 getEnabledInputMethodsAndSubtypeList();
376         final List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistory();
377         for (int i = 0; i < subtypeHistory.size(); ++i) {
378             final Pair<String, String> imeAndSubtype = subtypeHistory.get(i);
379             final String imeInTheHistory = imeAndSubtype.first;
380             // If imeId is empty, returns the first IME and subtype in the history
381             if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
382                 final String subtypeInTheHistory = imeAndSubtype.second;
383                 final String subtypeHashCode =
384                         getEnabledSubtypeHashCodeForInputMethodAndSubtype(
385                                 enabledImes, imeInTheHistory, subtypeInTheHistory);
386                 if (!TextUtils.isEmpty(subtypeHashCode)) {
387                     if (DEBUG) {
388                         Slog.d(TAG,
389                                 "Enabled subtype found in the history: " + subtypeHashCode);
390                     }
391                     return new Pair<>(imeInTheHistory, subtypeHashCode);
392                 }
393             }
394         }
395         if (DEBUG) {
396             Slog.d(TAG, "No enabled IME found in the history");
397         }
398         return null;
399     }
400 
getEnabledSubtypeHashCodeForInputMethodAndSubtype(List<Pair<String, ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode)401     private String getEnabledSubtypeHashCodeForInputMethodAndSubtype(List<Pair<String,
402             ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
403         final LocaleList localeList = SystemLocaleWrapper.get(mUserId);
404         for (int i = 0; i < enabledImes.size(); ++i) {
405             final Pair<String, ArrayList<String>> enabledIme = enabledImes.get(i);
406             if (enabledIme.first.equals(imeId)) {
407                 final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
408                 final InputMethodInfo imi = mMethodMap.get(imeId);
409                 if (explicitlyEnabledSubtypes.isEmpty()) {
410                     // If there are no explicitly enabled subtypes, applicable subtypes are
411                     // enabled implicitly.
412                     // If IME is enabled and no subtypes are enabled, applicable subtypes
413                     // are enabled implicitly, so needs to treat them to be enabled.
414                     if (imi != null && imi.getSubtypeCount() > 0) {
415                         List<InputMethodSubtype> implicitlyEnabledSubtypes =
416                                 SubtypeUtils.getImplicitlyApplicableSubtypes(localeList,
417                                         imi);
418                         final int numSubtypes = implicitlyEnabledSubtypes.size();
419                         for (int j = 0; j < numSubtypes; ++j) {
420                             final InputMethodSubtype st = implicitlyEnabledSubtypes.get(j);
421                             if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
422                                 return subtypeHashCode;
423                             }
424                         }
425                     }
426                 } else {
427                     for (int j = 0; j < explicitlyEnabledSubtypes.size(); ++j) {
428                         final String s = explicitlyEnabledSubtypes.get(j);
429                         if (s.equals(subtypeHashCode)) {
430                             // If both imeId and subtype are enabled, return subtypeId.
431                             try {
432                                 final int hashCode = Integer.parseInt(subtypeHashCode);
433                                 // Check whether the subtype is valid or not
434                                 if (SubtypeUtils.isValidSubtypeHashCode(imi, hashCode)) {
435                                     return s;
436                                 } else {
437                                     return INVALID_SUBTYPE_HASHCODE_STR;
438                                 }
439                             } catch (NumberFormatException e) {
440                                 return INVALID_SUBTYPE_HASHCODE_STR;
441                             }
442                         }
443                     }
444                 }
445                 // If imeId was enabled but subtype was disabled.
446                 return INVALID_SUBTYPE_HASHCODE_STR;
447             }
448         }
449         // If both imeId and subtype are disabled, return null
450         return null;
451     }
452 
loadInputMethodAndSubtypeHistory()453     private List<Pair<String, String>> loadInputMethodAndSubtypeHistory() {
454         ArrayList<Pair<String, String>> imsList = new ArrayList<>();
455         final String subtypeHistoryStr = getSubtypeHistoryStr();
456         if (TextUtils.isEmpty(subtypeHistoryStr)) {
457             return imsList;
458         }
459         final TextUtils.SimpleStringSplitter inputMethodSplitter =
460                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
461         final TextUtils.SimpleStringSplitter subtypeSplitter =
462                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
463         inputMethodSplitter.setString(subtypeHistoryStr);
464         while (inputMethodSplitter.hasNext()) {
465             String nextImsStr = inputMethodSplitter.next();
466             subtypeSplitter.setString(nextImsStr);
467             if (subtypeSplitter.hasNext()) {
468                 String subtypeHashCodeStr = INVALID_SUBTYPE_HASHCODE_STR;
469                 // The first element is ime id.
470                 String imeId = subtypeSplitter.next();
471                 while (subtypeSplitter.hasNext()) {
472                     subtypeHashCodeStr = subtypeSplitter.next();
473                     break;
474                 }
475                 imsList.add(new Pair<>(imeId, subtypeHashCodeStr));
476             }
477         }
478         return imsList;
479     }
480 
481     @NonNull
getSubtypeHistoryStr()482     private String getSubtypeHistoryStr() {
483         final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
484         if (DEBUG) {
485             Slog.d(TAG, "getSubtypeHistoryStr: " + history);
486         }
487         return history;
488     }
489 
putSelectedInputMethod(String imeId)490     void putSelectedInputMethod(String imeId) {
491         if (DEBUG) {
492             Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", " + mUserId);
493         }
494         putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
495     }
496 
putSelectedSubtype(int subtypeId)497     void putSelectedSubtype(int subtypeId) {
498         if (DEBUG) {
499             Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", " + mUserId);
500         }
501         putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
502     }
503 
504     @Nullable
getSelectedInputMethod()505     String getSelectedInputMethod() {
506         final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
507         if (DEBUG) {
508             Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
509         }
510         return imi;
511     }
512 
513     @Nullable
getSelectedDefaultDeviceInputMethod()514     String getSelectedDefaultDeviceInputMethod() {
515         final String imi = getString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, null);
516         if (DEBUG) {
517             Slog.d(TAG, "getSelectedDefaultDeviceInputMethodStr: " + imi + ", " + mUserId);
518         }
519         return imi;
520     }
521 
putSelectedDefaultDeviceInputMethod(String imeId)522     void putSelectedDefaultDeviceInputMethod(String imeId) {
523         if (DEBUG) {
524             Slog.d(TAG, "putSelectedDefaultDeviceInputMethodStr: " + imeId + ", " + mUserId);
525         }
526         putString(Settings.Secure.DEFAULT_DEVICE_INPUT_METHOD, imeId);
527     }
528 
putDefaultVoiceInputMethod(String imeId)529     void putDefaultVoiceInputMethod(String imeId) {
530         if (DEBUG) {
531             Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mUserId);
532         }
533         putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId);
534     }
535 
536     @Nullable
getDefaultVoiceInputMethod()537     String getDefaultVoiceInputMethod() {
538         final String imi = getString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, null);
539         if (DEBUG) {
540             Slog.d(TAG, "getDefaultVoiceInputMethodStr: " + imi);
541         }
542         return imi;
543     }
544 
getSelectedInputMethodSubtypeHashCode()545     private int getSelectedInputMethodSubtypeHashCode() {
546         return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, INVALID_SUBTYPE_HASHCODE);
547     }
548 
549     @UserIdInt
getUserId()550     public int getUserId() {
551         return mUserId;
552     }
553 
getSelectedInputMethodSubtypeId(String selectedImiId)554     int getSelectedInputMethodSubtypeId(String selectedImiId) {
555         final InputMethodInfo imi = mMethodMap.get(selectedImiId);
556         if (imi == null) {
557             return InputMethodUtils.NOT_A_SUBTYPE_ID;
558         }
559         final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
560         return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode);
561     }
562 
saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, InputMethodSubtype currentSubtype)563     void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId,
564             InputMethodSubtype currentSubtype) {
565         String subtypeHashCodeStr = INVALID_SUBTYPE_HASHCODE_STR;
566         if (currentSubtype != null) {
567             subtypeHashCodeStr = String.valueOf(currentSubtype.hashCode());
568         }
569         if (InputMethodUtils.canAddToLastInputMethod(currentSubtype)) {
570             addSubtypeToHistory(curMethodId, subtypeHashCodeStr);
571         }
572     }
573 
574     /**
575      * A variant of {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()} for
576      * non-current users.
577      *
578      * <p>TODO: Address code duplication between this and
579      * {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()}.</p>
580      *
581      * @return {@link InputMethodSubtype} if exists. {@code null} otherwise.
582      */
583     @Nullable
getCurrentInputMethodSubtypeForNonCurrentUsers()584     InputMethodSubtype getCurrentInputMethodSubtypeForNonCurrentUsers() {
585         final String selectedMethodId = getSelectedInputMethod();
586         if (selectedMethodId == null) {
587             return null;
588         }
589         final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
590         if (imi == null || imi.getSubtypeCount() == 0) {
591             return null;
592         }
593 
594         final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
595         if (subtypeHashCode != INVALID_SUBTYPE_HASHCODE) {
596             final int subtypeIndex = SubtypeUtils.getSubtypeIdFromHashCode(imi,
597                     subtypeHashCode);
598             if (subtypeIndex >= 0) {
599                 return imi.getSubtypeAt(subtypeIndex);
600             }
601         }
602 
603         // If there are no selected subtypes, the framework will try to find the most applicable
604         // subtype from explicitly or implicitly enabled subtypes.
605         final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
606                 getEnabledInputMethodSubtypeList(imi, true);
607         // If there is only one explicitly or implicitly enabled subtype, just returns it.
608         if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) {
609             return null;
610         }
611         if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
612             return explicitlyOrImplicitlyEnabledSubtypes.get(0);
613         }
614         final String locale = SystemLocaleWrapper.get(mUserId).get(0).toString();
615         final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtype(
616                 explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD,
617                 locale, true);
618         if (subtype != null) {
619             return subtype;
620         }
621         return SubtypeUtils.findLastResortApplicableSubtype(
622                 explicitlyOrImplicitlyEnabledSubtypes, null, locale, true);
623     }
624 
625     @NonNull
getNewAdditionalSubtypeMap(@onNull String imeId, @NonNull ArrayList<InputMethodSubtype> subtypes, @NonNull AdditionalSubtypeMap additionalSubtypeMap, @NonNull PackageManagerInternal packageManagerInternal, int callingUid)626     AdditionalSubtypeMap getNewAdditionalSubtypeMap(@NonNull String imeId,
627             @NonNull ArrayList<InputMethodSubtype> subtypes,
628             @NonNull AdditionalSubtypeMap additionalSubtypeMap,
629             @NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
630         final InputMethodInfo imi = mMethodMap.get(imeId);
631         if (imi == null) {
632             return additionalSubtypeMap;
633         }
634         if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
635                 imi.getPackageName())) {
636             return additionalSubtypeMap;
637         }
638 
639         final AdditionalSubtypeMap newMap;
640         if (subtypes.isEmpty()) {
641             newMap = additionalSubtypeMap.cloneWithRemoveOrSelf(imi.getId());
642         } else {
643             newMap = additionalSubtypeMap.cloneWithPut(imi.getId(), subtypes);
644         }
645         return newMap;
646     }
647 
setEnabledInputMethodSubtypes(@onNull String imeId, @NonNull int[] subtypeHashCodes)648     boolean setEnabledInputMethodSubtypes(@NonNull String imeId,
649             @NonNull int[] subtypeHashCodes) {
650         final InputMethodInfo imi = mMethodMap.get(imeId);
651         if (imi == null) {
652             return false;
653         }
654 
655         final IntArray validSubtypeHashCodes = new IntArray(subtypeHashCodes.length);
656         for (int subtypeHashCode : subtypeHashCodes) {
657             if (subtypeHashCode == INVALID_SUBTYPE_HASHCODE) {
658                 continue;  // INVALID_SUBTYPE_HASHCODE must not be saved
659             }
660             if (!SubtypeUtils.isValidSubtypeHashCode(imi, subtypeHashCode)) {
661                 continue;  // this subtype does not exist in InputMethodInfo.
662             }
663             if (validSubtypeHashCodes.indexOf(subtypeHashCode) >= 0) {
664                 continue;  // The entry is already added.  No need to add anymore.
665             }
666             validSubtypeHashCodes.add(subtypeHashCode);
667         }
668 
669         final String originalEnabledImesString = getEnabledInputMethodsStr();
670         final String updatedEnabledImesString = updateEnabledImeString(
671                 originalEnabledImesString, imi.getId(), validSubtypeHashCodes);
672         if (TextUtils.equals(originalEnabledImesString, updatedEnabledImesString)) {
673             return false;
674         }
675 
676         putEnabledInputMethodsStr(updatedEnabledImesString);
677         return true;
678     }
679 
680     @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
updateEnabledImeString(@onNull String enabledImesString, @NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes)681     static String updateEnabledImeString(@NonNull String enabledImesString,
682             @NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes) {
683         final TextUtils.SimpleStringSplitter imeSplitter =
684                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
685         final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
686                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
687 
688         final StringBuilder sb = new StringBuilder();
689 
690         imeSplitter.setString(enabledImesString);
691         boolean needsImeSeparator = false;
692         while (imeSplitter.hasNext()) {
693             final String nextImsStr = imeSplitter.next();
694             imeSubtypeSplitter.setString(nextImsStr);
695             if (imeSubtypeSplitter.hasNext()) {
696                 if (needsImeSeparator) {
697                     sb.append(INPUT_METHOD_SEPARATOR);
698                 }
699                 if (TextUtils.equals(imeId, imeSubtypeSplitter.next())) {
700                     sb.append(imeId);
701                     for (int i = 0; i < enabledSubtypeHashCodes.size(); ++i) {
702                         sb.append(INPUT_METHOD_SUBTYPE_SEPARATOR);
703                         sb.append(enabledSubtypeHashCodes.get(i));
704                     }
705                 } else {
706                     sb.append(nextImsStr);
707                 }
708                 needsImeSeparator = true;
709             }
710         }
711         return sb.toString();
712     }
713 
dump(final Printer pw, final String prefix)714     void dump(final Printer pw, final String prefix) {
715         pw.println(prefix + "mUserId=" + mUserId);
716     }
717 }
718