1 /* 2 * Copyright (C) 2009 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 libcore.icu; 18 19 import android.compat.annotation.ChangeId; 20 import android.compat.annotation.EnabledAfter; 21 import android.compat.annotation.UnsupportedAppUsage; 22 import android.compat.Compatibility; 23 import android.icu.text.DateFormatSymbols; 24 import android.icu.text.DecimalFormat; 25 import android.icu.text.DecimalFormatSymbols; 26 import android.icu.text.NumberFormat; 27 import android.icu.text.NumberingSystem; 28 import android.icu.util.Calendar; 29 import android.icu.util.GregorianCalendar; 30 import android.icu.util.ULocale; 31 32 import com.android.icu.text.DecimalFormatSymbolsBridge; 33 34 import dalvik.system.VMRuntime; 35 36 import java.text.DateFormat; 37 import java.util.HashMap; 38 import java.util.Locale; 39 import libcore.util.Objects; 40 41 /** 42 * Passes locale-specific from ICU native code to Java. 43 * <p> 44 * Note that you share these; you must not alter any of the fields, nor their array elements 45 * in the case of arrays. If you ever expose any of these things to user code, you must give 46 * them a clone rather than the original. 47 * @hide 48 */ 49 public final class LocaleData { 50 51 /** 52 * @see #USE_REAL_ROOT_LOCALE 53 */ 54 private static final Locale LOCALE_EN_US_POSIX = new Locale("en", "US", "POSIX"); 55 56 57 // In Android Q or before, when this class tries to load {@link Locale#ROOT} data, en_US_POSIX 58 // locale data is incorrectly loaded due to a bug b/159514442 (public bug b/159047832). 59 // 60 // This class used to pass "und" string as BCP47 language tag to our jni code, which then 61 // passes the string as as ICU Locale ID to ICU4C. ICU4C 63 or older version doesn't recognize 62 // "und" as a valid locale id, and fallback the default locale. The default locale is 63 // normally selected in the Locale picker in the Settings app by the user and set via 64 // frameworks. But this class statically cached the ROOT locale data before the 65 // default locale being set by framework, and without initialization, ICU4C uses en_US_POSIX 66 // as default locale. Thus, in Q or before, en_US_POSIX data is loaded. 67 // 68 // ICU version 64.1 resolved inconsistent behavior of 69 // "root", "und" and "" (empty) Locale ID which libcore previously relied on, and they are 70 // recognized correctly as {@link Locale#ROOT} since Android R. This ChangeId gated the change, 71 // and fallback to the old behavior by checking targetSdkVersion version. 72 // 73 // The below javadoc is shown in http://developer.android.com for consumption by app developers. 74 /** 75 * Since Android 11, formatter classes, e.g. java.text.SimpleDateFormat, no longer 76 * provide English data when Locale.ROOT format is requested. Please use 77 * Locale.ENGLISH to format in English. 78 * 79 * Note that Locale.ROOT is used as language/country neutral locale or fallback locale, 80 * and does not guarantee to represent English locale. 81 * 82 * This flag is only for documentation and can't be overridden by app. Please use 83 * {@code targetSdkVersion} to enable the new behavior. 84 */ 85 @ChangeId 86 @EnabledAfter(targetSdkVersion=29 /* Android Q */) 87 public static final long USE_REAL_ROOT_LOCALE = 159047832L; 88 89 // A cache for the locale-specific data. 90 private static final HashMap<String, LocaleData> localeDataCache = new HashMap<String, LocaleData>(); 91 static { 92 // Ensure that we pull in the locale data for the root locale, en_US, and the 93 // user's default locale. (All devices must support the root locale and en_US, 94 // and they're used for various system things like HTTP headers.) Pre-populating 95 // the cache is especially useful on Android because we'll share this via the Zygote. 96 get(Locale.ROOT); 97 get(Locale.US); Locale.getDefault()98 get(Locale.getDefault()); 99 } 100 101 // Used by Calendar. 102 @UnsupportedAppUsage 103 public Integer firstDayOfWeek; 104 @UnsupportedAppUsage 105 public Integer minimalDaysInFirstWeek; 106 107 // Used by DateFormatSymbols. 108 public String[] amPm; // "AM", "PM". 109 public String[] eras; // "BC", "AD". 110 111 public String[] longMonthNames; // "January", ... 112 @UnsupportedAppUsage 113 public String[] shortMonthNames; // "Jan", ... 114 public String[] tinyMonthNames; // "J", ... 115 public String[] longStandAloneMonthNames; // "January", ... 116 @UnsupportedAppUsage 117 public String[] shortStandAloneMonthNames; // "Jan", ... 118 public String[] tinyStandAloneMonthNames; // "J", ... 119 120 public String[] longWeekdayNames; // "Sunday", ... 121 public String[] shortWeekdayNames; // "Sun", ... 122 public String[] tinyWeekdayNames; // "S", ... 123 @UnsupportedAppUsage 124 public String[] longStandAloneWeekdayNames; // "Sunday", ... 125 @UnsupportedAppUsage 126 public String[] shortStandAloneWeekdayNames; // "Sun", ... 127 public String[] tinyStandAloneWeekdayNames; // "S", ... 128 129 // today and tomorrow is only kept for @UnsupportedAppUsage. 130 // Their value is hard-coded, not localized. 131 @UnsupportedAppUsage 132 public String today; // "Today". 133 @UnsupportedAppUsage 134 public String tomorrow; // "Tomorrow". 135 136 public String fullTimeFormat; 137 public String longTimeFormat; 138 public String mediumTimeFormat; 139 public String shortTimeFormat; 140 141 public String fullDateFormat; 142 public String longDateFormat; 143 public String mediumDateFormat; 144 public String shortDateFormat; 145 146 // timeFormat_hm and timeFormat_Hm are only kept for @UnsupportedAppUsage. 147 // Their value is hard-coded, not localized. 148 @UnsupportedAppUsage 149 public String timeFormat_hm; 150 @UnsupportedAppUsage 151 public String timeFormat_Hm; 152 153 // Used by DecimalFormatSymbols. 154 @UnsupportedAppUsage 155 public char zeroDigit; 156 public char decimalSeparator; 157 public char groupingSeparator; 158 public char patternSeparator; 159 public String percent; 160 public String perMill; 161 public char monetarySeparator; 162 public String minusSign; 163 public String exponentSeparator; 164 public String infinity; 165 public String NaN; 166 167 // Used by DecimalFormat and NumberFormat. 168 public String numberPattern; 169 public String integerPattern; 170 public String currencyPattern; 171 public String percentPattern; 172 173 private final Locale mLocale; 174 LocaleData(Locale locale)175 private LocaleData(Locale locale) { 176 mLocale = locale; 177 today = "Today"; 178 tomorrow = "Tomorrow"; 179 timeFormat_hm = "h:mm a"; 180 timeFormat_Hm = "HH:mm"; 181 } 182 183 @UnsupportedAppUsage mapInvalidAndNullLocales(Locale locale)184 public static Locale mapInvalidAndNullLocales(Locale locale) { 185 if (locale == null) { 186 return Locale.getDefault(); 187 } 188 189 if ("und".equals(locale.toLanguageTag())) { 190 return Locale.ROOT; 191 } 192 193 return locale; 194 } 195 196 /** 197 * Normally, this utility function is used by secondary cache above {@link LocaleData}, 198 * because the cache needs a correct key. 199 * @see #USE_REAL_ROOT_LOCALE 200 * @return a compatible locale for the bug b/159514442 201 */ getCompatibleLocaleForBug159514442(Locale locale)202 public static Locale getCompatibleLocaleForBug159514442(Locale locale) { 203 if (Locale.ROOT.equals(locale)) { 204 int targetSdkVersion = VMRuntime.getRuntime().getTargetSdkVersion(); 205 // Don't use Compatibility.isChangeEnabled(USE_REAL_ROOT_LOCALE) because the app compat 206 // framework lives in libcore and can depend on this class via various format methods, 207 // e.g. String.format(). See b/160912695. 208 if (targetSdkVersion <= 29 /* Android Q */) { 209 locale = LOCALE_EN_US_POSIX; 210 } 211 } 212 return locale; 213 } 214 215 /** 216 * Returns a shared LocaleData for the given locale. 217 */ 218 @UnsupportedAppUsage get(Locale locale)219 public static LocaleData get(Locale locale) { 220 if (locale == null) { 221 throw new NullPointerException("locale == null"); 222 } 223 224 locale = getCompatibleLocaleForBug159514442(locale); 225 226 final String languageTag = locale.toLanguageTag(); 227 synchronized (localeDataCache) { 228 LocaleData localeData = localeDataCache.get(languageTag); 229 if (localeData != null) { 230 return localeData; 231 } 232 } 233 LocaleData newLocaleData = initLocaleData(locale); 234 synchronized (localeDataCache) { 235 LocaleData localeData = localeDataCache.get(languageTag); 236 if (localeData != null) { 237 return localeData; 238 } 239 localeDataCache.put(languageTag, newLocaleData); 240 return newLocaleData; 241 } 242 } 243 toString()244 @Override public String toString() { 245 return Objects.toString(this); 246 } 247 getDateFormat(int style)248 public String getDateFormat(int style) { 249 switch (style) { 250 case DateFormat.SHORT: 251 return shortDateFormat; 252 case DateFormat.MEDIUM: 253 return mediumDateFormat; 254 case DateFormat.LONG: 255 return longDateFormat; 256 case DateFormat.FULL: 257 return fullDateFormat; 258 } 259 throw new AssertionError(); 260 } 261 getTimeFormat(int style)262 public String getTimeFormat(int style) { 263 // Do not cache ICU.getTimePattern() return value in the LocaleData instance 264 // because most users do not enable this setting, hurts performance in critical path, 265 // e.g. b/161846393, and ICU.getBestDateTimePattern will cache it in ICU.CACHED_PATTERNS 266 // on demand. 267 switch (style) { 268 case DateFormat.SHORT: 269 if (DateFormat.is24Hour == null) { 270 return shortTimeFormat; 271 } else { 272 return ICU.getTimePattern(mLocale, DateFormat.is24Hour, false); 273 } 274 case DateFormat.MEDIUM: 275 if (DateFormat.is24Hour == null) { 276 return mediumTimeFormat; 277 } else { 278 return ICU.getTimePattern(mLocale, DateFormat.is24Hour, true); 279 } 280 case DateFormat.LONG: 281 // CLDR doesn't really have anything we can use to obey the 12-/24-hour preference. 282 return longTimeFormat; 283 case DateFormat.FULL: 284 // CLDR doesn't really have anything we can use to obey the 12-/24-hour preference. 285 return fullTimeFormat; 286 } 287 throw new AssertionError(); 288 } 289 290 /* 291 * This method is made public for testing 292 */ initLocaleData(Locale locale)293 public static LocaleData initLocaleData(Locale locale) { 294 LocaleData localeData = new LocaleData(locale); 295 296 localeData.initializeDateTimePatterns(locale); 297 localeData.initializeDateFormatData(locale); 298 localeData.initializeDecimalFormatData(locale); 299 localeData.initializeCalendarData(locale); 300 301 // Libcore localizes pattern separator while ICU doesn't. http://b/112080617 302 initializePatternSeparator(localeData, locale); 303 304 // Fix up a couple of patterns. 305 if (localeData.fullTimeFormat != null) { 306 // There are some full time format patterns in ICU that use the pattern character 'v'. 307 // Java doesn't accept this, so we replace it with 'z' which has about the same result 308 // as 'v', the timezone name. 309 // 'v' -> "PT", 'z' -> "PST", v is the generic timezone and z the standard tz 310 // "vvvv" -> "Pacific Time", "zzzz" -> "Pacific Standard Time" 311 localeData.fullTimeFormat = localeData.fullTimeFormat.replace('v', 'z'); 312 } 313 if (localeData.numberPattern != null) { 314 // The number pattern might contain positive and negative subpatterns. Arabic, for 315 // example, might look like "#,##0.###;#,##0.###-" because the minus sign should be 316 // written last. Macedonian supposedly looks something like "#,##0.###;(#,##0.###)". 317 // (The negative subpattern is optional, though, and not present in most locales.) 318 // By only swallowing '#'es and ','s after the '.', we ensure that we don't 319 // accidentally eat too much. 320 localeData.integerPattern = localeData.numberPattern.replaceAll("\\.[#,]*", ""); 321 } 322 return localeData; 323 } 324 325 // Libcore localizes pattern separator while ICU doesn't. http://b/112080617 initializePatternSeparator(LocaleData localeData, Locale locale)326 private static void initializePatternSeparator(LocaleData localeData, Locale locale) { 327 ULocale uLocale = ULocale.forLocale(locale); 328 NumberingSystem ns = NumberingSystem.getInstance(uLocale); 329 // A numbering system could be numeric or algorithmic. DecimalFormat can only use 330 // a numeric and decimal-based (radix == 10) system. Fallback to a Latin, a known numeric 331 // and decimal-based if the default numbering system isn't. All locales should have data 332 // for Latin numbering system after locale data fallback. See Numbering system section 333 // in Unicode Technical Standard #35 for more details. 334 if (ns == null || ns.getRadix() != 10 || ns.isAlgorithmic()) { 335 ns = NumberingSystem.LATIN; 336 } 337 String patternSeparator = 338 DecimalFormatSymbolsBridge.getLocalizedPatternSeparator(uLocale, ns); 339 340 if (patternSeparator == null || patternSeparator.isEmpty()) { 341 patternSeparator = ";"; 342 } 343 344 // Pattern separator in libcore supports single java character only. 345 localeData.patternSeparator = patternSeparator.charAt(0); 346 } 347 initializeDateFormatData(Locale locale)348 private void initializeDateFormatData(Locale locale) { 349 DateFormatSymbols dfs = new DateFormatSymbols(GregorianCalendar.class, locale); 350 351 longMonthNames = dfs.getMonths(DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE); 352 shortMonthNames = dfs.getMonths(DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED); 353 tinyMonthNames = dfs.getMonths(DateFormatSymbols.FORMAT, DateFormatSymbols.NARROW); 354 longWeekdayNames = dfs.getWeekdays(DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE); 355 shortWeekdayNames = dfs 356 .getWeekdays(DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED); 357 tinyWeekdayNames = dfs.getWeekdays(DateFormatSymbols.FORMAT, DateFormatSymbols.NARROW); 358 359 longStandAloneMonthNames = dfs 360 .getMonths(DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE); 361 shortStandAloneMonthNames = dfs 362 .getMonths(DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED); 363 tinyStandAloneMonthNames = dfs 364 .getMonths(DateFormatSymbols.STANDALONE, DateFormatSymbols.NARROW); 365 longStandAloneWeekdayNames = dfs 366 .getWeekdays(DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE); 367 shortStandAloneWeekdayNames = dfs 368 .getWeekdays(DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED); 369 tinyStandAloneWeekdayNames = dfs 370 .getWeekdays(DateFormatSymbols.STANDALONE, DateFormatSymbols.NARROW); 371 372 amPm = dfs.getAmPmStrings(); 373 eras = dfs.getEras(); 374 375 } 376 initializeDecimalFormatData(Locale locale)377 private void initializeDecimalFormatData(Locale locale) { 378 DecimalFormatSymbols dfs = DecimalFormatSymbols.getInstance(locale); 379 380 decimalSeparator = dfs.getDecimalSeparator(); 381 groupingSeparator = dfs.getGroupingSeparator(); 382 patternSeparator = dfs.getPatternSeparator(); 383 percent = dfs.getPercentString(); 384 perMill = dfs.getPerMillString(); 385 monetarySeparator = dfs.getMonetaryDecimalSeparator(); 386 minusSign = dfs.getMinusSignString(); 387 exponentSeparator = dfs.getExponentSeparator(); 388 infinity = dfs.getInfinity(); 389 NaN = dfs.getNaN(); 390 zeroDigit = dfs.getZeroDigit(); 391 392 DecimalFormat df = (DecimalFormat) NumberFormat 393 .getInstance(locale, NumberFormat.NUMBERSTYLE); 394 numberPattern = df.toPattern(); 395 396 df = (DecimalFormat) NumberFormat.getInstance(locale, NumberFormat.CURRENCYSTYLE); 397 currencyPattern = df.toPattern(); 398 399 df = (DecimalFormat) NumberFormat.getInstance(locale, NumberFormat.PERCENTSTYLE); 400 percentPattern = df.toPattern(); 401 402 } 403 initializeCalendarData(Locale locale)404 private void initializeCalendarData(Locale locale) { 405 Calendar calendar = Calendar.getInstance(locale); 406 407 firstDayOfWeek = calendar.getFirstDayOfWeek(); 408 minimalDaysInFirstWeek = calendar.getMinimalDaysInFirstWeek(); 409 } 410 initializeDateTimePatterns(Locale locale)411 private void initializeDateTimePatterns(Locale locale) { 412 ULocale uLocale = ULocale.forLocale(locale); 413 String calType = "gregorian"; 414 415 fullTimeFormat = Calendar.getDateTimeFormatString(uLocale, calType, 416 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.FULL); 417 longTimeFormat = Calendar.getDateTimeFormatString(uLocale, calType, 418 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.LONG); 419 mediumTimeFormat = Calendar.getDateTimeFormatString(uLocale, calType, 420 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat. MEDIUM); 421 shortTimeFormat = Calendar.getDateTimeFormatString(uLocale, calType, 422 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.SHORT); 423 fullDateFormat = Calendar.getDateTimeFormatString(uLocale, calType, 424 android.icu.text.DateFormat.FULL, android.icu.text.DateFormat.NONE); 425 longDateFormat = Calendar.getDateTimeFormatString(uLocale, calType, 426 android.icu.text.DateFormat.LONG, android.icu.text.DateFormat.NONE); 427 mediumDateFormat = Calendar.getDateTimeFormatString(uLocale, calType, 428 android.icu.text.DateFormat.MEDIUM, android.icu.text.DateFormat.NONE); 429 shortDateFormat = Calendar.getDateTimeFormatString(uLocale, calType, 430 android.icu.text.DateFormat.SHORT, android.icu.text.DateFormat.NONE); 431 } 432 } 433