| /* |
| * Copyright (C) 2011 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package com.android.inputmethod.latin; |
| |
| import android.content.res.Configuration; |
| import android.content.res.Resources; |
| import android.text.TextUtils; |
| |
| import java.util.HashMap; |
| import java.util.Locale; |
| |
| /** |
| * A class to help with handling Locales in string form. |
| * |
| * This file has the same meaning and features (and shares all of its code) with |
| * the one in the dictionary pack. They need to be kept synchronized; for any |
| * update/bugfix to this file, consider also updating/fixing the version in the |
| * dictionary pack. |
| */ |
| public final class LocaleUtils { |
| private static final HashMap<String, Long> EMPTY_LT_HASH_MAP = CollectionUtils.newHashMap(); |
| private static final String LOCALE_AND_TIME_STR_SEPARATER = ","; |
| |
| private LocaleUtils() { |
| // Intentional empty constructor for utility class. |
| } |
| |
| // Locale match level constants. |
| // A higher level of match is guaranteed to have a higher numerical value. |
| // Some room is left within constants to add match cases that may arise necessary |
| // in the future, for example differentiating between the case where the countries |
| // are both present and different, and the case where one of the locales does not |
| // specify the countries. This difference is not needed now. |
| |
| // Nothing matches. |
| public static final int LOCALE_NO_MATCH = 0; |
| // The languages matches, but the country are different. Or, the reference locale requires a |
| // country and the tested locale does not have one. |
| public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3; |
| // The languages and country match, but the variants are different. Or, the reference locale |
| // requires a variant and the tested locale does not have one. |
| public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6; |
| // The required locale is null or empty so it will accept anything, and the tested locale |
| // is non-null and non-empty. |
| public static final int LOCALE_ANY_MATCH = 10; |
| // The language matches, and the tested locale specifies a country but the reference locale |
| // does not require one. |
| public static final int LOCALE_LANGUAGE_MATCH = 15; |
| // The language and the country match, and the tested locale specifies a variant but the |
| // reference locale does not require one. |
| public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20; |
| // The compared locales are fully identical. This is the best match level. |
| public static final int LOCALE_FULL_MATCH = 30; |
| |
| // The level at which a match is "normally" considered a locale match with standard algorithms. |
| // Don't use this directly, use #isMatch to test. |
| private static final int LOCALE_MATCH = LOCALE_ANY_MATCH; |
| |
| // Make this match the maximum match level. If this evolves to have more than 2 digits |
| // when written in base 10, also adjust the getMatchLevelSortedString method. |
| private static final int MATCH_LEVEL_MAX = 30; |
| |
| /** |
| * Return how well a tested locale matches a reference locale. |
| * |
| * This will check the tested locale against the reference locale and return a measure of how |
| * a well it matches the reference. The general idea is that the tested locale has to match |
| * every specified part of the required locale. A full match occur when they are equal, a |
| * partial match when the tested locale agrees with the reference locale but is more specific, |
| * and a difference when the tested locale does not comply with all requirements from the |
| * reference locale. |
| * In more detail, if the reference locale specifies at least a language and the testedLocale |
| * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the |
| * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH |
| * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and |
| * tested locale agree on the language, but not on the country, |
| * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country, |
| * and LOCALE_LANGUAGE_MATCH otherwise. |
| * If they agree on both the language and the country, but not on the variant, |
| * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale |
| * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches, |
| * LOCALE_FULL_MATCH is returned. |
| * Examples: |
| * en <=> en_US => LOCALE_LANGUAGE_MATCH |
| * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER |
| * en_US_POSIX <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER |
| * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH |
| * sp_US <=> en_US => LOCALE_NO_MATCH |
| * de <=> de => LOCALE_FULL_MATCH |
| * en_US <=> en_US => LOCALE_FULL_MATCH |
| * "" <=> en_US => LOCALE_ANY_MATCH |
| * |
| * @param referenceLocale the reference locale to test against. |
| * @param testedLocale the locale to test. |
| * @return a constant that measures how well the tested locale matches the reference locale. |
| */ |
| public static int getMatchLevel(String referenceLocale, String testedLocale) { |
| if (TextUtils.isEmpty(referenceLocale)) { |
| return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH; |
| } |
| if (null == testedLocale) return LOCALE_NO_MATCH; |
| String[] referenceParams = referenceLocale.split("_", 3); |
| String[] testedParams = testedLocale.split("_", 3); |
| // By spec of String#split, [0] cannot be null and length cannot be 0. |
| if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH; |
| switch (referenceParams.length) { |
| case 1: |
| return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH; |
| case 2: |
| if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; |
| if (!referenceParams[1].equals(testedParams[1])) |
| return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; |
| if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH; |
| return LOCALE_FULL_MATCH; |
| case 3: |
| if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; |
| if (!referenceParams[1].equals(testedParams[1])) |
| return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER; |
| if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; |
| if (!referenceParams[2].equals(testedParams[2])) |
| return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER; |
| return LOCALE_FULL_MATCH; |
| } |
| // It should be impossible to come here |
| return LOCALE_NO_MATCH; |
| } |
| |
| /** |
| * Return a string that represents this match level, with better matches first. |
| * |
| * The strings are sorted in lexicographic order: a better match will always be less than |
| * a worse match when compared together. |
| */ |
| public static String getMatchLevelSortedString(int matchLevel) { |
| // This works because the match levels are 0~99 (actually 0~30) |
| // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel |
| return String.format("%02d", MATCH_LEVEL_MAX - matchLevel); |
| } |
| |
| /** |
| * Find out whether a match level should be considered a match. |
| * |
| * This method takes a match level as returned by the #getMatchLevel method, and returns whether |
| * it should be considered a match in the usual sense with standard Locale functions. |
| * |
| * @param level the match level, as returned by getMatchLevel. |
| * @return whether this is a match or not. |
| */ |
| public static boolean isMatch(int level) { |
| return LOCALE_MATCH <= level; |
| } |
| |
| static final Object sLockForRunInLocale = new Object(); |
| |
| public abstract static class RunInLocale<T> { |
| protected abstract T job(Resources res); |
| |
| /** |
| * Execute {@link #job(Resources)} method in specified system locale exclusively. |
| * |
| * @param res the resources to use. Pass current resources. |
| * @param newLocale the locale to change to |
| * @return the value returned from {@link #job(Resources)}. |
| */ |
| public T runInLocale(final Resources res, final Locale newLocale) { |
| synchronized (sLockForRunInLocale) { |
| final Configuration conf = res.getConfiguration(); |
| final Locale oldLocale = conf.locale; |
| final boolean needsChange = (newLocale != null && !newLocale.equals(oldLocale)); |
| try { |
| if (needsChange) { |
| conf.locale = newLocale; |
| res.updateConfiguration(conf, null); |
| } |
| return job(res); |
| } finally { |
| if (needsChange) { |
| conf.locale = oldLocale; |
| res.updateConfiguration(conf, null); |
| } |
| } |
| } |
| } |
| } |
| |
| private static final HashMap<String, Locale> sLocaleCache = CollectionUtils.newHashMap(); |
| |
| /** |
| * Creates a locale from a string specification. |
| */ |
| public static Locale constructLocaleFromString(final String localeStr) { |
| if (localeStr == null) |
| return null; |
| synchronized (sLocaleCache) { |
| if (sLocaleCache.containsKey(localeStr)) |
| return sLocaleCache.get(localeStr); |
| Locale retval = null; |
| String[] localeParams = localeStr.split("_", 3); |
| if (localeParams.length == 1) { |
| retval = new Locale(localeParams[0]); |
| } else if (localeParams.length == 2) { |
| retval = new Locale(localeParams[0], localeParams[1]); |
| } else if (localeParams.length == 3) { |
| retval = new Locale(localeParams[0], localeParams[1], localeParams[2]); |
| } |
| if (retval != null) { |
| sLocaleCache.put(localeStr, retval); |
| } |
| return retval; |
| } |
| } |
| |
| public static HashMap<String, Long> localeAndTimeStrToHashMap(String str) { |
| if (TextUtils.isEmpty(str)) { |
| return EMPTY_LT_HASH_MAP; |
| } |
| final String[] ss = str.split(LOCALE_AND_TIME_STR_SEPARATER); |
| final int N = ss.length; |
| if (N < 2 || N % 2 != 0) { |
| return EMPTY_LT_HASH_MAP; |
| } |
| final HashMap<String, Long> retval = CollectionUtils.newHashMap(); |
| for (int i = 0; i < N / 2; ++i) { |
| final String localeStr = ss[i * 2]; |
| final long time = Long.valueOf(ss[i * 2 + 1]); |
| retval.put(localeStr, time); |
| } |
| return retval; |
| } |
| |
| public static String localeAndTimeHashMapToStr(HashMap<String, Long> map) { |
| if (map == null || map.isEmpty()) { |
| return ""; |
| } |
| final StringBuilder builder = new StringBuilder(); |
| for (String localeStr : map.keySet()) { |
| if (builder.length() > 0) { |
| builder.append(LOCALE_AND_TIME_STR_SEPARATER); |
| } |
| final Long time = map.get(localeStr); |
| builder.append(localeStr).append(LOCALE_AND_TIME_STR_SEPARATER); |
| builder.append(String.valueOf(time)); |
| } |
| return builder.toString(); |
| } |
| } |