| /* |
| * Copyright (C) 2006 The Android Open Source Project |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); |
| * you may not use this file except in compliance with the License. |
| * You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| package android.text.format; |
| |
| import android.annotation.NonNull; |
| import android.content.Context; |
| import android.os.UserHandle; |
| import android.provider.Settings; |
| import android.text.SpannableStringBuilder; |
| import android.text.Spanned; |
| import android.text.SpannedString; |
| |
| import libcore.icu.ICU; |
| import libcore.icu.LocaleData; |
| |
| import java.text.SimpleDateFormat; |
| import java.util.Calendar; |
| import java.util.Date; |
| import java.util.GregorianCalendar; |
| import java.util.Locale; |
| import java.util.TimeZone; |
| |
| /** |
| * Utility class for producing strings with formatted date/time. |
| * |
| * <p>Most callers should avoid supplying their own format strings to this |
| * class' {@code format} methods and rely on the correctly localized ones |
| * supplied by the system. This class' factory methods return |
| * appropriately-localized {@link java.text.DateFormat} instances, suitable |
| * for both formatting and parsing dates. For the canonical documentation |
| * of format strings, see {@link java.text.SimpleDateFormat}. |
| * |
| * <p>In cases where the system does not provide a suitable pattern, |
| * this class offers the {@link #getBestDateTimePattern} method. |
| * |
| * <p>The {@code format} methods in this class implement a subset of Unicode |
| * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a> patterns. |
| * The subset currently supported by this class includes the following format characters: |
| * {@code acdEHhLKkLMmsyz}. Up to API level 17, only {@code adEhkMmszy} were supported. |
| * Note that this class incorrectly implements {@code k} as if it were {@code H} for backwards |
| * compatibility. |
| * |
| * <p>See {@link java.text.SimpleDateFormat} for more documentation |
| * about patterns, or if you need a more complete or correct implementation. |
| * Note that the non-{@code format} methods in this class are implemented by |
| * {@code SimpleDateFormat}. |
| */ |
| public class DateFormat { |
| /** |
| * @deprecated Use a literal {@code '} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char QUOTE = '\''; |
| |
| /** |
| * @deprecated Use a literal {@code 'a'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char AM_PM = 'a'; |
| |
| /** |
| * @deprecated Use a literal {@code 'a'} instead; 'A' was always equivalent to 'a'. |
| * @removed |
| */ |
| @Deprecated |
| public static final char CAPITAL_AM_PM = 'A'; |
| |
| /** |
| * @deprecated Use a literal {@code 'd'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char DATE = 'd'; |
| |
| /** |
| * @deprecated Use a literal {@code 'E'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char DAY = 'E'; |
| |
| /** |
| * @deprecated Use a literal {@code 'h'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char HOUR = 'h'; |
| |
| /** |
| * @deprecated Use a literal {@code 'H'} (for compatibility with {@link SimpleDateFormat} |
| * and Unicode) or {@code 'k'} (for compatibility with Android releases up to and including |
| * Jelly Bean MR-1) instead. Note that the two are incompatible. |
| * |
| * @removed |
| */ |
| @Deprecated |
| public static final char HOUR_OF_DAY = 'k'; |
| |
| /** |
| * @deprecated Use a literal {@code 'm'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char MINUTE = 'm'; |
| |
| /** |
| * @deprecated Use a literal {@code 'M'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char MONTH = 'M'; |
| |
| /** |
| * @deprecated Use a literal {@code 'L'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char STANDALONE_MONTH = 'L'; |
| |
| /** |
| * @deprecated Use a literal {@code 's'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char SECONDS = 's'; |
| |
| /** |
| * @deprecated Use a literal {@code 'z'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char TIME_ZONE = 'z'; |
| |
| /** |
| * @deprecated Use a literal {@code 'y'} instead. |
| * @removed |
| */ |
| @Deprecated |
| public static final char YEAR = 'y'; |
| |
| |
| private static final Object sLocaleLock = new Object(); |
| private static Locale sIs24HourLocale; |
| private static boolean sIs24Hour; |
| |
| |
| /** |
| * Returns true if user preference is set to 24-hour format. |
| * @param context the context to use for the content resolver |
| * @return true if 24 hour time format is selected, false otherwise. |
| */ |
| public static boolean is24HourFormat(Context context) { |
| return is24HourFormat(context, UserHandle.myUserId()); |
| } |
| |
| /** |
| * Returns true if user preference with the given user handle is set to 24-hour format. |
| * @param context the context to use for the content resolver |
| * @param userHandle the user handle of the user to query. |
| * @return true if 24 hour time format is selected, false otherwise. |
| * |
| * @hide |
| */ |
| public static boolean is24HourFormat(Context context, int userHandle) { |
| final String value = Settings.System.getStringForUser(context.getContentResolver(), |
| Settings.System.TIME_12_24, userHandle); |
| if (value != null) { |
| return value.equals("24"); |
| } |
| |
| return is24HourLocale(context.getResources().getConfiguration().locale); |
| } |
| |
| /** |
| * Returns true if the specified locale uses a 24-hour time format by default, ignoring user |
| * settings. |
| * @param locale the locale to check |
| * @return true if the locale uses a 24 hour time format by default, false otherwise |
| * @hide |
| */ |
| public static boolean is24HourLocale(@NonNull Locale locale) { |
| synchronized (sLocaleLock) { |
| if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) { |
| return sIs24Hour; |
| } |
| } |
| |
| final java.text.DateFormat natural = |
| java.text.DateFormat.getTimeInstance(java.text.DateFormat.LONG, locale); |
| |
| final boolean is24Hour; |
| if (natural instanceof SimpleDateFormat) { |
| final SimpleDateFormat sdf = (SimpleDateFormat) natural; |
| final String pattern = sdf.toPattern(); |
| is24Hour = hasDesignator(pattern, 'H'); |
| } else { |
| is24Hour = false; |
| } |
| |
| synchronized (sLocaleLock) { |
| sIs24HourLocale = locale; |
| sIs24Hour = is24Hour; |
| } |
| |
| return is24Hour; |
| } |
| |
| /** |
| * Returns the best possible localized form of the given skeleton for the given |
| * locale. A skeleton is similar to, and uses the same format characters as, a Unicode |
| * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a> |
| * pattern. |
| * |
| * <p>One difference is that order is irrelevant. For example, "MMMMd" will return |
| * "MMMM d" in the {@code en_US} locale, but "d. MMMM" in the {@code de_CH} locale. |
| * |
| * <p>Note also in that second example that the necessary punctuation for German was |
| * added. For the same input in {@code es_ES}, we'd have even more extra text: |
| * "d 'de' MMMM". |
| * |
| * <p>This method will automatically correct for grammatical necessity. Given the |
| * same "MMMMd" input, this method will return "d LLLL" in the {@code fa_IR} locale, |
| * where stand-alone months are necessary. Lengths are preserved where meaningful, |
| * so "Md" would give a different result to "MMMd", say, except in a locale such as |
| * {@code ja_JP} where there is only one length of month. |
| * |
| * <p>This method will only return patterns that are in CLDR, and is useful whenever |
| * you know what elements you want in your format string but don't want to make your |
| * code specific to any one locale. |
| * |
| * @param locale the locale into which the skeleton should be localized |
| * @param skeleton a skeleton as described above |
| * @return a string pattern suitable for use with {@link java.text.SimpleDateFormat}. |
| */ |
| public static String getBestDateTimePattern(Locale locale, String skeleton) { |
| return ICU.getBestDateTimePattern(skeleton, locale); |
| } |
| |
| /** |
| * Returns a {@link java.text.DateFormat} object that can format the time according |
| * to the context's locale and the user's 12-/24-hour clock preference. |
| * @param context the application context |
| * @return the {@link java.text.DateFormat} object that properly formats the time. |
| */ |
| public static java.text.DateFormat getTimeFormat(Context context) { |
| final Locale locale = context.getResources().getConfiguration().locale; |
| return new java.text.SimpleDateFormat(getTimeFormatString(context), locale); |
| } |
| |
| /** |
| * Returns a String pattern that can be used to format the time according |
| * to the context's locale and the user's 12-/24-hour clock preference. |
| * @param context the application context |
| * @hide |
| */ |
| public static String getTimeFormatString(Context context) { |
| return getTimeFormatString(context, UserHandle.myUserId()); |
| } |
| |
| /** |
| * Returns a String pattern that can be used to format the time according |
| * to the context's locale and the user's 12-/24-hour clock preference. |
| * @param context the application context |
| * @param userHandle the user handle of the user to query the format for |
| * @hide |
| */ |
| public static String getTimeFormatString(Context context, int userHandle) { |
| final LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale); |
| return is24HourFormat(context, userHandle) ? d.timeFormat_Hm : d.timeFormat_hm; |
| } |
| |
| /** |
| * Returns a {@link java.text.DateFormat} object that can format the date |
| * in short form according to the context's locale. |
| * |
| * @param context the application context |
| * @return the {@link java.text.DateFormat} object that properly formats the date. |
| */ |
| public static java.text.DateFormat getDateFormat(Context context) { |
| final Locale locale = context.getResources().getConfiguration().locale; |
| return java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT, locale); |
| } |
| |
| /** |
| * Returns a {@link java.text.DateFormat} object that can format the date |
| * in long form (such as {@code Monday, January 3, 2000}) for the context's locale. |
| * @param context the application context |
| * @return the {@link java.text.DateFormat} object that formats the date in long form. |
| */ |
| public static java.text.DateFormat getLongDateFormat(Context context) { |
| final Locale locale = context.getResources().getConfiguration().locale; |
| return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG, locale); |
| } |
| |
| /** |
| * Returns a {@link java.text.DateFormat} object that can format the date |
| * in medium form (such as {@code Jan 3, 2000}) for the context's locale. |
| * @param context the application context |
| * @return the {@link java.text.DateFormat} object that formats the date in long form. |
| */ |
| public static java.text.DateFormat getMediumDateFormat(Context context) { |
| final Locale locale = context.getResources().getConfiguration().locale; |
| return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM, locale); |
| } |
| |
| /** |
| * Gets the current date format stored as a char array. Returns a 3 element |
| * array containing the day ({@code 'd'}), month ({@code 'M'}), and year ({@code 'y'})) |
| * in the order specified by the user's format preference. Note that this order is |
| * <i>only</i> appropriate for all-numeric dates; spelled-out (MEDIUM and LONG) |
| * dates will generally contain other punctuation, spaces, or words, |
| * not just the day, month, and year, and not necessarily in the same |
| * order returned here. |
| */ |
| public static char[] getDateFormatOrder(Context context) { |
| return ICU.getDateFormatOrder(getDateFormatString(context)); |
| } |
| |
| private static String getDateFormatString(Context context) { |
| final Locale locale = context.getResources().getConfiguration().locale; |
| java.text.DateFormat df = java.text.DateFormat.getDateInstance( |
| java.text.DateFormat.SHORT, locale); |
| if (df instanceof SimpleDateFormat) { |
| return ((SimpleDateFormat) df).toPattern(); |
| } |
| |
| throw new AssertionError("!(df instanceof SimpleDateFormat)"); |
| } |
| |
| /** |
| * Given a format string and a time in milliseconds since Jan 1, 1970 GMT, returns a |
| * CharSequence containing the requested date. |
| * @param inFormat the format string, as described in {@link android.text.format.DateFormat} |
| * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT |
| * @return a {@link CharSequence} containing the requested text |
| */ |
| public static CharSequence format(CharSequence inFormat, long inTimeInMillis) { |
| return format(inFormat, new Date(inTimeInMillis)); |
| } |
| |
| /** |
| * Given a format string and a {@link java.util.Date} object, returns a CharSequence containing |
| * the requested date. |
| * @param inFormat the format string, as described in {@link android.text.format.DateFormat} |
| * @param inDate the date to format |
| * @return a {@link CharSequence} containing the requested text |
| */ |
| public static CharSequence format(CharSequence inFormat, Date inDate) { |
| Calendar c = new GregorianCalendar(); |
| c.setTime(inDate); |
| return format(inFormat, c); |
| } |
| |
| /** |
| * Indicates whether the specified format string contains seconds. |
| * |
| * Always returns false if the input format is null. |
| * |
| * @param inFormat the format string, as described in {@link android.text.format.DateFormat} |
| * |
| * @return true if the format string contains {@link #SECONDS}, false otherwise |
| * |
| * @hide |
| */ |
| public static boolean hasSeconds(CharSequence inFormat) { |
| return hasDesignator(inFormat, SECONDS); |
| } |
| |
| /** |
| * Test if a format string contains the given designator. Always returns |
| * {@code false} if the input format is {@code null}. |
| * |
| * Note that this is intended for searching for designators, not arbitrary |
| * characters. So searching for a literal single quote would not work correctly. |
| * |
| * @hide |
| */ |
| public static boolean hasDesignator(CharSequence inFormat, char designator) { |
| if (inFormat == null) return false; |
| |
| final int length = inFormat.length(); |
| |
| boolean insideQuote = false; |
| for (int i = 0; i < length; i++) { |
| final char c = inFormat.charAt(i); |
| if (c == QUOTE) { |
| insideQuote = !insideQuote; |
| } else if (!insideQuote) { |
| if (c == designator) { |
| return true; |
| } |
| } |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence |
| * containing the requested date. |
| * @param inFormat the format string, as described in {@link android.text.format.DateFormat} |
| * @param inDate the date to format |
| * @return a {@link CharSequence} containing the requested text |
| */ |
| public static CharSequence format(CharSequence inFormat, Calendar inDate) { |
| SpannableStringBuilder s = new SpannableStringBuilder(inFormat); |
| int count; |
| |
| LocaleData localeData = LocaleData.get(Locale.getDefault()); |
| |
| int len = inFormat.length(); |
| |
| for (int i = 0; i < len; i += count) { |
| count = 1; |
| int c = s.charAt(i); |
| |
| if (c == QUOTE) { |
| count = appendQuotedText(s, i); |
| len = s.length(); |
| continue; |
| } |
| |
| while ((i + count < len) && (s.charAt(i + count) == c)) { |
| count++; |
| } |
| |
| String replacement; |
| switch (c) { |
| case 'A': |
| case 'a': |
| replacement = localeData.amPm[inDate.get(Calendar.AM_PM) - Calendar.AM]; |
| break; |
| case 'd': |
| replacement = zeroPad(inDate.get(Calendar.DATE), count); |
| break; |
| case 'c': |
| case 'E': |
| replacement = getDayOfWeekString(localeData, |
| inDate.get(Calendar.DAY_OF_WEEK), count, c); |
| break; |
| case 'K': // hour in am/pm (0-11) |
| case 'h': // hour in am/pm (1-12) |
| { |
| int hour = inDate.get(Calendar.HOUR); |
| if (c == 'h' && hour == 0) { |
| hour = 12; |
| } |
| replacement = zeroPad(hour, count); |
| } |
| break; |
| case 'H': // hour in day (0-23) |
| case 'k': // hour in day (1-24) [but see note below] |
| { |
| int hour = inDate.get(Calendar.HOUR_OF_DAY); |
| // Historically on Android 'k' was interpreted as 'H', which wasn't |
| // implemented, so pretty much all callers that want to format 24-hour |
| // times are abusing 'k'. http://b/8359981. |
| if (false && c == 'k' && hour == 0) { |
| hour = 24; |
| } |
| replacement = zeroPad(hour, count); |
| } |
| break; |
| case 'L': |
| case 'M': |
| replacement = getMonthString(localeData, |
| inDate.get(Calendar.MONTH), count, c); |
| break; |
| case 'm': |
| replacement = zeroPad(inDate.get(Calendar.MINUTE), count); |
| break; |
| case 's': |
| replacement = zeroPad(inDate.get(Calendar.SECOND), count); |
| break; |
| case 'y': |
| replacement = getYearString(inDate.get(Calendar.YEAR), count); |
| break; |
| case 'z': |
| replacement = getTimeZoneString(inDate, count); |
| break; |
| default: |
| replacement = null; |
| break; |
| } |
| |
| if (replacement != null) { |
| s.replace(i, i + count, replacement); |
| count = replacement.length(); // CARE: count is used in the for loop above |
| len = s.length(); |
| } |
| } |
| |
| if (inFormat instanceof Spanned) { |
| return new SpannedString(s); |
| } else { |
| return s.toString(); |
| } |
| } |
| |
| private static String getDayOfWeekString(LocaleData ld, int day, int count, int kind) { |
| boolean standalone = (kind == 'c'); |
| if (count == 5) { |
| return standalone ? ld.tinyStandAloneWeekdayNames[day] : ld.tinyWeekdayNames[day]; |
| } else if (count == 4) { |
| return standalone ? ld.longStandAloneWeekdayNames[day] : ld.longWeekdayNames[day]; |
| } else { |
| return standalone ? ld.shortStandAloneWeekdayNames[day] : ld.shortWeekdayNames[day]; |
| } |
| } |
| |
| private static String getMonthString(LocaleData ld, int month, int count, int kind) { |
| boolean standalone = (kind == 'L'); |
| if (count == 5) { |
| return standalone ? ld.tinyStandAloneMonthNames[month] : ld.tinyMonthNames[month]; |
| } else if (count == 4) { |
| return standalone ? ld.longStandAloneMonthNames[month] : ld.longMonthNames[month]; |
| } else if (count == 3) { |
| return standalone ? ld.shortStandAloneMonthNames[month] : ld.shortMonthNames[month]; |
| } else { |
| // Calendar.JANUARY == 0, so add 1 to month. |
| return zeroPad(month+1, count); |
| } |
| } |
| |
| private static String getTimeZoneString(Calendar inDate, int count) { |
| TimeZone tz = inDate.getTimeZone(); |
| if (count < 2) { // FIXME: shouldn't this be <= 2 ? |
| return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) + |
| inDate.get(Calendar.ZONE_OFFSET), |
| count); |
| } else { |
| boolean dst = inDate.get(Calendar.DST_OFFSET) != 0; |
| return tz.getDisplayName(dst, TimeZone.SHORT); |
| } |
| } |
| |
| private static String formatZoneOffset(int offset, int count) { |
| offset /= 1000; // milliseconds to seconds |
| StringBuilder tb = new StringBuilder(); |
| |
| if (offset < 0) { |
| tb.insert(0, "-"); |
| offset = -offset; |
| } else { |
| tb.insert(0, "+"); |
| } |
| |
| int hours = offset / 3600; |
| int minutes = (offset % 3600) / 60; |
| |
| tb.append(zeroPad(hours, 2)); |
| tb.append(zeroPad(minutes, 2)); |
| return tb.toString(); |
| } |
| |
| private static String getYearString(int year, int count) { |
| return (count <= 2) ? zeroPad(year % 100, 2) |
| : String.format(Locale.getDefault(), "%d", year); |
| } |
| |
| |
| /** |
| * Strips quotation marks from the {@code formatString} and appends the result back to the |
| * {@code formatString}. |
| * |
| * @param formatString the format string, as described in |
| * {@link android.text.format.DateFormat}, to be modified |
| * @param index index of the first quote |
| * @return the length of the quoted text that was appended. |
| * @hide |
| */ |
| public static int appendQuotedText(SpannableStringBuilder formatString, int index) { |
| int length = formatString.length(); |
| if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) { |
| formatString.delete(index, index + 1); |
| return 1; |
| } |
| |
| int count = 0; |
| |
| // delete leading quote |
| formatString.delete(index, index + 1); |
| length--; |
| |
| while (index < length) { |
| char c = formatString.charAt(index); |
| |
| if (c == QUOTE) { |
| // QUOTEQUOTE -> QUOTE |
| if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) { |
| |
| formatString.delete(index, index + 1); |
| length--; |
| count++; |
| index++; |
| } else { |
| // Closing QUOTE ends quoted text copying |
| formatString.delete(index, index + 1); |
| break; |
| } |
| } else { |
| index++; |
| count++; |
| } |
| } |
| |
| return count; |
| } |
| |
| private static String zeroPad(int inValue, int inMinDigits) { |
| return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue); |
| } |
| } |