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