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