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();
|
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()) {
|
if (displayMode != ConversationItemDisplayMode.DETAILED && messageRecord.isEditMessage() && messageRecord.isLatestRevision()) {
|
||||||
date = getContext().getString(R.string.ConversationItem_edited_timestamp_footer, date);
|
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.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A view level model used to pass arbitrary message related information needed
|
* 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())
|
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,
|
return new ConversationMessage(messageRecord,
|
||||||
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
|
styledAndMentionBody != null ? styledAndMentionBody : mentionsUpdate != null ? mentionsUpdate.getBody() : body,
|
||||||
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
|
mentionsUpdate != null ? mentionsUpdate.getMentions() : null,
|
||||||
|
|||||||
@@ -74,7 +74,6 @@ import java.util.Collections;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
|
||||||
import java.util.function.Consumer;
|
import java.util.function.Consumer;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
@@ -366,7 +365,7 @@ public abstract class MessageRecord extends DisplayRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected @NonNull String getCallDateString(@NonNull Context context) {
|
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,
|
protected static @NonNull UpdateDescription fromRecipient(@NonNull Recipient recipient,
|
||||||
|
|||||||
@@ -908,7 +908,7 @@ class StoryViewerPageFragment :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun presentDate(date: TextView, storyPost: StoryPost) {
|
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) {
|
if (date.text != formattedDate) {
|
||||||
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