diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java
index 369c600b51..8ca833d53c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/components/ConversationItemFooter.java
@@ -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);
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java
index d3351814e6..4228f037e3 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/ConversationMessage.java
@@ -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,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
index 6c59d61aaf..f708c08dcd 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
+++ b/app/src/main/java/org/thoughtcrime/securesms/database/model/MessageRecord.java
@@ -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,
diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt
index 690319781a..ac94aaa78c 100644
--- a/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt
+++ b/app/src/main/java/org/thoughtcrime/securesms/stories/viewer/page/StoryViewerPageFragment.kt
@@ -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
}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java
deleted file mode 100644
index 6c6c91bb2f..0000000000
--- a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.java
+++ /dev/null
@@ -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 .
- */
-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 DATE_FORMAT = new ThreadLocal<>();
- private static final ThreadLocal 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;
- }
-}
diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt
new file mode 100644
index 0000000000..7be0c4b33d
--- /dev/null
+++ b/app/src/main/java/org/thoughtcrime/securesms/util/DateUtils.kt
@@ -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 .
+ */
+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 = mutableMapOf()
+ private val dateFormatCache: MutableMap = mutableMapOf()
+ private val dateFormatSymbolsCache: MutableMap = 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)
+}