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