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 com.android.icu.util.ExtendedCalendar; 20 import java.text.DateFormat; 21 import java.text.SimpleDateFormat; 22 import java.util.Locale; 23 import java.util.Objects; 24 import java.util.concurrent.ConcurrentHashMap; 25 26 import sun.util.locale.LanguageTag; 27 28 /** 29 * Pattern cache for {@link SimpleDateFormat} 30 * 31 * @hide 32 */ 33 public class SimpleDateFormatData { 34 35 // TODO(http://b/217881004): Replace this with a LRU cache. 36 private static final ConcurrentHashMap<String, SimpleDateFormatData> CACHE = 37 new ConcurrentHashMap<>(/* initialCapacity */ 3); 38 39 40 private final Locale locale; 41 /** See {@link #isBug266731719Locale} for details */ 42 private final boolean usesAsciiSpace; 43 44 private final String fullTimeFormat; 45 private final String longTimeFormat; 46 private final String mediumTimeFormat; 47 private final String shortTimeFormat; 48 49 private final String fullDateFormat; 50 private final String longDateFormat; 51 private final String mediumDateFormat; 52 private final String shortDateFormat; 53 SimpleDateFormatData(Locale locale)54 private SimpleDateFormatData(Locale locale) { 55 this.locale = locale; 56 this.usesAsciiSpace = isBug266731719Locale(locale); 57 // libcore's java.text supports Gregorian calendar only. 58 ExtendedCalendar calendar = ICU.getExtendedCalendar(locale, "gregorian"); 59 60 String tmpFullTimeFormat = getDateTimePattern(calendar, 61 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.FULL); 62 63 // Fix up a couple of patterns. 64 if (tmpFullTimeFormat != null) { 65 // There are some full time format patterns in ICU that use the pattern character 'v'. 66 // Java doesn't accept this, so we replace it with 'z' which has about the same result 67 // as 'v', the timezone name. 68 // 'v' -> "PT", 'z' -> "PST", v is the generic timezone and z the standard tz 69 // "vvvv" -> "Pacific Time", "zzzz" -> "Pacific Standard Time" 70 tmpFullTimeFormat = tmpFullTimeFormat.replace('v', 'z'); 71 } 72 fullTimeFormat = tmpFullTimeFormat; 73 74 longTimeFormat = getDateTimePattern(calendar, 75 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.LONG); 76 mediumTimeFormat = getDateTimePattern(calendar, 77 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.MEDIUM); 78 shortTimeFormat = getDateTimePattern(calendar, 79 android.icu.text.DateFormat.NONE, android.icu.text.DateFormat.SHORT); 80 fullDateFormat = getDateTimePattern(calendar, 81 android.icu.text.DateFormat.FULL, android.icu.text.DateFormat.NONE); 82 longDateFormat = getDateTimePattern(calendar, 83 android.icu.text.DateFormat.LONG, android.icu.text.DateFormat.NONE); 84 mediumDateFormat = getDateTimePattern(calendar, 85 android.icu.text.DateFormat.MEDIUM, android.icu.text.DateFormat.NONE); 86 shortDateFormat = getDateTimePattern(calendar, 87 android.icu.text.DateFormat.SHORT, android.icu.text.DateFormat.NONE); 88 } 89 90 /** 91 * Returns an instance. 92 * 93 * @param locale can't be null 94 * @throws NullPointerException if {@code locale} is null 95 * @return a {@link SimpleDateFormatData} instance 96 */ getInstance(Locale locale)97 public static SimpleDateFormatData getInstance(Locale locale) { 98 Objects.requireNonNull(locale, "locale can't be null"); 99 100 locale = LocaleData.getCompatibleLocaleForBug159514442(locale); 101 102 final String languageTag = locale.toLanguageTag(); 103 104 SimpleDateFormatData data = CACHE.get(languageTag); 105 if (data != null) { 106 return data; 107 } 108 109 data = new SimpleDateFormatData(locale); 110 SimpleDateFormatData prev = CACHE.putIfAbsent(languageTag, data); 111 if (prev != null) { 112 return prev; 113 } 114 return data; 115 } 116 117 /** 118 * Ensure that we pull in the locale data for the root locale, en_US, and the user's default 119 * locale. All devices must support the root locale and en_US, and they're used for various 120 * system things. Pre-populating the cache is especially useful on Android because 121 * we'll share this via the Zygote. 122 */ initializeCacheInZygote()123 public static void initializeCacheInZygote() { 124 getInstance(Locale.ROOT); 125 getInstance(Locale.US); 126 getInstance(Locale.getDefault()); 127 } 128 129 /** 130 * @throws AssertionError if style is not one of the 4 styles specified in {@link DateFormat} 131 * @return a date pattern string 132 */ getDateFormat(int style)133 public String getDateFormat(int style) { 134 switch (style) { 135 case DateFormat.SHORT: 136 return shortDateFormat; 137 case DateFormat.MEDIUM: 138 return mediumDateFormat; 139 case DateFormat.LONG: 140 return longDateFormat; 141 case DateFormat.FULL: 142 return fullDateFormat; 143 } 144 // TODO: fix this legacy behavior of throwing AssertionError introduced in 145 // the commit 6ca85c4. 146 throw new AssertionError(); 147 } 148 149 /** 150 * @throws AssertionError if style is not one of the 4 styles specified in {@link DateFormat} 151 * @return a time pattern string 152 */ getTimeFormat(int style)153 public String getTimeFormat(int style) { 154 // Do not cache ICU.getTimePattern() return value in the LocaleData instance 155 // because most users do not enable this setting, hurts performance in critical path, 156 // e.g. b/161846393, and ICU.getBestDateTimePattern will cache it in ICU.CACHED_PATTERNS 157 // on demand. 158 switch (style) { 159 case DateFormat.SHORT: 160 if (DateFormat.is24Hour == null) { 161 return shortTimeFormat; 162 } else { 163 return getTimePattern(DateFormat.is24Hour, false); 164 } 165 case DateFormat.MEDIUM: 166 if (DateFormat.is24Hour == null) { 167 return mediumTimeFormat; 168 } else { 169 return getTimePattern(DateFormat.is24Hour, true); 170 } 171 case DateFormat.LONG: 172 // CLDR doesn't really have anything we can use to obey the 12-/24-hour preference. 173 return longTimeFormat; 174 case DateFormat.FULL: 175 // CLDR doesn't really have anything we can use to obey the 12-/24-hour preference. 176 return fullTimeFormat; 177 } 178 // TODO: fix this legacy behavior of throwing AssertionError introduced in 179 // the commit 6ca85c4. 180 throw new AssertionError(); 181 } 182 183 /** 184 * Returns the date and/or time pattern. 185 * 186 * @param dateStyle {@link android.icu.text.DateFormat} date style 187 * @param timeStyle {@link android.icu.text.DateFormat} time style 188 */ getDateTimePattern(ExtendedCalendar calendar, int dateStyle, int timeStyle)189 private String getDateTimePattern(ExtendedCalendar calendar, int dateStyle, int timeStyle) { 190 String pattern = ICU.transformIcuDateTimePattern_forJavaText( 191 calendar.getDateTimePattern(dateStyle, timeStyle)); 192 193 return postProcessPattern(pattern); 194 } 195 getTimePattern(boolean is24Hour, boolean withSecond)196 private String getTimePattern(boolean is24Hour, boolean withSecond) { 197 String pattern = ICU.getTimePattern(locale, is24Hour, withSecond); 198 199 return postProcessPattern(pattern); 200 } 201 postProcessPattern(String pattern)202 private String postProcessPattern(String pattern) { 203 if (pattern == null || !usesAsciiSpace) { 204 return pattern; 205 } 206 207 return pattern.replace('\u202f', ' '); 208 } 209 210 /** 211 * Returns {@code true} if the locale is "en" or "en-US" or "en-US-*" or 212 * "en-<unknown_region>-*" 213 * 214 * The first 2 locales, i.e. {@link Locale#ENGLISH} and {@link Locale#US}, are commonly 215 * hard-coded by developers for serialization and deserialization as shown in 216 * the bug http://b/266731719. The date time formats in these locales are basically frozen 217 * because they should be both backward and forward-compatible. 218 * 219 * We change both formatting and parsing behavior because the serialized string could be 220 * parsed via a network request and thus breaking Android apps. We could consider changing 221 * the formatted date / time, but the parser has to be compatible with both the old and new 222 * formatted string. 223 * 224 * This method returns true for "en-US-*" and "en-<unknown_region>-*" because the behavior 225 * should be consistent with the locale "en-US" and "en". The other English locales are not 226 * expected to be stable in this bug. 227 */ isBug266731719Locale(Locale locale)228 private static boolean isBug266731719Locale(Locale locale) { 229 if (locale == null) { 230 return false; 231 } 232 233 String language = locale.getLanguage(); 234 if (language == null) { 235 return false; 236 } 237 // Use LanguageTag.canonicalizeLanguage(s) instead of String.toUpperCase(s) to avoid 238 // non-ASCII character conversion. 239 language = LanguageTag.canonicalizeLanguage(language); 240 if (!("en".equals(language))) { 241 return false; 242 } 243 244 String region = locale.getCountry(); 245 if (region == null || region.isEmpty()) { 246 return true; 247 } 248 region = LanguageTag.canonicalizeRegion(region); 249 return "US".equals(region); 250 } 251 } 252