mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-22 03:58:48 +00:00
Convert DateUtils to kotlin, improve perf with caching.
This commit is contained in:
committed by
Nicholas Tinsley
parent
d505c00403
commit
9da149a868
@@ -326,7 +326,7 @@ public class ConversationItemFooter extends ConstraintLayout {
|
||||
timestamp = messageRecord.getDateSent();
|
||||
}
|
||||
}
|
||||
String date = DateUtils.getSimpleRelativeTimeSpanString(getContext(), locale, timestamp);
|
||||
String date = DateUtils.getDatelessRelativeTimeSpanString(getContext(), locale, timestamp);
|
||||
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
|
||||
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ import java.security.MessageDigest;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* A view level model used to pass arbitrary message related information needed
|
||||
@@ -206,7 +205,7 @@ public class ConversationMessage {
|
||||
}
|
||||
|
||||
String formattedDate = MessageRecordUtil.isScheduled(messageRecord) ? DateUtils.getOnlyTimeString(context, Locale.getDefault(), ((MediaMmsMessageRecord) messageRecord).getScheduledDate())
|
||||
: DateUtils.getSimpleRelativeTimeSpanString(context, Locale.getDefault(), messageRecord.getTimestamp());
|
||||
: DateUtils.getDatelessRelativeTimeSpanString(context, Locale.getDefault(), messageRecord.getTimestamp());
|
||||
return new ConversationMessage(messageRecord,
|
||||
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
|
||||
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
|
||||
|
||||
@@ -74,7 +74,6 @@ import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
|
||||
@@ -366,7 +365,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
||||
}
|
||||
|
||||
protected @NonNull String getCallDateString(@NonNull Context context) {
|
||||
return DateUtils.getSimpleRelativeTimeSpanString(context, Locale.getDefault(), getDateSent());
|
||||
return DateUtils.getDatelessRelativeTimeSpanString(context, Locale.getDefault(), getDateSent());
|
||||
}
|
||||
|
||||
protected static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient,
|
||||
|
||||
@@ -908,7 +908,7 @@ class StoryViewerPageFragment :
|
||||
}
|
||||
|
||||
private fun presentDate(date: TextView, storyPost: StoryPost) {
|
||||
val formattedDate = DateUtils.getBriefRelativeTimeSpanString(context, Locale.getDefault(), storyPost.dateInMilliseconds)
|
||||
val formattedDate = DateUtils.getBriefRelativeTimeSpanString(requireContext(), Locale.getDefault(), storyPost.dateInMilliseconds)
|
||||
if (date.text != formattedDate) {
|
||||
date.text = formattedDate
|
||||
}
|
||||
|
||||
@@ -1,346 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.util;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.text.format.DateFormat;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.signal.core.util.logging.Log;
|
||||
import org.thoughtcrime.securesms.R;
|
||||
|
||||
import java.text.DateFormatSymbols;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* Utility methods to help display dates in a nice, easily readable way.
|
||||
*/
|
||||
public class DateUtils extends android.text.format.DateUtils {
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private static final String TAG = Log.tag(DateUtils.class);
|
||||
private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = new ThreadLocal<>();
|
||||
private static final ThreadLocal<SimpleDateFormat> BRIEF_EXACT_FORMAT = new ThreadLocal<>();
|
||||
private static final long MAX_RELATIVE_TIMESTAMP = TimeUnit.MINUTES.toMillis(3);
|
||||
private static final int HALF_A_YEAR_IN_DAYS = 182;
|
||||
|
||||
private static boolean isWithin(final long millis, final long span, final TimeUnit unit) {
|
||||
return System.currentTimeMillis() - millis <= unit.toMillis(span);
|
||||
}
|
||||
|
||||
private static boolean isWithinAbs(final long millis, final long span, final TimeUnit unit) {
|
||||
return Math.abs(System.currentTimeMillis() - millis) <= unit.toMillis(span);
|
||||
}
|
||||
|
||||
private static boolean isYesterday(final long when) {
|
||||
return DateUtils.isToday(when + TimeUnit.DAYS.toMillis(1));
|
||||
}
|
||||
|
||||
private static int convertDelta(final long millis, TimeUnit to) {
|
||||
return (int) to.convert(System.currentTimeMillis() - millis, TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private static String getFormattedDateTime(long time, String template, Locale locale) {
|
||||
final String localizedPattern = getLocalizedPattern(template, locale);
|
||||
return setLowercaseAmPmStrings(new SimpleDateFormat(localizedPattern, locale), locale).format(new Date(time));
|
||||
}
|
||||
|
||||
public static String getBriefRelativeTimeSpanString(final Context c, final Locale locale, final long timestamp) {
|
||||
if (isWithin(timestamp, 1, TimeUnit.MINUTES)) {
|
||||
return c.getString(R.string.DateUtils_just_now);
|
||||
} else if (isWithin(timestamp, 1, TimeUnit.HOURS)) {
|
||||
int mins = convertDelta(timestamp, TimeUnit.MINUTES);
|
||||
return c.getResources().getString(R.string.DateUtils_minutes_ago, mins);
|
||||
} else if (isWithin(timestamp, 1, TimeUnit.DAYS)) {
|
||||
int hours = convertDelta(timestamp, TimeUnit.HOURS);
|
||||
return c.getResources().getQuantityString(R.plurals.hours_ago, hours, hours);
|
||||
} else if (isWithin(timestamp, 6, TimeUnit.DAYS)) {
|
||||
return getFormattedDateTime(timestamp, "EEE", locale);
|
||||
} else if (isWithin(timestamp, 365, TimeUnit.DAYS)) {
|
||||
return getFormattedDateTime(timestamp, "MMM d", locale);
|
||||
} else {
|
||||
return getFormattedDateTime(timestamp, "MMM d, yyyy", locale);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getExtendedRelativeTimeSpanString(final Context c, final Locale locale, final long timestamp) {
|
||||
if (isWithin(timestamp, 1, TimeUnit.MINUTES)) {
|
||||
return c.getString(R.string.DateUtils_just_now);
|
||||
} else if (isWithin(timestamp, 1, TimeUnit.HOURS)) {
|
||||
int mins = (int)TimeUnit.MINUTES.convert(System.currentTimeMillis() - timestamp, TimeUnit.MILLISECONDS);
|
||||
return c.getResources().getString(R.string.DateUtils_minutes_ago, mins);
|
||||
} else {
|
||||
StringBuilder format = new StringBuilder();
|
||||
if (isWithin(timestamp, 6, TimeUnit.DAYS)) format.append("EEE ");
|
||||
else if (isWithin(timestamp, 365, TimeUnit.DAYS)) format.append("MMM d, ");
|
||||
else format.append("MMM d, yyyy, ");
|
||||
|
||||
if (DateFormat.is24HourFormat(c)) format.append("HH:mm");
|
||||
else format.append("hh:mm a");
|
||||
|
||||
return getFormattedDateTime(timestamp, format.toString(), locale);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getSimpleRelativeTimeSpanString(final Context context, final Locale locale, final long timestamp) {
|
||||
if (isWithin(timestamp, 1, TimeUnit.MINUTES)) {
|
||||
return context.getString(R.string.DateUtils_just_now);
|
||||
} else if (isWithin(timestamp, 1, TimeUnit.HOURS)) {
|
||||
int mins = (int) TimeUnit.MINUTES.convert(System.currentTimeMillis() - timestamp, TimeUnit.MILLISECONDS);
|
||||
return context.getResources().getString(R.string.DateUtils_minutes_ago, mins);
|
||||
} else {
|
||||
return getOnlyTimeString(context, locale, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a given timestamp as just the time.
|
||||
*
|
||||
* For example:
|
||||
* For 12 hour locale: 7:23 pm
|
||||
* For 24 hour locale: 19:23
|
||||
*/
|
||||
public static String getOnlyTimeString(final Context context, final Locale locale, final long timestamp) {
|
||||
String format = DateFormat.is24HourFormat(context) ? "HH:mm" : "hh:mm a";
|
||||
return getFormattedDateTime(timestamp, format, locale);
|
||||
}
|
||||
|
||||
public static String getTimeString(final Context c, final Locale locale, final long timestamp) {
|
||||
StringBuilder format = new StringBuilder();
|
||||
|
||||
if (isSameDay(System.currentTimeMillis(), timestamp)) format.append("");
|
||||
else if (isWithinAbs(timestamp, 6, TimeUnit.DAYS)) format.append("EEE ");
|
||||
else if (isWithinAbs(timestamp, 364, TimeUnit.DAYS)) format.append("MMM d, ");
|
||||
else format.append("MMM d, yyyy, ");
|
||||
|
||||
if (DateFormat.is24HourFormat(c)) format.append("HH:mm");
|
||||
else format.append("hh:mm a");
|
||||
|
||||
return getFormattedDateTime(timestamp, format.toString(), locale);
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the passed timestamp based on the current time at a day precision.
|
||||
*
|
||||
* For example:
|
||||
* - Today
|
||||
* - Wed
|
||||
* - Mon
|
||||
* - Jan 31
|
||||
* - Feb 4
|
||||
* - Jan 12, 2033
|
||||
*/
|
||||
public static String getDayPrecisionTimeString(Context context, Locale locale, long timestamp) {
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
|
||||
|
||||
if (simpleDateFormat.format(System.currentTimeMillis()).equals(simpleDateFormat.format(timestamp))) {
|
||||
return context.getString(R.string.DeviceListItem_today);
|
||||
} else {
|
||||
String format;
|
||||
|
||||
if (isWithinAbs(timestamp, 6, TimeUnit.DAYS)) {
|
||||
format = "EEE ";
|
||||
} else if (isWithinAbs(timestamp, 365, TimeUnit.DAYS)) {
|
||||
format = "MMM d";
|
||||
} else {
|
||||
format = "MMM d, yyy";
|
||||
}
|
||||
|
||||
return getFormattedDateTime(timestamp, format, locale);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getDayPrecisionTimeSpanString(Context context, Locale locale, long timestamp) {
|
||||
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd");
|
||||
|
||||
if (simpleDateFormat.format(System.currentTimeMillis()).equals(simpleDateFormat.format(timestamp))) {
|
||||
return context.getString(R.string.DeviceListItem_today);
|
||||
} else {
|
||||
String format;
|
||||
|
||||
if (isWithin(timestamp, 6, TimeUnit.DAYS)) format = "EEE ";
|
||||
else if (isWithin(timestamp, 365, TimeUnit.DAYS)) format = "MMM d";
|
||||
else format = "MMM d, yyy";
|
||||
|
||||
return getFormattedDateTime(timestamp, format, locale);
|
||||
}
|
||||
}
|
||||
|
||||
public static SimpleDateFormat getDetailedDateFormatter(Context context, Locale locale) {
|
||||
String dateFormatPattern;
|
||||
|
||||
if (DateFormat.is24HourFormat(context)) {
|
||||
dateFormatPattern = getLocalizedPattern("MMM d, yyyy HH:mm:ss zzz", locale);
|
||||
} else {
|
||||
dateFormatPattern = getLocalizedPattern("MMM d, yyyy hh:mm:ss a zzz", locale);
|
||||
}
|
||||
|
||||
return new SimpleDateFormat(dateFormatPattern, locale);
|
||||
}
|
||||
|
||||
public static String getConversationDateHeaderString(@NonNull Context context,
|
||||
@NonNull Locale locale,
|
||||
long timestamp)
|
||||
{
|
||||
if (isToday(timestamp)) {
|
||||
return context.getString(R.string.DateUtils_today);
|
||||
} else if (isYesterday(timestamp)) {
|
||||
return context.getString(R.string.DateUtils_yesterday);
|
||||
} else if (isWithin(timestamp, HALF_A_YEAR_IN_DAYS, TimeUnit.DAYS)) {
|
||||
return formatDateWithDayOfWeek(locale, timestamp);
|
||||
} else {
|
||||
return formatDateWithYear(locale, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getScheduledMessagesDateHeaderString(@NonNull Context context,
|
||||
@NonNull Locale locale,
|
||||
long timestamp)
|
||||
{
|
||||
if (isToday(timestamp)) {
|
||||
return context.getString(R.string.DateUtils_today);
|
||||
} else if (isWithinAbs(timestamp, HALF_A_YEAR_IN_DAYS, TimeUnit.DAYS)) {
|
||||
return formatDateWithDayOfWeek(locale, timestamp);
|
||||
} else {
|
||||
return formatDateWithYear(locale, timestamp);
|
||||
}
|
||||
}
|
||||
|
||||
public static String getScheduledMessageDateString(@NonNull Context context, @NonNull Locale locale, long timestamp) {
|
||||
String dayModifier;
|
||||
if (isToday(timestamp)) {
|
||||
Calendar calendar = Calendar.getInstance(locale);
|
||||
if (calendar.get(Calendar.HOUR_OF_DAY) >= 19) {
|
||||
dayModifier = context.getString(R.string.DateUtils_tonight);
|
||||
} else {
|
||||
dayModifier = context.getString(R.string.DateUtils_today);
|
||||
}
|
||||
} else {
|
||||
dayModifier = context.getString(R.string.DateUtils_tomorrow);
|
||||
}
|
||||
String format = DateFormat.is24HourFormat(context) ? "HH:mm" : "hh:mm a";
|
||||
String time = getFormattedDateTime(timestamp, format, locale);
|
||||
|
||||
return context.getString(R.string.DateUtils_schedule_at, dayModifier, time);
|
||||
}
|
||||
|
||||
public static String formatDateWithDayOfWeek(@NonNull Locale locale, long timestamp) {
|
||||
return getFormattedDateTime(timestamp, "EEE, MMM d", locale);
|
||||
}
|
||||
|
||||
public static String formatDateWithYear(@NonNull Locale locale, long timestamp) {
|
||||
return getFormattedDateTime(timestamp, "MMM d, yyyy", locale);
|
||||
}
|
||||
|
||||
public static String formatDate(@NonNull Locale locale, long timestamp) {
|
||||
return getFormattedDateTime(timestamp, "EEE, MMM d, yyyy", locale);
|
||||
}
|
||||
|
||||
public static String formatDateWithMonthAndDay(@NonNull Locale locale, long timestamp) {
|
||||
return getFormattedDateTime(timestamp, "MMMM dd", locale);
|
||||
}
|
||||
|
||||
public static String formatDateWithoutDayOfWeek(@NonNull Locale locale, long timestamp) {
|
||||
return getFormattedDateTime(timestamp, "MMM d yyyy", locale);
|
||||
}
|
||||
|
||||
public static boolean isSameDay(long t1, long t2) {
|
||||
String d1 = getDateFormat().format(new Date(t1));
|
||||
String d2 = getDateFormat().format(new Date(t2));
|
||||
|
||||
return d1.equals(d2);
|
||||
}
|
||||
|
||||
public static boolean isSameExtendedRelativeTimestamp(long second, long first) {
|
||||
return second - first < MAX_RELATIVE_TIMESTAMP;
|
||||
}
|
||||
|
||||
private static String getLocalizedPattern(String template, Locale locale) {
|
||||
return DateFormat.getBestDateTimePattern(locale, template);
|
||||
}
|
||||
|
||||
private static @NonNull SimpleDateFormat setLowercaseAmPmStrings(@NonNull SimpleDateFormat format, @NonNull Locale locale) {
|
||||
DateFormatSymbols symbols = new DateFormatSymbols(locale);
|
||||
|
||||
symbols.setAmPmStrings(new String[] { "am", "pm"});
|
||||
format.setDateFormatSymbols(symbols);
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
/**
|
||||
* e.g. 2020-09-04T19:17:51Z
|
||||
* https://www.iso.org/iso-8601-date-and-time-format.html
|
||||
*
|
||||
* Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences.
|
||||
*
|
||||
* @return The timestamp if able to be parsed, otherwise -1.
|
||||
*/
|
||||
@SuppressLint({ "ObsoleteSdkInt", "NewApi" })
|
||||
public static long parseIso8601(@Nullable String date) {
|
||||
SimpleDateFormat format;
|
||||
if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) {
|
||||
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssX", Locale.getDefault());
|
||||
} else {
|
||||
format = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ", Locale.getDefault());
|
||||
}
|
||||
|
||||
if (Util.isEmpty(date)) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
try {
|
||||
return format.parse(date).getTime();
|
||||
} catch (ParseException e) {
|
||||
Log.w(TAG, "Failed to parse date.", e);
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private static SimpleDateFormat getDateFormat() {
|
||||
SimpleDateFormat format = DATE_FORMAT.get();
|
||||
|
||||
if (format == null) {
|
||||
format = new SimpleDateFormat("yyyyMMdd");
|
||||
DATE_FORMAT.set(format);
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private static SimpleDateFormat getBriefExactFormat() {
|
||||
SimpleDateFormat format = BRIEF_EXACT_FORMAT.get();
|
||||
|
||||
if (format == null) {
|
||||
format = new SimpleDateFormat();
|
||||
BRIEF_EXACT_FORMAT.set(format);
|
||||
}
|
||||
|
||||
return format;
|
||||
}
|
||||
}
|
||||
398
app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt
Normal file
398
app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt
Normal file
@@ -0,0 +1,398 @@
|
||||
/*
|
||||
* Copyright (C) 2014 Open Whisper Systems
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package org.thoughtcrime.securesms.util
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.format.DateFormat
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.thoughtcrime.securesms.R
|
||||
import java.text.DateFormatSymbols
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.math.abs
|
||||
import kotlin.time.Duration
|
||||
import kotlin.time.Duration.Companion.days
|
||||
import kotlin.time.Duration.Companion.hours
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlin.time.Duration.Companion.minutes
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
import kotlin.time.DurationUnit
|
||||
|
||||
/**
|
||||
* Utility methods to help display dates in a nice, easily readable way.
|
||||
*/
|
||||
object DateUtils : android.text.format.DateUtils() {
|
||||
private val TAG = Log.tag(DateUtils::class.java)
|
||||
private val MAX_RELATIVE_TIMESTAMP = 3.minutes.inWholeMilliseconds
|
||||
private const val HALF_A_YEAR_IN_DAYS = 182
|
||||
|
||||
private val sameDayDateFormat: SimpleDateFormat by lazy { SimpleDateFormat("yyyyMMdd") }
|
||||
|
||||
private val localizedTemplateCache: MutableMap<TemplateLocale, String> = mutableMapOf()
|
||||
private val dateFormatCache: MutableMap<TemplateLocale, SimpleDateFormat> = mutableMapOf()
|
||||
private val dateFormatSymbolsCache: MutableMap<Locale, DateFormatSymbols> = mutableMapOf()
|
||||
|
||||
private var is24HourDateCache: Is24HourDateEntry? = null
|
||||
|
||||
/**
|
||||
* A relative timestamp to use in space-constrained areas, like the conversation list.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getBriefRelativeTimeSpanString(c: Context, locale: Locale, timestamp: Long): String {
|
||||
return when {
|
||||
timestamp.isWithin(1.minutes) -> {
|
||||
c.getString(R.string.DateUtils_just_now)
|
||||
}
|
||||
timestamp.isWithin(1.hours) -> {
|
||||
val minutes = timestamp.convertDeltaTo(DurationUnit.MINUTES)
|
||||
c.resources.getString(R.string.DateUtils_minutes_ago, minutes)
|
||||
}
|
||||
timestamp.isWithin(1.days) -> {
|
||||
val hours = timestamp.convertDeltaTo(DurationUnit.HOURS)
|
||||
c.resources.getQuantityString(R.plurals.hours_ago, hours, hours)
|
||||
}
|
||||
timestamp.isWithin(6.days) -> {
|
||||
timestamp.toDateString("EEE", locale)
|
||||
}
|
||||
timestamp.isWithin(365.days) -> {
|
||||
timestamp.toDateString("MMM d", locale)
|
||||
}
|
||||
else -> {
|
||||
timestamp.toDateString("MMM d, yyyy", locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Similar to [getBriefRelativeTimeSpanString], except this will include additional time information in longer formats.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getExtendedRelativeTimeSpanString(context: Context, locale: Locale, timestamp: Long): String {
|
||||
return when {
|
||||
timestamp.isWithin(1.minutes) -> {
|
||||
context.getString(R.string.DateUtils_just_now)
|
||||
}
|
||||
timestamp.isWithin(1.hours) -> {
|
||||
val minutes = timestamp.convertDeltaTo(DurationUnit.MINUTES)
|
||||
context.resources.getString(R.string.DateUtils_minutes_ago, minutes)
|
||||
}
|
||||
else -> {
|
||||
val format = StringBuilder()
|
||||
|
||||
if (timestamp.isWithin(6.days)) {
|
||||
format.append("EEE ")
|
||||
} else if (timestamp.isWithin(365.days)) {
|
||||
format.append("MMM d, ")
|
||||
} else {
|
||||
format.append("MMM d, yyyy, ")
|
||||
}
|
||||
|
||||
if (context.is24HourFormat()) {
|
||||
format.append("HH:mm")
|
||||
} else {
|
||||
format.append("hh:mm a")
|
||||
}
|
||||
|
||||
timestamp.toDateString(format.toString(), locale)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a relative time string that will only use the time of day for longer intervals. The assumption is that it would be used in a context
|
||||
* that communicates the date elsewhere.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getDatelessRelativeTimeSpanString(context: Context, locale: Locale, timestamp: Long): String {
|
||||
return when {
|
||||
timestamp.isWithin(1.minutes) -> {
|
||||
context.getString(R.string.DateUtils_just_now)
|
||||
}
|
||||
timestamp.isWithin(1.hours) -> {
|
||||
val minutes = timestamp.convertDeltaTo(DurationUnit.MINUTES)
|
||||
context.resources.getString(R.string.DateUtils_minutes_ago, minutes)
|
||||
}
|
||||
else -> {
|
||||
getOnlyTimeString(context, locale, timestamp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats a given timestamp as just the time.
|
||||
*
|
||||
* For example:
|
||||
* For 12 hour locale: 7:23 pm
|
||||
* For 24 hour locale: 19:23
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getOnlyTimeString(context: Context, locale: Locale, timestamp: Long): String {
|
||||
val format = if (context.is24HourFormat()) "HH:mm" else "hh:mm a"
|
||||
return timestamp.toDateString(format, locale)
|
||||
}
|
||||
|
||||
/**
|
||||
* If on the same day, will return just the time. Otherwise it'll include relative date info.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getTimeString(context: Context, locale: Locale, timestamp: Long): String {
|
||||
val format = StringBuilder()
|
||||
|
||||
if (isSameDay(System.currentTimeMillis(), timestamp)) {
|
||||
format.append("")
|
||||
} else if (timestamp.isWithinAbs(6.days)) {
|
||||
format.append("EEE ")
|
||||
} else if (timestamp.isWithinAbs(364.days)) {
|
||||
format.append("MMM d, ")
|
||||
} else {
|
||||
format.append("MMM d, yyyy, ")
|
||||
}
|
||||
|
||||
if (context.is24HourFormat()) {
|
||||
format.append("HH:mm")
|
||||
} else {
|
||||
format.append("hh:mm a")
|
||||
}
|
||||
|
||||
return timestamp.toDateString(format.toString(), locale)
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the passed timestamp based on the current time at a day precision.
|
||||
*
|
||||
* For example:
|
||||
* - Today
|
||||
* - Wed
|
||||
* - Mon
|
||||
* - Jan 31
|
||||
* - Feb 4
|
||||
* - Jan 12, 2033
|
||||
*/
|
||||
fun getDayPrecisionTimeString(context: Context, locale: Locale, timestamp: Long): String {
|
||||
return if (isSameDay(System.currentTimeMillis(), timestamp)) {
|
||||
context.getString(R.string.DeviceListItem_today)
|
||||
} else {
|
||||
val format: String = when {
|
||||
timestamp.isWithinAbs(6.days) -> "EEE "
|
||||
timestamp.isWithinAbs(365.days) -> "MMM d"
|
||||
else -> "MMM d, yyy"
|
||||
}
|
||||
timestamp.toDateString(format, locale)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDayPrecisionTimeSpanString(context: Context, locale: Locale, timestamp: Long): String {
|
||||
return if (isSameDay(System.currentTimeMillis(), timestamp)) {
|
||||
context.getString(R.string.DeviceListItem_today)
|
||||
} else {
|
||||
val format: String = when {
|
||||
timestamp.isWithin(6.days) -> "EEE "
|
||||
timestamp.isWithin(365.days) -> "MMM d"
|
||||
else -> "MMM d, yyy"
|
||||
}
|
||||
|
||||
timestamp.toDateString(format, locale)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDetailedDateFormatter(context: Context, locale: Locale): SimpleDateFormat {
|
||||
val dateFormatPattern: String = if (context.is24HourFormat()) {
|
||||
"MMM d, yyyy HH:mm:ss zzz".localizeTemplate(locale)
|
||||
} else {
|
||||
"MMM d, yyyy hh:mm:ss a zzz".localizeTemplate(locale)
|
||||
}
|
||||
return SimpleDateFormat(dateFormatPattern, locale)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getConversationDateHeaderString(
|
||||
context: Context,
|
||||
locale: Locale,
|
||||
timestamp: Long
|
||||
): String {
|
||||
return if (isToday(timestamp)) {
|
||||
context.getString(R.string.DateUtils_today)
|
||||
} else if (isYesterday(timestamp)) {
|
||||
context.getString(R.string.DateUtils_yesterday)
|
||||
} else if (timestamp.isWithin(HALF_A_YEAR_IN_DAYS.days)) {
|
||||
formatDateWithDayOfWeek(locale, timestamp)
|
||||
} else {
|
||||
formatDateWithYear(locale, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getScheduledMessagesDateHeaderString(context: Context, locale: Locale, timestamp: Long): String {
|
||||
return if (isToday(timestamp)) {
|
||||
context.getString(R.string.DateUtils_today)
|
||||
} else if (timestamp.isWithinAbs(HALF_A_YEAR_IN_DAYS.days)) {
|
||||
formatDateWithDayOfWeek(locale, timestamp)
|
||||
} else {
|
||||
formatDateWithYear(locale, timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
fun getScheduledMessageDateString(context: Context, locale: Locale, timestamp: Long): String {
|
||||
val dayModifier: String = if (isToday(timestamp)) {
|
||||
val calendar = Calendar.getInstance(locale)
|
||||
if (calendar[Calendar.HOUR_OF_DAY] >= 19) {
|
||||
context.getString(R.string.DateUtils_tonight)
|
||||
} else {
|
||||
context.getString(R.string.DateUtils_today)
|
||||
}
|
||||
} else {
|
||||
context.getString(R.string.DateUtils_tomorrow)
|
||||
}
|
||||
val format = if (context.is24HourFormat()) "HH:mm" else "hh:mm a"
|
||||
val time = timestamp.toDateString(format, locale)
|
||||
return context.getString(R.string.DateUtils_schedule_at, dayModifier, time)
|
||||
}
|
||||
|
||||
fun formatDateWithDayOfWeek(locale: Locale, timestamp: Long): String {
|
||||
return timestamp.toDateString("EEE, MMM d", locale)
|
||||
}
|
||||
|
||||
fun formatDateWithYear(locale: Locale, timestamp: Long): String {
|
||||
return timestamp.toDateString("MMM d, yyyy", locale)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun formatDate(locale: Locale, timestamp: Long): String {
|
||||
return timestamp.toDateString("EEE, MMM d, yyyy", locale)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun formatDateWithoutDayOfWeek(locale: Locale, timestamp: Long): String {
|
||||
return timestamp.toDateString("MMM d yyyy", locale)
|
||||
}
|
||||
|
||||
/**
|
||||
* True if the two timestamps occur on the same day, otherwise false.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun isSameDay(t1: Long, t2: Long): Boolean {
|
||||
val d1 = sameDayDateFormat.format(Date(t1))
|
||||
val d2 = sameDayDateFormat.format(Date(t2))
|
||||
return d1 == d2
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isSameExtendedRelativeTimestamp(second: Long, first: Long): Boolean {
|
||||
return second - first < MAX_RELATIVE_TIMESTAMP
|
||||
}
|
||||
|
||||
/**
|
||||
* e.g. 2020-09-04T19:17:51Z
|
||||
* https://www.iso.org/iso-8601-date-and-time-format.html
|
||||
*
|
||||
* Note: SDK_INT == 0 check needed to pass unit tests due to JVM date parser differences.
|
||||
*
|
||||
* @return The timestamp if able to be parsed, otherwise -1.
|
||||
*/
|
||||
@JvmStatic
|
||||
@SuppressLint("ObsoleteSdkInt", "NewApi")
|
||||
fun parseIso8601(date: String?): Long {
|
||||
val format: SimpleDateFormat = if (Build.VERSION.SDK_INT == 0 || Build.VERSION.SDK_INT >= 24) {
|
||||
"yyyy-MM-dd'T'HH:mm:ssX".toSimpleDateFormat(Locale.getDefault())
|
||||
} else {
|
||||
"yyyy-MM-dd'T'HH:mm:ssZ".toSimpleDateFormat(Locale.getDefault())
|
||||
}
|
||||
|
||||
return if (date.isNullOrBlank()) {
|
||||
-1
|
||||
} else {
|
||||
try {
|
||||
format.parse(date)?.time ?: -1
|
||||
} catch (e: ParseException) {
|
||||
Log.w(TAG, "Failed to parse date.", e)
|
||||
-1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun Long.isWithin(duration: Duration): Boolean {
|
||||
return System.currentTimeMillis() - this <= duration.inWholeMilliseconds
|
||||
}
|
||||
|
||||
private fun Long.isWithinAbs(duration: Duration): Boolean {
|
||||
return abs(System.currentTimeMillis() - this) <= duration.inWholeMilliseconds
|
||||
}
|
||||
|
||||
private fun isYesterday(time: Long): Boolean {
|
||||
return isToday(time + TimeUnit.DAYS.toMillis(1))
|
||||
}
|
||||
|
||||
private fun Context.is24HourFormat(): Boolean {
|
||||
is24HourDateCache?.let {
|
||||
if (it.lastUpdated.isWithin(10.seconds)) {
|
||||
return it.value
|
||||
}
|
||||
}
|
||||
|
||||
val result = DateFormat.is24HourFormat(this)
|
||||
is24HourDateCache = Is24HourDateEntry(result, System.currentTimeMillis())
|
||||
return result
|
||||
}
|
||||
|
||||
private fun Long.convertDeltaTo(unit: DurationUnit): Int {
|
||||
return (System.currentTimeMillis() - this).milliseconds.toInt(unit)
|
||||
}
|
||||
|
||||
private fun Long.toDateString(template: String, locale: Locale): String {
|
||||
return template
|
||||
.localizeTemplate(locale)
|
||||
.toSimpleDateFormat(locale)
|
||||
.setLowercaseAmPmStrings(locale)
|
||||
.format(Date(this))
|
||||
}
|
||||
|
||||
private fun String.localizeTemplate(locale: Locale): String {
|
||||
val key = TemplateLocale(this, locale)
|
||||
return localizedTemplateCache.getOrPut(key) {
|
||||
DateFormat.getBestDateTimePattern(key.locale, key.template)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toSimpleDateFormat(locale: Locale): SimpleDateFormat {
|
||||
val key = TemplateLocale(this, locale)
|
||||
return dateFormatCache.getOrPut(key) {
|
||||
SimpleDateFormat(key.template, key.locale)
|
||||
}
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.setLowercaseAmPmStrings(locale: Locale): SimpleDateFormat {
|
||||
val symbols = dateFormatSymbolsCache.getOrPut(locale) {
|
||||
DateFormatSymbols(locale).apply {
|
||||
amPmStrings = arrayOf("am", "pm")
|
||||
}
|
||||
}
|
||||
this.dateFormatSymbols = symbols
|
||||
return this
|
||||
}
|
||||
|
||||
private data class TemplateLocale(val template: String, val locale: Locale)
|
||||
|
||||
private data class Is24HourDateEntry(val value: Boolean, val lastUpdated: Long)
|
||||
}
|
||||
Reference in New Issue
Block a user