• 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 
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