1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.inputmethod;
18 
19 import android.annotation.NonNull;
20 import android.annotation.Nullable;
21 import android.content.res.Resources;
22 import android.os.LocaleList;
23 import android.text.TextUtils;
24 import android.util.ArrayMap;
25 import android.util.Slog;
26 import android.view.inputmethod.InputMethodInfo;
27 import android.view.inputmethod.InputMethodSubtype;
28 
29 import com.android.internal.annotations.GuardedBy;
30 import com.android.internal.annotations.VisibleForTesting;
31 
32 import java.util.ArrayList;
33 import java.util.List;
34 import java.util.Locale;
35 
36 /**
37  * This class provides utility methods to handle and manage {@link InputMethodSubtype} for
38  * {@link InputMethodManagerService}.
39  *
40  * <p>This class is intentionally package-private.  Utility methods here are tightly coupled with
41  * implementation details in {@link InputMethodManagerService}.  Hence this class is not suitable
42  * for other components to directly use.</p>
43  */
44 final class SubtypeUtils {
45     private static final String TAG = "SubtypeUtils";
46     public static final boolean DEBUG = false;
47 
48     static final String SUBTYPE_MODE_ANY = null;
49     static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
50 
51     static final int NOT_A_SUBTYPE_ID = -1;
52     private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
53             "EnabledWhenDefaultIsNotAsciiCapable";
54 
55     // A temporary workaround for the performance concerns in
56     // #getImplicitlyApplicableSubtypesLocked(Resources, InputMethodInfo).
57     // TODO: Optimize all the critical paths including this one.
58     // TODO(b/235661780): Make the cache supports multi-users.
59     private static final Object sCacheLock = new Object();
60     @GuardedBy("sCacheLock")
61     private static LocaleList sCachedSystemLocales;
62     @GuardedBy("sCacheLock")
63     private static InputMethodInfo sCachedInputMethodInfo;
64     @GuardedBy("sCacheLock")
65     private static ArrayList<InputMethodSubtype> sCachedResult;
66 
containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, boolean checkCountry, String mode)67     static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale,
68             boolean checkCountry, String mode) {
69         if (locale == null) {
70             return false;
71         }
72         final int numSubtypes = imi.getSubtypeCount();
73         for (int i = 0; i < numSubtypes; ++i) {
74             final InputMethodSubtype subtype = imi.getSubtypeAt(i);
75             if (checkCountry) {
76                 final Locale subtypeLocale = subtype.getLocaleObject();
77                 if (subtypeLocale == null
78                         || !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())
79                         || !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
80                     continue;
81                 }
82             } else {
83                 final Locale subtypeLocale = new Locale(LocaleUtils.getLanguageFromLocaleString(
84                         subtype.getLocale()));
85                 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
86                     continue;
87                 }
88             }
89             if (mode == SUBTYPE_MODE_ANY || TextUtils.isEmpty(mode)
90                     || mode.equalsIgnoreCase(subtype.getMode())) {
91                 return true;
92             }
93         }
94         return false;
95     }
96 
getSubtypes(InputMethodInfo imi)97     static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
98         ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
99         final int subtypeCount = imi.getSubtypeCount();
100         for (int i = 0; i < subtypeCount; ++i) {
101             subtypes.add(imi.getSubtypeAt(i));
102         }
103         return subtypes;
104     }
105 
isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode)106     static boolean isValidSubtypeId(InputMethodInfo imi, int subtypeHashCode) {
107         return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
108     }
109 
getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode)110     static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
111         if (imi != null) {
112             final int subtypeCount = imi.getSubtypeCount();
113             for (int i = 0; i < subtypeCount; ++i) {
114                 InputMethodSubtype ims = imi.getSubtypeAt(i);
115                 if (subtypeHashCode == ims.hashCode()) {
116                     return i;
117                 }
118             }
119         }
120         return NOT_A_SUBTYPE_ID;
121     }
122 
123     private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
124             source -> source != null ? source.getLocaleObject() : null;
125 
126     @VisibleForTesting
127     @NonNull
getImplicitlyApplicableSubtypesLocked( Resources res, InputMethodInfo imi)128     static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLocked(
129             Resources res, InputMethodInfo imi) {
130         final LocaleList systemLocales = res.getConfiguration().getLocales();
131 
132         synchronized (sCacheLock) {
133             // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
134             // it does not check if subtypes are also identical.
135             if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
136                 return new ArrayList<>(sCachedResult);
137             }
138         }
139 
140         // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesLockedImpl().
141         // TODO: Refactor getImplicitlyApplicableSubtypesLockedImpl() so that it can receive
142         // LocaleList rather than Resource.
143         final ArrayList<InputMethodSubtype> result =
144                 getImplicitlyApplicableSubtypesLockedImpl(res, imi);
145         synchronized (sCacheLock) {
146             // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
147             sCachedSystemLocales = systemLocales;
148             sCachedInputMethodInfo = imi;
149             sCachedResult = new ArrayList<>(result);
150         }
151         return result;
152     }
153 
getImplicitlyApplicableSubtypesLockedImpl( Resources res, InputMethodInfo imi)154     private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesLockedImpl(
155             Resources res, InputMethodInfo imi) {
156         final List<InputMethodSubtype> subtypes = getSubtypes(imi);
157         final LocaleList systemLocales = res.getConfiguration().getLocales();
158         final String systemLocale = systemLocales.get(0).toString();
159         if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
160         final int numSubtypes = subtypes.size();
161 
162         // Handle overridesImplicitlyEnabledSubtype mechanism.
163         final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>();
164         for (int i = 0; i < numSubtypes; ++i) {
165             // scan overriding implicitly enabled subtypes.
166             final InputMethodSubtype subtype = subtypes.get(i);
167             if (subtype.overridesImplicitlyEnabledSubtype()) {
168                 final String mode = subtype.getMode();
169                 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
170                     applicableModeAndSubtypesMap.put(mode, subtype);
171                 }
172             }
173         }
174         if (applicableModeAndSubtypesMap.size() > 0) {
175             return new ArrayList<>(applicableModeAndSubtypesMap.values());
176         }
177 
178         final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
179                 new ArrayMap<>();
180         final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
181 
182         for (int i = 0; i < numSubtypes; ++i) {
183             final InputMethodSubtype subtype = subtypes.get(i);
184             final String mode = subtype.getMode();
185             if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
186                 keyboardSubtypes.add(subtype);
187             } else {
188                 if (!nonKeyboardSubtypesMap.containsKey(mode)) {
189                     nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
190                 }
191                 nonKeyboardSubtypesMap.get(mode).add(subtype);
192             }
193         }
194 
195         final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
196         LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
197                 applicableSubtypes);
198 
199         if (!applicableSubtypes.isEmpty()) {
200             boolean hasAsciiCapableKeyboard = false;
201             final int numApplicationSubtypes = applicableSubtypes.size();
202             for (int i = 0; i < numApplicationSubtypes; ++i) {
203                 final InputMethodSubtype subtype = applicableSubtypes.get(i);
204                 if (subtype.isAsciiCapable()) {
205                     hasAsciiCapableKeyboard = true;
206                     break;
207                 }
208             }
209             if (!hasAsciiCapableKeyboard) {
210                 final int numKeyboardSubtypes = keyboardSubtypes.size();
211                 for (int i = 0; i < numKeyboardSubtypes; ++i) {
212                     final InputMethodSubtype subtype = keyboardSubtypes.get(i);
213                     final String mode = subtype.getMode();
214                     if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
215                             TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
216                         applicableSubtypes.add(subtype);
217                     }
218                 }
219             }
220         }
221 
222         if (applicableSubtypes.isEmpty()) {
223             InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtypeLocked(
224                     res, subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
225             if (lastResortKeyboardSubtype != null) {
226                 applicableSubtypes.add(lastResortKeyboardSubtype);
227             }
228         }
229 
230         // For each non-keyboard mode, extract subtypes with system locales.
231         for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
232             LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
233                     applicableSubtypes);
234         }
235 
236         return applicableSubtypes;
237     }
238 
239     /**
240      * If there are no selected subtypes, tries finding the most applicable one according to the
241      * given locale.
242      *
243      * @param subtypes                    a list of {@link InputMethodSubtype} to search
244      * @param mode                        the mode used for filtering subtypes
245      * @param locale                      the locale used for filtering subtypes
246      * @param canIgnoreLocaleAsLastResort when set to {@code true}, if this function can't find the
247      *                                    most applicable subtype, it will return the first subtype
248      *                                    matched with mode
249      *
250      * @return the most applicable subtypeId
251      */
findLastResortApplicableSubtypeLocked( Resources res, List<InputMethodSubtype> subtypes, String mode, String locale, boolean canIgnoreLocaleAsLastResort)252     static InputMethodSubtype findLastResortApplicableSubtypeLocked(
253             Resources res, List<InputMethodSubtype> subtypes, String mode, String locale,
254             boolean canIgnoreLocaleAsLastResort) {
255         if (subtypes == null || subtypes.isEmpty()) {
256             return null;
257         }
258         if (TextUtils.isEmpty(locale)) {
259             locale = res.getConfiguration().locale.toString();
260         }
261         final String language = LocaleUtils.getLanguageFromLocaleString(locale);
262         boolean partialMatchFound = false;
263         InputMethodSubtype applicableSubtype = null;
264         InputMethodSubtype firstMatchedModeSubtype = null;
265         final int numSubtypes = subtypes.size();
266         for (int i = 0; i < numSubtypes; ++i) {
267             InputMethodSubtype subtype = subtypes.get(i);
268             final String subtypeLocale = subtype.getLocale();
269             final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale);
270             // An applicable subtype should match "mode". If mode is null, mode will be ignored,
271             // and all subtypes with all modes can be candidates.
272             if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
273                 if (firstMatchedModeSubtype == null) {
274                     firstMatchedModeSubtype = subtype;
275                 }
276                 if (locale.equals(subtypeLocale)) {
277                     // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
278                     applicableSubtype = subtype;
279                     break;
280                 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
281                     // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
282                     applicableSubtype = subtype;
283                     partialMatchFound = true;
284                 }
285             }
286         }
287 
288         if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
289             return firstMatchedModeSubtype;
290         }
291 
292         // The first subtype applicable to the system locale will be defined as the most applicable
293         // subtype.
294         if (DEBUG) {
295             if (applicableSubtype != null) {
296                 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
297                         + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
298             }
299         }
300         return applicableSubtype;
301     }
302 }
303