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.AnyThread;
20 import android.annotation.NonNull;
21 import android.annotation.Nullable;
22 import android.os.LocaleList;
23 import android.provider.Settings;
24 import android.text.TextUtils;
25 import android.util.ArrayMap;
26 import android.util.Slog;
27 import android.view.inputmethod.InputMethodInfo;
28 import android.view.inputmethod.InputMethodSubtype;
29 
30 import com.android.internal.annotations.GuardedBy;
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     // #getImplicitlyApplicableSubtypes(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 (TextUtils.isEmpty(mode) || mode.equalsIgnoreCase(subtype.getMode())) {
90                 return true;
91             }
92         }
93         return false;
94     }
95 
getSubtypes(InputMethodInfo imi)96     static ArrayList<InputMethodSubtype> getSubtypes(InputMethodInfo imi) {
97         ArrayList<InputMethodSubtype> subtypes = new ArrayList<>();
98         final int subtypeCount = imi.getSubtypeCount();
99         for (int i = 0; i < subtypeCount; ++i) {
100             subtypes.add(imi.getSubtypeAt(i));
101         }
102         return subtypes;
103     }
104 
isValidSubtypeHashCode(InputMethodInfo imi, int subtypeHashCode)105     static boolean isValidSubtypeHashCode(InputMethodInfo imi, int subtypeHashCode) {
106         return getSubtypeIdFromHashCode(imi, subtypeHashCode) != NOT_A_SUBTYPE_ID;
107     }
108 
getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode)109     static int getSubtypeIdFromHashCode(InputMethodInfo imi, int subtypeHashCode) {
110         if (imi != null) {
111             final int subtypeCount = imi.getSubtypeCount();
112             for (int i = 0; i < subtypeCount; ++i) {
113                 InputMethodSubtype ims = imi.getSubtypeAt(i);
114                 if (subtypeHashCode == ims.hashCode()) {
115                     return i;
116                 }
117             }
118         }
119         return NOT_A_SUBTYPE_ID;
120     }
121 
122     private static final LocaleUtils.LocaleExtractor<InputMethodSubtype> sSubtypeToLocale =
123             source -> source != null ? source.getLocaleObject() : null;
124 
125     @NonNull
getImplicitlyApplicableSubtypes( @onNull LocaleList systemLocales, InputMethodInfo imi)126     static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypes(
127             @NonNull LocaleList systemLocales, InputMethodInfo imi) {
128         synchronized (sCacheLock) {
129             // We intentionally do not use InputMethodInfo#equals(InputMethodInfo) here because
130             // it does not check if subtypes are also identical.
131             if (systemLocales.equals(sCachedSystemLocales) && sCachedInputMethodInfo == imi) {
132                 return new ArrayList<>(sCachedResult);
133             }
134         }
135 
136         // Note: Only resource info in "res" is used in getImplicitlyApplicableSubtypesImpl().
137         // TODO: Refactor getImplicitlyApplicableSubtypesImpl() so that it can receive
138         // LocaleList rather than Resource.
139         final ArrayList<InputMethodSubtype> result =
140                 getImplicitlyApplicableSubtypesImpl(systemLocales, imi);
141         synchronized (sCacheLock) {
142             // Both LocaleList and InputMethodInfo are immutable. No need to copy them here.
143             sCachedSystemLocales = systemLocales;
144             sCachedInputMethodInfo = imi;
145             sCachedResult = new ArrayList<>(result);
146         }
147         return result;
148     }
149 
getImplicitlyApplicableSubtypesImpl( @onNull LocaleList systemLocales, InputMethodInfo imi)150     private static ArrayList<InputMethodSubtype> getImplicitlyApplicableSubtypesImpl(
151             @NonNull LocaleList systemLocales, InputMethodInfo imi) {
152         final List<InputMethodSubtype> subtypes = getSubtypes(imi);
153         final String systemLocale = systemLocales.get(0).toString();
154         if (TextUtils.isEmpty(systemLocale)) return new ArrayList<>();
155         final int numSubtypes = subtypes.size();
156 
157         // Handle overridesImplicitlyEnabledSubtype mechanism.
158         final ArrayMap<String, InputMethodSubtype> applicableModeAndSubtypesMap = new ArrayMap<>();
159         for (int i = 0; i < numSubtypes; ++i) {
160             // scan overriding implicitly enabled subtypes.
161             final InputMethodSubtype subtype = subtypes.get(i);
162             if (subtype.overridesImplicitlyEnabledSubtype()) {
163                 final String mode = subtype.getMode();
164                 if (!applicableModeAndSubtypesMap.containsKey(mode)) {
165                     applicableModeAndSubtypesMap.put(mode, subtype);
166                 }
167             }
168         }
169         if (applicableModeAndSubtypesMap.size() > 0) {
170             return new ArrayList<>(applicableModeAndSubtypesMap.values());
171         }
172 
173         final ArrayMap<String, ArrayList<InputMethodSubtype>> nonKeyboardSubtypesMap =
174                 new ArrayMap<>();
175         final ArrayList<InputMethodSubtype> keyboardSubtypes = new ArrayList<>();
176 
177         for (int i = 0; i < numSubtypes; ++i) {
178             final InputMethodSubtype subtype = subtypes.get(i);
179             final String mode = subtype.getMode();
180             if (SUBTYPE_MODE_KEYBOARD.equals(mode)) {
181                 keyboardSubtypes.add(subtype);
182             } else {
183                 if (!nonKeyboardSubtypesMap.containsKey(mode)) {
184                     nonKeyboardSubtypesMap.put(mode, new ArrayList<>());
185                 }
186                 nonKeyboardSubtypesMap.get(mode).add(subtype);
187             }
188         }
189 
190         final ArrayList<InputMethodSubtype> applicableSubtypes = new ArrayList<>();
191         LocaleUtils.filterByLanguage(keyboardSubtypes, sSubtypeToLocale, systemLocales,
192                 applicableSubtypes);
193 
194         if (!applicableSubtypes.isEmpty()) {
195             boolean hasAsciiCapableKeyboard = false;
196             final int numApplicationSubtypes = applicableSubtypes.size();
197             for (int i = 0; i < numApplicationSubtypes; ++i) {
198                 final InputMethodSubtype subtype = applicableSubtypes.get(i);
199                 if (subtype.isAsciiCapable()) {
200                     hasAsciiCapableKeyboard = true;
201                     break;
202                 }
203             }
204             if (!hasAsciiCapableKeyboard) {
205                 final int numKeyboardSubtypes = keyboardSubtypes.size();
206                 for (int i = 0; i < numKeyboardSubtypes; ++i) {
207                     final InputMethodSubtype subtype = keyboardSubtypes.get(i);
208                     final String mode = subtype.getMode();
209                     if (SUBTYPE_MODE_KEYBOARD.equals(mode) && subtype.containsExtraValueKey(
210                             TAG_ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE)) {
211                         applicableSubtypes.add(subtype);
212                     }
213                 }
214             }
215         }
216 
217         if (applicableSubtypes.isEmpty()) {
218             InputMethodSubtype lastResortKeyboardSubtype = findLastResortApplicableSubtype(
219                     subtypes, SUBTYPE_MODE_KEYBOARD, systemLocale, true);
220             if (lastResortKeyboardSubtype != null) {
221                 applicableSubtypes.add(lastResortKeyboardSubtype);
222             }
223         }
224 
225         // For each non-keyboard mode, extract subtypes with system locales.
226         for (final ArrayList<InputMethodSubtype> subtypeList : nonKeyboardSubtypesMap.values()) {
227             LocaleUtils.filterByLanguage(subtypeList, sSubtypeToLocale, systemLocales,
228                     applicableSubtypes);
229         }
230 
231         return applicableSubtypes;
232     }
233 
234     /**
235      * If there are no selected subtypes, tries finding the most applicable one according to the
236      * given locale.
237      *
238      * @param subtypes                    a list of {@link InputMethodSubtype} to search
239      * @param mode                        the mode used for filtering subtypes
240      * @param locale                      the locale used for filtering subtypes
241      * @param canIgnoreLocaleAsLastResort when set to {@code true}, if this function can't find the
242      *                                    most applicable subtype, it will return the first subtype
243      *                                    matched with mode
244      *
245      * @return the most applicable subtypeId
246      */
findLastResortApplicableSubtype( List<InputMethodSubtype> subtypes, String mode, @NonNull String locale, boolean canIgnoreLocaleAsLastResort)247     static InputMethodSubtype findLastResortApplicableSubtype(
248             List<InputMethodSubtype> subtypes, String mode, @NonNull String locale,
249             boolean canIgnoreLocaleAsLastResort) {
250         if (subtypes == null || subtypes.isEmpty()) {
251             return null;
252         }
253         final String language = LocaleUtils.getLanguageFromLocaleString(locale);
254         boolean partialMatchFound = false;
255         InputMethodSubtype applicableSubtype = null;
256         InputMethodSubtype firstMatchedModeSubtype = null;
257         final int numSubtypes = subtypes.size();
258         for (int i = 0; i < numSubtypes; ++i) {
259             InputMethodSubtype subtype = subtypes.get(i);
260             final String subtypeLocale = subtype.getLocale();
261             final String subtypeLanguage = LocaleUtils.getLanguageFromLocaleString(subtypeLocale);
262             // An applicable subtype should match "mode". If mode is null, mode will be ignored,
263             // and all subtypes with all modes can be candidates.
264             if (mode == null || subtypes.get(i).getMode().equalsIgnoreCase(mode)) {
265                 if (firstMatchedModeSubtype == null) {
266                     firstMatchedModeSubtype = subtype;
267                 }
268                 if (locale.equals(subtypeLocale)) {
269                     // Exact match (e.g. system locale is "en_US" and subtype locale is "en_US")
270                     applicableSubtype = subtype;
271                     break;
272                 } else if (!partialMatchFound && language.equals(subtypeLanguage)) {
273                     // Partial match (e.g. system locale is "en_US" and subtype locale is "en")
274                     applicableSubtype = subtype;
275                     partialMatchFound = true;
276                 }
277             }
278         }
279 
280         if (applicableSubtype == null && canIgnoreLocaleAsLastResort) {
281             return firstMatchedModeSubtype;
282         }
283 
284         // The first subtype applicable to the system locale will be defined as the most applicable
285         // subtype.
286         if (DEBUG) {
287             if (applicableSubtype != null) {
288                 Slog.d(TAG, "Applicable InputMethodSubtype was found: "
289                         + applicableSubtype.getMode() + "," + applicableSubtype.getLocale());
290             }
291         }
292         return applicableSubtype;
293     }
294 
295     /**
296      * Returns a {@link InputMethodSubtype} available in {@code imi} based on
297      * {@link Settings.Secure#SELECTED_INPUT_METHOD_SUBTYPE}.
298      *
299      * @param imi            {@link InputMethodInfo} to find out the current
300      *                       {@link InputMethodSubtype}
301      * @param settings       {@link InputMethodSettings} to be used to find out the current
302      *                       {@link InputMethodSubtype}
303      * @param currentSubtype the current value that will be used as fallback
304      * @return {@link InputMethodSubtype} to be used as the current {@link InputMethodSubtype}
305      */
306     @AnyThread
307     @Nullable
getCurrentInputMethodSubtype( @onNull InputMethodInfo imi, @NonNull InputMethodSettings settings, @Nullable InputMethodSubtype currentSubtype)308     static InputMethodSubtype getCurrentInputMethodSubtype(
309             @NonNull InputMethodInfo imi, @NonNull InputMethodSettings settings,
310             @Nullable InputMethodSubtype currentSubtype) {
311         final int userId = settings.getUserId();
312         final int selectedSubtypeHashCode = SecureSettingsWrapper.getInt(
313                 Settings.Secure.SELECTED_INPUT_METHOD_SUBTYPE, NOT_A_SUBTYPE_ID, userId);
314         if (selectedSubtypeHashCode != NOT_A_SUBTYPE_ID && currentSubtype != null
315                 && isValidSubtypeHashCode(imi, currentSubtype.hashCode())) {
316             return currentSubtype;
317         }
318 
319         final int subtypeId = settings.getSelectedInputMethodSubtypeId(imi.getId());
320         if (subtypeId != NOT_A_SUBTYPE_ID) {
321             return imi.getSubtypeAt(subtypeId);
322         }
323 
324         // If there are no selected subtypes, the framework will try to find the most applicable
325         // subtype from explicitly or implicitly enabled subtypes.
326         final List<InputMethodSubtype> subtypes = settings.getEnabledInputMethodSubtypeList(imi,
327                 true);
328         if (subtypes.isEmpty()) {
329             return currentSubtype;
330         }
331         // If there is only one explicitly or implicitly enabled subtype,
332         // just returns it.
333         if (subtypes.size() == 1) {
334             return subtypes.get(0);
335         }
336         final String locale = SystemLocaleWrapper.get(userId).get(0).toString();
337         final var subtype = findLastResortApplicableSubtype(subtypes, SUBTYPE_MODE_KEYBOARD, locale,
338                 true);
339         if (subtype != null) {
340             return subtype;
341         }
342         return findLastResortApplicableSubtype(subtypes, null, locale, true);
343     }
344 }
345