1 /*
2  * Copyright (C) 2006 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 android.text.format;
18 
19 import android.annotation.NonNull;
20 import android.compat.annotation.UnsupportedAppUsage;
21 import android.content.Context;
22 import android.icu.text.DateFormatSymbols;
23 import android.icu.text.DateTimePatternGenerator;
24 import android.provider.Settings;
25 import android.text.SpannableStringBuilder;
26 import android.text.Spanned;
27 import android.text.SpannedString;
28 
29 import java.text.SimpleDateFormat;
30 import java.util.Calendar;
31 import java.util.Date;
32 import java.util.GregorianCalendar;
33 import java.util.Locale;
34 import java.util.TimeZone;
35 
36 /**
37  * Utility class for producing strings with formatted date/time.
38  *
39  * <p>Most callers should avoid supplying their own format strings to this
40  * class' {@code format} methods and rely on the correctly localized ones
41  * supplied by the system. This class' factory methods return
42  * appropriately-localized {@link java.text.DateFormat} instances, suitable
43  * for both formatting and parsing dates. For the canonical documentation
44  * of format strings, see {@link java.text.SimpleDateFormat}.
45  *
46  * <p>In cases where the system does not provide a suitable pattern,
47  * this class offers the {@link #getBestDateTimePattern} method.
48  *
49  * <p>The {@code format} methods in this class implement a subset of Unicode
50  * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a> patterns.
51  * The subset currently supported by this class includes the following format characters:
52  * {@code acdEHhLKkLMmsyz}. Up to API level 17, only {@code adEhkMmszy} were supported.
53  * Note that this class incorrectly implements {@code k} as if it were {@code H} for backwards
54  * compatibility.
55  *
56  * <p>See {@link java.text.SimpleDateFormat} for more documentation
57  * about patterns, or if you need a more complete or correct implementation.
58  * Note that the non-{@code format} methods in this class are implemented by
59  * {@code SimpleDateFormat}.
60  */
61 public class DateFormat {
62     /**
63      * @deprecated Use a literal {@code '} instead.
64      * @removed
65      */
66     @Deprecated
67     public  static final char    QUOTE                  =    '\'';
68 
69     /**
70      * @deprecated Use a literal {@code 'a'} instead.
71      * @removed
72      */
73     @Deprecated
74     public  static final char    AM_PM                  =    'a';
75 
76     /**
77      * @deprecated Use a literal {@code 'a'} instead; 'A' was always equivalent to 'a'.
78      * @removed
79      */
80     @Deprecated
81     public  static final char    CAPITAL_AM_PM          =    'A';
82 
83     /**
84      * @deprecated Use a literal {@code 'd'} instead.
85      * @removed
86      */
87     @Deprecated
88     public  static final char    DATE                   =    'd';
89 
90     /**
91      * @deprecated Use a literal {@code 'E'} instead.
92      * @removed
93      */
94     @Deprecated
95     public  static final char    DAY                    =    'E';
96 
97     /**
98      * @deprecated Use a literal {@code 'h'} instead.
99      * @removed
100      */
101     @Deprecated
102     public  static final char    HOUR                   =    'h';
103 
104     /**
105      * @deprecated Use a literal {@code 'H'} (for compatibility with {@link SimpleDateFormat}
106      * and Unicode) or {@code 'k'} (for compatibility with Android releases up to and including
107      * Jelly Bean MR-1) instead. Note that the two are incompatible.
108      *
109      * @removed
110      */
111     @Deprecated
112     public  static final char    HOUR_OF_DAY            =    'k';
113 
114     /**
115      * @deprecated Use a literal {@code 'm'} instead.
116      * @removed
117      */
118     @Deprecated
119     public  static final char    MINUTE                 =    'm';
120 
121     /**
122      * @deprecated Use a literal {@code 'M'} instead.
123      * @removed
124      */
125     @Deprecated
126     public  static final char    MONTH                  =    'M';
127 
128     /**
129      * @deprecated Use a literal {@code 'L'} instead.
130      * @removed
131      */
132     @Deprecated
133     public  static final char    STANDALONE_MONTH       =    'L';
134 
135     /**
136      * @deprecated Use a literal {@code 's'} instead.
137      * @removed
138      */
139     @Deprecated
140     public  static final char    SECONDS                =    's';
141 
142     /**
143      * @deprecated Use a literal {@code 'z'} instead.
144      * @removed
145      */
146     @Deprecated
147     public  static final char    TIME_ZONE              =    'z';
148 
149     /**
150      * @deprecated Use a literal {@code 'y'} instead.
151      * @removed
152      */
153     @Deprecated
154     public  static final char    YEAR                   =    'y';
155 
156 
157     private static final Object sLocaleLock = new Object();
158     private static Locale sIs24HourLocale;
159     private static boolean sIs24Hour;
160 
161     /**
162      * Returns true if times should be formatted as 24 hour times, false if times should be
163      * formatted as 12 hour (AM/PM) times. Based on the user's chosen locale and other preferences.
164      * @param context the context to use for the content resolver
165      * @return true if 24 hour time format is selected, false otherwise.
166      */
is24HourFormat(Context context)167     public static boolean is24HourFormat(Context context) {
168         return is24HourFormat(context, context.getUserId());
169     }
170 
171     /**
172      * Returns true if times should be formatted as 24 hour times, false if times should be
173      * formatted as 12 hour (AM/PM) times. Based on the user's chosen locale and other preferences.
174      * @param context the context to use for the content resolver
175      * @param userHandle the user handle of the user to query.
176      * @return true if 24 hour time format is selected, false otherwise.
177      *
178      * @hide
179      */
180     @UnsupportedAppUsage
is24HourFormat(Context context, int userHandle)181     public static boolean is24HourFormat(Context context, int userHandle) {
182         final String value = Settings.System.getStringForUser(context.getContentResolver(),
183                 Settings.System.TIME_12_24, userHandle);
184         if (value != null) {
185             return value.equals("24");
186         }
187 
188         return is24HourLocale(context.getResources().getConfiguration().locale);
189     }
190 
191     /**
192      * Returns true if the specified locale uses a 24-hour time format by default, ignoring user
193      * settings.
194      * @param locale the locale to check
195      * @return true if the locale uses a 24 hour time format by default, false otherwise
196      * @hide
197      */
is24HourLocale(@onNull Locale locale)198     public static boolean is24HourLocale(@NonNull Locale locale) {
199         synchronized (sLocaleLock) {
200             if (sIs24HourLocale != null && sIs24HourLocale.equals(locale)) {
201                 return sIs24Hour;
202             }
203         }
204 
205         final java.text.DateFormat natural =
206                 java.text.DateFormat.getTimeInstance(java.text.DateFormat.LONG, locale);
207 
208         final boolean is24Hour;
209         if (natural instanceof SimpleDateFormat) {
210             final SimpleDateFormat sdf = (SimpleDateFormat) natural;
211             final String pattern = sdf.toPattern();
212             is24Hour = hasDesignator(pattern, 'H');
213         } else {
214             is24Hour = false;
215         }
216 
217         synchronized (sLocaleLock) {
218             sIs24HourLocale = locale;
219             sIs24Hour = is24Hour;
220         }
221 
222         return is24Hour;
223     }
224 
225     /**
226      * Returns the best possible localized form of the given skeleton for the given
227      * locale. A skeleton is similar to, and uses the same format characters as, a Unicode
228      * <a href="http://www.unicode.org/reports/tr35/#Date_Format_Patterns">UTS #35</a>
229      * pattern.
230      *
231      * <p>One difference is that order is irrelevant. For example, "MMMMd" will return
232      * "MMMM d" in the {@code en_US} locale, but "d. MMMM" in the {@code de_CH} locale.
233      *
234      * <p>Note also in that second example that the necessary punctuation for German was
235      * added. For the same input in {@code es_ES}, we'd have even more extra text:
236      * "d 'de' MMMM".
237      *
238      * <p>This method will automatically correct for grammatical necessity. Given the
239      * same "MMMMd" input, this method will return "d LLLL" in the {@code fa_IR} locale,
240      * where stand-alone months are necessary. Lengths are preserved where meaningful,
241      * so "Md" would give a different result to "MMMd", say, except in a locale such as
242      * {@code ja_JP} where there is only one length of month.
243      *
244      * <p>This method will only return patterns that are in CLDR, and is useful whenever
245      * you know what elements you want in your format string but don't want to make your
246      * code specific to any one locale.
247      *
248      * @param locale the locale into which the skeleton should be localized
249      * @param skeleton a skeleton as described above
250      * @return a string pattern suitable for use with {@link java.text.SimpleDateFormat}.
251      */
getBestDateTimePattern(Locale locale, String skeleton)252     public static String getBestDateTimePattern(Locale locale, String skeleton) {
253         DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(locale);
254         return dtpg.getBestPattern(skeleton);
255     }
256 
257     /**
258      * Returns a {@link java.text.DateFormat} object that can format the time according
259      * to the context's locale and the user's 12-/24-hour clock preference.
260      * @param context the application context
261      * @return the {@link java.text.DateFormat} object that properly formats the time.
262      */
getTimeFormat(Context context)263     public static java.text.DateFormat getTimeFormat(Context context) {
264         final Locale locale = context.getResources().getConfiguration().locale;
265         return new java.text.SimpleDateFormat(getTimeFormatString(context), locale);
266     }
267 
268     /**
269      * Returns a String pattern that can be used to format the time according
270      * to the context's locale and the user's 12-/24-hour clock preference.
271      * @param context the application context
272      * @hide
273      */
274     @UnsupportedAppUsage
getTimeFormatString(Context context)275     public static String getTimeFormatString(Context context) {
276         return getTimeFormatString(context, context.getUserId());
277     }
278 
279     /**
280      * Returns a String pattern that can be used to format the time according
281      * to the context's locale and the user's 12-/24-hour clock preference.
282      * @param context the application context
283      * @param userHandle the user handle of the user to query the format for
284      * @hide
285      */
286     @UnsupportedAppUsage
getTimeFormatString(Context context, int userHandle)287     public static String getTimeFormatString(Context context, int userHandle) {
288         DateTimePatternGenerator dtpg = DateTimePatternGenerator.getInstance(
289                 context.getResources().getConfiguration().locale);
290         return is24HourFormat(context, userHandle) ? dtpg.getBestPattern("Hm")
291             : dtpg.getBestPattern("hm");
292     }
293 
294     /**
295      * Returns a {@link java.text.DateFormat} object that can format the date
296      * in short form according to the context's locale.
297      *
298      * @param context the application context
299      * @return the {@link java.text.DateFormat} object that properly formats the date.
300      */
getDateFormat(Context context)301     public static java.text.DateFormat getDateFormat(Context context) {
302         final Locale locale = context.getResources().getConfiguration().locale;
303         return java.text.DateFormat.getDateInstance(java.text.DateFormat.SHORT, locale);
304     }
305 
306     /**
307      * Returns a {@link java.text.DateFormat} object that can format the date
308      * in long form (such as {@code Monday, January 3, 2000}) for the context's locale.
309      * @param context the application context
310      * @return the {@link java.text.DateFormat} object that formats the date in long form.
311      */
getLongDateFormat(Context context)312     public static java.text.DateFormat getLongDateFormat(Context context) {
313         final Locale locale = context.getResources().getConfiguration().locale;
314         return java.text.DateFormat.getDateInstance(java.text.DateFormat.LONG, locale);
315     }
316 
317     /**
318      * Returns a {@link java.text.DateFormat} object that can format the date
319      * in medium form (such as {@code Jan 3, 2000}) for the context's locale.
320      * @param context the application context
321      * @return the {@link java.text.DateFormat} object that formats the date in long form.
322      */
getMediumDateFormat(Context context)323     public static java.text.DateFormat getMediumDateFormat(Context context) {
324         final Locale locale = context.getResources().getConfiguration().locale;
325         return java.text.DateFormat.getDateInstance(java.text.DateFormat.MEDIUM, locale);
326     }
327 
328     /**
329      * Gets the current date format stored as a char array. Returns a 3 element
330      * array containing the day ({@code 'd'}), month ({@code 'M'}), and year ({@code 'y'}))
331      * in the order specified by the user's format preference.  Note that this order is
332      * <i>only</i> appropriate for all-numeric dates; spelled-out (MEDIUM and LONG)
333      * dates will generally contain other punctuation, spaces, or words,
334      * not just the day, month, and year, and not necessarily in the same
335      * order returned here.
336      */
getDateFormatOrder(Context context)337     public static char[] getDateFormatOrder(Context context) {
338         return getDateFormatOrder(getDateFormatString(context));
339     }
340 
341     /**
342      * @hide Used by internal framework class {@link android.widget.DatePickerSpinnerDelegate}.
343      */
getDateFormatOrder(String pattern)344     public static char[] getDateFormatOrder(String pattern) {
345         char[] result = new char[3];
346         int resultIndex = 0;
347         boolean sawDay = false;
348         boolean sawMonth = false;
349         boolean sawYear = false;
350 
351         for (int i = 0; i < pattern.length(); ++i) {
352             char ch = pattern.charAt(i);
353             if (ch == 'd' || ch == 'L' || ch == 'M' || ch == 'y') {
354                 if (ch == 'd' && !sawDay) {
355                     result[resultIndex++] = 'd';
356                     sawDay = true;
357                 } else if ((ch == 'L' || ch == 'M') && !sawMonth) {
358                     result[resultIndex++] = 'M';
359                     sawMonth = true;
360                 } else if ((ch == 'y') && !sawYear) {
361                     result[resultIndex++] = 'y';
362                     sawYear = true;
363                 }
364             } else if (ch == 'G') {
365                 // Ignore the era specifier, if present.
366             } else if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
367                 throw new IllegalArgumentException("Bad pattern character '" + ch + "' in "
368                     + pattern);
369             } else if (ch == '\'') {
370                 if (i < pattern.length() - 1 && pattern.charAt(i + 1) == '\'') {
371                     ++i;
372                 } else {
373                     i = pattern.indexOf('\'', i + 1);
374                     if (i == -1) {
375                         throw new IllegalArgumentException("Bad quoting in " + pattern);
376                     }
377                     ++i;
378                 }
379             } else {
380                 // Ignore spaces and punctuation.
381             }
382         }
383         return result;
384     }
385 
getDateFormatString(Context context)386     private static String getDateFormatString(Context context) {
387         final Locale locale = context.getResources().getConfiguration().locale;
388         java.text.DateFormat df = java.text.DateFormat.getDateInstance(
389                 java.text.DateFormat.SHORT, locale);
390         if (df instanceof SimpleDateFormat) {
391             return ((SimpleDateFormat) df).toPattern();
392         }
393 
394         throw new AssertionError("!(df instanceof SimpleDateFormat)");
395     }
396 
397     /**
398      * Given a format string and a time in milliseconds since Jan 1, 1970 GMT, returns a
399      * CharSequence containing the requested date.
400      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
401      * @param inTimeInMillis in milliseconds since Jan 1, 1970 GMT
402      * @return a {@link CharSequence} containing the requested text
403      */
format(CharSequence inFormat, long inTimeInMillis)404     public static CharSequence format(CharSequence inFormat, long inTimeInMillis) {
405         return format(inFormat, new Date(inTimeInMillis));
406     }
407 
408     /**
409      * Given a format string and a {@link java.util.Date} object, returns a CharSequence containing
410      * the requested date.
411      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
412      * @param inDate the date to format
413      * @return a {@link CharSequence} containing the requested text
414      */
format(CharSequence inFormat, Date inDate)415     public static CharSequence format(CharSequence inFormat, Date inDate) {
416         Calendar c = new GregorianCalendar();
417         c.setTime(inDate);
418         return format(inFormat, c);
419     }
420 
421     /**
422      * Indicates whether the specified format string contains seconds.
423      *
424      * Always returns false if the input format is null.
425      *
426      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
427      *
428      * @return true if the format string contains {@link #SECONDS}, false otherwise
429      *
430      * @hide
431      */
432     @UnsupportedAppUsage
hasSeconds(CharSequence inFormat)433     public static boolean hasSeconds(CharSequence inFormat) {
434         return hasDesignator(inFormat, SECONDS);
435     }
436 
437     /**
438      * Test if a format string contains the given designator. Always returns
439      * {@code false} if the input format is {@code null}.
440      *
441      * Note that this is intended for searching for designators, not arbitrary
442      * characters. So searching for a literal single quote would not work correctly.
443      *
444      * @hide
445      */
446     @UnsupportedAppUsage
hasDesignator(CharSequence inFormat, char designator)447     public static boolean hasDesignator(CharSequence inFormat, char designator) {
448         if (inFormat == null) return false;
449 
450         final int length = inFormat.length();
451 
452         boolean insideQuote = false;
453         for (int i = 0; i < length; i++) {
454             final char c = inFormat.charAt(i);
455             if (c == QUOTE) {
456                 insideQuote = !insideQuote;
457             } else if (!insideQuote) {
458                 if (c == designator) {
459                     return true;
460                 }
461             }
462         }
463 
464         return false;
465     }
466 
467     /**
468      * Given a format string and a {@link java.util.Calendar} object, returns a CharSequence
469      * containing the requested date.
470      * @param inFormat the format string, as described in {@link android.text.format.DateFormat}
471      * @param inDate the date to format
472      * @return a {@link CharSequence} containing the requested text
473      */
format(CharSequence inFormat, Calendar inDate)474     public static CharSequence format(CharSequence inFormat, Calendar inDate) {
475         SpannableStringBuilder s = new SpannableStringBuilder(inFormat);
476         int count;
477 
478         DateFormatSymbols dfs = getIcuDateFormatSymbols(Locale.getDefault());
479         String[] amPm = dfs.getAmPmStrings();
480 
481         int len = inFormat.length();
482 
483         for (int i = 0; i < len; i += count) {
484             count = 1;
485             int c = s.charAt(i);
486 
487             if (c == QUOTE) {
488                 count = appendQuotedText(s, i);
489                 len = s.length();
490                 continue;
491             }
492 
493             while ((i + count < len) && (s.charAt(i + count) == c)) {
494                 count++;
495             }
496 
497             String replacement;
498             switch (c) {
499                 case 'A':
500                 case 'a':
501                     replacement = amPm[inDate.get(Calendar.AM_PM) - Calendar.AM];
502                     break;
503                 case 'd':
504                     replacement = zeroPad(inDate.get(Calendar.DATE), count);
505                     break;
506                 case 'c':
507                 case 'E':
508                     replacement = getDayOfWeekString(dfs,
509                                                      inDate.get(Calendar.DAY_OF_WEEK), count, c);
510                     break;
511                 case 'K': // hour in am/pm (0-11)
512                 case 'h': // hour in am/pm (1-12)
513                     {
514                         int hour = inDate.get(Calendar.HOUR);
515                         if (c == 'h' && hour == 0) {
516                             hour = 12;
517                         }
518                         replacement = zeroPad(hour, count);
519                     }
520                     break;
521                 case 'H': // hour in day (0-23)
522                 case 'k': // hour in day (1-24) [but see note below]
523                     {
524                         int hour = inDate.get(Calendar.HOUR_OF_DAY);
525                         // Historically on Android 'k' was interpreted as 'H', which wasn't
526                         // implemented, so pretty much all callers that want to format 24-hour
527                         // times are abusing 'k'. http://b/8359981.
528                         if (false && c == 'k' && hour == 0) {
529                             hour = 24;
530                         }
531                         replacement = zeroPad(hour, count);
532                     }
533                     break;
534                 case 'L':
535                 case 'M':
536                     replacement = getMonthString(dfs, inDate.get(Calendar.MONTH), count, c);
537                     break;
538                 case 'm':
539                     replacement = zeroPad(inDate.get(Calendar.MINUTE), count);
540                     break;
541                 case 's':
542                     replacement = zeroPad(inDate.get(Calendar.SECOND), count);
543                     break;
544                 case 'y':
545                     replacement = getYearString(inDate.get(Calendar.YEAR), count);
546                     break;
547                 case 'z':
548                     replacement = getTimeZoneString(inDate, count);
549                     break;
550                 default:
551                     replacement = null;
552                     break;
553             }
554 
555             if (replacement != null) {
556                 s.replace(i, i + count, replacement);
557                 count = replacement.length(); // CARE: count is used in the for loop above
558                 len = s.length();
559             }
560         }
561 
562         if (inFormat instanceof Spanned) {
563             return new SpannedString(s);
564         } else {
565             return s.toString();
566         }
567     }
568 
getDayOfWeekString(DateFormatSymbols dfs, int day, int count, int kind)569     private static String getDayOfWeekString(DateFormatSymbols dfs, int day, int count, int kind) {
570         boolean standalone = (kind == 'c');
571         int context = standalone ? DateFormatSymbols.STANDALONE : DateFormatSymbols.FORMAT;
572         final int width;
573         if (count == 5) {
574             width = DateFormatSymbols.NARROW;
575         } else if (count == 4) {
576             width = DateFormatSymbols.WIDE;
577         } else {
578             width = DateFormatSymbols.ABBREVIATED;
579         }
580         return dfs.getWeekdays(context, width)[day];
581     }
582 
getMonthString(DateFormatSymbols dfs, int month, int count, int kind)583     private static String getMonthString(DateFormatSymbols dfs, int month, int count, int kind) {
584         boolean standalone = (kind == 'L');
585         int monthContext = standalone ? DateFormatSymbols.STANDALONE : DateFormatSymbols.FORMAT;
586         if (count == 5) {
587             return dfs.getMonths(monthContext, DateFormatSymbols.NARROW)[month];
588         } else if (count == 4) {
589             return dfs.getMonths(monthContext, DateFormatSymbols.WIDE)[month];
590         } else if (count == 3) {
591             return dfs.getMonths(monthContext, DateFormatSymbols.ABBREVIATED)[month];
592         } else {
593             // Calendar.JANUARY == 0, so add 1 to month.
594             return zeroPad(month+1, count);
595         }
596     }
597 
getTimeZoneString(Calendar inDate, int count)598     private static String getTimeZoneString(Calendar inDate, int count) {
599         TimeZone tz = inDate.getTimeZone();
600         if (count < 2) { // FIXME: shouldn't this be <= 2 ?
601             return formatZoneOffset(inDate.get(Calendar.DST_OFFSET) +
602                                     inDate.get(Calendar.ZONE_OFFSET),
603                                     count);
604         } else {
605             boolean dst = inDate.get(Calendar.DST_OFFSET) != 0;
606             return tz.getDisplayName(dst, TimeZone.SHORT);
607         }
608     }
609 
formatZoneOffset(int offset, int count)610     private static String formatZoneOffset(int offset, int count) {
611         offset /= 1000; // milliseconds to seconds
612         StringBuilder tb = new StringBuilder();
613 
614         if (offset < 0) {
615             tb.insert(0, "-");
616             offset = -offset;
617         } else {
618             tb.insert(0, "+");
619         }
620 
621         int hours = offset / 3600;
622         int minutes = (offset % 3600) / 60;
623 
624         tb.append(zeroPad(hours, 2));
625         tb.append(zeroPad(minutes, 2));
626         return tb.toString();
627     }
628 
getYearString(int year, int count)629     private static String getYearString(int year, int count) {
630         return (count <= 2) ? zeroPad(year % 100, 2)
631                             : String.format(Locale.getDefault(), "%d", year);
632     }
633 
634 
635     /**
636      * Strips quotation marks from the {@code formatString} and appends the result back to the
637      * {@code formatString}.
638      *
639      * @param formatString the format string, as described in
640      *                     {@link android.text.format.DateFormat}, to be modified
641      * @param index        index of the first quote
642      * @return the length of the quoted text that was appended.
643      * @hide
644      */
appendQuotedText(SpannableStringBuilder formatString, int index)645     public static int appendQuotedText(SpannableStringBuilder formatString, int index) {
646         int length = formatString.length();
647         if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) {
648             formatString.delete(index, index + 1);
649             return 1;
650         }
651 
652         int count = 0;
653 
654         // delete leading quote
655         formatString.delete(index, index + 1);
656         length--;
657 
658         while (index < length) {
659             char c = formatString.charAt(index);
660 
661             if (c == QUOTE) {
662                 //  QUOTEQUOTE -> QUOTE
663                 if (index + 1 < length && formatString.charAt(index + 1) == QUOTE) {
664 
665                     formatString.delete(index, index + 1);
666                     length--;
667                     count++;
668                     index++;
669                 } else {
670                     //  Closing QUOTE ends quoted text copying
671                     formatString.delete(index, index + 1);
672                     break;
673                 }
674             } else {
675                 index++;
676                 count++;
677             }
678         }
679 
680         return count;
681     }
682 
zeroPad(int inValue, int inMinDigits)683     private static String zeroPad(int inValue, int inMinDigits) {
684         return String.format(Locale.getDefault(), "%0" + inMinDigits + "d", inValue);
685     }
686 
687     /**
688      * We use Gregorian calendar for date formats in android.text.format and various UI widget
689      * historically. It's a utility method to get an {@link DateFormatSymbols} instance. Note that
690      * {@link DateFormatSymbols} has cache, and external cache is not needed unless same instance is
691      * requested repeatedly in the performance critical code.
692      *
693      * @hide
694      */
getIcuDateFormatSymbols(Locale locale)695     public static DateFormatSymbols getIcuDateFormatSymbols(Locale locale) {
696         return new DateFormatSymbols(android.icu.util.GregorianCalendar.class, locale);
697     }
698 }
699