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; 18 19 import android.content.res.Configuration; 20 import android.content.res.Resources; 21 import android.text.TextUtils; 22 23 import java.util.HashMap; 24 import java.util.Locale; 25 26 /** 27 * A class to help with handling Locales in string form. 28 * 29 * This file has the same meaning and features (and shares all of its code) with 30 * the one in the dictionary pack. They need to be kept synchronized; for any 31 * update/bugfix to this file, consider also updating/fixing the version in the 32 * dictionary pack. 33 */ 34 public final class LocaleUtils { 35 private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap(); 36 private static final String LOCALE_AND_TIME_STR_SEPARATER = ","; 37 LocaleUtils()38 private LocaleUtils() { 39 // Intentional empty constructor for utility class. 40 } 41 42 // Locale match level constants. 43 // A higher level of match is guaranteed to have a higher numerical value. 44 // Some room is left within constants to add match cases that may arise necessary 45 // in the future, for example differentiating between the case where the countries 46 // are both present and different, and the case where one of the locales does not 47 // specify the countries. This difference is not needed now. 48 49 // Nothing matches. 50 public static final int LOCALE_NO_MATCH = 0; 51 // The languages matches, but the country are different. Or, the reference locale requires a 52 // country and the tested locale does not have one. 53 public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3; 54 // The languages and country match, but the variants are different. Or, the reference locale 55 // requires a variant and the tested locale does not have one. 56 public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6; 57 // The required locale is null or empty so it will accept anything, and the tested locale 58 // is non-null and non-empty. 59 public static final int LOCALE_ANY_MATCH = 10; 60 // The language matches, and the tested locale specifies a country but the reference locale 61 // does not require one. 62 public static final int LOCALE_LANGUAGE_MATCH = 15; 63 // The language and the country match, and the tested locale specifies a variant but the 64 // reference locale does not require one. 65 public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20; 66 // The compared locales are fully identical. This is the best match level. 67 public static final int LOCALE_FULL_MATCH = 30; 68 69 // The level at which a match is "normally" considered a locale match with standard algorithms. 70 // Don't use this directly, use #isMatch to test. 71 private static final int LOCALE_MATCH = LOCALE_ANY_MATCH; 72 73 // Make this match the maximum match level. If this evolves to have more than 2 digits 74 // when written in base 10, also adjust the getMatchLevelSortedString method. 75 private static final int MATCH_LEVEL_MAX = 30; 76 77 /** 78 * Return how well a tested locale matches a reference locale. 79 * 80 * This will check the tested locale against the reference locale and return a measure of how 81 * a well it matches the reference. The general idea is that the tested locale has to match 82 * every specified part of the required locale. A full match occur when they are equal, a 83 * partial match when the tested locale agrees with the reference locale but is more specific, 84 * and a difference when the tested locale does not comply with all requirements from the 85 * reference locale. 86 * In more detail, if the reference locale specifies at least a language and the testedLocale 87 * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the 88 * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH 89 * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and 90 * tested locale agree on the language, but not on the country, 91 * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country, 92 * and LOCALE_LANGUAGE_MATCH otherwise. 93 * If they agree on both the language and the country, but not on the variant, 94 * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale 95 * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches, 96 * LOCALE_FULL_MATCH is returned. 97 * Examples: 98 * en <=> en_US => LOCALE_LANGUAGE_MATCH 99 * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER 100 * en_US_POSIX <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER 101 * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH 102 * sp_US <=> en_US => LOCALE_NO_MATCH 103 * de <=> de => LOCALE_FULL_MATCH 104 * en_US <=> en_US => LOCALE_FULL_MATCH 105 * "" <=> en_US => LOCALE_ANY_MATCH 106 * 107 * @param referenceLocale the reference locale to test against. 108 * @param testedLocale the locale to test. 109 * @return a constant that measures how well the tested locale matches the reference locale. 110 */ getMatchLevel(String referenceLocale, String testedLocale)111 public static int getMatchLevel(String referenceLocale, String testedLocale) { 112 if (TextUtils.isEmpty(referenceLocale)) { 113 return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH; 114 } 115 if (null == testedLocale) return LOCALE_NO_MATCH; 116 String[] referenceParams = referenceLocale.split("_", 3); 117 String[] testedParams = testedLocale.split("_", 3); 118 // By spec of String#split, [0] cannot be null and length cannot be 0. 119 if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH; 120 switch (referenceParams.length) { 121 case 1: 122 return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH; 123 case 2: 124 if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; 125 if (!referenceParams[1].equals(testedParams[1])) 126 return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; 127 if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH; 128 return LOCALE_FULL_MATCH; 129 case 3: 130 if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; 131 if (!referenceParams[1].equals(testedParams[1])) 132 return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; 133 if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; 134 if (!referenceParams[2].equals(testedParams[2])) 135 return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; 136 return LOCALE_FULL_MATCH; 137 } 138 // It should be impossible to come here 139 return LOCALE_NO_MATCH; 140 } 141 142 /** 143 * Return a string that represents this match level, with better matches first. 144 * 145 * The strings are sorted in lexicographic order: a better match will always be less than 146 * a worse match when compared together. 147 */ getMatchLevelSortedString(int matchLevel)148 public static String getMatchLevelSortedString(int matchLevel) { 149 // This works because the match levels are 0~99 (actually 0~30) 150 // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel 151 return String.format("%02d", MATCH_LEVEL_MAX - matchLevel); 152 } 153 154 /** 155 * Find out whether a match level should be considered a match. 156 * 157 * This method takes a match level as returned by the #getMatchLevel method, and returns whether 158 * it should be considered a match in the usual sense with standard Locale functions. 159 * 160 * @param level the match level, as returned by getMatchLevel. 161 * @return whether this is a match or not. 162 */ isMatch(int level)163 public static boolean isMatch(int level) { 164 return LOCALE_MATCH <= level; 165 } 166 167 static final Object sLockForRunInLocale = new Object(); 168 169 public abstract static class RunInLocale<T> { job(Resources res)170 protected abstract T job(Resources res); 171 172 /** 173 * Execute {@link #job(Resources)} method in specified system locale exclusively. 174 * 175 * @param res the resources to use. Pass current resources. 176 * @param newLocale the locale to change to 177 * @return the value returned from {@link #job(Resources)}. 178 */ runInLocale(final Resources res, final Locale newLocale)179 public T runInLocale(final Resources res, final Locale newLocale) { 180 synchronized (sLockForRunInLocale) { 181 final Configuration conf = res.getConfiguration(); 182 final Locale oldLocale = conf.locale; 183 final boolean needsChange = (newLocale != null && !newLocale.equals(oldLocale)); 184 try { 185 if (needsChange) { 186 conf.locale = newLocale; 187 res.updateConfiguration(conf, null); 188 } 189 return job(res); 190 } finally { 191 if (needsChange) { 192 conf.locale = oldLocale; 193 res.updateConfiguration(conf, null); 194 } 195 } 196 } 197 } 198 } 199 200 private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap(); 201 202 /** 203 * Creates a locale from a string specification. 204 */ constructLocaleFromString(final String localeStr)205 public static Locale constructLocaleFromString(final String localeStr) { 206 if (localeStr == null) 207 return null; 208 synchronized (sLocaleCache) { 209 if (sLocaleCache.containsKey(localeStr)) 210 return sLocaleCache.get(localeStr); 211 Locale retval = null; 212 String[] localeParams = localeStr.split("_", 3); 213 if (localeParams.length == 1) { 214 retval = new Locale(localeParams[0]); 215 } else if (localeParams.length == 2) { 216 retval = new Locale(localeParams[0], localeParams[1]); 217 } else if (localeParams.length == 3) { 218 retval = new Locale(localeParams[0], localeParams[1], localeParams[2]); 219 } 220 if (retval != null) { 221 sLocaleCache.put(localeStr, retval); 222 } 223 return retval; 224 } 225 } 226 localeAndTimeStrToHashMap(String str)227 public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) { 228 if (TextUtils.isEmpty(str)) { 229 return EMPTY_LT_HASH_MAP; 230 } 231 final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER); 232 final int N = ss.length; 233 if (N < 2 || N % 2 != 0) { 234 return EMPTY_LT_HASH_MAP; 235 } 236 final HashMap<String, Long> retval = CollectionUtils.newHashMap(); 237 for (int i = 0; i < N / 2; ++i) { 238 final String localeStr = ss[i * 2]; 239 final long time = Long.valueOf(ss[i * 2 + 1]); 240 retval.put(localeStr, time); 241 } 242 return retval; 243 } 244 localeAndTimeHashMapToStr(HashMap<String, Long> map)245 public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) { 246 if (map == null || map.isEmpty()) { 247 return ""; 248 } 249 final StringBuilder builder = new StringBuilder(); 250 for (String localeStr : map.keySet()) { 251 if (builder.length() > 0) { 252 builder.append(LOCALE_AND_TIME_STR_SEPARATER); 253 } 254 final Long time = map.get(localeStr); 255 builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER); 256 builder.append(String.valueOf(time)); 257 } 258 return builder.toString(); 259 } 260 } 261