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