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 
17 package com.android.messaging.util;
18 
19 import android.content.Context;
20 import android.text.format.DateUtils;
21 import android.text.format.Time;
22 
23 import com.android.messaging.Factory;
24 import com.android.messaging.R;
25 import com.google.common.annotations.VisibleForTesting;
26 
27 import java.text.SimpleDateFormat;
28 import java.util.Date;
29 import java.util.Locale;
30 
31 /**
32  * Collection of date utilities.
33  */
34 public 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      */
Dates()52     private Dates() {
53     }
54 
getContext()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      */
getRelativeTimeSpanString(final long time)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 
getConversationTimeString(final long time)84     public static CharSequence getConversationTimeString(final long time) {
85         return getTimeString(time, true /*abbreviated*/, false /*minPeriodToday*/);
86     }
87 
getMessageTimeString(final long time)88     public static CharSequence getMessageTimeString(final long time) {
89         return getTimeString(time, false /*abbreviated*/, false /*minPeriodToday*/);
90     }
91 
getWidgetTimeString(final long time, final boolean abbreviated)92     public static CharSequence getWidgetTimeString(final long time, final boolean abbreviated) {
93         return getTimeString(time, abbreviated, true /*minPeriodToday*/);
94     }
95 
getFastScrollPreviewTimeString(final long time)96     public static CharSequence getFastScrollPreviewTimeString(final long time) {
97         return getTimeString(time, true /* abbreviated */, true /* minPeriodToday */);
98     }
99 
getMessageDetailsTimeString(final long time)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 
getTimeString(final long time, final boolean abbreviated, final boolean minPeriodToday)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
getTimestamp(final long time, final long now, final boolean abbreviated, final Locale locale, final int flags, final boolean minPeriodToday)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 
getLessThanAMinuteOldTimeString( final boolean abbreviated)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 
getLessThanAnHourOldTimeString(final long timeDiff, final int flags)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 
getTodayTimeStamp(final long time, final int flags)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 
getExplicitFormattedTime(final long time, final int flags, final String format24, final String format12)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 
getThisWeekTimestamp(final long time, final Locale locale, final boolean abbreviated, final int flags)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 
getThisYearTimestamp(final long time, final Locale locale, final boolean abbreviated, final int flags)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 
getOlderThanAYearTimestamp(final long time, final Locale locale, final boolean abbreviated, final int flags)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 
getShortRelativeTimeSpanString(final long time)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 
getNumberOfDaysPassed(final long date1, final long date2)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