1 /* 2 * Copyright (C) 2011 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.inputmethod.latin.utils; 18 19 import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.COMBINING_RULES; 20 import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET; 21 import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME; 22 23 import android.content.Context; 24 import android.content.res.Resources; 25 import android.os.Build; 26 import android.util.Log; 27 import android.view.inputmethod.InputMethodSubtype; 28 29 import com.android.inputmethod.latin.R; 30 import com.android.inputmethod.latin.common.LocaleUtils; 31 import com.android.inputmethod.latin.common.StringUtils; 32 33 import java.util.HashMap; 34 import java.util.Locale; 35 36 import javax.annotation.Nonnull; 37 import javax.annotation.Nullable; 38 39 /** 40 * A helper class to deal with subtype locales. 41 */ 42 // TODO: consolidate this into RichInputMethodSubtype 43 public final class SubtypeLocaleUtils { 44 static final String TAG = SubtypeLocaleUtils.class.getSimpleName(); 45 46 // This reference class {@link R} must be located in the same package as LatinIME.java. 47 private static final String RESOURCE_PACKAGE_NAME = R.class.getPackage().getName(); 48 49 // Special language code to represent "no language". 50 public static final String NO_LANGUAGE = "zz"; 51 public static final String QWERTY = "qwerty"; 52 public static final String EMOJI = "emoji"; 53 public static final int UNKNOWN_KEYBOARD_LAYOUT = R.string.subtype_generic; 54 55 private static volatile boolean sInitialized = false; 56 private static final Object sInitializeLock = new Object(); 57 private static Resources sResources; 58 // Keyboard layout to its display name map. 59 private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap = new HashMap<>(); 60 // Keyboard layout to subtype name resource id map. 61 private static final HashMap<String, Integer> sKeyboardLayoutToNameIdsMap = new HashMap<>(); 62 // Exceptional locale whose name should be displayed in Locale.ROOT. 63 private static final HashMap<String, Integer> sExceptionalLocaleDisplayedInRootLocale = 64 new HashMap<>(); 65 // Exceptional locale to subtype name resource id map. 66 private static final HashMap<String, Integer> sExceptionalLocaleToNameIdsMap = new HashMap<>(); 67 // Exceptional locale to subtype name with layout resource id map. 68 private static final HashMap<String, Integer> sExceptionalLocaleToWithLayoutNameIdsMap = 69 new HashMap<>(); 70 private static final String SUBTYPE_NAME_RESOURCE_PREFIX = 71 "string/subtype_"; 72 private static final String SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX = 73 "string/subtype_generic_"; 74 private static final String SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX = 75 "string/subtype_with_layout_"; 76 private static final String SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX = 77 "string/subtype_no_language_"; 78 private static final String SUBTYPE_NAME_RESOURCE_IN_ROOT_LOCALE_PREFIX = 79 "string/subtype_in_root_locale_"; 80 // Keyboard layout set name for the subtypes that don't have a keyboardLayoutSet extra value. 81 // This is for compatibility to keep the same subtype ids as pre-JellyBean. 82 private static final HashMap<String, String> sLocaleAndExtraValueToKeyboardLayoutSetMap = 83 new HashMap<>(); 84 85 private SubtypeLocaleUtils() { 86 // Intentional empty constructor for utility class. 87 } 88 89 // Note that this initialization method can be called multiple times. 90 public static void init(final Context context) { 91 synchronized (sInitializeLock) { 92 if (sInitialized == false) { 93 initLocked(context); 94 sInitialized = true; 95 } 96 } 97 } 98 99 private static void initLocked(final Context context) { 100 final Resources res = context.getResources(); 101 sResources = res; 102 103 final String[] predefinedLayoutSet = res.getStringArray(R.array.predefined_layouts); 104 final String[] layoutDisplayNames = res.getStringArray( 105 R.array.predefined_layout_display_names); 106 for (int i = 0; i < predefinedLayoutSet.length; i++) { 107 final String layoutName = predefinedLayoutSet[i]; 108 sKeyboardLayoutToDisplayNameMap.put(layoutName, layoutDisplayNames[i]); 109 final String resourceName = SUBTYPE_NAME_RESOURCE_GENERIC_PREFIX + layoutName; 110 final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME); 111 sKeyboardLayoutToNameIdsMap.put(layoutName, resId); 112 // Register subtype name resource id of "No language" with key "zz_<layout>" 113 final String noLanguageResName = SUBTYPE_NAME_RESOURCE_NO_LANGUAGE_PREFIX + layoutName; 114 final int noLanguageResId = res.getIdentifier( 115 noLanguageResName, null, RESOURCE_PACKAGE_NAME); 116 final String key = getNoLanguageLayoutKey(layoutName); 117 sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId); 118 } 119 120 final String[] exceptionalLocaleInRootLocale = res.getStringArray( 121 R.array.subtype_locale_displayed_in_root_locale); 122 for (int i = 0; i < exceptionalLocaleInRootLocale.length; i++) { 123 final String localeString = exceptionalLocaleInRootLocale[i]; 124 final String resourceName = SUBTYPE_NAME_RESOURCE_IN_ROOT_LOCALE_PREFIX + localeString; 125 final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME); 126 sExceptionalLocaleDisplayedInRootLocale.put(localeString, resId); 127 } 128 129 final String[] exceptionalLocales = res.getStringArray( 130 R.array.subtype_locale_exception_keys); 131 for (int i = 0; i < exceptionalLocales.length; i++) { 132 final String localeString = exceptionalLocales[i]; 133 final String resourceName = SUBTYPE_NAME_RESOURCE_PREFIX + localeString; 134 final int resId = res.getIdentifier(resourceName, null, RESOURCE_PACKAGE_NAME); 135 sExceptionalLocaleToNameIdsMap.put(localeString, resId); 136 final String resourceNameWithLayout = 137 SUBTYPE_NAME_RESOURCE_WITH_LAYOUT_PREFIX + localeString; 138 final int resIdWithLayout = res.getIdentifier( 139 resourceNameWithLayout, null, RESOURCE_PACKAGE_NAME); 140 sExceptionalLocaleToWithLayoutNameIdsMap.put(localeString, resIdWithLayout); 141 } 142 143 final String[] keyboardLayoutSetMap = res.getStringArray( 144 R.array.locale_and_extra_value_to_keyboard_layout_set_map); 145 for (int i = 0; i + 1 < keyboardLayoutSetMap.length; i += 2) { 146 final String key = keyboardLayoutSetMap[i]; 147 final String keyboardLayoutSet = keyboardLayoutSetMap[i + 1]; 148 sLocaleAndExtraValueToKeyboardLayoutSetMap.put(key, keyboardLayoutSet); 149 } 150 } 151 152 public static boolean isExceptionalLocale(final String localeString) { 153 return sExceptionalLocaleToNameIdsMap.containsKey(localeString); 154 } 155 156 private static final String getNoLanguageLayoutKey(final String keyboardLayoutName) { 157 return NO_LANGUAGE + "_" + keyboardLayoutName; 158 } 159 160 public static int getSubtypeNameId(final String localeString, final String keyboardLayoutName) { 161 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN 162 && isExceptionalLocale(localeString)) { 163 return sExceptionalLocaleToWithLayoutNameIdsMap.get(localeString); 164 } 165 final String key = NO_LANGUAGE.equals(localeString) 166 ? getNoLanguageLayoutKey(keyboardLayoutName) 167 : keyboardLayoutName; 168 final Integer nameId = sKeyboardLayoutToNameIdsMap.get(key); 169 return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId; 170 } 171 172 @Nonnull 173 public static Locale getDisplayLocaleOfSubtypeLocale(@Nonnull final String localeString) { 174 if (NO_LANGUAGE.equals(localeString)) { 175 return sResources.getConfiguration().locale; 176 } 177 if (sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) { 178 return Locale.ROOT; 179 } 180 return LocaleUtils.constructLocaleFromString(localeString); 181 } 182 183 public static String getSubtypeLocaleDisplayNameInSystemLocale( 184 @Nonnull final String localeString) { 185 final Locale displayLocale = sResources.getConfiguration().locale; 186 return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale); 187 } 188 189 @Nonnull 190 public static String getSubtypeLocaleDisplayName(@Nonnull final String localeString) { 191 final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString); 192 return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale); 193 } 194 195 @Nonnull 196 public static String getSubtypeLanguageDisplayName(@Nonnull final String localeString) { 197 final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString); 198 final String languageString; 199 if (sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) { 200 languageString = localeString; 201 } else { 202 languageString = LocaleUtils.constructLocaleFromString(localeString).getLanguage(); 203 } 204 return getSubtypeLocaleDisplayNameInternal(languageString, displayLocale); 205 } 206 207 @Nonnull 208 private static String getSubtypeLocaleDisplayNameInternal(@Nonnull final String localeString, 209 @Nonnull final Locale displayLocale) { 210 if (NO_LANGUAGE.equals(localeString)) { 211 // No language subtype should be displayed in system locale. 212 return sResources.getString(R.string.subtype_no_language); 213 } 214 final Integer exceptionalNameResId; 215 if (displayLocale.equals(Locale.ROOT) 216 && sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) { 217 exceptionalNameResId = sExceptionalLocaleDisplayedInRootLocale.get(localeString); 218 } else if (sExceptionalLocaleToNameIdsMap.containsKey(localeString)) { 219 exceptionalNameResId = sExceptionalLocaleToNameIdsMap.get(localeString); 220 } else { 221 exceptionalNameResId = null; 222 } 223 224 final String displayName; 225 if (exceptionalNameResId != null) { 226 final RunInLocale<String> getExceptionalName = new RunInLocale<String>() { 227 @Override 228 protected String job(final Resources res) { 229 return res.getString(exceptionalNameResId); 230 } 231 }; 232 displayName = getExceptionalName.runInLocale(sResources, displayLocale); 233 } else { 234 displayName = LocaleUtils.constructLocaleFromString(localeString) 235 .getDisplayName(displayLocale); 236 } 237 return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale); 238 } 239 240 // InputMethodSubtype's display name in its locale. 241 // isAdditionalSubtype (T=true, F=false) 242 // locale layout | display name 243 // ------ ------- - ---------------------- 244 // en_US qwerty F English (US) exception 245 // en_GB qwerty F English (UK) exception 246 // es_US spanish F Espaol (EE.UU.) exception 247 // fr azerty F Franais 248 // fr_CA qwerty F Franais (Canada) 249 // fr_CH swiss F Franais (Suisse) 250 // de qwertz F Deutsch 251 // de_CH swiss T Deutsch (Schweiz) 252 // zz qwerty F Alphabet (QWERTY) in system locale 253 // fr qwertz T Franais (QWERTZ) 254 // de qwerty T Deutsch (QWERTY) 255 // en_US azerty T English (US) (AZERTY) exception 256 // zz azerty T Alphabet (AZERTY) in system locale 257 258 @Nonnull 259 private static String getReplacementString(@Nonnull final InputMethodSubtype subtype, 260 @Nonnull final Locale displayLocale) { 261 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN 262 && subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)) { 263 return subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME); 264 } 265 return getSubtypeLocaleDisplayNameInternal(subtype.getLocale(), displayLocale); 266 } 267 268 @Nonnull 269 public static String getSubtypeDisplayNameInSystemLocale( 270 @Nonnull final InputMethodSubtype subtype) { 271 final Locale displayLocale = sResources.getConfiguration().locale; 272 return getSubtypeDisplayNameInternal(subtype, displayLocale); 273 } 274 275 @Nonnull 276 public static String getSubtypeNameForLogging(@Nullable final InputMethodSubtype subtype) { 277 if (subtype == null) { 278 return "<null subtype>"; 279 } 280 return getSubtypeLocale(subtype) + "/" + getKeyboardLayoutSetName(subtype); 281 } 282 283 @Nonnull 284 private static String getSubtypeDisplayNameInternal(@Nonnull final InputMethodSubtype subtype, 285 @Nonnull final Locale displayLocale) { 286 final String replacementString = getReplacementString(subtype, displayLocale); 287 // TODO: rework this for multi-lingual subtypes 288 final int nameResId = subtype.getNameResId(); 289 final RunInLocale<String> getSubtypeName = new RunInLocale<String>() { 290 @Override 291 protected String job(final Resources res) { 292 try { 293 return res.getString(nameResId, replacementString); 294 } catch (Resources.NotFoundException e) { 295 // TODO: Remove this catch when InputMethodManager.getCurrentInputMethodSubtype 296 // is fixed. 297 Log.w(TAG, "Unknown subtype: mode=" + subtype.getMode() 298 + " nameResId=" + subtype.getNameResId() 299 + " locale=" + subtype.getLocale() 300 + " extra=" + subtype.getExtraValue() 301 + "\n" + DebugLogUtils.getStackTrace()); 302 return ""; 303 } 304 } 305 }; 306 return StringUtils.capitalizeFirstCodePoint( 307 getSubtypeName.runInLocale(sResources, displayLocale), displayLocale); 308 } 309 310 @Nonnull 311 public static Locale getSubtypeLocale(@Nonnull final InputMethodSubtype subtype) { 312 final String localeString = subtype.getLocale(); 313 return LocaleUtils.constructLocaleFromString(localeString); 314 } 315 316 @Nonnull 317 public static String getKeyboardLayoutSetDisplayName( 318 @Nonnull final InputMethodSubtype subtype) { 319 final String layoutName = getKeyboardLayoutSetName(subtype); 320 return getKeyboardLayoutSetDisplayName(layoutName); 321 } 322 323 @Nonnull 324 public static String getKeyboardLayoutSetDisplayName(@Nonnull final String layoutName) { 325 return sKeyboardLayoutToDisplayNameMap.get(layoutName); 326 } 327 328 @Nonnull 329 public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) { 330 String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET); 331 if (keyboardLayoutSet == null) { 332 // This subtype doesn't have a keyboardLayoutSet extra value, so lookup its keyboard 333 // layout set in sLocaleAndExtraValueToKeyboardLayoutSetMap to keep it compatible with 334 // pre-JellyBean. 335 final String key = subtype.getLocale() + ":" + subtype.getExtraValue(); 336 keyboardLayoutSet = sLocaleAndExtraValueToKeyboardLayoutSetMap.get(key); 337 } 338 // TODO: Remove this null check when InputMethodManager.getCurrentInputMethodSubtype is 339 // fixed. 340 if (keyboardLayoutSet == null) { 341 android.util.Log.w(TAG, "KeyboardLayoutSet not found, use QWERTY: " + 342 "locale=" + subtype.getLocale() + " extraValue=" + subtype.getExtraValue()); 343 return QWERTY; 344 } 345 return keyboardLayoutSet; 346 } 347 348 public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) { 349 return subtype.getExtraValueOf(COMBINING_RULES); 350 } 351 } 352