• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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