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