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