Convert DateUtils to kotlin, improve perf with caching.

This commit is contained in:
Greyson Parrelli
2023-08-31 11:38:05 -04:00
committed by Nicholas Tinsley
parent d505c00403
commit 9da149a868
6 changed files with 402 additions and 352 deletions

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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,

View File

@@ -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
}

View File

@@ -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;
}
}

View 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)
}