blob: 49f0c7fd78ec89d9c7e3c9dfd315db99fc6f194c [file] [log] [blame]
/*
* 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 com.android.messaging.Factory;
import com.android.messaging.R;
import com.google.common.annotations.VisibleForTesting;
import java.text.SimpleDateFormat;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.temporal.ChronoUnit;
import java.time.ZoneId;
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 long getNumberOfDaysPassed(final long date1, final long date2) {
LocalDateTime dateTime1 = LocalDateTime.ofInstant(Instant.ofEpochMilli(date1),
ZoneId.systemDefault());
LocalDateTime dateTime2 = LocalDateTime.ofInstant(Instant.ofEpochMilli(date2),
ZoneId.systemDefault());
return Math.abs(ChronoUnit.DAYS.between(dateTime2, dateTime1));
}
}