1 /*
2  * Copyright (C) 2013 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.inputmethod;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.annotation.UserHandleAware;
22 import android.annotation.UserIdInt;
23 import android.content.ComponentName;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.pm.ApplicationInfo;
27 import android.content.pm.PackageManager;
28 import android.content.pm.PackageManagerInternal;
29 import android.content.pm.ResolveInfo;
30 import android.content.res.Resources;
31 import android.os.Build;
32 import android.os.UserHandle;
33 import android.provider.Settings;
34 import android.text.TextUtils;
35 import android.util.ArrayMap;
36 import android.util.ArraySet;
37 import android.util.IntArray;
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.VisibleForTesting;
46 import com.android.internal.inputmethod.StartInputFlags;
47 import com.android.server.LocalServices;
48 import com.android.server.pm.UserManagerInternal;
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.List;
55 import java.util.function.Predicate;
56 
57 /**
58  * This class provides random static utility methods for {@link InputMethodManagerService} and its
59  * utility classes.
60  *
61  * <p>This class is intentionally package-private.  Utility methods here are tightly coupled with
62  * implementation details in {@link InputMethodManagerService}.  Hence this class is not suitable
63  * for other components to directly use.</p>
64  */
65 final class InputMethodUtils {
66     public static final boolean DEBUG = false;
67     static final int NOT_A_SUBTYPE_ID = -1;
68     private static final String TAG = "InputMethodUtils";
69     private static final String NOT_A_SUBTYPE_ID_STR = String.valueOf(NOT_A_SUBTYPE_ID);
70 
71     // The string for enabled input method is saved as follows:
72     // example: ("ime0;subtype0;subtype1;subtype2:ime1:ime2;subtype0")
73     private static final char INPUT_METHOD_SEPARATOR = ':';
74     private static final char INPUT_METHOD_SUBTYPE_SEPARATOR = ';';
75 
InputMethodUtils()76     private InputMethodUtils() {
77         // This utility class is not publicly instantiable.
78     }
79 
80     // ----------------------------------------------------------------------
81 
canAddToLastInputMethod(InputMethodSubtype subtype)82     static boolean canAddToLastInputMethod(InputMethodSubtype subtype) {
83         if (subtype == null) return true;
84         return !subtype.isAuxiliary();
85     }
86 
87     @UserHandleAware
setNonSelectedSystemImesDisabledUntilUsed(PackageManager packageManagerForUser, List<InputMethodInfo> enabledImis)88     static void setNonSelectedSystemImesDisabledUntilUsed(PackageManager packageManagerForUser,
89             List<InputMethodInfo> enabledImis) {
90         if (DEBUG) {
91             Slog.d(TAG, "setNonSelectedSystemImesDisabledUntilUsed");
92         }
93         final String[] systemImesDisabledUntilUsed = Resources.getSystem().getStringArray(
94                 com.android.internal.R.array.config_disabledUntilUsedPreinstalledImes);
95         if (systemImesDisabledUntilUsed == null || systemImesDisabledUntilUsed.length == 0) {
96             return;
97         }
98         // Only the current spell checker should be treated as an enabled one.
99         final SpellCheckerInfo currentSpellChecker =
100                 TextServicesManagerInternal.get().getCurrentSpellCheckerForUser(
101                         packageManagerForUser.getUserId());
102         for (final String packageName : systemImesDisabledUntilUsed) {
103             if (DEBUG) {
104                 Slog.d(TAG, "check " + packageName);
105             }
106             boolean enabledIme = false;
107             for (int j = 0; j < enabledImis.size(); ++j) {
108                 final InputMethodInfo imi = enabledImis.get(j);
109                 if (packageName.equals(imi.getPackageName())) {
110                     enabledIme = true;
111                     break;
112                 }
113             }
114             if (enabledIme) {
115                 // enabled ime. skip
116                 continue;
117             }
118             if (currentSpellChecker != null
119                     && packageName.equals(currentSpellChecker.getPackageName())) {
120                 // enabled spell checker. skip
121                 if (DEBUG) {
122                     Slog.d(TAG, packageName + " is the current spell checker. skip");
123                 }
124                 continue;
125             }
126             ApplicationInfo ai;
127             try {
128                 ai = packageManagerForUser.getApplicationInfo(packageName,
129                         PackageManager.ApplicationInfoFlags.of(
130                                 PackageManager.GET_DISABLED_UNTIL_USED_COMPONENTS));
131             } catch (PackageManager.NameNotFoundException e) {
132                 // This is not an error.  No need to show scary error messages.
133                 if (DEBUG) {
134                     Slog.d(TAG, packageName
135                             + " does not exist for userId=" + packageManagerForUser.getUserId());
136                 }
137                 continue;
138             }
139             if (ai == null) {
140                 // No app found for packageName
141                 continue;
142             }
143             final boolean isSystemPackage = (ai.flags & ApplicationInfo.FLAG_SYSTEM) != 0;
144             if (!isSystemPackage) {
145                 continue;
146             }
147             setDisabledUntilUsed(packageManagerForUser, packageName);
148         }
149     }
150 
setDisabledUntilUsed(PackageManager packageManagerForUser, String packageName)151     private static void setDisabledUntilUsed(PackageManager packageManagerForUser,
152             String packageName) {
153         final int state;
154         try {
155             state = packageManagerForUser.getApplicationEnabledSetting(packageName);
156         } catch (IllegalArgumentException e) {
157             Slog.w(TAG, "getApplicationEnabledSetting failed. packageName=" + packageName
158                     + " userId=" + packageManagerForUser.getUserId(), e);
159             return;
160         }
161         if (state == PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
162                 || state == PackageManager.COMPONENT_ENABLED_STATE_ENABLED) {
163             if (DEBUG) {
164                 Slog.d(TAG, "Update state(" + packageName + "): DISABLED_UNTIL_USED");
165             }
166             try {
167                 packageManagerForUser.setApplicationEnabledSetting(packageName,
168                         PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED,
169                         0 /* newState */);
170             } catch (IllegalArgumentException e) {
171                 Slog.w(TAG, "setApplicationEnabledSetting failed. packageName=" + packageName
172                         + " userId=" + packageManagerForUser.getUserId(), e);
173                 return;
174             }
175         } else {
176             if (DEBUG) {
177                 Slog.d(TAG, packageName + " is already DISABLED_UNTIL_USED");
178             }
179         }
180     }
181 
182     /**
183      * Returns true if a package name belongs to a UID.
184      *
185      * <p>This is a simple wrapper of
186      * {@link PackageManagerInternal#getPackageUid(String, long, int)}.</p>
187      * @param packageManagerInternal the {@link PackageManagerInternal} object to be used for the
188      *                               validation.
189      * @param uid the UID to be validated.
190      * @param packageName the package name.
191      * @return {@code true} if the package name belongs to the UID.
192      */
checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal, int uid, String packageName)193     static boolean checkIfPackageBelongsToUid(PackageManagerInternal packageManagerInternal,
194             int uid, String packageName) {
195         // PackageManagerInternal#getPackageUid() doesn't check MATCH_INSTANT/MATCH_APEX as of
196         // writing. So setting 0 should be fine.
197         return packageManagerInternal.isSameApp(packageName, /* flags= */ 0, uid,
198             UserHandle.getUserId(uid));
199     }
200 
201     /**
202      * Utility class for putting and getting settings for InputMethod.
203      *
204      * This is used in two ways:
205      * - Singleton instance in {@link InputMethodManagerService}, which is updated on user-switch to
206      * follow the current user.
207      * - On-demand instances when we need settings for non-current users.
208      *
209      * TODO: Move all putters and getters of settings to this class.
210      */
211     @UserHandleAware
212     public static class InputMethodSettings {
213         private final TextUtils.SimpleStringSplitter mInputMethodSplitter =
214                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
215 
216         private final TextUtils.SimpleStringSplitter mSubtypeSplitter =
217                 new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
218 
219         @NonNull
220         private Context mUserAwareContext;
221         private Resources mRes;
222         private ContentResolver mResolver;
223         private final ArrayMap<String, InputMethodInfo> mMethodMap;
224 
225         /**
226          * On-memory data store to emulate when {@link #mCopyOnWrite} is {@code true}.
227          */
228         private final ArrayMap<String, String> mCopyOnWriteDataStore = new ArrayMap<>();
229 
230         private static final ArraySet<String> CLONE_TO_MANAGED_PROFILE = new ArraySet<>();
231         static {
232             Settings.Secure.getCloneToManagedProfileSettings(CLONE_TO_MANAGED_PROFILE);
233         }
234 
235         private static final UserManagerInternal sUserManagerInternal =
236                 LocalServices.getService(UserManagerInternal.class);
237 
238         private boolean mCopyOnWrite = false;
239         @NonNull
240         private String mEnabledInputMethodsStrCache = "";
241         @UserIdInt
242         private int mCurrentUserId;
243         private int[] mCurrentProfileIds = new int[0];
244 
buildEnabledInputMethodsSettingString( StringBuilder builder, Pair<String, ArrayList<String>> ime)245         private static void buildEnabledInputMethodsSettingString(
246                 StringBuilder builder, Pair<String, ArrayList<String>> ime) {
247             builder.append(ime.first);
248             // Inputmethod and subtypes are saved in the settings as follows:
249             // ime0;subtype0;subtype1:ime1;subtype0:ime2:ime3;subtype0;subtype1
250             for (String subtypeId: ime.second) {
251                 builder.append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(subtypeId);
252             }
253         }
254 
buildInputMethodsAndSubtypeList( String enabledInputMethodsStr, TextUtils.SimpleStringSplitter inputMethodSplitter, TextUtils.SimpleStringSplitter subtypeSplitter)255         private static List<Pair<String, ArrayList<String>>> buildInputMethodsAndSubtypeList(
256                 String enabledInputMethodsStr,
257                 TextUtils.SimpleStringSplitter inputMethodSplitter,
258                 TextUtils.SimpleStringSplitter subtypeSplitter) {
259             ArrayList<Pair<String, ArrayList<String>>> imsList = new ArrayList<>();
260             if (TextUtils.isEmpty(enabledInputMethodsStr)) {
261                 return imsList;
262             }
263             inputMethodSplitter.setString(enabledInputMethodsStr);
264             while (inputMethodSplitter.hasNext()) {
265                 String nextImsStr = inputMethodSplitter.next();
266                 subtypeSplitter.setString(nextImsStr);
267                 if (subtypeSplitter.hasNext()) {
268                     ArrayList<String> subtypeHashes = new ArrayList<>();
269                     // The first element is ime id.
270                     String imeId = subtypeSplitter.next();
271                     while (subtypeSplitter.hasNext()) {
272                         subtypeHashes.add(subtypeSplitter.next());
273                     }
274                     imsList.add(new Pair<>(imeId, subtypeHashes));
275                 }
276             }
277             return imsList;
278         }
279 
initContentWithUserContext(@onNull Context context, @UserIdInt int userId)280         private void initContentWithUserContext(@NonNull Context context, @UserIdInt int userId) {
281             mUserAwareContext = context.getUserId() == userId
282                     ? context
283                     : context.createContextAsUser(UserHandle.of(userId), 0 /* flags */);
284             mRes = mUserAwareContext.getResources();
285             mResolver = mUserAwareContext.getContentResolver();
286         }
287 
InputMethodSettings(@onNull Context context, ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId, boolean copyOnWrite)288         InputMethodSettings(@NonNull Context context,
289                 ArrayMap<String, InputMethodInfo> methodMap, @UserIdInt int userId,
290                 boolean copyOnWrite) {
291             mMethodMap = methodMap;
292             initContentWithUserContext(context, userId);
293             switchCurrentUser(userId, copyOnWrite);
294         }
295 
296         /**
297          * Must be called when the current user is changed.
298          *
299          * @param userId The user ID.
300          * @param copyOnWrite If {@code true}, for each settings key
301          * (e.g. {@link Settings.Secure#ACTION_INPUT_METHOD_SUBTYPE_SETTINGS}) we use the actual
302          * settings on the {@link Settings.Secure} until we do the first write operation.
303          */
switchCurrentUser(@serIdInt int userId, boolean copyOnWrite)304         void switchCurrentUser(@UserIdInt int userId, boolean copyOnWrite) {
305             if (DEBUG) {
306                 Slog.d(TAG, "--- Switch the current user from " + mCurrentUserId + " to " + userId);
307             }
308             if (mCurrentUserId != userId || mCopyOnWrite != copyOnWrite) {
309                 mCopyOnWriteDataStore.clear();
310                 mEnabledInputMethodsStrCache = "";
311                 // TODO: mCurrentProfileIds should be cleared here.
312             }
313             if (mUserAwareContext.getUserId() != userId) {
314                 initContentWithUserContext(mUserAwareContext, userId);
315             }
316             mCurrentUserId = userId;
317             mCopyOnWrite = copyOnWrite;
318             // TODO: mCurrentProfileIds should be updated here.
319         }
320 
putString(@onNull String key, @Nullable String str)321         private void putString(@NonNull String key, @Nullable String str) {
322             if (mCopyOnWrite) {
323                 mCopyOnWriteDataStore.put(key, str);
324             } else {
325                 final int userId = CLONE_TO_MANAGED_PROFILE.contains(key)
326                         ? sUserManagerInternal.getProfileParentId(mCurrentUserId) : mCurrentUserId;
327                 Settings.Secure.putStringForUser(mResolver, key, str, userId);
328             }
329         }
330 
331         @Nullable
getString(@onNull String key, @Nullable String defaultValue)332         private String getString(@NonNull String key, @Nullable String defaultValue) {
333             return getStringForUser(key, defaultValue, mCurrentUserId);
334         }
335 
336         @Nullable
getStringForUser( @onNull String key, @Nullable String defaultValue, @UserIdInt int userId)337         private String getStringForUser(
338                 @NonNull String key, @Nullable String defaultValue, @UserIdInt int userId) {
339             final String result;
340             if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
341                 result = mCopyOnWriteDataStore.get(key);
342             } else {
343                 result = Settings.Secure.getStringForUser(mResolver, key, userId);
344             }
345             return result != null ? result : defaultValue;
346         }
347 
putInt(String key, int value)348         private void putInt(String key, int value) {
349             if (mCopyOnWrite) {
350                 mCopyOnWriteDataStore.put(key, String.valueOf(value));
351             } else {
352                 final int userId = CLONE_TO_MANAGED_PROFILE.contains(key)
353                         ? sUserManagerInternal.getProfileParentId(mCurrentUserId) : mCurrentUserId;
354                 Settings.Secure.putIntForUser(mResolver, key, value, userId);
355             }
356         }
357 
getInt(String key, int defaultValue)358         private int getInt(String key, int defaultValue) {
359             if (mCopyOnWrite && mCopyOnWriteDataStore.containsKey(key)) {
360                 final String result = mCopyOnWriteDataStore.get(key);
361                 return result != null ? Integer.parseInt(result) : defaultValue;
362             }
363             return Settings.Secure.getIntForUser(mResolver, key, defaultValue, mCurrentUserId);
364         }
365 
putBoolean(String key, boolean value)366         private void putBoolean(String key, boolean value) {
367             putInt(key, value ? 1 : 0);
368         }
369 
getBoolean(String key, boolean defaultValue)370         private boolean getBoolean(String key, boolean defaultValue) {
371             return getInt(key, defaultValue ? 1 : 0) == 1;
372         }
373 
setCurrentProfileIds(int[] currentProfileIds)374         public void setCurrentProfileIds(int[] currentProfileIds) {
375             synchronized (this) {
376                 mCurrentProfileIds = currentProfileIds;
377             }
378         }
379 
isCurrentProfile(int userId)380         public boolean isCurrentProfile(int userId) {
381             synchronized (this) {
382                 if (userId == mCurrentUserId) return true;
383                 for (int i = 0; i < mCurrentProfileIds.length; i++) {
384                     if (userId == mCurrentProfileIds[i]) return true;
385                 }
386                 return false;
387             }
388         }
389 
getEnabledInputMethodListLocked()390         ArrayList<InputMethodInfo> getEnabledInputMethodListLocked() {
391             return getEnabledInputMethodListWithFilterLocked(null /* matchingCondition */);
392         }
393 
394         @NonNull
getEnabledInputMethodListWithFilterLocked( @ullable Predicate<InputMethodInfo> matchingCondition)395         ArrayList<InputMethodInfo> getEnabledInputMethodListWithFilterLocked(
396                 @Nullable Predicate<InputMethodInfo> matchingCondition) {
397             return createEnabledInputMethodListLocked(
398                     getEnabledInputMethodsAndSubtypeListLocked(), matchingCondition);
399         }
400 
getEnabledInputMethodSubtypeListLocked( InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes)401         List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(
402                 InputMethodInfo imi, boolean allowsImplicitlyEnabledSubtypes) {
403             List<InputMethodSubtype> enabledSubtypes =
404                     getEnabledInputMethodSubtypeListLocked(imi);
405             if (allowsImplicitlyEnabledSubtypes && enabledSubtypes.isEmpty()) {
406                 enabledSubtypes = SubtypeUtils.getImplicitlyApplicableSubtypesLocked(mRes, imi);
407             }
408             return InputMethodSubtype.sort(imi, enabledSubtypes);
409         }
410 
getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi)411         List<InputMethodSubtype> getEnabledInputMethodSubtypeListLocked(InputMethodInfo imi) {
412             List<Pair<String, ArrayList<String>>> imsList =
413                     getEnabledInputMethodsAndSubtypeListLocked();
414             ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
415             if (imi != null) {
416                 for (Pair<String, ArrayList<String>> imsPair : imsList) {
417                     InputMethodInfo info = mMethodMap.get(imsPair.first);
418                     if (info != null && info.getId().equals(imi.getId())) {
419                         final int subtypeCount = info.getSubtypeCount();
420                         for (int i = 0; i < subtypeCount; ++i) {
421                             InputMethodSubtype ims = info.getSubtypeAt(i);
422                             for (String s: imsPair.second) {
423                                 if (String.valueOf(ims.hashCode()).equals(s)) {
424                                     enabledSubtypes.add(ims);
425                                 }
426                             }
427                         }
428                         break;
429                     }
430                 }
431             }
432             return enabledSubtypes;
433         }
434 
getEnabledInputMethodsAndSubtypeListLocked()435         List<Pair<String, ArrayList<String>>> getEnabledInputMethodsAndSubtypeListLocked() {
436             return buildInputMethodsAndSubtypeList(getEnabledInputMethodsStr(),
437                     mInputMethodSplitter,
438                     mSubtypeSplitter);
439         }
440 
getEnabledInputMethodNames()441         List<String> getEnabledInputMethodNames() {
442             List<String> result = new ArrayList<>();
443             for (Pair<String, ArrayList<String>> pair :
444                     getEnabledInputMethodsAndSubtypeListLocked()) {
445                 result.add(pair.first);
446             }
447             return result;
448         }
449 
appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr)450         void appendAndPutEnabledInputMethodLocked(String id, boolean reloadInputMethodStr) {
451             if (reloadInputMethodStr) {
452                 getEnabledInputMethodsStr();
453             }
454             if (TextUtils.isEmpty(mEnabledInputMethodsStrCache)) {
455                 // Add in the newly enabled input method.
456                 putEnabledInputMethodsStr(id);
457             } else {
458                 putEnabledInputMethodsStr(
459                         mEnabledInputMethodsStrCache + INPUT_METHOD_SEPARATOR + id);
460             }
461         }
462 
463         /**
464          * Build and put a string of EnabledInputMethods with removing specified Id.
465          * @return the specified id was removed or not.
466          */
buildAndPutEnabledInputMethodsStrRemovingIdLocked( StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id)467         boolean buildAndPutEnabledInputMethodsStrRemovingIdLocked(
468                 StringBuilder builder, List<Pair<String, ArrayList<String>>> imsList, String id) {
469             boolean isRemoved = false;
470             boolean needsAppendSeparator = false;
471             for (Pair<String, ArrayList<String>> ims: imsList) {
472                 String curId = ims.first;
473                 if (curId.equals(id)) {
474                     // We are disabling this input method, and it is
475                     // currently enabled.  Skip it to remove from the
476                     // new list.
477                     isRemoved = true;
478                 } else {
479                     if (needsAppendSeparator) {
480                         builder.append(INPUT_METHOD_SEPARATOR);
481                     } else {
482                         needsAppendSeparator = true;
483                     }
484                     buildEnabledInputMethodsSettingString(builder, ims);
485                 }
486             }
487             if (isRemoved) {
488                 // Update the setting with the new list of input methods.
489                 putEnabledInputMethodsStr(builder.toString());
490             }
491             return isRemoved;
492         }
493 
createEnabledInputMethodListLocked( List<Pair<String, ArrayList<String>>> imsList, Predicate<InputMethodInfo> matchingCondition)494         private ArrayList<InputMethodInfo> createEnabledInputMethodListLocked(
495                 List<Pair<String, ArrayList<String>>> imsList,
496                 Predicate<InputMethodInfo> matchingCondition) {
497             final ArrayList<InputMethodInfo> res = new ArrayList<>();
498             for (Pair<String, ArrayList<String>> ims: imsList) {
499                 InputMethodInfo info = mMethodMap.get(ims.first);
500                 if (info != null && !info.isVrOnly()
501                         && (matchingCondition == null || matchingCondition.test(info))) {
502                     res.add(info);
503                 }
504             }
505             return res;
506         }
507 
putEnabledInputMethodsStr(@ullable String str)508         void putEnabledInputMethodsStr(@Nullable String str) {
509             if (DEBUG) {
510                 Slog.d(TAG, "putEnabledInputMethodStr: " + str);
511             }
512             if (TextUtils.isEmpty(str)) {
513                 // OK to coalesce to null, since getEnabledInputMethodsStr() can take care of the
514                 // empty data scenario.
515                 putString(Settings.Secure.ENABLED_INPUT_METHODS, null);
516             } else {
517                 putString(Settings.Secure.ENABLED_INPUT_METHODS, str);
518             }
519             // TODO: Update callers of putEnabledInputMethodsStr to make str @NonNull.
520             mEnabledInputMethodsStrCache = (str != null ? str : "");
521         }
522 
523         @NonNull
getEnabledInputMethodsStr()524         String getEnabledInputMethodsStr() {
525             mEnabledInputMethodsStrCache = getString(Settings.Secure.ENABLED_INPUT_METHODS, "");
526             if (DEBUG) {
527                 Slog.d(TAG, "getEnabledInputMethodsStr: " + mEnabledInputMethodsStrCache
528                         + ", " + mCurrentUserId);
529             }
530             return mEnabledInputMethodsStrCache;
531         }
532 
saveSubtypeHistory( List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId)533         private void saveSubtypeHistory(
534                 List<Pair<String, String>> savedImes, String newImeId, String newSubtypeId) {
535             StringBuilder builder = new StringBuilder();
536             boolean isImeAdded = false;
537             if (!TextUtils.isEmpty(newImeId) && !TextUtils.isEmpty(newSubtypeId)) {
538                 builder.append(newImeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
539                         newSubtypeId);
540                 isImeAdded = true;
541             }
542             for (Pair<String, String> ime: savedImes) {
543                 String imeId = ime.first;
544                 String subtypeId = ime.second;
545                 if (TextUtils.isEmpty(subtypeId)) {
546                     subtypeId = NOT_A_SUBTYPE_ID_STR;
547                 }
548                 if (isImeAdded) {
549                     builder.append(INPUT_METHOD_SEPARATOR);
550                 } else {
551                     isImeAdded = true;
552                 }
553                 builder.append(imeId).append(INPUT_METHOD_SUBTYPE_SEPARATOR).append(
554                         subtypeId);
555             }
556             // Remove the last INPUT_METHOD_SEPARATOR
557             putSubtypeHistoryStr(builder.toString());
558         }
559 
addSubtypeToHistory(String imeId, String subtypeId)560         private void addSubtypeToHistory(String imeId, String subtypeId) {
561             List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
562             for (Pair<String, String> ime: subtypeHistory) {
563                 if (ime.first.equals(imeId)) {
564                     if (DEBUG) {
565                         Slog.v(TAG, "Subtype found in the history: " + imeId + ", "
566                                 + ime.second);
567                     }
568                     // We should break here
569                     subtypeHistory.remove(ime);
570                     break;
571                 }
572             }
573             if (DEBUG) {
574                 Slog.v(TAG, "Add subtype to the history: " + imeId + ", " + subtypeId);
575             }
576             saveSubtypeHistory(subtypeHistory, imeId, subtypeId);
577         }
578 
putSubtypeHistoryStr(@onNull String str)579         private void putSubtypeHistoryStr(@NonNull String str) {
580             if (DEBUG) {
581                 Slog.d(TAG, "putSubtypeHistoryStr: " + str);
582             }
583             if (TextUtils.isEmpty(str)) {
584                 // OK to coalesce to null, since getSubtypeHistoryStr() can take care of the empty
585                 // data scenario.
586                 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, null);
587             } else {
588                 putString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, str);
589             }
590         }
591 
getLastInputMethodAndSubtypeLocked()592         Pair<String, String> getLastInputMethodAndSubtypeLocked() {
593             // Gets the first one from the history
594             return getLastSubtypeForInputMethodLockedInternal(null);
595         }
596 
597         @Nullable
getLastInputMethodSubtypeLocked()598         InputMethodSubtype getLastInputMethodSubtypeLocked() {
599             final Pair<String, String> lastIme = getLastInputMethodAndSubtypeLocked();
600             // TODO: Handle the case of the last IME with no subtypes
601             if (lastIme == null || TextUtils.isEmpty(lastIme.first)
602                     || TextUtils.isEmpty(lastIme.second)) return null;
603             final InputMethodInfo lastImi = mMethodMap.get(lastIme.first);
604             if (lastImi == null) return null;
605             try {
606                 final int lastSubtypeHash = Integer.parseInt(lastIme.second);
607                 final int lastSubtypeId = SubtypeUtils.getSubtypeIdFromHashCode(lastImi,
608                         lastSubtypeHash);
609                 if (lastSubtypeId < 0 || lastSubtypeId >= lastImi.getSubtypeCount()) {
610                     return null;
611                 }
612                 return lastImi.getSubtypeAt(lastSubtypeId);
613             } catch (NumberFormatException e) {
614                 return null;
615             }
616         }
617 
getLastSubtypeForInputMethodLocked(String imeId)618         String getLastSubtypeForInputMethodLocked(String imeId) {
619             Pair<String, String> ime = getLastSubtypeForInputMethodLockedInternal(imeId);
620             if (ime != null) {
621                 return ime.second;
622             } else {
623                 return null;
624             }
625         }
626 
getLastSubtypeForInputMethodLockedInternal(String imeId)627         private Pair<String, String> getLastSubtypeForInputMethodLockedInternal(String imeId) {
628             List<Pair<String, ArrayList<String>>> enabledImes =
629                     getEnabledInputMethodsAndSubtypeListLocked();
630             List<Pair<String, String>> subtypeHistory = loadInputMethodAndSubtypeHistoryLocked();
631             for (Pair<String, String> imeAndSubtype : subtypeHistory) {
632                 final String imeInTheHistory = imeAndSubtype.first;
633                 // If imeId is empty, returns the first IME and subtype in the history
634                 if (TextUtils.isEmpty(imeId) || imeInTheHistory.equals(imeId)) {
635                     final String subtypeInTheHistory = imeAndSubtype.second;
636                     final String subtypeHashCode =
637                             getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(
638                                     enabledImes, imeInTheHistory, subtypeInTheHistory);
639                     if (!TextUtils.isEmpty(subtypeHashCode)) {
640                         if (DEBUG) {
641                             Slog.d(TAG, "Enabled subtype found in the history: " + subtypeHashCode);
642                         }
643                         return new Pair<>(imeInTheHistory, subtypeHashCode);
644                     }
645                 }
646             }
647             if (DEBUG) {
648                 Slog.d(TAG, "No enabled IME found in the history");
649             }
650             return null;
651         }
652 
getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String, ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode)653         private String getEnabledSubtypeHashCodeForInputMethodAndSubtypeLocked(List<Pair<String,
654                 ArrayList<String>>> enabledImes, String imeId, String subtypeHashCode) {
655             for (Pair<String, ArrayList<String>> enabledIme: enabledImes) {
656                 if (enabledIme.first.equals(imeId)) {
657                     final ArrayList<String> explicitlyEnabledSubtypes = enabledIme.second;
658                     final InputMethodInfo imi = mMethodMap.get(imeId);
659                     if (explicitlyEnabledSubtypes.size() == 0) {
660                         // If there are no explicitly enabled subtypes, applicable subtypes are
661                         // enabled implicitly.
662                         // If IME is enabled and no subtypes are enabled, applicable subtypes
663                         // are enabled implicitly, so needs to treat them to be enabled.
664                         if (imi != null && imi.getSubtypeCount() > 0) {
665                             List<InputMethodSubtype> implicitlyEnabledSubtypes =
666                                     SubtypeUtils.getImplicitlyApplicableSubtypesLocked(mRes, imi);
667                             final int numSubtypes = implicitlyEnabledSubtypes.size();
668                             for (int i = 0; i < numSubtypes; ++i) {
669                                 final InputMethodSubtype st = implicitlyEnabledSubtypes.get(i);
670                                 if (String.valueOf(st.hashCode()).equals(subtypeHashCode)) {
671                                     return subtypeHashCode;
672                                 }
673                             }
674                         }
675                     } else {
676                         for (String s: explicitlyEnabledSubtypes) {
677                             if (s.equals(subtypeHashCode)) {
678                                 // If both imeId and subtypeId are enabled, return subtypeId.
679                                 try {
680                                     final int hashCode = Integer.parseInt(subtypeHashCode);
681                                     // Check whether the subtype id is valid or not
682                                     if (SubtypeUtils.isValidSubtypeId(imi, hashCode)) {
683                                         return s;
684                                     } else {
685                                         return NOT_A_SUBTYPE_ID_STR;
686                                     }
687                                 } catch (NumberFormatException e) {
688                                     return NOT_A_SUBTYPE_ID_STR;
689                                 }
690                             }
691                         }
692                     }
693                     // If imeId was enabled but subtypeId was disabled.
694                     return NOT_A_SUBTYPE_ID_STR;
695                 }
696             }
697             // If both imeId and subtypeId are disabled, return null
698             return null;
699         }
700 
loadInputMethodAndSubtypeHistoryLocked()701         private List<Pair<String, String>> loadInputMethodAndSubtypeHistoryLocked() {
702             ArrayList<Pair<String, String>> imsList = new ArrayList<>();
703             final String subtypeHistoryStr = getSubtypeHistoryStr();
704             if (TextUtils.isEmpty(subtypeHistoryStr)) {
705                 return imsList;
706             }
707             mInputMethodSplitter.setString(subtypeHistoryStr);
708             while (mInputMethodSplitter.hasNext()) {
709                 String nextImsStr = mInputMethodSplitter.next();
710                 mSubtypeSplitter.setString(nextImsStr);
711                 if (mSubtypeSplitter.hasNext()) {
712                     String subtypeId = NOT_A_SUBTYPE_ID_STR;
713                     // The first element is ime id.
714                     String imeId = mSubtypeSplitter.next();
715                     while (mSubtypeSplitter.hasNext()) {
716                         subtypeId = mSubtypeSplitter.next();
717                         break;
718                     }
719                     imsList.add(new Pair<>(imeId, subtypeId));
720                 }
721             }
722             return imsList;
723         }
724 
725         @NonNull
getSubtypeHistoryStr()726         private String getSubtypeHistoryStr() {
727             final String history = getString(Settings.Secure.INPUT_METHODS_SUBTYPE_HISTORY, "");
728             if (DEBUG) {
729                 Slog.d(TAG, "getSubtypeHistoryStr: " + history);
730             }
731             return history;
732         }
733 
putSelectedInputMethod(String imeId)734         void putSelectedInputMethod(String imeId) {
735             if (DEBUG) {
736                 Slog.d(TAG, "putSelectedInputMethodStr: " + imeId + ", "
737                         + mCurrentUserId);
738             }
739             putString(Settings.Secure.DEFAULT_INPUT_METHOD, imeId);
740         }
741 
putSelectedSubtype(int subtypeId)742         void putSelectedSubtype(int subtypeId) {
743             if (DEBUG) {
744                 Slog.d(TAG, "putSelectedInputMethodSubtypeStr: " + subtypeId + ", "
745                         + mCurrentUserId);
746             }
747             putInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, subtypeId);
748         }
749 
750         @Nullable
getSelectedInputMethod()751         String getSelectedInputMethod() {
752             final String imi = getString(Settings.Secure.DEFAULT_INPUT_METHOD, null);
753             if (DEBUG) {
754                 Slog.d(TAG, "getSelectedInputMethodStr: " + imi);
755             }
756             return imi;
757         }
758 
759         @Nullable
getSelectedInputMethodForUser(@serIdInt int userId)760         String getSelectedInputMethodForUser(@UserIdInt int userId) {
761             final String imi =
762                     getStringForUser(Settings.Secure.DEFAULT_INPUT_METHOD, null, userId);
763             if (DEBUG) {
764                 Slog.d(TAG, "getSelectedInputMethodForUserStr: " + imi);
765             }
766             return imi;
767         }
768 
putDefaultVoiceInputMethod(String imeId)769         void putDefaultVoiceInputMethod(String imeId) {
770             if (DEBUG) {
771                 Slog.d(TAG, "putDefaultVoiceInputMethodStr: " + imeId + ", " + mCurrentUserId);
772             }
773             putString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, imeId);
774         }
775 
776         @Nullable
getDefaultVoiceInputMethod()777         String getDefaultVoiceInputMethod() {
778             final String imi = getString(Settings.Secure.DEFAULT_VOICE_INPUT_METHOD, null);
779             if (DEBUG) {
780                 Slog.d(TAG, "getDefaultVoiceInputMethodStr: " + imi);
781             }
782             return imi;
783         }
784 
isSubtypeSelected()785         boolean isSubtypeSelected() {
786             return getSelectedInputMethodSubtypeHashCode() != NOT_A_SUBTYPE_ID;
787         }
788 
getSelectedInputMethodSubtypeHashCode()789         private int getSelectedInputMethodSubtypeHashCode() {
790             return getInt(Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID);
791         }
792 
isShowImeWithHardKeyboardEnabled()793         boolean isShowImeWithHardKeyboardEnabled() {
794             return getBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, false);
795         }
796 
setShowImeWithHardKeyboard(boolean show)797         void setShowImeWithHardKeyboard(boolean show) {
798             putBoolean(Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD, show);
799         }
800 
801         @UserIdInt
getCurrentUserId()802         public int getCurrentUserId() {
803             return mCurrentUserId;
804         }
805 
getSelectedInputMethodSubtypeId(String selectedImiId)806         int getSelectedInputMethodSubtypeId(String selectedImiId) {
807             final InputMethodInfo imi = mMethodMap.get(selectedImiId);
808             if (imi == null) {
809                 return NOT_A_SUBTYPE_ID;
810             }
811             final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
812             return SubtypeUtils.getSubtypeIdFromHashCode(imi, subtypeHashCode);
813         }
814 
saveCurrentInputMethodAndSubtypeToHistory(String curMethodId, InputMethodSubtype currentSubtype)815         void saveCurrentInputMethodAndSubtypeToHistory(String curMethodId,
816                 InputMethodSubtype currentSubtype) {
817             String subtypeId = NOT_A_SUBTYPE_ID_STR;
818             if (currentSubtype != null) {
819                 subtypeId = String.valueOf(currentSubtype.hashCode());
820             }
821             if (canAddToLastInputMethod(currentSubtype)) {
822                 addSubtypeToHistory(curMethodId, subtypeId);
823             }
824         }
825 
826         /**
827          * A variant of {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()} for
828          * non-current users.
829          *
830          * <p>TODO: Address code duplication between this and
831          * {@link InputMethodManagerService#getCurrentInputMethodSubtypeLocked()}.</p>
832          *
833          * @return {@link InputMethodSubtype} if exists. {@code null} otherwise.
834          */
835         @Nullable
getCurrentInputMethodSubtypeForNonCurrentUsers()836         InputMethodSubtype getCurrentInputMethodSubtypeForNonCurrentUsers() {
837             final String selectedMethodId = getSelectedInputMethod();
838             if (selectedMethodId == null) {
839                 return null;
840             }
841             final InputMethodInfo imi = mMethodMap.get(selectedMethodId);
842             if (imi == null || imi.getSubtypeCount() == 0) {
843                 return null;
844             }
845 
846             final int subtypeHashCode = getSelectedInputMethodSubtypeHashCode();
847             if (subtypeHashCode != InputMethodUtils.NOT_A_SUBTYPE_ID) {
848                 final int subtypeIndex = SubtypeUtils.getSubtypeIdFromHashCode(imi,
849                         subtypeHashCode);
850                 if (subtypeIndex >= 0) {
851                     return imi.getSubtypeAt(subtypeIndex);
852                 }
853             }
854 
855             // If there are no selected subtypes, the framework will try to find the most applicable
856             // subtype from explicitly or implicitly enabled subtypes.
857             final List<InputMethodSubtype> explicitlyOrImplicitlyEnabledSubtypes =
858                     getEnabledInputMethodSubtypeListLocked(imi, true);
859             // If there is only one explicitly or implicitly enabled subtype, just returns it.
860             if (explicitlyOrImplicitlyEnabledSubtypes.isEmpty()) {
861                 return null;
862             }
863             if (explicitlyOrImplicitlyEnabledSubtypes.size() == 1) {
864                 return explicitlyOrImplicitlyEnabledSubtypes.get(0);
865             }
866             final InputMethodSubtype subtype = SubtypeUtils.findLastResortApplicableSubtypeLocked(
867                     mRes, explicitlyOrImplicitlyEnabledSubtypes, SubtypeUtils.SUBTYPE_MODE_KEYBOARD,
868                     null, true);
869             if (subtype != null) {
870                 return subtype;
871             }
872             return SubtypeUtils.findLastResortApplicableSubtypeLocked(mRes,
873                     explicitlyOrImplicitlyEnabledSubtypes, null, null, true);
874         }
875 
setAdditionalInputMethodSubtypes(@onNull String imeId, @NonNull ArrayList<InputMethodSubtype> subtypes, @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap, @NonNull PackageManagerInternal packageManagerInternal, int callingUid)876         boolean setAdditionalInputMethodSubtypes(@NonNull String imeId,
877                 @NonNull ArrayList<InputMethodSubtype> subtypes,
878                 @NonNull ArrayMap<String, List<InputMethodSubtype>> additionalSubtypeMap,
879                 @NonNull PackageManagerInternal packageManagerInternal, int callingUid) {
880             final InputMethodInfo imi = mMethodMap.get(imeId);
881             if (imi == null) {
882                 return false;
883             }
884             if (!InputMethodUtils.checkIfPackageBelongsToUid(packageManagerInternal, callingUid,
885                     imi.getPackageName())) {
886                 return false;
887             }
888 
889             if (subtypes.isEmpty()) {
890                 additionalSubtypeMap.remove(imi.getId());
891             } else {
892                 additionalSubtypeMap.put(imi.getId(), subtypes);
893             }
894             AdditionalSubtypeUtils.save(additionalSubtypeMap, mMethodMap, getCurrentUserId());
895             return true;
896         }
897 
setEnabledInputMethodSubtypes(@onNull String imeId, @NonNull int[] subtypeHashCodes)898         boolean setEnabledInputMethodSubtypes(@NonNull String imeId,
899                 @NonNull int[] subtypeHashCodes) {
900             final InputMethodInfo imi = mMethodMap.get(imeId);
901             if (imi == null) {
902                 return false;
903             }
904 
905             final IntArray validSubtypeHashCodes = new IntArray(subtypeHashCodes.length);
906             for (int subtypeHashCode : subtypeHashCodes) {
907                 if (subtypeHashCode == NOT_A_SUBTYPE_ID) {
908                     continue;  // NOT_A_SUBTYPE_ID must not be saved
909                 }
910                 if (!SubtypeUtils.isValidSubtypeId(imi, subtypeHashCode)) {
911                     continue;  // this subtype does not exist in InputMethodInfo.
912                 }
913                 if (validSubtypeHashCodes.indexOf(subtypeHashCode) >= 0) {
914                     continue;  // The entry is already added.  No need to add anymore.
915                 }
916                 validSubtypeHashCodes.add(subtypeHashCode);
917             }
918 
919             final String originalEnabledImesString = getEnabledInputMethodsStr();
920             final String updatedEnabledImesString = updateEnabledImeString(
921                     originalEnabledImesString, imi.getId(), validSubtypeHashCodes);
922             if (TextUtils.equals(originalEnabledImesString, updatedEnabledImesString)) {
923                 return false;
924             }
925 
926             putEnabledInputMethodsStr(updatedEnabledImesString);
927             return true;
928         }
929 
930         @VisibleForTesting(visibility = VisibleForTesting.Visibility.PRIVATE)
updateEnabledImeString(@onNull String enabledImesString, @NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes)931         static String updateEnabledImeString(@NonNull String enabledImesString,
932                 @NonNull String imeId, @NonNull IntArray enabledSubtypeHashCodes) {
933             final TextUtils.SimpleStringSplitter imeSplitter =
934                     new TextUtils.SimpleStringSplitter(INPUT_METHOD_SEPARATOR);
935             final TextUtils.SimpleStringSplitter imeSubtypeSplitter =
936                     new TextUtils.SimpleStringSplitter(INPUT_METHOD_SUBTYPE_SEPARATOR);
937 
938             final StringBuilder sb = new StringBuilder();
939 
940             imeSplitter.setString(enabledImesString);
941             boolean needsImeSeparator = false;
942             while (imeSplitter.hasNext()) {
943                 final String nextImsStr = imeSplitter.next();
944                 imeSubtypeSplitter.setString(nextImsStr);
945                 if (imeSubtypeSplitter.hasNext()) {
946                     if (needsImeSeparator) {
947                         sb.append(INPUT_METHOD_SEPARATOR);
948                     }
949                     if (TextUtils.equals(imeId, imeSubtypeSplitter.next())) {
950                         sb.append(imeId);
951                         for (int i = 0; i < enabledSubtypeHashCodes.size(); ++i) {
952                             sb.append(INPUT_METHOD_SUBTYPE_SEPARATOR);
953                             sb.append(enabledSubtypeHashCodes.get(i));
954                         }
955                     } else {
956                         sb.append(nextImsStr);
957                     }
958                     needsImeSeparator = true;
959                 }
960             }
961             return sb.toString();
962         }
963 
dumpLocked(final Printer pw, final String prefix)964         public void dumpLocked(final Printer pw, final String prefix) {
965             pw.println(prefix + "mCurrentUserId=" + mCurrentUserId);
966             pw.println(prefix + "mCurrentProfileIds=" + Arrays.toString(mCurrentProfileIds));
967             pw.println(prefix + "mCopyOnWrite=" + mCopyOnWrite);
968             pw.println(prefix + "mEnabledInputMethodsStrCache=" + mEnabledInputMethodsStrCache);
969         }
970     }
971 
isSoftInputModeStateVisibleAllowed(int targetSdkVersion, @StartInputFlags int startInputFlags)972     static boolean isSoftInputModeStateVisibleAllowed(int targetSdkVersion,
973             @StartInputFlags int startInputFlags) {
974         if (targetSdkVersion < Build.VERSION_CODES.P) {
975             // for compatibility.
976             return true;
977         }
978         if ((startInputFlags & StartInputFlags.VIEW_HAS_FOCUS) == 0) {
979             return false;
980         }
981         if ((startInputFlags & StartInputFlags.IS_TEXT_EDITOR) == 0) {
982             return false;
983         }
984         return true;
985     }
986 
987     /**
988      * Converts a user ID, which can be a pseudo user ID such as {@link UserHandle#USER_ALL} to a
989      * list of real user IDs.
990      *
991      * @param userIdToBeResolved A user ID. Two pseudo user ID {@link UserHandle#USER_CURRENT} and
992      *                           {@link UserHandle#USER_ALL} are also supported
993      * @param currentUserId A real user ID, which will be used when {@link UserHandle#USER_CURRENT}
994      *                      is specified in {@code userIdToBeResolved}.
995      * @param warningWriter A {@link PrintWriter} to output some debug messages. {@code null} if
996      *                      no debug message is required.
997      * @return An integer array that contain user IDs.
998      */
resolveUserId(@serIdInt int userIdToBeResolved, @UserIdInt int currentUserId, @Nullable PrintWriter warningWriter)999     static int[] resolveUserId(@UserIdInt int userIdToBeResolved,
1000             @UserIdInt int currentUserId, @Nullable PrintWriter warningWriter) {
1001         final UserManagerInternal userManagerInternal =
1002                 LocalServices.getService(UserManagerInternal.class);
1003 
1004         if (userIdToBeResolved == UserHandle.USER_ALL) {
1005             return userManagerInternal.getUserIds();
1006         }
1007 
1008         final int sourceUserId;
1009         if (userIdToBeResolved == UserHandle.USER_CURRENT) {
1010             sourceUserId = currentUserId;
1011         } else if (userIdToBeResolved < 0) {
1012             if (warningWriter != null) {
1013                 warningWriter.print("Pseudo user ID ");
1014                 warningWriter.print(userIdToBeResolved);
1015                 warningWriter.println(" is not supported.");
1016             }
1017             return new int[]{};
1018         } else if (userManagerInternal.exists(userIdToBeResolved)) {
1019             sourceUserId = userIdToBeResolved;
1020         } else {
1021             if (warningWriter != null) {
1022                 warningWriter.print("User #");
1023                 warningWriter.print(userIdToBeResolved);
1024                 warningWriter.println(" does not exit.");
1025             }
1026             return new int[]{};
1027         }
1028         return new int[]{sourceUserId};
1029     }
1030 
1031     /**
1032      * Convert the input method ID to a component name
1033      *
1034      * @param id A unique ID for this input method.
1035      * @return The component name of the input method.
1036      * @see InputMethodInfo#computeId(ResolveInfo)
1037      */
1038     @Nullable
convertIdToComponentName(@onNull String id)1039     public static ComponentName convertIdToComponentName(@NonNull String id) {
1040         return ComponentName.unflattenFromString(id);
1041     }
1042 }
1043