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.icu.text.DateFormatSymbols; 23 import android.icu.util.Calendar; 24 import android.icu.util.GregorianCalendar; 25 26 import dalvik.system.VMRuntime; 27 import sun.util.locale.provider.CalendarDataUtility; 28 29 import java.util.HashMap; 30 import java.util.Locale; 31 import libcore.util.Objects; 32 33 /** 34 * Passes locale-specific from ICU native code to Java. 35 * <p> 36 * Note that you share these; you must not alter any of the fields, nor their array elements 37 * in the case of arrays. If you ever expose any of these things to user code, you must give 38 * them a clone rather than the original. 39 * @hide 40 */ 41 public final class LocaleData { 42 43 /** 44 * @see #USE_REAL_ROOT_LOCALE 45 */ 46 private static final Locale LOCALE_EN_US_POSIX = new Locale("en", "US", "POSIX"); 47 48 49 // In Android Q or before, when this class tries to load {@link Locale#ROOT} data, en_US_POSIX 50 // locale data is incorrectly loaded due to a bug b/159514442 (public bug b/159047832). 51 // 52 // This class used to pass "und" string as BCP47 language tag to our jni code, which then 53 // passes the string as as ICU Locale ID to ICU4C. ICU4C 63 or older version doesn't recognize 54 // "und" as a valid locale id, and fallback the default locale. The default locale is 55 // normally selected in the Locale picker in the Settings app by the user and set via 56 // frameworks. But this class statically cached the ROOT locale data before the 57 // default locale being set by framework, and without initialization, ICU4C uses en_US_POSIX 58 // as default locale. Thus, in Q or before, en_US_POSIX data is loaded. 59 // 60 // ICU version 64.1 resolved inconsistent behavior of 61 // "root", "und" and "" (empty) Locale ID which libcore previously relied on, and they are 62 // recognized correctly as {@link Locale#ROOT} since Android R. This ChangeId gated the change, 63 // and fallback to the old behavior by checking targetSdkVersion version. 64 // 65 // The below javadoc is shown in http://developer.android.com for consumption by app developers. 66 /** 67 * Since Android 11, formatter classes, e.g. java.text.SimpleDateFormat, no longer 68 * provide English data when Locale.ROOT format is requested. Please use 69 * Locale.ENGLISH to format in English. 70 * 71 * Note that Locale.ROOT is used as language/country neutral locale or fallback locale, 72 * and does not guarantee to represent English locale. 73 * 74 * This flag is only for documentation and can't be overridden by app. Please use 75 * {@code targetSdkVersion} to enable the new behavior. 76 */ 77 @ChangeId 78 @EnabledAfter(targetSdkVersion=29 /* Android Q */) 79 public static final long USE_REAL_ROOT_LOCALE = 159047832L; 80 81 // TODO(http://b/217881004): Replace this with a LRU cache. 82 // A cache for the locale-specific data. 83 private static final HashMap<String, LocaleData> localeDataCache = new HashMap<String, LocaleData>(); 84 static { 85 // Ensure that we pull in the locale data for the root locale, en_US, and the 86 // user's default locale. (All devices must support the root locale and en_US, 87 // and they're used for various system things like HTTP headers.) Pre-populating 88 // the cache is especially useful on Android because we'll share this via the Zygote. 89 get(Locale.ROOT); 90 get(Locale.US); Locale.getDefault()91 get(Locale.getDefault()); 92 } 93 94 // Used by Calendar. 95 @UnsupportedAppUsage 96 public Integer firstDayOfWeek; 97 @UnsupportedAppUsage 98 public Integer minimalDaysInFirstWeek; 99 100 // Used by DateFormatSymbols. 101 public String[] amPm; // "AM", "PM". 102 public String[] eras; // "BC", "AD". 103 104 public String[] longMonthNames; // "January", ... 105 @UnsupportedAppUsage 106 public String[] shortMonthNames; // "Jan", ... 107 public String[] tinyMonthNames; // "J", ... 108 public String[] longStandAloneMonthNames; // "January", ... 109 @UnsupportedAppUsage 110 public String[] shortStandAloneMonthNames; // "Jan", ... 111 public String[] tinyStandAloneMonthNames; // "J", ... 112 113 public String[] longWeekdayNames; // "Sunday", ... 114 public String[] shortWeekdayNames; // "Sun", ... 115 public String[] tinyWeekdayNames; // "S", ... 116 @UnsupportedAppUsage 117 public String[] longStandAloneWeekdayNames; // "Sunday", ... 118 @UnsupportedAppUsage 119 public String[] shortStandAloneWeekdayNames; // "Sun", ... 120 public String[] tinyStandAloneWeekdayNames; // "S", ... 121 122 // zeroDigit, today and tomorrow is only kept for @UnsupportedAppUsage. 123 // Their value is hard-coded, not localized. 124 @UnsupportedAppUsage 125 public String today; // "Today". 126 @UnsupportedAppUsage 127 public String tomorrow; // "Tomorrow". 128 @UnsupportedAppUsage 129 public char zeroDigit; // '0' 130 131 // timeFormat_hm and timeFormat_Hm are only kept for @UnsupportedAppUsage. 132 // Their value is hard-coded, not localized. 133 @UnsupportedAppUsage 134 public String timeFormat_hm; 135 @UnsupportedAppUsage 136 public String timeFormat_Hm; 137 LocaleData()138 private LocaleData() { 139 today = "Today"; 140 tomorrow = "Tomorrow"; 141 zeroDigit = '0'; 142 timeFormat_hm = "h:mm a"; 143 timeFormat_Hm = "HH:mm"; 144 } 145 146 @UnsupportedAppUsage mapInvalidAndNullLocales(Locale locale)147 public static Locale mapInvalidAndNullLocales(Locale locale) { 148 if (locale == null) { 149 return Locale.getDefault(); 150 } 151 152 if ("und".equals(locale.toLanguageTag())) { 153 return Locale.ROOT; 154 } 155 156 return locale; 157 } 158 159 /** 160 * Normally, this utility function is used by secondary cache above {@link LocaleData}, 161 * because the cache needs a correct key. 162 * @see #USE_REAL_ROOT_LOCALE 163 * @return a compatible locale for the bug b/159514442 164 */ getCompatibleLocaleForBug159514442(Locale locale)165 public static Locale getCompatibleLocaleForBug159514442(Locale locale) { 166 if (Locale.ROOT.equals(locale)) { 167 int targetSdkVersion = VMRuntime.getRuntime().getTargetSdkVersion(); 168 // Don't use Compatibility.isChangeEnabled(USE_REAL_ROOT_LOCALE) because the app compat 169 // framework lives in libcore and can depend on this class via various format methods, 170 // e.g. String.format(). See b/160912695. 171 if (targetSdkVersion <= 29 /* Android Q */) { 172 locale = LOCALE_EN_US_POSIX; 173 } 174 } 175 return locale; 176 } 177 178 /** 179 * Returns a shared LocaleData for the given locale. 180 */ 181 @UnsupportedAppUsage get(Locale locale)182 public static LocaleData get(Locale locale) { 183 if (locale == null) { 184 throw new NullPointerException("locale == null"); 185 } 186 187 locale = getCompatibleLocaleForBug159514442(locale); 188 189 final String languageTag = locale.toLanguageTag(); 190 synchronized (localeDataCache) { 191 LocaleData localeData = localeDataCache.get(languageTag); 192 if (localeData != null) { 193 return localeData; 194 } 195 } 196 LocaleData newLocaleData = initLocaleData(locale); 197 synchronized (localeDataCache) { 198 LocaleData localeData = localeDataCache.get(languageTag); 199 if (localeData != null) { 200 return localeData; 201 } 202 localeDataCache.put(languageTag, newLocaleData); 203 return newLocaleData; 204 } 205 } 206 toString()207 @Override public String toString() { 208 return Objects.toString(this); 209 } 210 211 /* 212 * This method is made public for testing 213 */ initLocaleData(Locale locale)214 public static LocaleData initLocaleData(Locale locale) { 215 LocaleData localeData = new LocaleData(); 216 217 localeData.initializeDateFormatData(locale); 218 localeData.initializeCalendarData(locale); 219 220 return localeData; 221 } 222 initializeDateFormatData(Locale locale)223 private void initializeDateFormatData(Locale locale) { 224 DateFormatSymbols dfs = new DateFormatSymbols(GregorianCalendar.class, locale); 225 226 longMonthNames = dfs.getMonths(DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE); 227 shortMonthNames = dfs.getMonths(DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED); 228 tinyMonthNames = dfs.getMonths(DateFormatSymbols.FORMAT, DateFormatSymbols.NARROW); 229 longWeekdayNames = dfs.getWeekdays(DateFormatSymbols.FORMAT, DateFormatSymbols.WIDE); 230 shortWeekdayNames = dfs 231 .getWeekdays(DateFormatSymbols.FORMAT, DateFormatSymbols.ABBREVIATED); 232 tinyWeekdayNames = dfs.getWeekdays(DateFormatSymbols.FORMAT, DateFormatSymbols.NARROW); 233 234 longStandAloneMonthNames = dfs 235 .getMonths(DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE); 236 shortStandAloneMonthNames = dfs 237 .getMonths(DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED); 238 tinyStandAloneMonthNames = dfs 239 .getMonths(DateFormatSymbols.STANDALONE, DateFormatSymbols.NARROW); 240 longStandAloneWeekdayNames = dfs 241 .getWeekdays(DateFormatSymbols.STANDALONE, DateFormatSymbols.WIDE); 242 shortStandAloneWeekdayNames = dfs 243 .getWeekdays(DateFormatSymbols.STANDALONE, DateFormatSymbols.ABBREVIATED); 244 tinyStandAloneWeekdayNames = dfs 245 .getWeekdays(DateFormatSymbols.STANDALONE, DateFormatSymbols.NARROW); 246 247 amPm = dfs.getAmPmStrings(); 248 eras = dfs.getEras(); 249 250 } 251 initializeCalendarData(Locale locale)252 private void initializeCalendarData(Locale locale) { 253 Calendar calendar = Calendar.getInstance(locale); 254 255 firstDayOfWeek = CalendarDataUtility.retrieveFirstDayOfWeek(locale, 256 calendar.getFirstDayOfWeek()); 257 minimalDaysInFirstWeek = calendar.getMinimalDaysInFirstWeek(); 258 } 259 } 260