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