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