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