• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 static com.android.server.inputmethod.InputMethodSettings.INVALID_SUBTYPE_HASHCODE;
20 import static com.android.server.inputmethod.InputMethodUtils.NOT_A_SUBTYPE_INDEX;
21 
22 import android.annotation.AnyThread;
23 import android.annotation.NonNull;
24 import android.annotation.Nullable;
25 import android.os.LocaleList;
26 import android.provider.Settings;
27 import android.text.TextUtils;
28 import android.util.ArrayMap;
29 import android.util.Slog;
30 import android.view.inputmethod.InputMethodInfo;
31 import android.view.inputmethod.InputMethodSubtype;
32 
33 import com.android.internal.annotations.GuardedBy;
34 
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.Locale;
38 
39 /**
40  * This class provides utility methods to handle and manage {@link InputMethodSubtype} for
41  * {@link InputMethodManagerService}.
42  *
43  * <p>This class is intentionally package-private.  Utility methods here are tightly coupled with
44  * implementation details in {@link InputMethodManagerService}.  Hence this class is not suitable
45  * for other components to directly use.</p>
46  */
47 final class SubtypeUtils {
48     private static final String TAG = "SubtypeUtils";
49     public static final boolean DEBUG = false;
50 
51     static final String SUBTYPE_MODE_ANY = null;
52     static final String SUBTYPE_MODE_KEYBOARD = "keyboard";
53 
54     private static final String TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE =
55             "EnabledWhenDefaultIsNotAsciiCapable";
56 
57     // A temporary workaround for the performance concerns in
58     // #getImplicitlyApplicableSubtypes(Resources, InputMethodInfo).
59     // TODO: Optimize all the critical paths including this one.
60     // TODO(b/235661780): Make the cache supports multi-users.
61     private static final Object sCacheLock = new Object();
62     @GuardedBy("sCacheLock")
63     private static LocaleList sCachedSystemLocales;
64     @GuardedBy("sCacheLock")
65     private static InputMethodInfo sCachedInputMethodInfo;
66     @GuardedBy("sCacheLock")
67     private static ArrayList<InputMethodSubtype> sCachedResult;
68 
containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale, boolean checkCountry, String mode)69     static boolean containsSubtypeOf(InputMethodInfo imi, @Nullable Locale locale,
70             boolean checkCountry, String mode) {
71         if (locale == null) {
72             return false;
73         }
74         final int numSubtypes = imi.getSubtypeCount();
75         for (int i = 0; i < numSubtypes; ++i) {
76             final InputMethodSubtype subtype = imi.getSubtypeAt(i);
77             if (checkCountry) {
78                 final Locale subtypeLocale = subtype.getLocaleObject();
79                 if (subtypeLocale == null
80                         || !TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())
81                         || !TextUtils.equals(subtypeLocale.getCountry(), locale.getCountry())) {
82                     continue;
83                 }
84             } else {
85                 final Locale subtypeLocale = new Locale(LocaleUtils.getLanguageFromLocaleString(
86                         subtype.getLocale()));
87                 if (!TextUtils.equals(subtypeLocale.getLanguage(), locale.getLanguage())) {
88                     continue;
89                 }
90             }
91             if (TextUtils.isEmpty(mode) || mode.equalsIgnoreCase(subtype.getMode())) {
92                 return true;
93             }
94         }
95         return false;
96     }
97 
getSubtypes(InputMethodInfo imi)98     static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
99         ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
100         final int subtypeCount = imi.getSubtypeCount();
101         for (int i = 0; i < subtypeCount; ++i) {
102             subtypes.add(imi.getSubtypeAt(i));
103         }
104         return subtypes;
105     }
106 
isValidSubtypeHashCode(InputMethodInfo imi, int subtypeHashCode)107     static boolean isValidSubtypeHashCode(InputMethodInfo imi, int subtypeHashCode) {
108         return getSubtypeIndexFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_INDEX;
109     }
110 
111     /**
112      * Returns the index to be specified to {@link InputMethodInfo#getSubtypeAt(int)}.
113      *
114      * @param imi             {@link InputMethodInfo} to be queried about
115      * @param subtypeHashCode {@link InputMethodSubtype#hashCode()} to be queried about
116      *
117      * @return The index to be specified to {@link InputMethodInfo#getSubtypeAt(int)}.
118      *         {@link InputMethodUtils#NOT_A_SUBTYPE_INDEX} if not found
119      */
getSubtypeIndexFromHashCode(InputMethodInfo imi, int subtypeHashCode)120     static int getSubtypeIndexFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
121         if (imi != null) {
122             final int subtypeCount = imi.getSubtypeCount();
123             for (int i = 0; i < subtypeCount; ++i) {
124                 InputMethodSubtype ims = imi.getSubtypeAt(i);
125                 if (subtypeHashCode == ims.hashCode()) {
126                     return i;
127                 }
128             }
129         }
130         return NOT_A_SUBTYPE_INDEX;
131     }
132 
133     private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
134             source -> source != null ? source.getLocaleObject() : null;
135 
136     @NonNull
getImplicitlyApplicableSubtypes( @onNull LocaleList systemLocales, InputMethodInfo imi)137     static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypes(
138             @NonNull LocaleList systemLocales, InputMethodInfo imi) {
139         synchronized (sCacheLock) {
140             // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
141             // it does not check if subtypes are also identical.
142             if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
143                 return new ArrayList<>(sCachedResult);
144             }
145         }
146 
147         // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesImpl().
148         // TODO: Refactor getImplicitlyApplicableSubtypesImpl() so that it can receive
149         // LocaleList rather than Resource.
150         final ArrayList<InputMethodSubtype> result =
151                 getImplicitlyApplicableSubtypesImpl(systemLocales, imi);
152         synchronized (sCacheLock) {
153             // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
154             sCachedSystemLocales = systemLocales;
155             sCachedInputMethodInfo = imi;
156             sCachedResult = new ArrayList<>(result);
157         }
158         return result;
159     }
160 
getImplicitlyApplicableSubtypesImpl( @onNull LocaleList systemLocales, InputMethodInfo imi)161     private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesImpl(
162             @NonNull LocaleList systemLocales, InputMethodInfo imi) {
163         final List<InputMethodSubtype> subtypes = getSubtypes(imi);
164         final String systemLocale = systemLocales.get(0).toString();
165         if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
166         final int numSubtypes = subtypes.size();
167 
168         // Handle overridesImplicitlyEnabledSubtype mechanism.
169         final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>();
170         for (int i = 0; i < numSubtypes; ++i) {
171             // scan overriding implicitly enabled subtypes.
172             final InputMethodSubtype subtype = subtypes.get(i);
173             if (subtype.overridesImplicitlyEnabledSubtype()) {
174                 final String mode = subtype.getMode();
175                 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
176                     applicableModeAndSubtypesMap.put(mode, subtype);
177                 }
178             }
179         }
180         if (applicableModeAndSubtypesMap.size() > 0) {
181             return new ArrayList<>(applicableModeAndSubtypesMap.values());
182         }
183 
184         final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
185                 new ArrayMap<>();
186         final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
187 
188         for (int i = 0; i < numSubtypes; ++i) {
189             final InputMethodSubtype subtype = subtypes.get(i);
190             final String mode = subtype.getMode();
191             if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
192                 keyboardSubtypes.add(subtype);
193             } else {
194                 if (!nonKeyboardSubtypesMap.containsKey(mode)) {
195                     nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
196                 }
197                 nonKeyboardSubtypesMap.get(mode).add(subtype);
198             }
199         }
200 
201         final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
202         LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
203                 applicableSubtypes);
204 
205         if (!applicableSubtypes.isEmpty()) {
206             boolean hasAsciiCapableKeyboard = false;
207             final int numApplicationSubtypes = applicableSubtypes.size();
208             for (int i = 0; i < numApplicationSubtypes; ++i) {
209                 final InputMethodSubtype subtype = applicableSubtypes.get(i);
210                 if (subtype.isAsciiCapable()) {
211                     hasAsciiCapableKeyboard = true;
212                     break;
213                 }
214             }
215             if (!hasAsciiCapableKeyboard) {
216                 final int numKeyboardSubtypes = keyboardSubtypes.size();
217                 for (int i = 0; i < numKeyboardSubtypes; ++i) {
218                     final InputMethodSubtype subtype = keyboardSubtypes.get(i);
219                     final String mode = subtype.getMode();
220                     if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
221                             TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
222                         applicableSubtypes.add(subtype);
223                     }
224                 }
225             }
226         }
227 
228         if (applicableSubtypes.isEmpty()) {
229             InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtype(
230                     subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
231             if (lastResortKeyboardSubtype != null) {
232                 applicableSubtypes.add(lastResortKeyboardSubtype);
233             }
234         }
235 
236         // For each non-keyboard mode, extract subtypes with system locales.
237         for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
238             LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
239                     applicableSubtypes);
240         }
241 
242         return applicableSubtypes;
243     }
244 
245     /**
246      * If there are no selected subtypes, tries finding the most applicable one according to the
247      * given locale.
248      *
249      * @param subtypes                    a list of {@link InputMethodSubtype} to search
250      * @param mode                        the mode used for filtering subtypes
251      * @param locale                      the locale used for filtering subtypes
252      * @param canIgnoreLocaleAsLastResort when set to {@code true}, if this function can't find the
253      *                                    most applicable subtype, it will return the first subtype
254      *                                    matched with mode
255      *
256      * @return the most applicable {@link InputMethodSubtype}
257      */
findLastResortApplicableSubtype( List<InputMethodSubtype> subtypes, String mode, @NonNull String locale, boolean canIgnoreLocaleAsLastResort)258     static InputMethodSubtype findLastResortApplicableSubtype(
259             List<InputMethodSubtype> subtypes, String mode, @NonNull String locale,
260             boolean canIgnoreLocaleAsLastResort) {
261         if (subtypes == null || subtypes.isEmpty()) {
262             return null;
263         }
264         final String language = LocaleUtils.getLanguageFromLocaleString(locale);
265         boolean partialMatchFound = false;
266         InputMethodSubtype applicableSubtype = null;
267         InputMethodSubtype firstMatchedModeSubtype = null;
268         final int numSubtypes = subtypes.size();
269         for (int i = 0; i < numSubtypes; ++i) {
270             InputMethodSubtype subtype = subtypes.get(i);
271             final String subtypeLocale = subtype.getLocale();
272             final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale);
273             // An applicable subtype should match "mode". If mode is null, mode will be ignored,
274             // and all subtypes with all modes can be candidates.
275             if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
276                 if (firstMatchedModeSubtype == null) {
277                     firstMatchedModeSubtype = subtype;
278                 }
279                 if (locale.equals(subtypeLocale)) {
280                     // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
281                     applicableSubtype = subtype;
282                     break;
283                 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
284                     // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
285                     applicableSubtype = subtype;
286                     partialMatchFound = true;
287                 }
288             }
289         }
290 
291         if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
292             return firstMatchedModeSubtype;
293         }
294 
295         // The first subtype applicable to the system locale will be defined as the most applicable
296         // subtype.
297         if (DEBUG) {
298             if (applicableSubtype != null) {
299                 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
300                         + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
301             }
302         }
303         return applicableSubtype;
304     }
305 
306     /**
307      * Returns a {@link InputMethodSubtype} available in {@code imi} based on
308      * {@link Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE}.
309      *
310      * @param imi            {@link InputMethodInfo} to find out the current
311      *                       {@link InputMethodSubtype}
312      * @param settings       {@link InputMethodSettings} to be used to find out the current
313      *                       {@link InputMethodSubtype}
314      * @param currentSubtype the current value that will be used as fallback
315      * @return {@link InputMethodSubtype} to be used as the current {@link InputMethodSubtype}
316      */
317     @AnyThread
318     @Nullable
getCurrentInputMethodSubtype( @onNull InputMethodInfo imi, @NonNull InputMethodSettings settings, @Nullable InputMethodSubtype currentSubtype)319     static InputMethodSubtype getCurrentInputMethodSubtype(
320             @NonNull InputMethodInfo imi, @NonNull InputMethodSettings settings,
321             @Nullable InputMethodSubtype currentSubtype) {
322         final int userId = settings.getUserId();
323         final int selectedSubtypeHashCode = SecureSettingsWrapper.getInt(
324                 Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, INVALID_SUBTYPE_HASHCODE, userId);
325         if (selectedSubtypeHashCode != INVALID_SUBTYPE_HASHCODE && currentSubtype != null
326                 && isValidSubtypeHashCode(imi, currentSubtype.hashCode())) {
327             return currentSubtype;
328         }
329 
330         final int subtypeIndex = settings.getSelectedInputMethodSubtypeIndex(imi.getId());
331         if (subtypeIndex != NOT_A_SUBTYPE_INDEX) {
332             return imi.getSubtypeAt(subtypeIndex);
333         }
334 
335         // If there are no selected subtypes, the framework will try to find the most applicable
336         // subtype from explicitly or implicitly enabled subtypes.
337         final List<InputMethodSubtype> subtypes = settings.getEnabledInputMethodSubtypeList(imi,
338                 true);
339         if (subtypes.isEmpty()) {
340             return currentSubtype;
341         }
342         // If there is only one explicitly or implicitly enabled subtype,
343         // just returns it.
344         if (subtypes.size() == 1) {
345             return subtypes.get(0);
346         }
347         final String locale = SystemLocaleWrapper.get(userId).get(0).toString();
348         final var subtype = findLastResortApplicableSubtype(subtypes, SUBTYPE_MODE_KEYBOARD, locale,
349                 true);
350         if (subtype != null) {
351             return subtype;
352         }
353         return findLastResortApplicableSubtype(subtypes, null, locale, true);
354     }
355 }
356