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