DateUtils.java revision 84c863d9af1270a2ccd2ccd2b8c1a753826dfe6d
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
17package android.text.format;
18
19import com.android.internal.R;
20
21import android.content.Context;
22import android.content.res.Configuration;
23import android.content.res.Resources;
24import android.pim.DateException;
25
26import java.util.Calendar;
27import java.util.Date;
28import java.util.GregorianCalendar;
29import java.util.TimeZone;
30
31/**
32 * This class contains various date-related utilities for creating text for things like
33 * elapsed time and date ranges, strings for days of the week and months, and AM/PM text etc.
34 */
35public class DateUtils
36{
37    private static final Object sLock = new Object();
38    private static final int[] sDaysLong = new int[] {
39            com.android.internal.R.string.day_of_week_long_sunday,
40            com.android.internal.R.string.day_of_week_long_monday,
41            com.android.internal.R.string.day_of_week_long_tuesday,
42            com.android.internal.R.string.day_of_week_long_wednesday,
43            com.android.internal.R.string.day_of_week_long_thursday,
44            com.android.internal.R.string.day_of_week_long_friday,
45            com.android.internal.R.string.day_of_week_long_saturday,
46        };
47    private static final int[] sDaysMedium = new int[] {
48            com.android.internal.R.string.day_of_week_medium_sunday,
49            com.android.internal.R.string.day_of_week_medium_monday,
50            com.android.internal.R.string.day_of_week_medium_tuesday,
51            com.android.internal.R.string.day_of_week_medium_wednesday,
52            com.android.internal.R.string.day_of_week_medium_thursday,
53            com.android.internal.R.string.day_of_week_medium_friday,
54            com.android.internal.R.string.day_of_week_medium_saturday,
55        };
56    private static final int[] sDaysShort = new int[] {
57            com.android.internal.R.string.day_of_week_short_sunday,
58            com.android.internal.R.string.day_of_week_short_monday,
59            com.android.internal.R.string.day_of_week_short_tuesday,
60            com.android.internal.R.string.day_of_week_short_wednesday,
61            com.android.internal.R.string.day_of_week_short_thursday,
62            com.android.internal.R.string.day_of_week_short_friday,
63            com.android.internal.R.string.day_of_week_short_saturday,
64        };
65    private static final int[] sDaysShortest = new int[] {
66            com.android.internal.R.string.day_of_week_shortest_sunday,
67            com.android.internal.R.string.day_of_week_shortest_monday,
68            com.android.internal.R.string.day_of_week_shortest_tuesday,
69            com.android.internal.R.string.day_of_week_shortest_wednesday,
70            com.android.internal.R.string.day_of_week_shortest_thursday,
71            com.android.internal.R.string.day_of_week_shortest_friday,
72            com.android.internal.R.string.day_of_week_shortest_saturday,
73        };
74    private static final int[] sMonthsStandaloneLong = new int [] {
75            com.android.internal.R.string.month_long_standalone_january,
76            com.android.internal.R.string.month_long_standalone_february,
77            com.android.internal.R.string.month_long_standalone_march,
78            com.android.internal.R.string.month_long_standalone_april,
79            com.android.internal.R.string.month_long_standalone_may,
80            com.android.internal.R.string.month_long_standalone_june,
81            com.android.internal.R.string.month_long_standalone_july,
82            com.android.internal.R.string.month_long_standalone_august,
83            com.android.internal.R.string.month_long_standalone_september,
84            com.android.internal.R.string.month_long_standalone_october,
85            com.android.internal.R.string.month_long_standalone_november,
86            com.android.internal.R.string.month_long_standalone_december,
87        };
88    private static final int[] sMonthsLong = new int [] {
89            com.android.internal.R.string.month_long_january,
90            com.android.internal.R.string.month_long_february,
91            com.android.internal.R.string.month_long_march,
92            com.android.internal.R.string.month_long_april,
93            com.android.internal.R.string.month_long_may,
94            com.android.internal.R.string.month_long_june,
95            com.android.internal.R.string.month_long_july,
96            com.android.internal.R.string.month_long_august,
97            com.android.internal.R.string.month_long_september,
98            com.android.internal.R.string.month_long_october,
99            com.android.internal.R.string.month_long_november,
100            com.android.internal.R.string.month_long_december,
101        };
102    private static final int[] sMonthsMedium = new int [] {
103            com.android.internal.R.string.month_medium_january,
104            com.android.internal.R.string.month_medium_february,
105            com.android.internal.R.string.month_medium_march,
106            com.android.internal.R.string.month_medium_april,
107            com.android.internal.R.string.month_medium_may,
108            com.android.internal.R.string.month_medium_june,
109            com.android.internal.R.string.month_medium_july,
110            com.android.internal.R.string.month_medium_august,
111            com.android.internal.R.string.month_medium_september,
112            com.android.internal.R.string.month_medium_october,
113            com.android.internal.R.string.month_medium_november,
114            com.android.internal.R.string.month_medium_december,
115        };
116    private static final int[] sMonthsShortest = new int [] {
117            com.android.internal.R.string.month_shortest_january,
118            com.android.internal.R.string.month_shortest_february,
119            com.android.internal.R.string.month_shortest_march,
120            com.android.internal.R.string.month_shortest_april,
121            com.android.internal.R.string.month_shortest_may,
122            com.android.internal.R.string.month_shortest_june,
123            com.android.internal.R.string.month_shortest_july,
124            com.android.internal.R.string.month_shortest_august,
125            com.android.internal.R.string.month_shortest_september,
126            com.android.internal.R.string.month_shortest_october,
127            com.android.internal.R.string.month_shortest_november,
128            com.android.internal.R.string.month_shortest_december,
129        };
130    private static final int[] sAmPm = new int[] {
131            com.android.internal.R.string.am,
132            com.android.internal.R.string.pm,
133        };
134    private static Configuration sLastConfig;
135    private static java.text.DateFormat sStatusTimeFormat;
136    private static String sElapsedFormatMMSS;
137    private static String sElapsedFormatHMMSS;
138
139    private static final String FAST_FORMAT_HMMSS = "%1$d:%2$02d:%3$02d";
140    private static final String FAST_FORMAT_MMSS = "%1$02d:%2$02d";
141    private static final char TIME_PADDING = '0';
142    private static final char TIME_SEPARATOR = ':';
143
144
145    public static final long SECOND_IN_MILLIS = 1000;
146    public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
147    public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
148    public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
149    public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7;
150    /**
151     * This constant is actually the length of 364 days, not of a year!
152     */
153    public static final long YEAR_IN_MILLIS = WEEK_IN_MILLIS * 52;
154
155    // The following FORMAT_* symbols are used for specifying the format of
156    // dates and times in the formatDateRange method.
157    public static final int FORMAT_SHOW_TIME = 0x00001;
158    public static final int FORMAT_SHOW_WEEKDAY = 0x00002;
159    public static final int FORMAT_SHOW_YEAR = 0x00004;
160    public static final int FORMAT_NO_YEAR = 0x00008;
161    public static final int FORMAT_SHOW_DATE = 0x00010;
162    public static final int FORMAT_NO_MONTH_DAY = 0x00020;
163    public static final int FORMAT_12HOUR = 0x00040;
164    public static final int FORMAT_24HOUR = 0x00080;
165    public static final int FORMAT_CAP_AMPM = 0x00100;
166    public static final int FORMAT_NO_NOON = 0x00200;
167    public static final int FORMAT_CAP_NOON = 0x00400;
168    public static final int FORMAT_NO_MIDNIGHT = 0x00800;
169    public static final int FORMAT_CAP_MIDNIGHT = 0x01000;
170    public static final int FORMAT_UTC = 0x02000;
171    public static final int FORMAT_ABBREV_TIME = 0x04000;
172    public static final int FORMAT_ABBREV_WEEKDAY = 0x08000;
173    public static final int FORMAT_ABBREV_MONTH = 0x10000;
174    public static final int FORMAT_NUMERIC_DATE = 0x20000;
175    public static final int FORMAT_ABBREV_RELATIVE = 0x40000;
176    public static final int FORMAT_ABBREV_ALL = 0x80000;
177    public static final int FORMAT_CAP_NOON_MIDNIGHT = (FORMAT_CAP_NOON | FORMAT_CAP_MIDNIGHT);
178    public static final int FORMAT_NO_NOON_MIDNIGHT = (FORMAT_NO_NOON | FORMAT_NO_MIDNIGHT);
179
180    // Date and time format strings that are constant and don't need to be
181    // translated.
182    /**
183     * This is not actually the preferred 24-hour date format in all locales.
184     */
185    public static final String HOUR_MINUTE_24 = "%H:%M";
186    public static final String MONTH_FORMAT = "%B";
187    public static final String ABBREV_MONTH_FORMAT = "%b";
188    public static final String NUMERIC_MONTH_FORMAT = "%m";
189    public static final String MONTH_DAY_FORMAT = "%-d";
190    public static final String YEAR_FORMAT = "%Y";
191    public static final String YEAR_FORMAT_TWO_DIGITS = "%g";
192    public static final String WEEKDAY_FORMAT = "%A";
193    public static final String ABBREV_WEEKDAY_FORMAT = "%a";
194
195    // This table is used to lookup the resource string id of a format string
196    // used for formatting a start and end date that fall in the same year.
197    // The index is constructed from a bit-wise OR of the boolean values:
198    // {showTime, showYear, showWeekDay}.  For example, if showYear and
199    // showWeekDay are both true, then the index would be 3.
200    public static final int sameYearTable[] = {
201        com.android.internal.R.string.same_year_md1_md2,
202        com.android.internal.R.string.same_year_wday1_md1_wday2_md2,
203        com.android.internal.R.string.same_year_mdy1_mdy2,
204        com.android.internal.R.string.same_year_wday1_mdy1_wday2_mdy2,
205        com.android.internal.R.string.same_year_md1_time1_md2_time2,
206        com.android.internal.R.string.same_year_wday1_md1_time1_wday2_md2_time2,
207        com.android.internal.R.string.same_year_mdy1_time1_mdy2_time2,
208        com.android.internal.R.string.same_year_wday1_mdy1_time1_wday2_mdy2_time2,
209
210        // Numeric date strings
211        com.android.internal.R.string.numeric_md1_md2,
212        com.android.internal.R.string.numeric_wday1_md1_wday2_md2,
213        com.android.internal.R.string.numeric_mdy1_mdy2,
214        com.android.internal.R.string.numeric_wday1_mdy1_wday2_mdy2,
215        com.android.internal.R.string.numeric_md1_time1_md2_time2,
216        com.android.internal.R.string.numeric_wday1_md1_time1_wday2_md2_time2,
217        com.android.internal.R.string.numeric_mdy1_time1_mdy2_time2,
218        com.android.internal.R.string.numeric_wday1_mdy1_time1_wday2_mdy2_time2,
219    };
220
221    // This table is used to lookup the resource string id of a format string
222    // used for formatting a start and end date that fall in the same month.
223    // The index is constructed from a bit-wise OR of the boolean values:
224    // {showTime, showYear, showWeekDay}.  For example, if showYear and
225    // showWeekDay are both true, then the index would be 3.
226    public static final int sameMonthTable[] = {
227        com.android.internal.R.string.same_month_md1_md2,
228        com.android.internal.R.string.same_month_wday1_md1_wday2_md2,
229        com.android.internal.R.string.same_month_mdy1_mdy2,
230        com.android.internal.R.string.same_month_wday1_mdy1_wday2_mdy2,
231        com.android.internal.R.string.same_month_md1_time1_md2_time2,
232        com.android.internal.R.string.same_month_wday1_md1_time1_wday2_md2_time2,
233        com.android.internal.R.string.same_month_mdy1_time1_mdy2_time2,
234        com.android.internal.R.string.same_month_wday1_mdy1_time1_wday2_mdy2_time2,
235
236        com.android.internal.R.string.numeric_md1_md2,
237        com.android.internal.R.string.numeric_wday1_md1_wday2_md2,
238        com.android.internal.R.string.numeric_mdy1_mdy2,
239        com.android.internal.R.string.numeric_wday1_mdy1_wday2_mdy2,
240        com.android.internal.R.string.numeric_md1_time1_md2_time2,
241        com.android.internal.R.string.numeric_wday1_md1_time1_wday2_md2_time2,
242        com.android.internal.R.string.numeric_mdy1_time1_mdy2_time2,
243        com.android.internal.R.string.numeric_wday1_mdy1_time1_wday2_mdy2_time2,
244    };
245
246    /**
247     * Request the full spelled-out name. For use with the 'abbrev' parameter of
248     * {@link #getDayOfWeekString} and {@link #getMonthString}.
249     *
250     * @more <p>
251     *       e.g. "Sunday" or "January"
252     */
253    public static final int LENGTH_LONG = 10;
254
255    /**
256     * Request an abbreviated version of the name. For use with the 'abbrev'
257     * parameter of {@link #getDayOfWeekString} and {@link #getMonthString}.
258     *
259     * @more <p>
260     *       e.g. "Sun" or "Jan"
261     */
262    public static final int LENGTH_MEDIUM = 20;
263
264    /**
265     * Request a shorter abbreviated version of the name.
266     * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}.
267     * @more
268     * <p>e.g. "Su" or "Jan"
269     * <p>In most languages, the results returned for LENGTH_SHORT will be the same as
270     * the results returned for {@link #LENGTH_MEDIUM}.
271     */
272    public static final int LENGTH_SHORT = 30;
273
274    /**
275     * Request an even shorter abbreviated version of the name.
276     * Do not use this.  Currently this will always return the same result
277     * as {@link #LENGTH_SHORT}.
278     */
279    public static final int LENGTH_SHORTER = 40;
280
281    /**
282     * Request an even shorter abbreviated version of the name.
283     * For use with the 'abbrev' parameter of {@link #getDayOfWeekString} and {@link #getMonthString}.
284     * @more
285     * <p>e.g. "S", "T", "T" or "J"
286     * <p>In some languages, the results returned for LENGTH_SHORTEST will be the same as
287     * the results returned for {@link #LENGTH_SHORT}.
288     */
289    public static final int LENGTH_SHORTEST = 50;
290
291    /**
292     * Return a string for the day of the week.
293     * @param dayOfWeek One of {@link Calendar#SUNDAY Calendar.SUNDAY},
294     *               {@link Calendar#MONDAY Calendar.MONDAY}, etc.
295     * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_SHORT},
296     *               {@link #LENGTH_MEDIUM}, or {@link #LENGTH_SHORTEST}.
297     *               Note that in most languages, {@link #LENGTH_SHORT}
298     *               will return the same as {@link #LENGTH_MEDIUM}.
299     *               Undefined lengths will return {@link #LENGTH_MEDIUM}
300     *               but may return something different in the future.
301     * @throws IndexOutOfBoundsException if the dayOfWeek is out of bounds.
302     */
303    public static String getDayOfWeekString(int dayOfWeek, int abbrev) {
304        int[] list;
305        switch (abbrev) {
306            case LENGTH_LONG:       list = sDaysLong;       break;
307            case LENGTH_MEDIUM:     list = sDaysMedium;     break;
308            case LENGTH_SHORT:      list = sDaysShort;      break;
309            case LENGTH_SHORTER:    list = sDaysShort;      break;
310            case LENGTH_SHORTEST:   list = sDaysShortest;   break;
311            default:                list = sDaysMedium;     break;
312        }
313
314        Resources r = Resources.getSystem();
315        return r.getString(list[dayOfWeek - Calendar.SUNDAY]);
316    }
317
318    /**
319     * Return a localized string for AM or PM.
320     * @param ampm Either {@link Calendar#AM Calendar.AM} or {@link Calendar#PM Calendar.PM}.
321     * @throws IndexOutOfBoundsException if the ampm is out of bounds.
322     * @return Localized version of "AM" or "PM".
323     */
324    public static String getAMPMString(int ampm) {
325        Resources r = Resources.getSystem();
326        return r.getString(sAmPm[ampm - Calendar.AM]);
327    }
328
329    /**
330     * Return a localized string for the month of the year.
331     * @param month One of {@link Calendar#JANUARY Calendar.JANUARY},
332     *               {@link Calendar#FEBRUARY Calendar.FEBRUARY}, etc.
333     * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_MEDIUM},
334     *               or {@link #LENGTH_SHORTEST}.
335     *               Undefined lengths will return {@link #LENGTH_MEDIUM}
336     *               but may return something different in the future.
337     * @return Localized month of the year.
338     */
339    public static String getMonthString(int month, int abbrev) {
340        // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER.
341        // This is a shortcut to not spam the translators with too many variations
342        // of the same string.  If we find that in a language the distinction
343        // is necessary, we can can add more without changing this API.
344        int[] list;
345        switch (abbrev) {
346            case LENGTH_LONG:       list = sMonthsLong;     break;
347            case LENGTH_MEDIUM:     list = sMonthsMedium;   break;
348            case LENGTH_SHORT:      list = sMonthsMedium;   break;
349            case LENGTH_SHORTER:    list = sMonthsMedium;   break;
350            case LENGTH_SHORTEST:   list = sMonthsShortest; break;
351            default:                list = sMonthsMedium;   break;
352        }
353
354        Resources r = Resources.getSystem();
355        return r.getString(list[month - Calendar.JANUARY]);
356    }
357
358    /**
359     * Return a localized string for the month of the year, for
360     * contexts where the month is not formatted together with
361     * a day of the month.
362     *
363     * @param month One of {@link Calendar#JANUARY Calendar.JANUARY},
364     *               {@link Calendar#FEBRUARY Calendar.FEBRUARY}, etc.
365     * @param abbrev One of {@link #LENGTH_LONG}, {@link #LENGTH_MEDIUM},
366     *               or {@link #LENGTH_SHORTEST}.
367     *               Undefined lengths will return {@link #LENGTH_MEDIUM}
368     *               but may return something different in the future.
369     * @return Localized month of the year.
370     * @hide Pending API council approval
371     */
372    public static String getStandaloneMonthString(int month, int abbrev) {
373        // Note that here we use sMonthsMedium for MEDIUM, SHORT and SHORTER.
374        // This is a shortcut to not spam the translators with too many variations
375        // of the same string.  If we find that in a language the distinction
376        // is necessary, we can can add more without changing this API.
377        int[] list;
378        switch (abbrev) {
379            case LENGTH_LONG:       list = sMonthsStandaloneLong;
380                                                            break;
381            case LENGTH_MEDIUM:     list = sMonthsMedium;   break;
382            case LENGTH_SHORT:      list = sMonthsMedium;   break;
383            case LENGTH_SHORTER:    list = sMonthsMedium;   break;
384            case LENGTH_SHORTEST:   list = sMonthsShortest; break;
385            default:                list = sMonthsMedium;   break;
386        }
387
388        Resources r = Resources.getSystem();
389        return r.getString(list[month - Calendar.JANUARY]);
390    }
391
392    /**
393     * Returns a string describing the elapsed time since startTime.
394     * @param startTime some time in the past.
395     * @return a String object containing the elapsed time.
396     * @see #getRelativeTimeSpanString(long, long, long)
397     */
398    public static CharSequence getRelativeTimeSpanString(long startTime) {
399        return getRelativeTimeSpanString(startTime, System.currentTimeMillis(), MINUTE_IN_MILLIS);
400    }
401
402    /**
403     * Returns a string describing 'time' as a time relative to 'now'.
404     * <p>
405     * Time spans in the past are formatted like "42 minutes ago".
406     * Time spans in the future are formatted like "in 42 minutes".
407     *
408     * @param time the time to describe, in milliseconds
409     * @param now the current time in milliseconds
410     * @param minResolution the minimum timespan to report. For example, a time 3 seconds in the
411     *     past will be reported as "0 minutes ago" if this is set to MINUTE_IN_MILLIS. Pass one of
412     *     0, MINUTE_IN_MILLIS, HOUR_IN_MILLIS, DAY_IN_MILLIS, WEEK_IN_MILLIS
413     */
414    public static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution) {
415        int flags = FORMAT_SHOW_DATE | FORMAT_SHOW_YEAR | FORMAT_ABBREV_MONTH;
416        return getRelativeTimeSpanString(time, now, minResolution, flags);
417    }
418
419    /**
420     * Returns a string describing 'time' as a time relative to 'now'.
421     * <p>
422     * Time spans in the past are formatted like "42 minutes ago". Time spans in
423     * the future are formatted like "in 42 minutes".
424     * <p>
425     * Can use {@link #FORMAT_ABBREV_RELATIVE} flag to use abbreviated relative
426     * times, like "42 mins ago".
427     *
428     * @param time the time to describe, in milliseconds
429     * @param now the current time in milliseconds
430     * @param minResolution the minimum timespan to report. For example, a time
431     *            3 seconds in the past will be reported as "0 minutes ago" if
432     *            this is set to MINUTE_IN_MILLIS. Pass one of 0,
433     *            MINUTE_IN_MILLIS, HOUR_IN_MILLIS, DAY_IN_MILLIS,
434     *            WEEK_IN_MILLIS
435     * @param flags a bit mask of formatting options, such as
436     *            {@link #FORMAT_NUMERIC_DATE} or
437     *            {@link #FORMAT_ABBREV_RELATIVE}
438     */
439    public static CharSequence getRelativeTimeSpanString(long time, long now, long minResolution,
440            int flags) {
441        Resources r = Resources.getSystem();
442        boolean abbrevRelative = (flags & (FORMAT_ABBREV_RELATIVE | FORMAT_ABBREV_ALL)) != 0;
443
444        boolean past = (now >= time);
445        long duration = Math.abs(now - time);
446
447        int resId;
448        long count;
449        if (duration < MINUTE_IN_MILLIS && minResolution < MINUTE_IN_MILLIS) {
450            count = duration / SECOND_IN_MILLIS;
451            if (past) {
452                if (abbrevRelative) {
453                    resId = com.android.internal.R.plurals.abbrev_num_seconds_ago;
454                } else {
455                    resId = com.android.internal.R.plurals.num_seconds_ago;
456                }
457            } else {
458                if (abbrevRelative) {
459                    resId = com.android.internal.R.plurals.abbrev_in_num_seconds;
460                } else {
461                    resId = com.android.internal.R.plurals.in_num_seconds;
462                }
463            }
464        } else if (duration < HOUR_IN_MILLIS && minResolution < HOUR_IN_MILLIS) {
465            count = duration / MINUTE_IN_MILLIS;
466            if (past) {
467                if (abbrevRelative) {
468                    resId = com.android.internal.R.plurals.abbrev_num_minutes_ago;
469                } else {
470                    resId = com.android.internal.R.plurals.num_minutes_ago;
471                }
472            } else {
473                if (abbrevRelative) {
474                    resId = com.android.internal.R.plurals.abbrev_in_num_minutes;
475                } else {
476                    resId = com.android.internal.R.plurals.in_num_minutes;
477                }
478            }
479        } else if (duration < DAY_IN_MILLIS && minResolution < DAY_IN_MILLIS) {
480            count = duration / HOUR_IN_MILLIS;
481            if (past) {
482                if (abbrevRelative) {
483                    resId = com.android.internal.R.plurals.abbrev_num_hours_ago;
484                } else {
485                    resId = com.android.internal.R.plurals.num_hours_ago;
486                }
487            } else {
488                if (abbrevRelative) {
489                    resId = com.android.internal.R.plurals.abbrev_in_num_hours;
490                } else {
491                    resId = com.android.internal.R.plurals.in_num_hours;
492                }
493            }
494        } else if (duration < WEEK_IN_MILLIS && minResolution < WEEK_IN_MILLIS) {
495            count = duration / DAY_IN_MILLIS;
496            if (past) {
497                if (abbrevRelative) {
498                    resId = com.android.internal.R.plurals.abbrev_num_days_ago;
499                } else {
500                    resId = com.android.internal.R.plurals.num_days_ago;
501                }
502            } else {
503                if (abbrevRelative) {
504                    resId = com.android.internal.R.plurals.abbrev_in_num_days;
505                } else {
506                    resId = com.android.internal.R.plurals.in_num_days;
507                }
508            }
509        } else {
510            // We know that we won't be showing the time, so it is safe to pass
511            // in a null context.
512            return formatDateRange(null, time, time, flags);
513        }
514
515        String format = r.getQuantityString(resId, (int) count);
516        return String.format(format, count);
517    }
518
519    /**
520     * Return string describing the elapsed time since startTime formatted like
521     * "[relative time/date], [time]".
522     * <p>
523     * Example output strings for the US date format.
524     * <ul>
525     * <li>3 mins ago, 10:15 AM</li>
526     * <li>yesterday, 12:20 PM</li>
527     * <li>Dec 12, 4:12 AM</li>
528     * <li>11/14/2007, 8:20 AM</li>
529     * </ul>
530     *
531     * @param time some time in the past.
532     * @param minResolution the minimum elapsed time (in milliseconds) to report
533     *            when showing relative times. For example, a time 3 seconds in
534     *            the past will be reported as "0 minutes ago" if this is set to
535     *            {@link #MINUTE_IN_MILLIS}.
536     * @param transitionResolution the elapsed time (in milliseconds) at which
537     *            to stop reporting relative measurements. Elapsed times greater
538     *            than this resolution will default to normal date formatting.
539     *            For example, will transition from "6 days ago" to "Dec 12"
540     *            when using {@link #WEEK_IN_MILLIS}.
541     */
542    public static CharSequence getRelativeDateTimeString(Context c, long time, long minResolution,
543            long transitionResolution, int flags) {
544        Resources r = Resources.getSystem();
545
546        long now = System.currentTimeMillis();
547        long duration = Math.abs(now - time);
548
549        // getRelativeTimeSpanString() doesn't correctly format relative dates
550        // above a week or exact dates below a day, so clamp
551        // transitionResolution as needed.
552        if (transitionResolution > WEEK_IN_MILLIS) {
553            transitionResolution = WEEK_IN_MILLIS;
554        } else if (transitionResolution < DAY_IN_MILLIS) {
555            transitionResolution = DAY_IN_MILLIS;
556        }
557
558        CharSequence timeClause = formatDateRange(c, time, time, FORMAT_SHOW_TIME);
559
560        String result;
561        if (duration < transitionResolution) {
562            CharSequence relativeClause = getRelativeTimeSpanString(time, now, minResolution, flags);
563            result = r.getString(com.android.internal.R.string.relative_time, relativeClause, timeClause);
564        } else {
565            CharSequence dateClause = getRelativeTimeSpanString(c, time, false);
566            result = r.getString(com.android.internal.R.string.date_time, dateClause, timeClause);
567        }
568
569        return result;
570    }
571
572    /**
573     * Returns a string describing a day relative to the current day. For example if the day is
574     * today this function returns "Today", if the day was a week ago it returns "7 days ago", and
575     * if the day is in 2 weeks it returns "in 14 days".
576     *
577     * @param r the resources to get the strings from
578     * @param day the relative day to describe in UTC milliseconds
579     * @param today the current time in UTC milliseconds
580     * @return a formatting string
581     */
582    private static final String getRelativeDayString(Resources r, long day, long today) {
583        Time startTime = new Time();
584        startTime.set(day);
585        Time currentTime = new Time();
586        currentTime.set(today);
587
588        int startDay = Time.getJulianDay(day, startTime.gmtoff);
589        int currentDay = Time.getJulianDay(today, currentTime.gmtoff);
590
591        int days = Math.abs(currentDay - startDay);
592        boolean past = (today > day);
593
594        if (days == 1) {
595            if (past) {
596                return r.getString(com.android.internal.R.string.yesterday);
597            } else {
598                return r.getString(com.android.internal.R.string.tomorrow);
599            }
600        } else if (days == 0) {
601            return r.getString(com.android.internal.R.string.today);
602        }
603
604        int resId;
605        if (past) {
606            resId = com.android.internal.R.plurals.num_days_ago;
607        } else {
608            resId = com.android.internal.R.plurals.in_num_days;
609        }
610
611        String format = r.getQuantityString(resId, days);
612        return String.format(format, days);
613    }
614
615    private static void initFormatStrings() {
616        synchronized (sLock) {
617            Resources r = Resources.getSystem();
618            Configuration cfg = r.getConfiguration();
619            if (sLastConfig == null || !sLastConfig.equals(cfg)) {
620                sLastConfig = cfg;
621                sStatusTimeFormat = java.text.DateFormat.getTimeInstance(java.text.DateFormat.SHORT);
622                sElapsedFormatMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_mm_ss);
623                sElapsedFormatHMMSS = r.getString(com.android.internal.R.string.elapsed_time_short_format_h_mm_ss);
624            }
625        }
626    }
627
628    /**
629     * Format a time so it appears like it would in the status bar clock.
630     * @deprecated use {@link #DateFormat.getTimeFormat(Context)} instead.
631     * @hide
632     */
633    public static final CharSequence timeString(long millis) {
634        initFormatStrings();
635        return sStatusTimeFormat.format(millis);
636    }
637
638    /**
639     * Formats an elapsed time in the form "MM:SS" or "H:MM:SS"
640     * for display on the call-in-progress screen.
641     * @param elapsedSeconds the elapsed time in seconds.
642     */
643    public static String formatElapsedTime(long elapsedSeconds) {
644        return formatElapsedTime(null, elapsedSeconds);
645    }
646
647    /**
648     * Formats an elapsed time in the form "MM:SS" or "H:MM:SS"
649     * for display on the call-in-progress screen.
650     *
651     * @param recycle {@link StringBuilder} to recycle, if possible
652     * @param elapsedSeconds the elapsed time in seconds.
653     */
654    public static String formatElapsedTime(StringBuilder recycle, long elapsedSeconds) {
655        initFormatStrings();
656
657        long hours = 0;
658        long minutes = 0;
659        long seconds = 0;
660
661        if (elapsedSeconds >= 3600) {
662            hours = elapsedSeconds / 3600;
663            elapsedSeconds -= hours * 3600;
664        }
665        if (elapsedSeconds >= 60) {
666            minutes = elapsedSeconds / 60;
667            elapsedSeconds -= minutes * 60;
668        }
669        seconds = elapsedSeconds;
670
671        String result;
672        if (hours > 0) {
673            return formatElapsedTime(recycle, sElapsedFormatHMMSS, hours, minutes, seconds);
674        } else {
675            return formatElapsedTime(recycle, sElapsedFormatMMSS, minutes, seconds);
676        }
677    }
678
679    /**
680     * Fast formatting of h:mm:ss
681     */
682    private static String formatElapsedTime(StringBuilder recycle, String format, long hours,
683            long minutes, long seconds) {
684        if (FAST_FORMAT_HMMSS.equals(format)) {
685            StringBuilder sb = recycle;
686            if (sb == null) {
687                sb = new StringBuilder(8);
688            } else {
689                sb.setLength(0);
690            }
691            sb.append(hours);
692            sb.append(TIME_SEPARATOR);
693            if (minutes < 10) {
694                sb.append(TIME_PADDING);
695            } else {
696                sb.append(toDigitChar(minutes / 10));
697            }
698            sb.append(toDigitChar(minutes % 10));
699            sb.append(TIME_SEPARATOR);
700            if (seconds < 10) {
701                sb.append(TIME_PADDING);
702            } else {
703                sb.append(toDigitChar(seconds / 10));
704            }
705            sb.append(toDigitChar(seconds % 10));
706            return sb.toString();
707        } else {
708            return String.format(format, hours, minutes, seconds);
709        }
710    }
711
712    /**
713     * Fast formatting of m:ss
714     */
715    private static String formatElapsedTime(StringBuilder recycle, String format, long minutes,
716            long seconds) {
717        if (FAST_FORMAT_MMSS.equals(format)) {
718            StringBuilder sb = recycle;
719            if (sb == null) {
720                sb = new StringBuilder(8);
721            } else {
722                sb.setLength(0);
723            }
724            if (minutes < 10) {
725                sb.append(TIME_PADDING);
726            } else {
727                sb.append(toDigitChar(minutes / 10));
728            }
729            sb.append(toDigitChar(minutes % 10));
730            sb.append(TIME_SEPARATOR);
731            if (seconds < 10) {
732                sb.append(TIME_PADDING);
733            } else {
734                sb.append(toDigitChar(seconds / 10));
735            }
736            sb.append(toDigitChar(seconds % 10));
737            return sb.toString();
738        } else {
739            return String.format(format, minutes, seconds);
740        }
741    }
742
743    private static char toDigitChar(long digit) {
744        return (char) (digit + '0');
745    }
746
747    /**
748     * Format a date / time such that if the then is on the same day as now, it shows
749     * just the time and if it's a different day, it shows just the date.
750     *
751     * <p>The parameters dateFormat and timeFormat should each be one of
752     * {@link java.text.DateFormat#DEFAULT},
753     * {@link java.text.DateFormat#FULL},
754     * {@link java.text.DateFormat#LONG},
755     * {@link java.text.DateFormat#MEDIUM}
756     * or
757     * {@link java.text.DateFormat#SHORT}
758     *
759     * @param then the date to format
760     * @param now the base time
761     * @param dateStyle how to format the date portion.
762     * @param timeStyle how to format the time portion.
763     */
764    public static final CharSequence formatSameDayTime(long then, long now,
765            int dateStyle, int timeStyle) {
766        Calendar thenCal = new GregorianCalendar();
767        thenCal.setTimeInMillis(then);
768        Date thenDate = thenCal.getTime();
769        Calendar nowCal = new GregorianCalendar();
770        nowCal.setTimeInMillis(now);
771
772        java.text.DateFormat f;
773
774        if (thenCal.get(Calendar.YEAR) == nowCal.get(Calendar.YEAR)
775                && thenCal.get(Calendar.MONTH) == nowCal.get(Calendar.MONTH)
776                && thenCal.get(Calendar.DAY_OF_MONTH) == nowCal.get(Calendar.DAY_OF_MONTH)) {
777            f = java.text.DateFormat.getTimeInstance(timeStyle);
778        } else {
779            f = java.text.DateFormat.getDateInstance(dateStyle);
780        }
781        return f.format(thenDate);
782    }
783
784    /**
785     * @hide
786     * @deprecated use {@link android.text.format.Time}
787     */
788    public static Calendar newCalendar(boolean zulu)
789    {
790        if (zulu)
791            return Calendar.getInstance(TimeZone.getTimeZone("GMT"));
792
793        return Calendar.getInstance();
794    }
795
796    /**
797     * @return true if the supplied when is today else false
798     */
799    public static boolean isToday(long when) {
800        Time time = new Time();
801        time.set(when);
802
803        int thenYear = time.year;
804        int thenMonth = time.month;
805        int thenMonthDay = time.monthDay;
806
807        time.set(System.currentTimeMillis());
808        return (thenYear == time.year)
809                && (thenMonth == time.month)
810                && (thenMonthDay == time.monthDay);
811    }
812
813    /**
814     * @hide
815     * @deprecated use {@link android.text.format.Time}
816     */
817    private static final int ctoi(String str, int index)
818                                                throws DateException
819    {
820        char c = str.charAt(index);
821        if (c >= '0' && c <= '9') {
822            return (int)(c - '0');
823        }
824        throw new DateException("Expected numeric character.  Got '" +
825                                            c + "'");
826    }
827
828    /**
829     * @hide
830     * @deprecated use {@link android.text.format.Time}
831     */
832    private static final int check(int lowerBound, int upperBound, int value)
833                                                throws DateException
834    {
835        if (value >= lowerBound && value <= upperBound) {
836            return value;
837        }
838        throw new DateException("field out of bounds.  max=" + upperBound
839                                        + " value=" + value);
840    }
841
842    /**
843     * @hide
844     * @deprecated use {@link android.text.format.Time}
845     * Return true if this date string is local time
846     */
847    public static boolean isUTC(String s)
848    {
849        if (s.length() == 16 && s.charAt(15) == 'Z') {
850            return true;
851        }
852        if (s.length() == 9 && s.charAt(8) == 'Z') {
853            // XXX not sure if this case possible/valid
854            return true;
855        }
856        return false;
857    }
858
859
860    // note that month in Calendar is 0 based and in all other human
861    // representations, it's 1 based.
862    // Returns if the Z was present, meaning that the time is in UTC
863    /**
864     * @hide
865     * @deprecated use {@link android.text.format.Time}
866     */
867    public static boolean parseDateTime(String str, Calendar cal)
868                                                throws DateException
869    {
870        int len = str.length();
871        boolean dateTime = (len == 15 || len == 16) && str.charAt(8) == 'T';
872        boolean justDate = len == 8;
873        if (dateTime || justDate) {
874            cal.clear();
875            cal.set(Calendar.YEAR,
876                            ctoi(str, 0)*1000 + ctoi(str, 1)*100
877                            + ctoi(str, 2)*10 + ctoi(str, 3));
878            cal.set(Calendar.MONTH,
879                            check(0, 11, ctoi(str, 4)*10 + ctoi(str, 5) - 1));
880            cal.set(Calendar.DAY_OF_MONTH,
881                            check(1, 31, ctoi(str, 6)*10 + ctoi(str, 7)));
882            if (dateTime) {
883                cal.set(Calendar.HOUR_OF_DAY,
884                            check(0, 23, ctoi(str, 9)*10 + ctoi(str, 10)));
885                cal.set(Calendar.MINUTE,
886                            check(0, 59, ctoi(str, 11)*10 + ctoi(str, 12)));
887                cal.set(Calendar.SECOND,
888                            check(0, 59, ctoi(str, 13)*10 + ctoi(str, 14)));
889            }
890            if (justDate) {
891                cal.set(Calendar.HOUR_OF_DAY, 0);
892                cal.set(Calendar.MINUTE, 0);
893                cal.set(Calendar.SECOND, 0);
894                return true;
895            }
896            if (len == 15) {
897                return false;
898            }
899            if (str.charAt(15) == 'Z') {
900                return true;
901            }
902        }
903        throw new DateException("Invalid time (expected "
904                                + "YYYYMMDDThhmmssZ? got '" + str + "').");
905    }
906
907    /**
908     * Given a timezone string which can be null, and a dateTime string,
909     * set that time into a calendar.
910     * @hide
911     * @deprecated use {@link android.text.format.Time}
912     */
913    public static void parseDateTime(String tz, String dateTime, Calendar out)
914                                                throws DateException
915    {
916        TimeZone timezone;
917        if (DateUtils.isUTC(dateTime)) {
918            timezone = TimeZone.getTimeZone("UTC");
919        }
920        else if (tz == null) {
921            timezone = TimeZone.getDefault();
922        }
923        else {
924            timezone = TimeZone.getTimeZone(tz);
925        }
926
927        Calendar local = new GregorianCalendar(timezone);
928        DateUtils.parseDateTime(dateTime, local);
929
930        out.setTimeInMillis(local.getTimeInMillis());
931    }
932
933
934    /**
935     * Return a string containing the date and time in RFC2445 format.
936     * Ensures that the time is written in UTC.  The Calendar class doesn't
937     * really help out with this, so this is slower than it ought to be.
938     *
939     * @param cal the date and time to write
940     * @hide
941     * @deprecated use {@link android.text.format.Time}
942     */
943    public static String writeDateTime(Calendar cal)
944    {
945        TimeZone tz = TimeZone.getTimeZone("GMT");
946        GregorianCalendar c = new GregorianCalendar(tz);
947        c.setTimeInMillis(cal.getTimeInMillis());
948        return writeDateTime(c, true);
949    }
950
951    /**
952     * Return a string containing the date and time in RFC2445 format.
953     *
954     * @param cal the date and time to write
955     * @param zulu If the calendar is in UTC, pass true, and a Z will
956     * be written at the end as per RFC2445.  Otherwise, the time is
957     * considered in localtime.
958     * @hide
959     * @deprecated use {@link android.text.format.Time}
960     */
961    public static String writeDateTime(Calendar cal, boolean zulu)
962    {
963        StringBuilder sb = new StringBuilder();
964        sb.ensureCapacity(16);
965        if (zulu) {
966            sb.setLength(16);
967            sb.setCharAt(15, 'Z');
968        } else {
969            sb.setLength(15);
970        }
971        return writeDateTime(cal, sb);
972    }
973
974    /**
975     * Return a string containing the date and time in RFC2445 format.
976     *
977     * @param cal the date and time to write
978     * @param sb a StringBuilder to use.  It is assumed that setLength
979     *           has already been called on sb to the appropriate length
980     *           which is sb.setLength(zulu ? 16 : 15)
981     * @hide
982     * @deprecated use {@link android.text.format.Time}
983     */
984    public static String writeDateTime(Calendar cal, StringBuilder sb)
985    {
986        int n;
987
988        n = cal.get(Calendar.YEAR);
989        sb.setCharAt(3, (char)('0'+n%10));
990        n /= 10;
991        sb.setCharAt(2, (char)('0'+n%10));
992        n /= 10;
993        sb.setCharAt(1, (char)('0'+n%10));
994        n /= 10;
995        sb.setCharAt(0, (char)('0'+n%10));
996
997        n = cal.get(Calendar.MONTH) + 1;
998        sb.setCharAt(5, (char)('0'+n%10));
999        n /= 10;
1000        sb.setCharAt(4, (char)('0'+n%10));
1001
1002        n = cal.get(Calendar.DAY_OF_MONTH);
1003        sb.setCharAt(7, (char)('0'+n%10));
1004        n /= 10;
1005        sb.setCharAt(6, (char)('0'+n%10));
1006
1007        sb.setCharAt(8, 'T');
1008
1009        n = cal.get(Calendar.HOUR_OF_DAY);
1010        sb.setCharAt(10, (char)('0'+n%10));
1011        n /= 10;
1012        sb.setCharAt(9, (char)('0'+n%10));
1013
1014        n = cal.get(Calendar.MINUTE);
1015        sb.setCharAt(12, (char)('0'+n%10));
1016        n /= 10;
1017        sb.setCharAt(11, (char)('0'+n%10));
1018
1019        n = cal.get(Calendar.SECOND);
1020        sb.setCharAt(14, (char)('0'+n%10));
1021        n /= 10;
1022        sb.setCharAt(13, (char)('0'+n%10));
1023
1024        return sb.toString();
1025    }
1026
1027    /**
1028     * @hide
1029     * @deprecated use {@link android.text.format.Time}
1030     */
1031    public static void assign(Calendar lval, Calendar rval)
1032    {
1033        // there should be a faster way.
1034        lval.clear();
1035        lval.setTimeInMillis(rval.getTimeInMillis());
1036    }
1037
1038    /**
1039     * Formats a date or a time range according to the local conventions.
1040     *
1041     * <p>
1042     * Example output strings (date formats in these examples are shown using
1043     * the US date format convention but that may change depending on the
1044     * local settings):
1045     * <ul>
1046     *   <li>10:15am</li>
1047     *   <li>3:00pm - 4:00pm</li>
1048     *   <li>3pm - 4pm</li>
1049     *   <li>3PM - 4PM</li>
1050     *   <li>08:00 - 17:00</li>
1051     *   <li>Oct 9</li>
1052     *   <li>Tue, Oct 9</li>
1053     *   <li>October 9, 2007</li>
1054     *   <li>Oct 9 - 10</li>
1055     *   <li>Oct 9 - 10, 2007</li>
1056     *   <li>Oct 28 - Nov 3, 2007</li>
1057     *   <li>Dec 31, 2007 - Jan 1, 2008</li>
1058     *   <li>Oct 9, 8:00am - Oct 10, 5:00pm</li>
1059     *   <li>12/31/2007 - 01/01/2008</li>
1060     * </ul>
1061     *
1062     * <p>
1063     * The flags argument is a bitmask of options from the following list:
1064     *
1065     * <ul>
1066     *   <li>FORMAT_SHOW_TIME</li>
1067     *   <li>FORMAT_SHOW_WEEKDAY</li>
1068     *   <li>FORMAT_SHOW_YEAR</li>
1069     *   <li>FORMAT_NO_YEAR</li>
1070     *   <li>FORMAT_SHOW_DATE</li>
1071     *   <li>FORMAT_NO_MONTH_DAY</li>
1072     *   <li>FORMAT_12HOUR</li>
1073     *   <li>FORMAT_24HOUR</li>
1074     *   <li>FORMAT_CAP_AMPM</li>
1075     *   <li>FORMAT_NO_NOON</li>
1076     *   <li>FORMAT_CAP_NOON</li>
1077     *   <li>FORMAT_NO_MIDNIGHT</li>
1078     *   <li>FORMAT_CAP_MIDNIGHT</li>
1079     *   <li>FORMAT_UTC</li>
1080     *   <li>FORMAT_ABBREV_TIME</li>
1081     *   <li>FORMAT_ABBREV_WEEKDAY</li>
1082     *   <li>FORMAT_ABBREV_MONTH</li>
1083     *   <li>FORMAT_ABBREV_ALL</li>
1084     *   <li>FORMAT_NUMERIC_DATE</li>
1085     * </ul>
1086     *
1087     * <p>
1088     * If FORMAT_SHOW_TIME is set, the time is shown as part of the date range.
1089     * If the start and end time are the same, then just the start time is
1090     * shown.
1091     *
1092     * <p>
1093     * If FORMAT_SHOW_WEEKDAY is set, then the weekday is shown.
1094     *
1095     * <p>
1096     * If FORMAT_SHOW_YEAR is set, then the year is always shown.
1097     * If FORMAT_NO_YEAR is set, then the year is not shown.
1098     * If neither FORMAT_SHOW_YEAR nor FORMAT_NO_YEAR are set, then the year
1099     * is shown only if it is different from the current year, or if the start
1100     * and end dates fall on different years.  If both are set,
1101     * FORMAT_SHOW_YEAR takes precedence.
1102     *
1103     * <p>
1104     * Normally the date is shown unless the start and end day are the same.
1105     * If FORMAT_SHOW_DATE is set, then the date is always shown, even for
1106     * same day ranges.
1107     *
1108     * <p>
1109     * If FORMAT_NO_MONTH_DAY is set, then if the date is shown, just the
1110     * month name will be shown, not the day of the month.  For example,
1111     * "January, 2008" instead of "January 6 - 12, 2008".
1112     *
1113     * <p>
1114     * If FORMAT_CAP_AMPM is set and 12-hour time is used, then the "AM"
1115     * and "PM" are capitalized.  You should not use this flag
1116     * because in some locales these terms cannot be capitalized, and in
1117     * many others it doesn't make sense to do so even though it is possible.
1118     *
1119     * <p>
1120     * If FORMAT_NO_NOON is set and 12-hour time is used, then "12pm" is
1121     * shown instead of "noon".
1122     *
1123     * <p>
1124     * If FORMAT_CAP_NOON is set and 12-hour time is used, then "Noon" is
1125     * shown instead of "noon".  You should probably not use this flag
1126     * because in many locales it will not make sense to capitalize
1127     * the term.
1128     *
1129     * <p>
1130     * If FORMAT_NO_MIDNIGHT is set and 12-hour time is used, then "12am" is
1131     * shown instead of "midnight".
1132     *
1133     * <p>
1134     * If FORMAT_CAP_MIDNIGHT is set and 12-hour time is used, then "Midnight"
1135     * is shown instead of "midnight".  You should probably not use this
1136     * flag because in many locales it will not make sense to capitalize
1137     * the term.
1138     *
1139     * <p>
1140     * If FORMAT_12HOUR is set and the time is shown, then the time is
1141     * shown in the 12-hour time format. You should not normally set this.
1142     * Instead, let the time format be chosen automatically according to the
1143     * system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then
1144     * FORMAT_24HOUR takes precedence.
1145     *
1146     * <p>
1147     * If FORMAT_24HOUR is set and the time is shown, then the time is
1148     * shown in the 24-hour time format. You should not normally set this.
1149     * Instead, let the time format be chosen automatically according to the
1150     * system settings. If both FORMAT_12HOUR and FORMAT_24HOUR are set, then
1151     * FORMAT_24HOUR takes precedence.
1152     *
1153     * <p>
1154     * If FORMAT_UTC is set, then the UTC timezone is used for the start
1155     * and end milliseconds.
1156     *
1157     * <p>
1158     * If FORMAT_ABBREV_TIME is set and 12-hour time format is used, then the
1159     * start and end times (if shown) are abbreviated by not showing the minutes
1160     * if they are zero.  For example, instead of "3:00pm" the time would be
1161     * abbreviated to "3pm".
1162     *
1163     * <p>
1164     * If FORMAT_ABBREV_WEEKDAY is set, then the weekday (if shown) is
1165     * abbreviated to a 3-letter string.
1166     *
1167     * <p>
1168     * If FORMAT_ABBREV_MONTH is set, then the month (if shown) is abbreviated
1169     * to a 3-letter string.
1170     *
1171     * <p>
1172     * If FORMAT_ABBREV_ALL is set, then the weekday and the month (if shown)
1173     * are abbreviated to 3-letter strings.
1174     *
1175     * <p>
1176     * If FORMAT_NUMERIC_DATE is set, then the date is shown in numeric format
1177     * instead of using the name of the month.  For example, "12/31/2008"
1178     * instead of "December 31, 2008".
1179     *
1180     * @param context the context is required only if the time is shown
1181     * @param startMillis the start time in UTC milliseconds
1182     * @param endMillis the end time in UTC milliseconds
1183     * @param flags a bit mask of options
1184     *
1185     * @return a string containing the formatted date/time range.
1186     */
1187    public static String formatDateRange(Context context, long startMillis,
1188                long endMillis, int flags) {
1189        Resources res = Resources.getSystem();
1190        boolean showTime = (flags & FORMAT_SHOW_TIME) != 0;
1191        boolean showWeekDay = (flags & FORMAT_SHOW_WEEKDAY) != 0;
1192        boolean showYear = (flags & FORMAT_SHOW_YEAR) != 0;
1193        boolean noYear = (flags & FORMAT_NO_YEAR) != 0;
1194        boolean useUTC = (flags & FORMAT_UTC) != 0;
1195        boolean abbrevWeekDay = (flags & (FORMAT_ABBREV_WEEKDAY | FORMAT_ABBREV_ALL)) != 0;
1196        boolean abbrevMonth = (flags & (FORMAT_ABBREV_MONTH | FORMAT_ABBREV_ALL)) != 0;
1197        boolean noMonthDay = (flags & FORMAT_NO_MONTH_DAY) != 0;
1198        boolean numericDate = (flags & FORMAT_NUMERIC_DATE) != 0;
1199
1200        // If we're getting called with a single instant in time (from
1201        // e.g. formatDateTime(), below), then we can skip a lot of
1202        // computation below that'd otherwise be thrown out.
1203        boolean isInstant = (startMillis == endMillis);
1204
1205        Time startDate = useUTC ? new Time(Time.TIMEZONE_UTC) : new Time();
1206        startDate.set(startMillis);
1207
1208        Time endDate;
1209        int dayDistance;
1210        if (isInstant) {
1211            endDate = startDate;
1212            dayDistance = 0;
1213        } else {
1214            endDate = useUTC ? new Time(Time.TIMEZONE_UTC) : new Time();
1215            endDate.set(endMillis);
1216            int startJulianDay = Time.getJulianDay(startMillis, startDate.gmtoff);
1217            int endJulianDay = Time.getJulianDay(endMillis, endDate.gmtoff);
1218            dayDistance = endJulianDay - startJulianDay;
1219        }
1220
1221        // If the end date ends at 12am at the beginning of a day,
1222        // then modify it to make it look like it ends at midnight on
1223        // the previous day.  This will allow us to display "8pm - midnight",
1224        // for example, instead of "Nov 10, 8pm - Nov 11, 12am". But we only do
1225        // this if it is midnight of the same day as the start date because
1226        // for multiple-day events, an end time of "midnight on Nov 11" is
1227        // ambiguous and confusing (is that midnight the start of Nov 11, or
1228        // the end of Nov 11?).
1229        // If we are not showing the time then also adjust the end date
1230        // for multiple-day events.  This is to allow us to display, for
1231        // example, "Nov 10 -11" for an event with a start date of Nov 10
1232        // and an end date of Nov 12 at 00:00.
1233        // If the start and end time are the same, then skip this and don't
1234        // adjust the date.
1235        if (!isInstant
1236            && (endDate.hour | endDate.minute | endDate.second) == 0
1237            && (!showTime || dayDistance <= 1)) {
1238            endDate.monthDay -= 1;
1239            endDate.normalize(true /* ignore isDst */);
1240        }
1241
1242        int startDay = startDate.monthDay;
1243        int startMonthNum = startDate.month;
1244        int startYear = startDate.year;
1245
1246        int endDay = endDate.monthDay;
1247        int endMonthNum = endDate.month;
1248        int endYear = endDate.year;
1249
1250        String startWeekDayString = "";
1251        String endWeekDayString = "";
1252        if (showWeekDay) {
1253            String weekDayFormat = "";
1254            if (abbrevWeekDay) {
1255                weekDayFormat = ABBREV_WEEKDAY_FORMAT;
1256            } else {
1257                weekDayFormat = WEEKDAY_FORMAT;
1258            }
1259            startWeekDayString = startDate.format(weekDayFormat);
1260            endWeekDayString = isInstant ? startWeekDayString : endDate.format(weekDayFormat);
1261        }
1262
1263        String startTimeString = "";
1264        String endTimeString = "";
1265        if (showTime) {
1266            String startTimeFormat = "";
1267            String endTimeFormat = "";
1268            boolean force24Hour = (flags & FORMAT_24HOUR) != 0;
1269            boolean force12Hour = (flags & FORMAT_12HOUR) != 0;
1270            boolean use24Hour;
1271            if (force24Hour) {
1272                use24Hour = true;
1273            } else if (force12Hour) {
1274                use24Hour = false;
1275            } else {
1276                use24Hour = DateFormat.is24HourFormat(context);
1277            }
1278            if (use24Hour) {
1279                startTimeFormat = endTimeFormat =
1280                    res.getString(com.android.internal.R.string.hour_minute_24);
1281            } else {
1282                boolean abbrevTime = (flags & (FORMAT_ABBREV_TIME | FORMAT_ABBREV_ALL)) != 0;
1283                boolean capAMPM = (flags & FORMAT_CAP_AMPM) != 0;
1284                boolean noNoon = (flags & FORMAT_NO_NOON) != 0;
1285                boolean capNoon = (flags & FORMAT_CAP_NOON) != 0;
1286                boolean noMidnight = (flags & FORMAT_NO_MIDNIGHT) != 0;
1287                boolean capMidnight = (flags & FORMAT_CAP_MIDNIGHT) != 0;
1288
1289                boolean startOnTheHour = startDate.minute == 0 && startDate.second == 0;
1290                boolean endOnTheHour = endDate.minute == 0 && endDate.second == 0;
1291                if (abbrevTime && startOnTheHour) {
1292                    if (capAMPM) {
1293                        startTimeFormat = res.getString(com.android.internal.R.string.hour_cap_ampm);
1294                    } else {
1295                        startTimeFormat = res.getString(com.android.internal.R.string.hour_ampm);
1296                    }
1297                } else {
1298                    if (capAMPM) {
1299                        startTimeFormat = res.getString(com.android.internal.R.string.hour_minute_cap_ampm);
1300                    } else {
1301                        startTimeFormat = res.getString(com.android.internal.R.string.hour_minute_ampm);
1302                    }
1303                }
1304
1305                // Don't waste time on setting endTimeFormat when
1306                // we're dealing with an instant, where we'll never
1307                // need the end point.  (It's the same as the start
1308                // point)
1309                if (!isInstant) {
1310                    if (abbrevTime && endOnTheHour) {
1311                        if (capAMPM) {
1312                            endTimeFormat = res.getString(com.android.internal.R.string.hour_cap_ampm);
1313                        } else {
1314                            endTimeFormat = res.getString(com.android.internal.R.string.hour_ampm);
1315                        }
1316                    } else {
1317                        if (capAMPM) {
1318                            endTimeFormat = res.getString(com.android.internal.R.string.hour_minute_cap_ampm);
1319                        } else {
1320                            endTimeFormat = res.getString(com.android.internal.R.string.hour_minute_ampm);
1321                        }
1322                    }
1323
1324                    if (endDate.hour == 12 && endOnTheHour && !noNoon) {
1325                        if (capNoon) {
1326                            endTimeFormat = res.getString(com.android.internal.R.string.Noon);
1327                        } else {
1328                            endTimeFormat = res.getString(com.android.internal.R.string.noon);
1329                        }
1330                    } else if (endDate.hour == 0 && endOnTheHour && !noMidnight) {
1331                        if (capMidnight) {
1332                            endTimeFormat = res.getString(com.android.internal.R.string.Midnight);
1333                        } else {
1334                            endTimeFormat = res.getString(com.android.internal.R.string.midnight);
1335                        }
1336                    }
1337                }
1338
1339                if (startDate.hour == 12 && startOnTheHour && !noNoon) {
1340                    if (capNoon) {
1341                        startTimeFormat = res.getString(com.android.internal.R.string.Noon);
1342                    } else {
1343                        startTimeFormat = res.getString(com.android.internal.R.string.noon);
1344                    }
1345                    // Don't show the start time starting at midnight.  Show
1346                    // 12am instead.
1347                }
1348            }
1349
1350            startTimeString = startDate.format(startTimeFormat);
1351            endTimeString = isInstant ? startTimeString : endDate.format(endTimeFormat);
1352        }
1353
1354        // Show the year if the user specified FORMAT_SHOW_YEAR or if
1355        // the starting and end years are different from each other
1356        // or from the current year.  But don't show the year if the
1357        // user specified FORMAT_NO_YEAR.
1358        if (showYear) {
1359            // No code... just a comment for clarity.  Keep showYear
1360            // on, as they enabled it with FORMAT_SHOW_YEAR.  This
1361            // takes precedence over them setting FORMAT_NO_YEAR.
1362        } else if (noYear) {
1363            // They explicitly didn't want a year.
1364            showYear = false;
1365        } else if (startYear != endYear) {
1366            showYear = true;
1367        } else {
1368            // Show the year if it's not equal to the current year.
1369            Time currentTime = new Time();
1370            currentTime.setToNow();
1371            showYear = startYear != currentTime.year;
1372        }
1373
1374        String defaultDateFormat, fullFormat, dateRange;
1375        if (numericDate) {
1376            defaultDateFormat = res.getString(com.android.internal.R.string.numeric_date);
1377        } else if (showYear) {
1378            if (abbrevMonth) {
1379                if (noMonthDay) {
1380                    defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_year);
1381                } else {
1382                    defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_day_year);
1383                }
1384            } else {
1385                if (noMonthDay) {
1386                    defaultDateFormat = res.getString(com.android.internal.R.string.month_year);
1387                } else {
1388                    defaultDateFormat = res.getString(com.android.internal.R.string.month_day_year);
1389                }
1390            }
1391        } else {
1392            if (abbrevMonth) {
1393                if (noMonthDay) {
1394                    defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month);
1395                } else {
1396                    defaultDateFormat = res.getString(com.android.internal.R.string.abbrev_month_day);
1397                }
1398            } else {
1399                if (noMonthDay) {
1400                    defaultDateFormat = res.getString(com.android.internal.R.string.month);
1401                } else {
1402                    defaultDateFormat = res.getString(com.android.internal.R.string.month_day);
1403                }
1404            }
1405        }
1406
1407        if (showWeekDay) {
1408            if (showTime) {
1409                fullFormat = res.getString(com.android.internal.R.string.wday1_date1_time1_wday2_date2_time2);
1410            } else {
1411                fullFormat = res.getString(com.android.internal.R.string.wday1_date1_wday2_date2);
1412            }
1413        } else {
1414            if (showTime) {
1415                fullFormat = res.getString(com.android.internal.R.string.date1_time1_date2_time2);
1416            } else {
1417                fullFormat = res.getString(com.android.internal.R.string.date1_date2);
1418            }
1419        }
1420
1421        if (noMonthDay && startMonthNum == endMonthNum) {
1422            // Example: "January, 2008"
1423            String startDateString = startDate.format(defaultDateFormat);
1424            return startDateString;
1425        }
1426
1427        if (startYear != endYear || noMonthDay) {
1428            // Different year or we are not showing the month day number.
1429            // Example: "December 31, 2007 - January 1, 2008"
1430            // Or: "January - February, 2008"
1431            String startDateString = startDate.format(defaultDateFormat);
1432            String endDateString = endDate.format(defaultDateFormat);
1433
1434            // The values that are used in a fullFormat string are specified
1435            // by position.
1436            dateRange = String.format(fullFormat,
1437                    startWeekDayString, startDateString, startTimeString,
1438                    endWeekDayString, endDateString, endTimeString);
1439            return dateRange;
1440        }
1441
1442        // Get the month, day, and year strings for the start and end dates
1443        String monthFormat;
1444        if (numericDate) {
1445            monthFormat = NUMERIC_MONTH_FORMAT;
1446        } else if (abbrevMonth) {
1447            monthFormat = ABBREV_MONTH_FORMAT;
1448        } else {
1449            monthFormat = MONTH_FORMAT;
1450        }
1451        String startMonthString = startDate.format(monthFormat);
1452        String startMonthDayString = startDate.format(MONTH_DAY_FORMAT);
1453        String startYearString = startDate.format(YEAR_FORMAT);
1454
1455        String endMonthString = isInstant ? null : endDate.format(monthFormat);
1456        String endMonthDayString = isInstant ? null : endDate.format(MONTH_DAY_FORMAT);
1457        String endYearString = isInstant ? null : endDate.format(YEAR_FORMAT);
1458
1459        if (startMonthNum != endMonthNum) {
1460            // Same year, different month.
1461            // Example: "October 28 - November 3"
1462            // or: "Wed, Oct 31 - Sat, Nov 3, 2007"
1463            // or: "Oct 31, 8am - Sat, Nov 3, 2007, 5pm"
1464
1465            int index = 0;
1466            if (showWeekDay) index = 1;
1467            if (showYear) index += 2;
1468            if (showTime) index += 4;
1469            if (numericDate) index += 8;
1470            int resId = sameYearTable[index];
1471            fullFormat = res.getString(resId);
1472
1473            // The values that are used in a fullFormat string are specified
1474            // by position.
1475            dateRange = String.format(fullFormat,
1476                    startWeekDayString, startMonthString, startMonthDayString,
1477                    startYearString, startTimeString,
1478                    endWeekDayString, endMonthString, endMonthDayString,
1479                    endYearString, endTimeString);
1480            return dateRange;
1481        }
1482
1483        if (startDay != endDay) {
1484            // Same month, different day.
1485            int index = 0;
1486            if (showWeekDay) index = 1;
1487            if (showYear) index += 2;
1488            if (showTime) index += 4;
1489            if (numericDate) index += 8;
1490            int resId = sameMonthTable[index];
1491            fullFormat = res.getString(resId);
1492
1493            // The values that are used in a fullFormat string are specified
1494            // by position.
1495            dateRange = String.format(fullFormat,
1496                    startWeekDayString, startMonthString, startMonthDayString,
1497                    startYearString, startTimeString,
1498                    endWeekDayString, endMonthString, endMonthDayString,
1499                    endYearString, endTimeString);
1500            return dateRange;
1501        }
1502
1503        // Same start and end day
1504        boolean showDate = (flags & FORMAT_SHOW_DATE) != 0;
1505
1506        // If nothing was specified, then show the date.
1507        if (!showTime && !showDate && !showWeekDay) showDate = true;
1508
1509        // Compute the time string (example: "10:00 - 11:00 am")
1510        String timeString = "";
1511        if (showTime) {
1512            // If the start and end time are the same, then just show the
1513            // start time.
1514            if (isInstant) {
1515                // Same start and end time.
1516                // Example: "10:15 AM"
1517                timeString = startTimeString;
1518            } else {
1519                // Example: "10:00 - 11:00 am"
1520                String timeFormat = res.getString(com.android.internal.R.string.time1_time2);
1521                timeString = String.format(timeFormat, startTimeString, endTimeString);
1522            }
1523        }
1524
1525        // Figure out which full format to use.
1526        fullFormat = "";
1527        String dateString = "";
1528        if (showDate) {
1529            dateString = startDate.format(defaultDateFormat);
1530            if (showWeekDay) {
1531                if (showTime) {
1532                    // Example: "10:00 - 11:00 am, Tue, Oct 9"
1533                    fullFormat = res.getString(com.android.internal.R.string.time_wday_date);
1534                } else {
1535                    // Example: "Tue, Oct 9"
1536                    fullFormat = res.getString(com.android.internal.R.string.wday_date);
1537                }
1538            } else {
1539                if (showTime) {
1540                    // Example: "10:00 - 11:00 am, Oct 9"
1541                    fullFormat = res.getString(com.android.internal.R.string.time_date);
1542                } else {
1543                    // Example: "Oct 9"
1544                    return dateString;
1545                }
1546            }
1547        } else if (showWeekDay) {
1548            if (showTime) {
1549                // Example: "10:00 - 11:00 am, Tue"
1550                fullFormat = res.getString(com.android.internal.R.string.time_wday);
1551            } else {
1552                // Example: "Tue"
1553                return startWeekDayString;
1554            }
1555        } else if (showTime) {
1556            return timeString;
1557        }
1558
1559        // The values that are used in a fullFormat string are specified
1560        // by position.
1561        dateRange = String.format(fullFormat, timeString, startWeekDayString, dateString);
1562        return dateRange;
1563    }
1564
1565    /**
1566     * Formats a date or a time according to the local conventions. There are
1567     * lots of options that allow the caller to control, for example, if the
1568     * time is shown, if the day of the week is shown, if the month name is
1569     * abbreviated, if noon is shown instead of 12pm, and so on. For the
1570     * complete list of options, see the documentation for
1571     * {@link #formatDateRange}.
1572     * <p>
1573     * Example output strings (date formats in these examples are shown using
1574     * the US date format convention but that may change depending on the
1575     * local settings):
1576     * <ul>
1577     *   <li>10:15am</li>
1578     *   <li>3:00pm</li>
1579     *   <li>3pm</li>
1580     *   <li>3PM</li>
1581     *   <li>08:00</li>
1582     *   <li>17:00</li>
1583     *   <li>noon</li>
1584     *   <li>Noon</li>
1585     *   <li>midnight</li>
1586     *   <li>Midnight</li>
1587     *   <li>Oct 31</li>
1588     *   <li>Oct 31, 2007</li>
1589     *   <li>October 31, 2007</li>
1590     *   <li>10am, Oct 31</li>
1591     *   <li>17:00, Oct 31</li>
1592     *   <li>Wed</li>
1593     *   <li>Wednesday</li>
1594     *   <li>10am, Wed, Oct 31</li>
1595     *   <li>Wed, Oct 31</li>
1596     *   <li>Wednesday, Oct 31</li>
1597     *   <li>Wed, Oct 31, 2007</li>
1598     *   <li>Wed, October 31</li>
1599     *   <li>10/31/2007</li>
1600     * </ul>
1601     *
1602     * @param context the context is required only if the time is shown
1603     * @param millis a point in time in UTC milliseconds
1604     * @param flags a bit mask of formatting options
1605     * @return a string containing the formatted date/time.
1606     */
1607    public static String formatDateTime(Context context, long millis, int flags) {
1608        return formatDateRange(context, millis, millis, flags);
1609    }
1610
1611    /**
1612     * @return a relative time string to display the time expressed by millis.  Times
1613     * are counted starting at midnight, which means that assuming that the current
1614     * time is March 31st, 0:30:
1615     * <ul>
1616     *   <li>"millis=0:10 today" will be displayed as "0:10"</li>
1617     *   <li>"millis=11:30pm the day before" will be displayed as "Mar 30"</li>
1618     * </ul>
1619     * If the given millis is in a different year, then the full date is
1620     * returned in numeric format (e.g., "10/12/2008").
1621     *
1622     * @param withPreposition If true, the string returned will include the correct
1623     * preposition ("at 9:20am", "on 10/12/2008" or "on May 29").
1624     */
1625    public static CharSequence getRelativeTimeSpanString(Context c, long millis,
1626            boolean withPreposition) {
1627
1628        long now = System.currentTimeMillis();
1629        long span = now - millis;
1630
1631        if (sNowTime == null) {
1632            sNowTime = new Time();
1633            sThenTime = new Time();
1634        }
1635
1636        sNowTime.set(now);
1637        sThenTime.set(millis);
1638
1639        String result;
1640        int prepositionId;
1641        if (span < DAY_IN_MILLIS && sNowTime.weekDay == sThenTime.weekDay) {
1642            // Same day
1643            int flags = FORMAT_SHOW_TIME;
1644            result = formatDateRange(c, millis, millis, flags);
1645            prepositionId = R.string.preposition_for_time;
1646        } else if (sNowTime.year != sThenTime.year) {
1647            // Different years
1648            int flags = FORMAT_SHOW_DATE | FORMAT_SHOW_YEAR | FORMAT_NUMERIC_DATE;
1649            result = formatDateRange(c, millis, millis, flags);
1650
1651            // This is a date (like "10/31/2008" so use the date preposition)
1652            prepositionId = R.string.preposition_for_date;
1653        } else {
1654            // Default
1655            int flags = FORMAT_SHOW_DATE | FORMAT_ABBREV_MONTH;
1656            result = formatDateRange(c, millis, millis, flags);
1657            prepositionId = R.string.preposition_for_date;
1658        }
1659        if (withPreposition) {
1660            Resources res = c.getResources();
1661            result = res.getString(prepositionId, result);
1662        }
1663        return result;
1664    }
1665
1666    /**
1667     * Convenience function to return relative time string without preposition.
1668     * @param c context for resources
1669     * @param millis time in milliseconds
1670     * @return {@link CharSequence} containing relative time.
1671     * @see #getRelativeTimeSpanString(Context, long, boolean)
1672     */
1673    public static CharSequence getRelativeTimeSpanString(Context c, long millis) {
1674        return getRelativeTimeSpanString(c, millis, false /* no preposition */);
1675    }
1676
1677    private static Time sNowTime;
1678    private static Time sThenTime;
1679}
1680