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 */ 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