1/*
2 * Copyright (C) 2015 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 com.android.messaging.util;
18
19import android.content.Context;
20import android.text.format.DateUtils;
21import android.text.format.Time;
22
23import com.android.messaging.Factory;
24import com.android.messaging.R;
25import com.google.common.annotations.VisibleForTesting;
26
27import java.text.SimpleDateFormat;
28import java.util.Date;
29import java.util.Locale;
30
31/**
32 * Collection of date utilities.
33 */
34public class Dates {
35    public static final long SECOND_IN_MILLIS = 1000;
36    public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60;
37    public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60;
38    public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24;
39    public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7;
40
41    // Flags to specify whether or not to use 12 or 24 hour mode.
42    // Callers of methods in this class should never have to specify these; this is really
43    // intended only for unit tests.
44    @SuppressWarnings("deprecation")
45    @VisibleForTesting public static final int FORCE_12_HOUR = DateUtils.FORMAT_12HOUR;
46    @SuppressWarnings("deprecation")
47    @VisibleForTesting public static final int FORCE_24_HOUR = DateUtils.FORMAT_24HOUR;
48
49    /**
50     * Private default constructor
51     */
52    private Dates() {
53    }
54
55    private static Context getContext() {
56        return Factory.get().getApplicationContext();
57    }
58    /**
59     * Get the relative time as a string
60     *
61     * @param time The time
62     *
63     * @return The relative time
64     */
65    public static CharSequence getRelativeTimeSpanString(final long time) {
66        final long now = System.currentTimeMillis();
67        if (now - time < DateUtils.MINUTE_IN_MILLIS) {
68            // Also fixes bug where posts appear in the future
69            return getContext().getResources().getText(R.string.posted_just_now);
70        }
71
72        // Workaround for b/5657035. The platform method {@link DateUtils#getRelativeTimeSpan()}
73        // passes a null context to other platform methods. However, on some devices, this
74        // context is dereferenced when it shouldn't be and an NPE is thrown. We catch that
75        // here and use a slightly less precise time.
76        try {
77            return DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS,
78                    DateUtils.FORMAT_ABBREV_RELATIVE).toString();
79        } catch (final NullPointerException npe) {
80            return getShortRelativeTimeSpanString(time);
81        }
82    }
83
84    public static CharSequence getConversationTimeString(final long time) {
85        return getTimeString(time, true /*abbreviated*/, false /*minPeriodToday*/);
86    }
87
88    public static CharSequence getMessageTimeString(final long time) {
89        return getTimeString(time, false /*abbreviated*/, false /*minPeriodToday*/);
90    }
91
92    public static CharSequence getWidgetTimeString(final long time, final boolean abbreviated) {
93        return getTimeString(time, abbreviated, true /*minPeriodToday*/);
94    }
95
96    public static CharSequence getFastScrollPreviewTimeString(final long time) {
97        return getTimeString(time, true /* abbreviated */, true /* minPeriodToday */);
98    }
99
100    public static CharSequence getMessageDetailsTimeString(final long time) {
101        final Context context = getContext();
102        int flags;
103        if (android.text.format.DateFormat.is24HourFormat(context)) {
104            flags = FORCE_24_HOUR;
105        } else {
106            flags = FORCE_12_HOUR;
107        }
108        return getOlderThanAYearTimestamp(time,
109                context.getResources().getConfiguration().locale, false /*abbreviated*/,
110                flags);
111    }
112
113    private static CharSequence getTimeString(final long time, final boolean abbreviated,
114            final boolean minPeriodToday) {
115        final Context context = getContext();
116        int flags;
117        if (android.text.format.DateFormat.is24HourFormat(context)) {
118            flags = FORCE_24_HOUR;
119        } else {
120            flags = FORCE_12_HOUR;
121        }
122        return getTimestamp(time, System.currentTimeMillis(), abbreviated,
123                context.getResources().getConfiguration().locale, flags, minPeriodToday);
124    }
125
126    @VisibleForTesting
127    public static CharSequence getTimestamp(final long time, final long now,
128            final boolean abbreviated, final Locale locale, final int flags,
129            final boolean minPeriodToday) {
130        final long timeDiff = now - time;
131
132        if (!minPeriodToday && timeDiff < DateUtils.MINUTE_IN_MILLIS) {
133            return getLessThanAMinuteOldTimeString(abbreviated);
134        } else if (!minPeriodToday && timeDiff < DateUtils.HOUR_IN_MILLIS) {
135            return getLessThanAnHourOldTimeString(timeDiff, flags);
136        } else if (getNumberOfDaysPassed(time, now) == 0) {
137            return getTodayTimeStamp(time, flags);
138        } else if (timeDiff < DateUtils.WEEK_IN_MILLIS) {
139            return getThisWeekTimestamp(time, locale, abbreviated, flags);
140        } else if (timeDiff < DateUtils.YEAR_IN_MILLIS) {
141            return getThisYearTimestamp(time, locale, abbreviated, flags);
142        } else {
143            return getOlderThanAYearTimestamp(time, locale, abbreviated, flags);
144        }
145    }
146
147    private static CharSequence getLessThanAMinuteOldTimeString(
148            final boolean abbreviated) {
149        return getContext().getResources().getText(
150                abbreviated ? R.string.posted_just_now : R.string.posted_now);
151    }
152
153    private static CharSequence getLessThanAnHourOldTimeString(final long timeDiff,
154            final int flags) {
155        final long count = (timeDiff / MINUTE_IN_MILLIS);
156        final String format = getContext().getResources().getQuantityString(
157                R.plurals.num_minutes_ago, (int) count);
158        return String.format(format, count);
159    }
160
161    private static CharSequence getTodayTimeStamp(final long time, final int flags) {
162        return DateUtils.formatDateTime(getContext(), time,
163                DateUtils.FORMAT_SHOW_TIME | flags);
164    }
165
166    private static CharSequence getExplicitFormattedTime(final long time, final int flags,
167            final String format24, final String format12) {
168        SimpleDateFormat formatter;
169        if ((flags & FORCE_24_HOUR) == FORCE_24_HOUR) {
170            formatter = new SimpleDateFormat(format24);
171        } else {
172            formatter = new SimpleDateFormat(format12);
173        }
174        return formatter.format(new Date(time));
175    }
176
177    private static CharSequence getThisWeekTimestamp(final long time,
178            final Locale locale, final boolean abbreviated, final int flags) {
179        final Context context = getContext();
180        if (abbreviated) {
181            return DateUtils.formatDateTime(context, time,
182                    DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY | flags);
183        } else {
184            if (locale.equals(Locale.US)) {
185                return getExplicitFormattedTime(time, flags, "EEE HH:mm", "EEE h:mmaa");
186            } else {
187                return DateUtils.formatDateTime(context, time,
188                        DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_TIME
189                        | DateUtils.FORMAT_ABBREV_WEEKDAY
190                        | flags);
191            }
192        }
193    }
194
195    private static CharSequence getThisYearTimestamp(final long time, final Locale locale,
196            final boolean abbreviated, final int flags) {
197        final Context context = getContext();
198        if (abbreviated) {
199            return DateUtils.formatDateTime(context, time,
200                    DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH
201                    | DateUtils.FORMAT_NO_YEAR | flags);
202        } else {
203            if (locale.equals(Locale.US)) {
204                return getExplicitFormattedTime(time, flags, "MMM d, HH:mm", "MMM d, h:mmaa");
205            } else {
206                return DateUtils.formatDateTime(context, time,
207                        DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME
208                        | DateUtils.FORMAT_ABBREV_MONTH
209                        | DateUtils.FORMAT_NO_YEAR
210                        | flags);
211            }
212        }
213    }
214
215    private static CharSequence getOlderThanAYearTimestamp(final long time,
216            final Locale locale, final boolean abbreviated, final int flags) {
217        final Context context = getContext();
218        if (abbreviated) {
219            if (locale.equals(Locale.US)) {
220                return getExplicitFormattedTime(time, flags, "M/d/yy", "M/d/yy");
221            } else {
222                return DateUtils.formatDateTime(context, time,
223                        DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
224                        | DateUtils.FORMAT_NUMERIC_DATE);
225            }
226        } else {
227            if (locale.equals(Locale.US)) {
228                return getExplicitFormattedTime(time, flags, "M/d/yy, HH:mm", "M/d/yy, h:mmaa");
229            } else {
230                return DateUtils.formatDateTime(context, time,
231                        DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME
232                        | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR
233                        | flags);
234            }
235        }
236    }
237
238    public static CharSequence getShortRelativeTimeSpanString(final long time) {
239        final long now = System.currentTimeMillis();
240        final long duration = Math.abs(now - time);
241
242        int resId;
243        long count;
244
245        final Context context = getContext();
246
247        if (duration < HOUR_IN_MILLIS) {
248            count = duration / MINUTE_IN_MILLIS;
249            resId = R.plurals.num_minutes_ago;
250        } else if (duration < DAY_IN_MILLIS) {
251            count = duration / HOUR_IN_MILLIS;
252            resId = R.plurals.num_hours_ago;
253        } else if (duration < WEEK_IN_MILLIS) {
254            count = getNumberOfDaysPassed(time, now);
255            resId = R.plurals.num_days_ago;
256        } else {
257            // Although we won't be showing a time, there is a bug on some devices that use
258            // the passed in context. On these devices, passing in a {@code null} context
259            // here will generate an NPE. See b/5657035.
260            return DateUtils.formatDateRange(context, time, time,
261                    DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_ABBREV_RELATIVE);
262        }
263
264        final String format = context.getResources().getQuantityString(resId, (int) count);
265        return String.format(format, count);
266    }
267
268    private static synchronized long getNumberOfDaysPassed(final long date1, final long date2) {
269        if (sThenTime == null) {
270            sThenTime = new Time();
271        }
272        sThenTime.set(date1);
273        final int day1 = Time.getJulianDay(date1, sThenTime.gmtoff);
274        sThenTime.set(date2);
275        final int day2 = Time.getJulianDay(date2, sThenTime.gmtoff);
276        return Math.abs(day2 - day1);
277    }
278
279    private static Time sThenTime;
280}
281