• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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