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