[go: nahoru, domu]

LocaleList.java revision 1c686f2ce6cbfa3fdb598f452aa31d38f3eb2320
1/*
2 * Copyright (C) 2015 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
17package android.util;
18
19import android.annotation.NonNull;
20import android.annotation.Nullable;
21import android.annotation.Size;
22import android.icu.util.ULocale;
23import android.os.Parcel;
24import android.os.Parcelable;
25
26import com.android.internal.annotations.GuardedBy;
27
28import java.util.HashSet;
29import java.util.Locale;
30
31// TODO: We don't except too many LocaleLists to exist at the same time, and
32// we need access to the data at native level, so we should pass the data
33// down to the native level, create a map of every list seen there, take a
34// pointer back, and just keep that pointer in the Java-level object, so
35// things could be copied very quickly.
36
37/**
38 * LocaleList is an immutable list of Locales, typically used to keep an
39 * ordered user preferences for locales.
40 */
41public final class LocaleList implements Parcelable {
42    private final Locale[] mList;
43    // This is a comma-separated list of the locales in the LocaleList created at construction time,
44    // basically the result of running each locale's toLanguageTag() method and concatenating them
45    // with commas in between.
46    @NonNull
47    private final String mStringRepresentation;
48
49    private static final Locale[] sEmptyList = new Locale[0];
50    private static final LocaleList sEmptyLocaleList = new LocaleList();
51
52    public Locale get(int location) {
53        return location < mList.length ? mList[location] : null;
54    }
55
56    @Nullable
57    public Locale getPrimary() {
58        return mList.length == 0 ? null : get(0);
59    }
60
61    public boolean isEmpty() {
62        return mList.length == 0;
63    }
64
65    public int size() {
66        return mList.length;
67    }
68
69    @Override
70    public boolean equals(Object other) {
71        if (other == this)
72            return true;
73        if (!(other instanceof LocaleList))
74            return false;
75        final Locale[] otherList = ((LocaleList) other).mList;
76        if (mList.length != otherList.length)
77            return false;
78        for (int i = 0; i < mList.length; ++i) {
79            if (!mList[i].equals(otherList[i]))
80                return false;
81        }
82        return true;
83    }
84
85    @Override
86    public int hashCode() {
87        int result = 1;
88        for (int i = 0; i < mList.length; ++i) {
89            result = 31 * result + mList[i].hashCode();
90        }
91        return result;
92    }
93
94    @Override
95    public String toString() {
96        StringBuilder sb = new StringBuilder();
97        sb.append("[");
98        for (int i = 0; i < mList.length; ++i) {
99            sb.append(mList[i]);
100            if (i < mList.length - 1) {
101                sb.append(',');
102            }
103        }
104        sb.append("]");
105        return sb.toString();
106    }
107
108    @Override
109    public int describeContents() {
110        return 0;
111    }
112
113    @Override
114    public void writeToParcel(Parcel dest, int parcelableFlags) {
115        dest.writeString(mStringRepresentation);
116    }
117
118    @NonNull
119    public String toLanguageTags() {
120        return mStringRepresentation;
121    }
122
123    /**
124     * It is almost always better to call {@link #getEmptyLocaleList()} instead which returns
125     * a pre-constructed empty locale list.
126     */
127    public LocaleList() {
128        mList = sEmptyList;
129        mStringRepresentation = "";
130    }
131
132    /**
133     * @throws NullPointerException if any of the input locales is <code>null</code>.
134     * @throws IllegalArgumentException if any of the input locales repeat.
135     */
136    public LocaleList(@Nullable Locale locale) {
137        if (locale == null) {
138            mList = sEmptyList;
139            mStringRepresentation = "";
140        } else {
141            mList = new Locale[1];
142            mList[0] = (Locale) locale.clone();
143            mStringRepresentation = locale.toLanguageTag();
144        }
145    }
146
147    /**
148     * @throws NullPointerException if any of the input locales is <code>null</code>.
149     * @throws IllegalArgumentException if any of the input locales repeat.
150     */
151    public LocaleList(@Nullable Locale[] list) {
152        if (list == null || list.length == 0) {
153            mList = sEmptyList;
154            mStringRepresentation = "";
155        } else {
156            final Locale[] localeList = new Locale[list.length];
157            final HashSet<Locale> seenLocales = new HashSet<Locale>();
158            final StringBuilder sb = new StringBuilder();
159            for (int i = 0; i < list.length; ++i) {
160                final Locale l = list[i];
161                if (l == null) {
162                    throw new NullPointerException();
163                } else if (seenLocales.contains(l)) {
164                    throw new IllegalArgumentException();
165                } else {
166                    final Locale localeClone = (Locale) l.clone();
167                    localeList[i] = localeClone;
168                    sb.append(localeClone.toLanguageTag());
169                    if (i < list.length - 1) {
170                        sb.append(',');
171                    }
172                    seenLocales.add(localeClone);
173                }
174            }
175            mList = localeList;
176            mStringRepresentation = sb.toString();
177        }
178    }
179
180    public static final Parcelable.Creator<LocaleList> CREATOR
181            = new Parcelable.Creator<LocaleList>() {
182        @Override
183        public LocaleList createFromParcel(Parcel source) {
184            return LocaleList.forLanguageTags(source.readString());
185        }
186
187        @Override
188        public LocaleList[] newArray(int size) {
189            return new LocaleList[size];
190        }
191    };
192
193    @NonNull
194    public static LocaleList getEmptyLocaleList() {
195        return sEmptyLocaleList;
196    }
197
198    @NonNull
199    public static LocaleList forLanguageTags(@Nullable String list) {
200        if (list == null || list.equals("")) {
201            return getEmptyLocaleList();
202        } else {
203            final String[] tags = list.split(",");
204            final Locale[] localeArray = new Locale[tags.length];
205            for (int i = 0; i < localeArray.length; ++i) {
206                localeArray[i] = Locale.forLanguageTag(tags[i]);
207            }
208            return new LocaleList(localeArray);
209        }
210    }
211
212    private static String getLikelyScript(Locale locale) {
213        final String script = locale.getScript();
214        if (!script.isEmpty()) {
215            return script;
216        } else {
217            // TODO: Cache the results if this proves to be too slow
218            return ULocale.addLikelySubtags(ULocale.forLocale(locale)).getScript();
219        }
220    }
221
222    private static final String STRING_EN_XA = "en-XA";
223    private static final String STRING_AR_XB = "ar-XB";
224    private static final Locale LOCALE_EN_XA = new Locale("en", "XA");
225    private static final Locale LOCALE_AR_XB = new Locale("ar", "XB");
226    private static final int NUM_PSEUDO_LOCALES = 2;
227
228    private static boolean isPseudoLocale(String locale) {
229        return STRING_EN_XA.equals(locale) || STRING_AR_XB.equals(locale);
230    }
231
232    private static boolean isPseudoLocale(Locale locale) {
233        return LOCALE_EN_XA.equals(locale) || LOCALE_AR_XB.equals(locale);
234    }
235
236    private static int matchScore(Locale supported, Locale desired) {
237        if (supported.equals(desired)) {
238            return 1;  // return early so we don't do unnecessary computation
239        }
240        if (!supported.getLanguage().equals(desired.getLanguage())) {
241            return 0;
242        }
243        if (isPseudoLocale(supported) || isPseudoLocale(desired)) {
244            // The locales are not the same, but the languages are the same, and one of the locales
245            // is a pseudo-locale. So this is not a match.
246            return 0;
247        }
248        // There is no match if the two locales use different scripts. This will most imporantly
249        // take care of traditional vs simplified Chinese.
250        final String supportedScr = getLikelyScript(supported);
251        final String desiredScr = getLikelyScript(desired);
252        return supportedScr.equals(desiredScr) ? 1 : 0;
253    }
254
255    /**
256     * Returns the first match in the locale list given an unordered array of supported locales
257     * in BCP47 format.
258     *
259     * If the locale list is empty, null would be returned.
260     */
261    @Nullable
262    public Locale getFirstMatch(String[] supportedLocales) {
263        if (mList.length == 1) {  // just one locale, perhaps the most common scenario
264            return mList[0];
265        }
266        if (mList.length == 0) {  // empty locale list
267            return null;
268        }
269        int bestIndex = Integer.MAX_VALUE;
270        for (String tag : supportedLocales) {
271            final Locale supportedLocale = Locale.forLanguageTag(tag);
272            // We expect the average length of locale lists used for locale resolution to be
273            // smaller than three, so it's OK to do this as an O(mn) algorithm.
274            for (int idx = 0; idx < mList.length; idx++) {
275                final int score = matchScore(supportedLocale, mList[idx]);
276                if (score > 0) {
277                    if (idx == 0) {  // We have a match on the first locale, which is good enough
278                        return mList[0];
279                    } else if (idx < bestIndex) {
280                        bestIndex = idx;
281                    }
282                }
283            }
284        }
285        if (bestIndex == Integer.MAX_VALUE) {  // no match was found
286            return mList[0];
287        } else {
288            return mList[bestIndex];
289        }
290    }
291
292    /**
293     * Returns true if the array of locale tags only contains empty locales and pseudolocales.
294     * Assumes that there is no repetition in the input.
295     * {@hide}
296     */
297    public static boolean isPseudoLocalesOnly(String[] supportedLocales) {
298        if (supportedLocales.length > NUM_PSEUDO_LOCALES + 1) {
299            // This is for optimization. Since there's no repetition in the input, if we have more
300            // than the number of pseudo-locales plus one for the empty string, it's guaranteed
301            // that we have some meaninful locale in the list, so the list is not "practically
302            // empty".
303            return false;
304        }
305        for (String locale : supportedLocales) {
306            if (!locale.isEmpty() && !isPseudoLocale(locale)) {
307                return false;
308            }
309        }
310        return true;
311    }
312
313    private final static Object sLock = new Object();
314
315    @GuardedBy("sLock")
316    private static LocaleList sDefaultLocaleList;
317
318    // TODO: fix this to return the default system locale list once we have that
319    @NonNull @Size(min=1)
320    public static LocaleList getDefault() {
321        Locale defaultLocale = Locale.getDefault();
322        synchronized (sLock) {
323            if (sDefaultLocaleList == null || sDefaultLocaleList.size() != 1
324                    || !defaultLocale.equals(sDefaultLocaleList.getPrimary())) {
325                sDefaultLocaleList = new LocaleList(defaultLocale);
326            }
327        }
328        return sDefaultLocaleList;
329    }
330}
331