1 /*
2  * Copyright (C) 2022 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 androidx.core.text.util;
18 
19 import android.icu.number.LocalizedNumberFormatter;
20 import android.icu.number.NumberFormatter;
21 import android.icu.text.DateFormat;
22 import android.icu.text.DateTimePatternGenerator;
23 import android.icu.util.MeasureUnit;
24 import android.os.Build;
25 import android.os.Build.VERSION_CODES;
26 
27 import androidx.annotation.RequiresApi;
28 import androidx.annotation.RestrictTo;
29 import androidx.annotation.StringDef;
30 
31 import org.jspecify.annotations.NonNull;
32 
33 import java.lang.annotation.Retention;
34 import java.lang.annotation.RetentionPolicy;
35 import java.util.Arrays;
36 import java.util.Locale;
37 import java.util.Locale.Category;
38 
39 /**
40  * Provides friendly APIs to get the user's locale preferences. The data can refer to
41  * external/cldr/common/main/en.xml.
42  */
43 @RequiresApi(VERSION_CODES.LOLLIPOP)
44 public final class LocalePreferences {
45     private static final String TAG = LocalePreferences.class.getSimpleName();
46 
47     /** APIs to get the user's preference of the hour cycle. */
48     public static class HourCycle {
49         private static final String U_EXTENSION_TAG = "hc";
50 
51         /** 12 Hour System (0-11) */
52         public static final String H11 = "h11";
53         /** 12 Hour System (1-12) */
54         public static final String H12 = "h12";
55         /** 24 Hour System (0-23) */
56         public static final String H23 = "h23";
57         /** 24 Hour System (1-24) */
58         public static final String H24 = "h24";
59         /** Default hour cycle for the locale */
60         public static final String DEFAULT = "";
61 
62         @RestrictTo(RestrictTo.Scope.LIBRARY)
63         @StringDef({
64                 H11,
65                 H12,
66                 H23,
67                 H24,
68                 DEFAULT
69         })
70         @Retention(RetentionPolicy.SOURCE)
71         public @interface HourCycleTypes {
72         }
73 
HourCycle()74         private HourCycle() {
75         }
76     }
77 
78     /**
79      * Return the user's preference of the hour cycle which is from
80      * {@link Locale#getDefault(Locale.Category)}. The returned result is resolved and
81      * bases on the {@code Locale#getDefault(Locale.Category)}. It is one of the strings defined in
82      * {@see HourCycle}, e.g. {@code HourCycle#H11}.
83      */
84     @HourCycle.HourCycleTypes
getHourCycle()85     public static @NonNull String getHourCycle() {
86         return getHourCycle(true);
87     }
88 
89     /**
90      * Return the hour cycle setting of the inputted {@link Locale}. The returned result is resolved
91      * and based on the input {@code Locale}. It is one of the strings defined in
92      * {@see HourCycle}, e.g. {@code HourCycle#H11}.
93      */
94     @HourCycle.HourCycleTypes
getHourCycle(@onNull Locale locale)95     public static @NonNull String getHourCycle(@NonNull Locale locale) {
96         return getHourCycle(locale, true);
97     }
98 
99     /**
100      * Return the user's preference of the hour cycle which is from
101      * {@link Locale#getDefault(Locale.Category)}, e.g. {@code HourCycle#H11}.
102      *
103      * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains hour cycle subtag,
104      *                 this argument is ignored. If the
105      *                 {@code Locale#getDefault(Locale.Category)} doesn't contain hour cycle subtag
106      *                 and the resolved argument is true, this function tries to find the default
107      *                 hour cycle for the {@code Locale#getDefault(Locale.Category)}. If the
108      *                 {@code Locale#getDefault(Locale.Category)} doesn't contain hour cycle subtag
109      *                 and the resolved argument is false, this function returns empty string
110      *                 , i.e. {@code HourCycle#DEFAULT}.
111      * @return {@link HourCycle.HourCycleTypes} If the malformed hour cycle format was specified
112      * in the hour cycle subtag, e.g. en-US-u-hc-h32, this function returns empty string, i.e.
113      * {@code HourCycle#DEFAULT}.
114      */
115     @HourCycle.HourCycleTypes
getHourCycle( boolean resolved)116     public static @NonNull String getHourCycle(
117             boolean resolved) {
118         Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
119                 ? Api24Impl.getDefaultLocale()
120                 : getDefaultLocale();
121         return getHourCycle(defaultLocale, resolved);
122     }
123 
124     /**
125      * Return the hour cycle setting of the inputted {@link Locale}. E.g. "en-US-u-hc-h23".
126      *
127      * @param locale   The {@code Locale} to get the hour cycle.
128      * @param resolved If the given {@code Locale} contains hour cycle subtag, this argument is
129      *                 ignored. If the given {@code Locale} doesn't contain hour cycle subtag and
130      *                 the resolved argument is true, this function tries to find the default
131      *                 hour cycle for the given {@code Locale}. If the given {@code Locale} doesn't
132      *                 contain hour cycle subtag and the resolved argument is false, this function
133      *                 return empty string, i.e. {@code HourCycle#DEFAULT}.
134      * @return {@link HourCycle.HourCycleTypes} If the malformed hour cycle format was specified
135      * in the hour cycle subtag, e.g. en-US-u-hc-h32, this function returns empty string, i.e.
136      * {@code HourCycle#DEFAULT}.
137      */
138     @HourCycle.HourCycleTypes
getHourCycle(@onNull Locale locale, boolean resolved)139     public static @NonNull String getHourCycle(@NonNull Locale locale, boolean resolved) {
140         String result = getUnicodeLocaleType(HourCycle.U_EXTENSION_TAG,
141                 HourCycle.DEFAULT, locale, resolved);
142         if (result != null) {
143             return result;
144         }
145         if (Build.VERSION.SDK_INT >= 33) {
146             return Api33Impl.getHourCycle(locale);
147         } else {
148             return getBaseHourCycle(locale);
149         }
150     }
151 
152     /** APIs to get the user's preference of Calendar. */
153     public static class CalendarType {
154         private static final String U_EXTENSION_TAG = "ca";
155         /** Chinese Calendar */
156         public static final String CHINESE = "chinese";
157         /** Dangi Calendar (Korea Calendar) */
158         public static final String DANGI = "dangi";
159         /** Gregorian Calendar */
160         public static final String GREGORIAN = "gregorian";
161         /** Hebrew Calendar */
162         public static final String HEBREW = "hebrew";
163         /** Indian National Calendar */
164         public static final String INDIAN = "indian";
165         /** Islamic Calendar */
166         public static final String ISLAMIC = "islamic";
167         /** Islamic Calendar (tabular, civil epoch) */
168         public static final String ISLAMIC_CIVIL = "islamic-civil";
169         /** Islamic Calendar (Saudi Arabia, sighting) */
170         public static final String ISLAMIC_RGSA = "islamic-rgsa";
171         /** Islamic Calendar (tabular, astronomical epoch) */
172         public static final String ISLAMIC_TBLA = "islamic-tbla";
173         /** Islamic Calendar (Umm al-Qura) */
174         public static final String ISLAMIC_UMALQURA = "islamic-umalqura";
175         /** Persian Calendar */
176         public static final String PERSIAN = "persian";
177         /** Default calendar for the locale */
178         public static final String DEFAULT = "";
179 
180         @RestrictTo(RestrictTo.Scope.LIBRARY)
181         @StringDef({
182                 CHINESE,
183                 DANGI,
184                 GREGORIAN,
185                 HEBREW,
186                 INDIAN,
187                 ISLAMIC,
188                 ISLAMIC_CIVIL,
189                 ISLAMIC_RGSA,
190                 ISLAMIC_TBLA,
191                 ISLAMIC_UMALQURA,
192                 PERSIAN,
193                 DEFAULT
194         })
195         @Retention(RetentionPolicy.SOURCE)
196         public @interface CalendarTypes {
197         }
198 
CalendarType()199         private CalendarType() {
200         }
201     }
202 
203     /**
204      * Return the user's preference of the calendar type which is from {@link
205      * Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on
206      * the {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
207      * {@see CalendarType}, e.g. {@code CalendarType#CHINESE}.
208      */
209     @CalendarType.CalendarTypes
getCalendarType()210     public static @NonNull String getCalendarType() {
211         return getCalendarType(true);
212     }
213 
214     /**
215      * Return the calendar type of the inputted {@link Locale}. The returned result is resolved and
216      * based on the input {@link Locale} settings. It is one of the strings defined in
217      * {@see CalendarType}, e.g. {@code CalendarType#CHINESE}.
218      */
219     @CalendarType.CalendarTypes
getCalendarType(@onNull Locale locale)220     public static @NonNull String getCalendarType(@NonNull Locale locale) {
221         return getCalendarType(locale, true);
222     }
223 
224     /**
225      * Return the user's preference of the calendar type which is from {@link
226      * Locale#getDefault(Category)}, e.g. {@code CalendarType#CHINESE}.
227      *
228      * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains calendar type
229      *                 subtag, this argument is ignored. If the
230      *                 {@code Locale#getDefault(Locale.Category)} doesn't contain calendar type
231      *                 subtag and the resolved argument is true, this function tries to find
232      *                 the default calendar type for the
233      *                 {@code Locale#getDefault(Locale.Category)}. If the
234      *                 {@code Locale#getDefault(Locale.Category)} doesn't contain calendar type
235      *                 subtag and the resolved argument is false, this function returns empty string
236      *                 , i.e. {@code CalendarType#DEFAULT}.
237      * @return {@link CalendarType.CalendarTypes} If the malformed calendar type format was
238      * specified in the calendar type subtag, e.g. en-US-u-ca-calendar, this function returns
239      * empty string, i.e. {@code CalendarType#DEFAULT}.
240      */
241     @CalendarType.CalendarTypes
getCalendarType(boolean resolved)242     public static @NonNull String getCalendarType(boolean resolved) {
243         Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
244                 ? Api24Impl.getDefaultLocale()
245                 : getDefaultLocale();
246         return getCalendarType(defaultLocale, resolved);
247     }
248 
249     /**
250      * Return the calendar type of the inputted {@link Locale}, e.g. {@code CalendarType#CHINESE}.
251      *
252      * @param locale   The {@link Locale} to get the calendar type.
253      * @param resolved If the given {@code Locale} contains calendar type subtag, this argument is
254      *                 ignored. If the given {@code Locale} doesn't contain calendar type subtag and
255      *                 the resolved argument is true, this function tries to find the default
256      *                 calendar type for the given {@code Locale}. If the given {@code Locale}
257      *                 doesn't contain calendar type subtag and the resolved argument is false, this
258      *                 function return empty string, i.e. {@code CalendarType#DEFAULT}.
259      * @return {@link CalendarType.CalendarTypes} If the malformed calendar type format was
260      * specified in the calendar type subtag, e.g. en-US-u-ca-calendar, this function returns
261      * empty string, i.e. {@code CalendarType#DEFAULT}.
262      */
263     @CalendarType.CalendarTypes
getCalendarType(@onNull Locale locale, boolean resolved)264     public static @NonNull String getCalendarType(@NonNull Locale locale, boolean resolved) {
265         String result = getUnicodeLocaleType(CalendarType.U_EXTENSION_TAG,
266                 CalendarType.DEFAULT, locale, resolved);
267         if (result != null) {
268             return result;
269         }
270         if (Build.VERSION.SDK_INT >= VERSION_CODES.N) {
271             return Api24Impl.getCalendarType(locale);
272         } else {
273             return resolved ? CalendarType.GREGORIAN : CalendarType.DEFAULT;
274         }
275     }
276 
277     /** APIs to get the user's preference of temperature unit. */
278     public static class TemperatureUnit {
279         private static final String U_EXTENSION_TAG = "mu";
280         /** Celsius */
281         public static final String CELSIUS = "celsius";
282         /** Fahrenheit */
283         public static final String FAHRENHEIT = "fahrenhe";
284         /** Kelvin */
285         public static final String KELVIN = "kelvin";
286         /** Default Temperature for the locale */
287         public static final String DEFAULT = "";
288 
289         @RestrictTo(RestrictTo.Scope.LIBRARY)
290         @StringDef({
291                 CELSIUS,
292                 FAHRENHEIT,
293                 KELVIN,
294                 DEFAULT
295         })
296         @Retention(RetentionPolicy.SOURCE)
297         public @interface TemperatureUnits {
298         }
299 
TemperatureUnit()300         private TemperatureUnit() {
301         }
302     }
303 
304     /**
305      * Return the user's preference of the temperature unit which is from {@link
306      * Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on the
307      * {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
308      * {@see TemperatureUnit}, e.g. {@code TemperatureUnit#FAHRENHEIT}.
309      */
310     @TemperatureUnit.TemperatureUnits
getTemperatureUnit()311     public static @NonNull String getTemperatureUnit() {
312         return getTemperatureUnit(true);
313     }
314 
315     /**
316      * Return the temperature unit of the inputted {@link Locale}. It is one of the strings
317      * defined in {@see TemperatureUnit}, e.g. {@code TemperatureUnit#FAHRENHEIT}.
318      */
319     @TemperatureUnit.TemperatureUnits
getTemperatureUnit( @onNull Locale locale)320     public static @NonNull String getTemperatureUnit(
321             @NonNull Locale locale) {
322         return getTemperatureUnit(locale, true);
323     }
324 
325     /**
326      * Return the user's preference of the temperature unit which is from {@link
327      * Locale#getDefault(Locale.Category)}, e.g. {@code TemperatureUnit#FAHRENHEIT}.
328      *
329      * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains temperature unit
330      *                 subtag, this argument is ignored. If the
331      *                 {@code Locale#getDefault(Locale.Category)} doesn't contain temperature unit
332      *                 subtag and the resolved argument is true, this function tries to find
333      *                 the default temperature unit for the
334      *                 {@code Locale#getDefault(Locale.Category)}. If the
335      *                 {@code Locale#getDefault(Locale.Category)} doesn't contain temperature unit
336      *                 subtag and the resolved argument is false, this function returns empty string
337      *                 , i.e. {@code TemperatureUnit#DEFAULT}.
338      * @return {@link TemperatureUnit.TemperatureUnits} If the malformed temperature unit format was
339      * specified in the temperature unit subtag, e.g. en-US-u-mu-temperature, this function returns
340      * empty string, i.e. {@code TemperatureUnit#DEFAULT}.
341      */
342     @TemperatureUnit.TemperatureUnits
getTemperatureUnit(boolean resolved)343     public static @NonNull String getTemperatureUnit(boolean resolved) {
344         Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
345                 ? Api24Impl.getDefaultLocale()
346                 : getDefaultLocale();
347         return getTemperatureUnit(defaultLocale, resolved);
348     }
349 
350     /**
351      * Return the temperature unit of the inputted {@link Locale}. E.g. "fahrenheit"
352      *
353      * @param locale   The {@link Locale} to get the temperature unit.
354      * @param resolved If the given {@code Locale} contains temperature unit subtag, this argument
355      *                 is ignored. If the given {@code Locale} doesn't contain temperature unit
356      *                 subtag and the resolved argument is true, this function tries to find
357      *                 the default temperature unit for the given {@code Locale}. If the given
358      *                 {@code Locale} doesn't contain temperature unit subtag and the resolved
359      *                 argument is false, this function return empty string, i.e.
360      *                 {@code TemperatureUnit#DEFAULT}.
361      * @return {@link TemperatureUnit.TemperatureUnits} If the malformed temperature unit format was
362      * specified in the temperature unit subtag, e.g. en-US-u-mu-temperature, this function returns
363      * empty string, i.e. {@code TemperatureUnit#DEFAULT}.
364      */
365     @TemperatureUnit.TemperatureUnits
getTemperatureUnit(@onNull Locale locale, boolean resolved)366     public static @NonNull String getTemperatureUnit(@NonNull Locale locale, boolean resolved) {
367         String result = getUnicodeLocaleType(TemperatureUnit.U_EXTENSION_TAG,
368                 TemperatureUnit.DEFAULT, locale, resolved);
369         if (result != null) {
370             return result;
371         }
372         if (Build.VERSION.SDK_INT >= 33) {
373             return Api33Impl.getResolvedTemperatureUnit(locale);
374         } else {
375             return getTemperatureHardCoded(locale);
376         }
377     }
378 
379     /** APIs to get the user's preference of the first day of week. */
380     public static class FirstDayOfWeek {
381         private static final String U_EXTENSION_TAG = "fw";
382         /** Sunday */
383         public static final String SUNDAY = "sun";
384         /** Monday */
385         public static final String MONDAY = "mon";
386         /** Tuesday */
387         public static final String TUESDAY = "tue";
388         /** Wednesday */
389         public static final String WEDNESDAY = "wed";
390         /** Thursday */
391         public static final String THURSDAY = "thu";
392         /** Friday */
393         public static final String FRIDAY = "fri";
394         /** Saturday */
395         public static final String SATURDAY = "sat";
396         /** Default first day of week for the locale */
397         public static final String DEFAULT = "";
398 
399         @RestrictTo(RestrictTo.Scope.LIBRARY)
400         @StringDef({
401                 SUNDAY,
402                 MONDAY,
403                 TUESDAY,
404                 WEDNESDAY,
405                 THURSDAY,
406                 FRIDAY,
407                 SATURDAY,
408                 DEFAULT
409         })
410         @Retention(RetentionPolicy.SOURCE)
411         public @interface Days {
412         }
413 
FirstDayOfWeek()414         private FirstDayOfWeek() {
415         }
416     }
417 
418     /**
419      * Return the user's preference of the first day of week which is from
420      * {@link Locale#getDefault(Locale.Category)}. The returned result is resolved and bases on the
421      * {@code Locale#getDefault(Locale.Category)} settings. It is one of the strings defined in
422      * {@see FirstDayOfWeek}, e.g. {@code FirstDayOfWeek#SUNDAY}.
423      */
424     @FirstDayOfWeek.Days
getFirstDayOfWeek()425     public static @NonNull String getFirstDayOfWeek() {
426         return getFirstDayOfWeek(true);
427     }
428 
429     /**
430      * Return the first day of week of the inputted {@link Locale}. The returned result is resolved
431      * and based on the input {@code Locale} settings. It is one of the strings defined in
432      * {@see FirstDayOfWeek}, e.g. {@code FirstDayOfWeek#SUNDAY}.
433      */
434     @FirstDayOfWeek.Days
getFirstDayOfWeek(@onNull Locale locale)435     public static @NonNull String getFirstDayOfWeek(@NonNull Locale locale) {
436         return getFirstDayOfWeek(locale, true);
437     }
438 
439     /**
440      * Return the user's preference of the first day of week which is from {@link
441      * Locale#getDefault(Locale.Category)}, e.g. {@code FirstDayOfWeek#SUNDAY}.
442      *
443      * @param resolved If the {@code Locale#getDefault(Locale.Category)} contains first day of week
444      *                 subtag, this argument is ignored. If the
445      *                 {@code Locale#getDefault(Locale.Category)} doesn't contain first day of week
446      *                 subtag and the resolved argument is true, this function tries to find
447      *                 the default first day of week for the
448      *                 {@code Locale#getDefault(Locale.Category)}. If the
449      *                 {@code Locale#getDefault(Locale.Category)} doesn't contain first day of week
450      *                 subtag and the resolved argument is false, this function returns empty string
451      *                 , i.e. {@code FirstDayOfWeek#DEFAULT}.
452      * @return {@link FirstDayOfWeek.Days} If the malformed first day of week format was specified
453      * in the first day of week subtag, e.g. en-US-u-fw-days, this function returns empty string,
454      * i.e. {@code FirstDayOfWeek#DEFAULT}.
455      */
456     @FirstDayOfWeek.Days
getFirstDayOfWeek(boolean resolved)457     public static @NonNull String getFirstDayOfWeek(boolean resolved) {
458         Locale defaultLocale = (Build.VERSION.SDK_INT >= VERSION_CODES.N)
459                 ? Api24Impl.getDefaultLocale()
460                 : getDefaultLocale();
461         return getFirstDayOfWeek(defaultLocale, resolved);
462     }
463 
464     /**
465      * Return the first day of week of the inputted {@link Locale},
466      * e.g. {@code FirstDayOfWeek#SUNDAY}.
467      *
468      * @param locale   The {@link Locale} to get the first day of week.
469      * @param resolved If the given {@code Locale} contains first day of week subtag, this argument
470      *                 is ignored. If the given {@code Locale} doesn't contain first day of week
471      *                 subtag and the resolved argument is true, this function tries to find
472      *                 the default first day of week for the given {@code Locale}. If the given
473      *                 {@code Locale} doesn't contain first day of week subtag and the resolved
474      *                 argument is false, this function return empty string, i.e.
475      *                 {@code FirstDayOfWeek#DEFAULT}.
476      * @return {@link FirstDayOfWeek.Days} If the malformed first day of week format was
477      * specified in the first day of week subtag, e.g. en-US-u-fw-days, this function returns
478      * empty string, i.e. {@code FirstDayOfWeek#DEFAULT}.
479      */
480     @FirstDayOfWeek.Days
getFirstDayOfWeek( @onNull Locale locale, boolean resolved)481     public static @NonNull String getFirstDayOfWeek(
482             @NonNull Locale locale, boolean resolved) {
483         String result = getUnicodeLocaleType(FirstDayOfWeek.U_EXTENSION_TAG,
484                 FirstDayOfWeek.DEFAULT, locale, resolved);
485         return result != null ? result : getBaseFirstDayOfWeek(locale);
486     }
487 
getUnicodeLocaleType(String tag, String defaultValue, Locale locale, boolean resolved)488     private static String getUnicodeLocaleType(String tag, String defaultValue, Locale locale,
489             boolean resolved) {
490         String ext = locale.getUnicodeLocaleType(tag);
491         if (ext != null) {
492             return ext;
493         }
494         if (!resolved) {
495             return defaultValue;
496         }
497         return null;
498     }
499 
500 
501     // Warning: This list of country IDs must be in alphabetical order for binarySearch to
502     // work correctly.
503     private static final String[] WEATHER_FAHRENHEIT_COUNTRIES =
504             {"BS", "BZ", "KY", "PR", "PW", "US"};
505 
506     @TemperatureUnit.TemperatureUnits
getTemperatureHardCoded(Locale locale)507     private static String getTemperatureHardCoded(Locale locale) {
508         return Arrays.binarySearch(WEATHER_FAHRENHEIT_COUNTRIES, locale.getCountry()) >= 0
509                 ? TemperatureUnit.FAHRENHEIT
510                 : TemperatureUnit.CELSIUS;
511     }
512 
513     @HourCycle.HourCycleTypes
getBaseHourCycle(@onNull Locale locale)514     private static String getBaseHourCycle(@NonNull Locale locale) {
515         String pattern =
516                 android.text.format.DateFormat.getBestDateTimePattern(
517                         locale, "jm");
518         return pattern.contains("H") ? HourCycle.H23 : HourCycle.H12;
519     }
520 
521     @FirstDayOfWeek.Days
getBaseFirstDayOfWeek(@onNull Locale locale)522     private static String getBaseFirstDayOfWeek(@NonNull Locale locale) {
523         // A known bug affects both the {@code android.icu.util.Calendar} and
524         // {@code java.util.Calendar}: they ignore the "fw" field in the -u- extension, even if
525         // present. So please do not remove the explicit check on getUnicodeLocaleType,
526         // which protects us from that bug.
527         return getStringOfFirstDayOfWeek(
528                 java.util.Calendar.getInstance(locale).getFirstDayOfWeek());
529     }
530 
getStringOfFirstDayOfWeek(int fw)531     private static String getStringOfFirstDayOfWeek(int fw) {
532         String[] arrDays = {
533                 FirstDayOfWeek.SUNDAY,
534                 FirstDayOfWeek.MONDAY,
535                 FirstDayOfWeek.TUESDAY,
536                 FirstDayOfWeek.WEDNESDAY,
537                 FirstDayOfWeek.THURSDAY,
538                 FirstDayOfWeek.FRIDAY,
539                 FirstDayOfWeek.SATURDAY};
540         return fw >= 1 && fw <= 7 ? arrDays[fw - 1] : FirstDayOfWeek.DEFAULT;
541     }
542 
getDefaultLocale()543     private static Locale getDefaultLocale() {
544         return Locale.getDefault();
545     }
546 
547     @RequiresApi(VERSION_CODES.N)
548     private static class Api24Impl {
549         @CalendarType.CalendarTypes
getCalendarType(@onNull Locale locale)550         static String getCalendarType(@NonNull Locale locale) {
551             return android.icu.util.Calendar.getInstance(locale).getType();
552         }
553 
getDefaultLocale()554         static Locale getDefaultLocale() {
555             return Locale.getDefault(Category.FORMAT);
556         }
557 
Api24Impl()558         private Api24Impl() {
559         }
560     }
561 
562     @RequiresApi(VERSION_CODES.TIRAMISU)
563     private static class Api33Impl {
564         @TemperatureUnit.TemperatureUnits
getResolvedTemperatureUnit(@onNull Locale locale)565         static String getResolvedTemperatureUnit(@NonNull Locale locale) {
566             LocalizedNumberFormatter nf = NumberFormatter.with()
567                     .usage("weather")
568                     .unit(MeasureUnit.CELSIUS)
569                     .locale(locale);
570             String unit = nf.format(1).getOutputUnit().getIdentifier();
571             if (unit.startsWith(TemperatureUnit.FAHRENHEIT)) {
572                 return TemperatureUnit.FAHRENHEIT;
573             }
574             return unit;
575         }
576 
577         @HourCycle.HourCycleTypes
getHourCycle(@onNull Locale locale)578         static String getHourCycle(@NonNull Locale locale) {
579             return getHourCycleType(
580                     DateTimePatternGenerator.getInstance(locale).getDefaultHourCycle());
581         }
582 
583         @HourCycle.HourCycleTypes
getHourCycleType( DateFormat.HourCycle hourCycle)584         private static String getHourCycleType(
585                 DateFormat.HourCycle hourCycle) {
586             switch (hourCycle) {
587                 case HOUR_CYCLE_11:
588                     return HourCycle.H11;
589                 case HOUR_CYCLE_12:
590                     return HourCycle.H12;
591                 case HOUR_CYCLE_23:
592                     return HourCycle.H23;
593                 case HOUR_CYCLE_24:
594                     return HourCycle.H24;
595                 default:
596                     return HourCycle.DEFAULT;
597             }
598         }
599 
Api33Impl()600         private Api33Impl() {
601         }
602     }
603 
LocalePreferences()604     private LocalePreferences() {
605     }
606 }
607