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