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