/* * Copyright (C) 2015 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.messaging.util; import android.content.Context; import android.text.format.DateUtils; import android.text.format.Time; import com.android.messaging.Factory; import com.android.messaging.R; import com.google.common.annotations.VisibleForTesting; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; /** * Collection of date utilities. */ public class Dates { public static final long SECOND_IN_MILLIS = 1000; public static final long MINUTE_IN_MILLIS = SECOND_IN_MILLIS * 60; public static final long HOUR_IN_MILLIS = MINUTE_IN_MILLIS * 60; public static final long DAY_IN_MILLIS = HOUR_IN_MILLIS * 24; public static final long WEEK_IN_MILLIS = DAY_IN_MILLIS * 7; // Flags to specify whether or not to use 12 or 24 hour mode. // Callers of methods in this class should never have to specify these; this is really // intended only for unit tests. @SuppressWarnings("deprecation") @VisibleForTesting public static final int FORCE_12_HOUR = DateUtils.FORMAT_12HOUR; @SuppressWarnings("deprecation") @VisibleForTesting public static final int FORCE_24_HOUR = DateUtils.FORMAT_24HOUR; /** * Private default constructor */ private Dates() { } private static Context getContext() { return Factory.get().getApplicationContext(); } /** * Get the relative time as a string * * @param time The time * * @return The relative time */ public static CharSequence getRelativeTimeSpanString(final long time) { final long now = System.currentTimeMillis(); if (now - time < DateUtils.MINUTE_IN_MILLIS) { // Also fixes bug where posts appear in the future return getContext().getResources().getText(R.string.posted_just_now); } // Workaround for b/5657035. The platform method {@link DateUtils#getRelativeTimeSpan()} // passes a null context to other platform methods. However, on some devices, this // context is dereferenced when it shouldn't be and an NPE is thrown. We catch that // here and use a slightly less precise time. try { return DateUtils.getRelativeTimeSpanString(time, now, DateUtils.MINUTE_IN_MILLIS, DateUtils.FORMAT_ABBREV_RELATIVE).toString(); } catch (final NullPointerException npe) { return getShortRelativeTimeSpanString(time); } } public static CharSequence getConversationTimeString(final long time) { return getTimeString(time, true /*abbreviated*/, false /*minPeriodToday*/); } public static CharSequence getMessageTimeString(final long time) { return getTimeString(time, false /*abbreviated*/, false /*minPeriodToday*/); } public static CharSequence getWidgetTimeString(final long time, final boolean abbreviated) { return getTimeString(time, abbreviated, true /*minPeriodToday*/); } public static CharSequence getFastScrollPreviewTimeString(final long time) { return getTimeString(time, true /* abbreviated */, true /* minPeriodToday */); } public static CharSequence getMessageDetailsTimeString(final long time) { final Context context = getContext(); int flags; if (android.text.format.DateFormat.is24HourFormat(context)) { flags = FORCE_24_HOUR; } else { flags = FORCE_12_HOUR; } return getOlderThanAYearTimestamp(time, context.getResources().getConfiguration().locale, false /*abbreviated*/, flags); } private static CharSequence getTimeString(final long time, final boolean abbreviated, final boolean minPeriodToday) { final Context context = getContext(); int flags; if (android.text.format.DateFormat.is24HourFormat(context)) { flags = FORCE_24_HOUR; } else { flags = FORCE_12_HOUR; } return getTimestamp(time, System.currentTimeMillis(), abbreviated, context.getResources().getConfiguration().locale, flags, minPeriodToday); } @VisibleForTesting public static CharSequence getTimestamp(final long time, final long now, final boolean abbreviated, final Locale locale, final int flags, final boolean minPeriodToday) { final long timeDiff = now - time; if (!minPeriodToday && timeDiff < DateUtils.MINUTE_IN_MILLIS) { return getLessThanAMinuteOldTimeString(abbreviated); } else if (!minPeriodToday && timeDiff < DateUtils.HOUR_IN_MILLIS) { return getLessThanAnHourOldTimeString(timeDiff, flags); } else if (getNumberOfDaysPassed(time, now) == 0) { return getTodayTimeStamp(time, flags); } else if (timeDiff < DateUtils.WEEK_IN_MILLIS) { return getThisWeekTimestamp(time, locale, abbreviated, flags); } else if (timeDiff < DateUtils.YEAR_IN_MILLIS) { return getThisYearTimestamp(time, locale, abbreviated, flags); } else { return getOlderThanAYearTimestamp(time, locale, abbreviated, flags); } } private static CharSequence getLessThanAMinuteOldTimeString( final boolean abbreviated) { return getContext().getResources().getText( abbreviated ? R.string.posted_just_now : R.string.posted_now); } private static CharSequence getLessThanAnHourOldTimeString(final long timeDiff, final int flags) { final long count = (timeDiff / MINUTE_IN_MILLIS); final String format = getContext().getResources().getQuantityString( R.plurals.num_minutes_ago, (int) count); return String.format(format, count); } private static CharSequence getTodayTimeStamp(final long time, final int flags) { return DateUtils.formatDateTime(getContext(), time, DateUtils.FORMAT_SHOW_TIME | flags); } private static CharSequence getExplicitFormattedTime(final long time, final int flags, final String format24, final String format12) { SimpleDateFormat formatter; if ((flags & FORCE_24_HOUR) == FORCE_24_HOUR) { formatter = new SimpleDateFormat(format24); } else { formatter = new SimpleDateFormat(format12); } return formatter.format(new Date(time)); } private static CharSequence getThisWeekTimestamp(final long time, final Locale locale, final boolean abbreviated, final int flags) { final Context context = getContext(); if (abbreviated) { return DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_ABBREV_WEEKDAY | flags); } else { if (locale.equals(Locale.US)) { return getExplicitFormattedTime(time, flags, "EEE HH:mm", "EEE h:mmaa"); } else { return DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_WEEKDAY | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_WEEKDAY | flags); } } } private static CharSequence getThisYearTimestamp(final long time, final Locale locale, final boolean abbreviated, final int flags) { final Context context = getContext(); if (abbreviated) { return DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_NO_YEAR | flags); } else { if (locale.equals(Locale.US)) { return getExplicitFormattedTime(time, flags, "MMM d, HH:mm", "MMM d, h:mmaa"); } else { return DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_NO_YEAR | flags); } } } private static CharSequence getOlderThanAYearTimestamp(final long time, final Locale locale, final boolean abbreviated, final int flags) { final Context context = getContext(); if (abbreviated) { if (locale.equals(Locale.US)) { return getExplicitFormattedTime(time, flags, "M/d/yy", "M/d/yy"); } else { return DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR | DateUtils.FORMAT_NUMERIC_DATE); } } else { if (locale.equals(Locale.US)) { return getExplicitFormattedTime(time, flags, "M/d/yy, HH:mm", "M/d/yy, h:mmaa"); } else { return DateUtils.formatDateTime(context, time, DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_NUMERIC_DATE | DateUtils.FORMAT_SHOW_YEAR | flags); } } } public static CharSequence getShortRelativeTimeSpanString(final long time) { final long now = System.currentTimeMillis(); final long duration = Math.abs(now - time); int resId; long count; final Context context = getContext(); if (duration < HOUR_IN_MILLIS) { count = duration / MINUTE_IN_MILLIS; resId = R.plurals.num_minutes_ago; } else if (duration < DAY_IN_MILLIS) { count = duration / HOUR_IN_MILLIS; resId = R.plurals.num_hours_ago; } else if (duration < WEEK_IN_MILLIS) { count = getNumberOfDaysPassed(time, now); resId = R.plurals.num_days_ago; } else { // Although we won't be showing a time, there is a bug on some devices that use // the passed in context. On these devices, passing in a {@code null} context // here will generate an NPE. See b/5657035. return DateUtils.formatDateRange(context, time, time, DateUtils.FORMAT_ABBREV_MONTH | DateUtils.FORMAT_ABBREV_RELATIVE); } final String format = context.getResources().getQuantityString(resId, (int) count); return String.format(format, count); } private static synchronized long getNumberOfDaysPassed(final long date1, final long date2) { if (sThenTime == null) { sThenTime = new Time(); } sThenTime.set(date1); final int day1 = Time.getJulianDay(date1, sThenTime.gmtoff); sThenTime.set(date2); final int day2 = Time.getJulianDay(date2, sThenTime.gmtoff); return Math.abs(day2 - day1); } private static Time sThenTime; }