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.i18n
18 
19 import android.content.Context
20 import android.icu.text.SimpleDateFormat
21 import android.os.Build
22 import android.provider.Settings
23 import android.text.format.DateFormat
24 import androidx.annotation.RequiresApi
25 import androidx.core.i18n.LocaleCompatUtils.getDefaultFormattingLocale
26 import java.util.Calendar
27 import java.util.Date
28 import java.util.Locale
29 
30 /**
31  * DateTimeFormatter is a class for international-aware date/time formatting.
32  *
33  * It is designed to encourage best i18n practices, and work correctly on old / new Android
34  * versions, without having to test the API level everywhere.
35  *
36  * @param context the application context.
37  * @param options various options for the formatter (what fields should be rendered, length, etc.).
38  * @param locale the locale used for formatting. If missing then the application locale will be
39  *   used.
40  */
41 public class DateTimeFormatter {
42 
43     private val dateFormatter: IDateTimeFormatterImpl
44 
45     @JvmOverloads
46     public constructor(
47         context: Context,
48         options: DateTimeFormatterSkeletonOptions,
49         locale: Locale = getDefaultFormattingLocale()
50     ) {
51         val resolvedSkeleton = skeletonRespectingPrefs(context, locale, options.toString())
52         dateFormatter =
53             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
54                 DateTimeFormatterImplIcu(resolvedSkeleton, locale)
55             } else {
56                 DateTimeFormatterImplAndroid(resolvedSkeleton, locale)
57             }
58     }
59 
60     @JvmOverloads
61     public constructor(
62         options: DateTimeFormatterJdkStyleOptions,
63         locale: Locale = getDefaultFormattingLocale()
64     ) {
65         dateFormatter = DateTimeFormatterImplJdkStyle(options.dateStyle, options.timeStyle, locale)
66     }
67 
skeletonRespectingPrefsnull68     private fun skeletonRespectingPrefs(context: Context, locale: Locale, options: String): String {
69         var strSkeleton =
70             if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) {
71                 options
72             } else {
73                 // "B" is not supported on older Android
74                 // Mapping skeleton to pattern will add an `a` for period, if needed
75                 options.replace("B", "")
76             }
77 
78         if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
79             // "v" is not supported on older Android
80             // If the string comes from Skeleton it is not possible to have
81             // both 'v' and 'z'. But just in case.
82             if (strSkeleton.contains('z')) {
83                 // Keep the "z", remove the "v"
84                 strSkeleton = strSkeleton.replace("v", "")
85             } else {
86                 strSkeleton = strSkeleton.replace('v', 'z')
87                 strSkeleton = strSkeleton.replace('O', 'z')
88             }
89         }
90 
91         if (!strSkeleton.contains('j')) {
92             // No hour, the caller forced the hour to 12h or 24h ("h" or "H")
93             return strSkeleton
94         }
95 
96         // The caller does not force 12h/24h, look at the Android user prefs.
97         val deviceHour =
98             Settings.System.getString(context.contentResolver, Settings.System.TIME_12_24)
99 
100         return when (deviceHour) {
101             "12" -> strSkeleton.replace('j', 'h')
102             "24" -> strSkeleton.replace('j', 'H')
103             else ->
104                 if (is24HourLocale(locale)) {
105                     // The locale is a 24h locale, the time period ( `a` or `B` ) does not matter
106                     strSkeleton
107                 } else {
108                     strSkeleton.replace("a", "")
109                     strSkeleton.replace('j', 'h')
110                 }
111         }
112     }
113 
is24HourLocalenull114     private fun is24HourLocale(locale: Locale): Boolean {
115         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
116             return Api24Utils.is24HourLocale(locale)
117         }
118         val tempPattern = DateFormat.getBestDateTimePattern(locale, "jm")
119         return tempPattern.contains('H')
120     }
121 
122     /**
123      * Formats an epoch time into a user friendly, locale aware, date/time string.
124      *
125      * @param milliseconds the date / time to format expressed in milliseconds since January 1,
126      *   1970, 00:00:00 GMT.
127      * @return the formatted date / time string.
128      */
formatnull129     public fun format(milliseconds: Long): String {
130         val calendar = Calendar.getInstance()
131         calendar.timeInMillis = milliseconds
132         return format(calendar)
133     }
134 
135     /**
136      * Formats a Date object into a user friendly locale aware date/time string.
137      *
138      * @param date the date / time to format.
139      * @return the formatted date / time string.
140      */
formatnull141     public fun format(date: Date): String {
142         val calendar = Calendar.getInstance()
143         calendar.time = date
144         return format(calendar)
145     }
146 
147     /**
148      * Formats a Calendar object into a user friendly locale aware date/time string.
149      *
150      * @param calendar the date / time to format.
151      * @return the formatted date / time string.
152      */
formatnull153     public fun format(calendar: Calendar): String {
154         return dateFormatter.format(calendar)
155     }
156 
157     private companion object {
158         // To avoid ClassVerificationFailure
159         @RequiresApi(Build.VERSION_CODES.N)
160         private class Api24Utils {
161             companion object {
is24HourLocalenull162                 fun is24HourLocale(locale: Locale): Boolean {
163                     val tmpDf = android.icu.text.DateFormat.getInstanceForSkeleton("jm", locale)
164                     val tmpPattern =
165                         // This is true for all ICU implementation until now, but just in case
166                         if (tmpDf is SimpleDateFormat) {
167                             tmpDf.toPattern()
168                         } else {
169                             DateFormat.getBestDateTimePattern(locale, "jm")
170                         }
171                     return tmpPattern.contains('H')
172                 }
173             }
174         }
175     }
176 }
177