blob: 1becfb4a0e233b1cc7290d5abaa7d213299a9b28 [file] [log] [blame]
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -07001/*
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
Roozbeh Pournadera23748a2015-08-31 14:30:36 -070019import android.annotation.NonNull;
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070020import android.annotation.Nullable;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -070021import android.annotation.Size;
Roozbeh Pournader2591cc82015-12-08 22:21:24 -080022import android.icu.util.ULocale;
Yohei Yukawa789d8fd2015-12-03 11:27:05 -080023import android.os.Parcel;
24import android.os.Parcelable;
Roozbeh Pournadera23748a2015-08-31 14:30:36 -070025
26import com.android.internal.annotations.GuardedBy;
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070027
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
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070033// 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
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070035// 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 */
Yohei Yukawa789d8fd2015-12-03 11:27:05 -080041public final class LocaleList implements Parcelable {
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070042 private final Locale[] mList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -070043 // 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.
Yohei Yukawa789d8fd2015-12-03 11:27:05 -080046 @NonNull
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -070047 private final String mStringRepresentation;
48
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070049 private static final Locale[] sEmptyList = new Locale[0];
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070050 private static final LocaleList sEmptyLocaleList = new LocaleList();
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070051
52 public Locale get(int location) {
53 return location < mList.length ? mList[location] : null;
54 }
55
Roozbeh Pournader8bca6982015-11-18 17:41:24 -080056 @Nullable
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -070057 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
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -070069 @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
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800108 @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
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700118 @NonNull
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700119 public String toLanguageTags() {
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700120 return mStringRepresentation;
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700121 }
122
123 /**
124 * It is almost always better to call {@link #getEmptyLocaleList()} instead which returns
125 * a pre-constructed empty locale list.
126 */
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700127 public LocaleList() {
128 mList = sEmptyList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700129 mStringRepresentation = "";
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700130 }
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 */
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700136 public LocaleList(@Nullable Locale locale) {
137 if (locale == null) {
138 mList = sEmptyList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700139 mStringRepresentation = "";
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700140 } else {
141 mList = new Locale[1];
142 mList[0] = (Locale) locale.clone();
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700143 mStringRepresentation = locale.toLanguageTag();
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700144 }
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 */
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700151 public LocaleList(@Nullable Locale[] list) {
152 if (list == null || list.length == 0) {
153 mList = sEmptyList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700154 mStringRepresentation = "";
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700155 } else {
156 final Locale[] localeList = new Locale[list.length];
157 final HashSet<Locale> seenLocales = new HashSet<Locale>();
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700158 final StringBuilder sb = new StringBuilder();
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700159 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 {
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700166 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);
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700173 }
174 }
175 mList = localeList;
Roozbeh Pournaderf036ead2015-10-19 16:56:39 -0700176 mStringRepresentation = sb.toString();
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700177 }
178 }
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700179
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800180 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
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700194 public static LocaleList getEmptyLocaleList() {
195 return sEmptyLocaleList;
196 }
197
Yohei Yukawa789d8fd2015-12-03 11:27:05 -0800198 @NonNull
Roozbeh Pournaderb46fdd42015-08-19 14:56:22 -0700199 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 }
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700211
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800212 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 int matchScore(Locale supported, Locale desired) {
223 if (supported.equals(desired)) {
224 return 1; // return early so we don't do unnecessary computation
225 }
226 if (!supported.getLanguage().equals(desired.getLanguage())) {
227 return 0;
228 }
229 // There is no match if the two locales use different scripts. This will most imporantly
230 // take care of traditional vs simplified Chinese.
231 final String supportedScr = getLikelyScript(supported);
232 final String desiredScr = getLikelyScript(desired);
233 return supportedScr.equals(desiredScr) ? 1 : 0;
234 }
235
236 /**
237 * Returns the first match in the locale list given an unordered array of supported locales
238 * in BCP47 format.
239 *
240 * If the locale list is empty, null would be returned.
241 */
Roozbeh Pournader8bca6982015-11-18 17:41:24 -0800242 @Nullable
Roozbeh Pournader2591cc82015-12-08 22:21:24 -0800243 public Locale getFirstMatch(String[] supportedLocales) {
244 if (mList.length == 1) { // just one locale, perhaps the most common scenario
245 return mList[0];
246 }
247 if (mList.length == 0) { // empty locale list
248 return null;
249 }
250 // TODO: Figure out what to if en-XA or ar-XB are in the locale list
251 int bestIndex = Integer.MAX_VALUE;
252 for (String tag : supportedLocales) {
253 final Locale supportedLocale = Locale.forLanguageTag(tag);
254 // We expect the average length of locale lists used for locale resolution to be
255 // smaller than three, so it's OK to do this as an O(mn) algorithm.
256 for (int idx = 0; idx < mList.length; idx++) {
257 final int score = matchScore(supportedLocale, mList[idx]);
258 if (score > 0) {
259 if (idx == 0) { // We have a match on the first locale, which is good enough
260 return mList[0];
261 } else if (idx < bestIndex) {
262 bestIndex = idx;
263 }
264 }
265 }
266 }
267 if (bestIndex == Integer.MAX_VALUE) { // no match was found
268 return mList[0];
269 } else {
270 return mList[bestIndex];
271 }
Roozbeh Pournader8bca6982015-11-18 17:41:24 -0800272 }
273
Roozbeh Pournadera23748a2015-08-31 14:30:36 -0700274 private final static Object sLock = new Object();
275
276 @GuardedBy("sLock")
277 private static LocaleList sDefaultLocaleList;
278
279 // TODO: fix this to return the default system locale list once we have that
280 @NonNull @Size(min=1)
281 public static LocaleList getDefault() {
282 Locale defaultLocale = Locale.getDefault();
283 synchronized (sLock) {
284 if (sDefaultLocaleList == null || sDefaultLocaleList.size() != 1
285 || !defaultLocale.equals(sDefaultLocaleList.getPrimary())) {
286 sDefaultLocaleList = new LocaleList(defaultLocale);
287 }
288 }
289 return sDefaultLocaleList;
290 }
Roozbeh Pournader0ba0d6b2015-08-19 11:19:51 -0700291}