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