This commit is contained in:
Andrew 2023-05-31 16:43:49 -05:00 committed by GitHub
commit 1f58dd23b7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 290 additions and 78 deletions

View File

@ -21,13 +21,21 @@ import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageActivityH
import org.session.libsession.utilities.dynamiclanguage.DynamicLanguageContextWrapper;
import org.thoughtcrime.securesms.conversation.v2.WindowUtil;
import org.thoughtcrime.securesms.util.ActivityUtilitiesKt;
import org.thoughtcrime.securesms.util.DateUtil;
import org.thoughtcrime.securesms.util.ThemeState;
import org.thoughtcrime.securesms.util.UiModeUtilities;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import network.loki.messenger.R;
@AndroidEntryPoint
public abstract class BaseActionBarActivity extends AppCompatActivity {
private static final String TAG = BaseActionBarActivity.class.getSimpleName();
@Inject DateUtil dateUtil;
public ThemeState currentThemeState;
private TextSecurePreferences getPreferences() {

View File

@ -77,12 +77,10 @@ import org.thoughtcrime.securesms.mms.GlideRequests;
import org.thoughtcrime.securesms.mms.Slide;
import org.thoughtcrime.securesms.permissions.Permissions;
import org.thoughtcrime.securesms.util.AttachmentUtil;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.SaveAttachmentTask;
import org.thoughtcrime.securesms.util.SaveAttachmentTask.Attachment;
import java.io.IOException;
import java.util.Locale;
import java.util.WeakHashMap;
import network.loki.messenger.R;
@ -243,7 +241,7 @@ public class MediaPreviewActivity extends PassphraseRequiredActionBarActivity im
CharSequence relativeTimeSpan;
if (mediaItem.date > 0) {
relativeTimeSpan = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), mediaItem.date);
relativeTimeSpan = dateUtil.format(mediaItem.date);
} else {
relativeTimeSpan = getString(R.string.MediaPreviewActivity_draft);
}

View File

@ -151,6 +151,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
@Inject lateinit var storage: Storage
@Inject lateinit var reactionDb: ReactionDatabase
@Inject lateinit var viewModelFactory: ConversationViewModel.AssistedFactory
@Inject lateinit var dateUtil: DateUtil
private val screenshotObserver by lazy {
ScreenshotObserver(this, Handler(Looper.getMainLooper())) {
@ -1730,7 +1731,7 @@ class ConversationActivityV2 : PassphraseRequiredActionBarActivity(), InputBarDe
val body = MentionUtilities.highlightMentions(message.body, viewModel.threadId, this)
if (TextUtils.isEmpty(body)) { continue }
if (messageSize > 1) {
val formattedTimestamp = DateUtils.getDisplayFormattedTimeSpanString(this, Locale.getDefault(), message.timestamp)
val formattedTimestamp = dateUtil.format(message.timestamp)
builder.append("$formattedTimestamp: ")
}
builder.append(body)

View File

@ -43,18 +43,21 @@ import org.thoughtcrime.securesms.database.model.MessageRecord;
import org.thoughtcrime.securesms.database.model.ReactionRecord;
import org.thoughtcrime.securesms.dependencies.DatabaseComponent;
import org.thoughtcrime.securesms.util.AnimationCompleteListener;
import org.thoughtcrime.securesms.util.DateUtils;
import org.thoughtcrime.securesms.util.DateUtil;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import javax.inject.Inject;
import dagger.hilt.android.AndroidEntryPoint;
import kotlin.Unit;
import network.loki.messenger.R;
@AndroidEntryPoint
public final class ConversationReactionOverlay extends FrameLayout {
@Inject DateUtil dateUtil;
public static final float LONG_PRESS_SCALE_FACTOR = 0.95f;
private static final Interpolator INTERPOLATOR = new DecelerateInterpolator();
@ -169,7 +172,7 @@ public final class ConversationReactionOverlay extends FrameLayout {
conversationBubble.setLayoutParams(new LinearLayout.LayoutParams(conversationItemSnapshot.getWidth(), conversationItemSnapshot.getHeight()));
conversationBubble.setBackground(new BitmapDrawable(getResources(), conversationItemSnapshot));
TextView conversationTimestamp = conversationItem.findViewById(R.id.conversation_item_timestamp);
conversationTimestamp.setText(DateUtils.getDisplayFormattedTimeSpanString(getContext(), Locale.getDefault(), messageRecord.getTimestamp()));
conversationTimestamp.setText(dateUtil.format(messageRecord.getTimestamp()));
updateConversationTimestamp(messageRecord);

View File

@ -7,12 +7,18 @@ import android.view.View
import android.widget.LinearLayout
import androidx.core.content.res.ResourcesCompat
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewControlMessageBinding
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.util.DateUtil
import javax.inject.Inject
@AndroidEntryPoint
class ControlMessageView : LinearLayout {
@Inject lateinit var dateUtil: DateUtil
private lateinit var binding: ViewControlMessageBinding
// region Lifecycle
@ -28,7 +34,7 @@ class ControlMessageView : LinearLayout {
// region Updating
fun bind(message: MessageRecord, previous: MessageRecord?) {
binding.dateBreakTextView.showDateBreak(message, previous)
binding.dateBreakTextView.showDateBreak(dateUtil, message, previous)
binding.iconImageView.visibility = View.GONE
var messageBody: CharSequence = message.getDisplayBody(context)
binding.root.contentDescription= null

View File

@ -3,13 +3,12 @@ package org.thoughtcrime.securesms.conversation.v2.messages
import android.widget.TextView
import androidx.core.view.isVisible
import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
import org.thoughtcrime.securesms.util.DateUtil
private const val maxTimeBetweenBreaks = 5 * 60 * 1000L // 5 minutes
fun TextView.showDateBreak(message: MessageRecord, previous: MessageRecord?) {
fun TextView.showDateBreak(dateUtil: DateUtil, message: MessageRecord, previous: MessageRecord?) {
val showDateBreak = (previous == null || message.timestamp - previous.timestamp > maxTimeBetweenBreaks)
isVisible = showDateBreak
text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else ""
text = if (showDateBreak) dateUtil.format(message.timestamp) else ""
}

View File

@ -47,12 +47,8 @@ import org.thoughtcrime.securesms.database.model.MessageRecord
import org.thoughtcrime.securesms.groups.OpenGroupManager
import org.thoughtcrime.securesms.home.UserDetailsBottomSheet
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.toDp
import org.thoughtcrime.securesms.util.toPx
import org.thoughtcrime.securesms.util.*
import java.util.Date
import java.util.Locale
import javax.inject.Inject
import kotlin.math.abs
import kotlin.math.min
@ -62,6 +58,7 @@ import kotlin.math.sqrt
@AndroidEntryPoint
class VisibleMessageView : LinearLayout {
@Inject lateinit var dateUtil: DateUtil
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var lokiThreadDb: LokiThreadDatabase
@Inject lateinit var lokiApiDb: LokiAPIDatabase
@ -193,7 +190,7 @@ class VisibleMessageView : LinearLayout {
binding.senderNameTextView.text = contact?.displayName(contactContext) ?: senderSessionID
// Date break
val showDateBreak = isStartOfMessageCluster || snIsSelected
binding.dateBreakTextView.text = if (showDateBreak) DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), message.timestamp) else null
binding.dateBreakTextView.text = if (showDateBreak) dateUtil.format(message.timestamp) else null
binding.dateBreakTextView.isVisible = showDateBreak
// Message status indicator
if (message.isOutgoing) {

View File

@ -8,6 +8,8 @@ import org.session.libsession.utilities.AppTextSecurePreferences
import org.session.libsession.utilities.TextSecurePreferences
import org.thoughtcrime.securesms.repository.ConversationRepository
import org.thoughtcrime.securesms.repository.DefaultConversationRepository
import org.thoughtcrime.securesms.util.AndroidClock
import org.thoughtcrime.securesms.util.Clock
@Module
@InstallIn(SingletonComponent::class)
@ -19,4 +21,7 @@ abstract class AppModule {
@Binds
abstract fun bindConversationRepository(repository: DefaultConversationRepository): ConversationRepository
@Binds
abstract fun bindAndroidClock(androidClock: AndroidClock): Clock
}

View File

@ -6,7 +6,6 @@ import android.graphics.Typeface
import android.graphics.drawable.ColorDrawable
import android.util.AttributeSet
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.widget.LinearLayout
import androidx.core.content.ContextCompat
@ -20,11 +19,13 @@ import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_ALL
import org.thoughtcrime.securesms.database.RecipientDatabase.NOTIFY_TYPE_NONE
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.DateUtil
import org.thoughtcrime.securesms.util.getAccentColor
import java.util.Locale
import javax.inject.Inject
class ConversationView : LinearLayout {
@Inject lateinit var dateUtil: DateUtil
private val binding: ViewConversationBinding by lazy { ViewConversationBinding.bind(this) }
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
var thread: ThreadRecord? = null
@ -85,7 +86,7 @@ class ConversationView : LinearLayout {
val senderDisplayName = getUserDisplayName(thread.recipient)
?: thread.recipient.address.toString()
binding.conversationViewDisplayNameTextView.text = senderDisplayName
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
binding.timestampTextView.text = dateUtil.format(thread.date)
val recipient = thread.recipient
binding.muteIndicatorImageView.isVisible = recipient.isMuted || recipient.notifyType != NOTIFY_TYPE_ALL
val drawableRes = if (recipient.isMuted || recipient.notifyType == NOTIFY_TYPE_NONE) {

View File

@ -63,15 +63,8 @@ import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.onboarding.SeedActivity
import org.thoughtcrime.securesms.onboarding.SeedReminderViewDelegate
import org.thoughtcrime.securesms.preferences.SettingsActivity
import org.thoughtcrime.securesms.util.ConfigurationMessageUtilities
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.IP2Country
import org.thoughtcrime.securesms.util.disableClipping
import org.thoughtcrime.securesms.util.push
import org.thoughtcrime.securesms.util.show
import org.thoughtcrime.securesms.util.themeState
import org.thoughtcrime.securesms.util.*
import java.io.IOException
import java.util.Locale
import javax.inject.Inject
@AndroidEntryPoint
@ -84,6 +77,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
private lateinit var glide: GlideRequests
private var broadcastReceiver: BroadcastReceiver? = null
@Inject lateinit var dateUtil: DateUtil
@Inject lateinit var threadDb: ThreadDatabase
@Inject lateinit var mmsSmsDatabase: MmsSmsDatabase
@Inject lateinit var recipientDatabase: RecipientDatabase
@ -100,7 +94,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
HomeAdapter(context = this, listener = this)
}
private val globalSearchAdapter = GlobalSearchAdapter { model ->
private val globalSearchAdapter = GlobalSearchAdapter(dateUtil) { model ->
when (model) {
is GlobalSearchAdapter.Model.Message -> {
val threadId = model.messageResult.threadId
@ -290,11 +284,7 @@ class HomeActivity : PassphraseRequiredActionBarActivity(),
if (messageRequestCount > 0 && !textSecurePreferences.hasHiddenMessageRequests()) {
with(ViewMessageRequestBannerBinding.inflate(layoutInflater)) {
unreadCountTextView.text = messageRequestCount.toString()
timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(
this@HomeActivity,
Locale.getDefault(),
threadDb.latestUnapprovedConversationTimestamp
)
timestampTextView.text = dateUtil.format(threadDb.latestUnapprovedConversationTimestamp)
root.setOnClickListener { showMessageRequests() }
root.setOnLongClickListener { hideMessageRequests(); true }
root.layoutParams = RecyclerView.LayoutParams(RecyclerView.LayoutParams.MATCH_PARENT, RecyclerView.LayoutParams.WRAP_CONTENT)

View File

@ -6,6 +6,7 @@ import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewGlobalSearchHeaderBinding
import network.loki.messenger.databinding.ViewGlobalSearchResultBinding
@ -13,10 +14,14 @@ import org.session.libsession.utilities.GroupRecord
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.mms.GlideApp
import org.thoughtcrime.securesms.search.model.MessageResult
import org.thoughtcrime.securesms.util.DateUtil
import java.security.InvalidParameterException
import org.session.libsession.messaging.contacts.Contact as ContactModel
class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerView.Adapter<RecyclerView.ViewHolder>() {
class GlobalSearchAdapter(
private val dateUtil: DateUtil,
private val modelCallback: (Model) -> Unit
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
companion object {
const val HEADER_VIEW_TYPE = 0
@ -47,8 +52,10 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
} else {
ContentView(
LayoutInflater.from(parent.context)
.inflate(R.layout.view_global_search_result, parent, false)
, modelCallback)
.inflate(R.layout.view_global_search_result, parent, false),
dateUtil,
modelCallback
)
}
override fun onBindViewHolder(
@ -69,7 +76,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
onBindViewHolder(holder,position, mutableListOf())
onBindViewHolder(holder, position, mutableListOf())
}
class HeaderView(view: View) : RecyclerView.ViewHolder(view) {
@ -87,8 +94,11 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
}
}
class ContentView(view: View, private val modelCallback: (Model) -> Unit) : RecyclerView.ViewHolder(view) {
class ContentView(
view: View,
private val dateUtil: DateUtil,
private val modelCallback: (Model) -> Unit
) : RecyclerView.ViewHolder(view) {
val binding = ViewGlobalSearchResultBinding.bind(view).apply {
searchResultProfilePicture.root.glide = GlideApp.with(root)
}
@ -102,7 +112,7 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
when (model) {
is Model.GroupConversation -> bindModel(query, model)
is Model.Contact -> bindModel(query, model)
is Model.Message -> bindModel(query, model)
is Model.Message -> bindModel(dateUtil, query, model)
is Model.SavedMessages -> bindModel(model)
is Model.Header -> throw InvalidParameterException("Can't display Model.Header as ContentView")
}
@ -112,14 +122,14 @@ class GlobalSearchAdapter (private val modelCallback: (Model)->Unit): RecyclerVi
}
data class MessageModel(
val threadRecipient: Recipient,
val messageRecipient: Recipient,
val messageSnippet: String
val threadRecipient: Recipient,
val messageRecipient: Recipient,
val messageSnippet: String
)
sealed class Model {
data class Header(@StringRes val title: Int) : Model()
data class SavedMessages(val currentUserPublicKey: String): Model()
data class SavedMessages(val currentUserPublicKey: String) : Model()
data class Contact(val contact: ContactModel) : Model()
data class GroupConversation(val groupRecord: GroupRecord) : Model()
data class Message(val messageResult: MessageResult, val unread: Int) : Model()

View File

@ -14,7 +14,7 @@ import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.ContentView
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.GroupConversation
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Message
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.SavedMessages
import org.thoughtcrime.securesms.util.DateUtils
import org.thoughtcrime.securesms.util.DateUtil
import org.thoughtcrime.securesms.util.SearchUtil
import java.util.Locale
import org.thoughtcrime.securesms.home.search.GlobalSearchAdapter.Model.Contact as ContactModel
@ -125,7 +125,7 @@ fun ContentView.bindModel(model: SavedMessages) {
binding.searchResultSavedMessages.isVisible = true
}
fun ContentView.bindModel(query: String?, model: Message) {
fun ContentView.bindModel(dateUtil: DateUtil, query: String?, model: Message) {
binding.searchResultProfilePicture.root.isVisible = true
binding.searchResultSavedMessages.isVisible = false
binding.searchResultTimestamp.isVisible = true
@ -134,7 +134,7 @@ fun ContentView.bindModel(query: String?, model: Message) {
// if (hasUnreads) {
// binding.unreadCountTextView.text = model.unread.toString()
// }
binding.searchResultTimestamp.text = DateUtils.getDisplayFormattedTimeSpanString(binding.root.context, Locale.getDefault(), model.messageResult.sentTimestampMs)
binding.searchResultTimestamp.text = dateUtil.format(model.messageResult.sentTimestampMs)
binding.searchResultProfilePicture.root.update(model.messageResult.conversationRecipient)
val textSpannable = SpannableStringBuilder()
if (model.messageResult.conversationRecipient != model.messageResult.messageRecipient) {

View File

@ -6,16 +6,19 @@ import android.util.AttributeSet
import android.view.LayoutInflater
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import dagger.hilt.android.AndroidEntryPoint
import network.loki.messenger.R
import network.loki.messenger.databinding.ViewMessageRequestBinding
import org.session.libsession.utilities.recipients.Recipient
import org.thoughtcrime.securesms.conversation.v2.utilities.MentionUtilities.highlightMentions
import org.thoughtcrime.securesms.database.model.ThreadRecord
import org.thoughtcrime.securesms.mms.GlideRequests
import org.thoughtcrime.securesms.util.DateUtils
import java.util.Locale
import org.thoughtcrime.securesms.util.DateUtil
@AndroidEntryPoint
class MessageRequestView : LinearLayout {
lateinit var dateUtil: DateUtil
private lateinit var binding: ViewMessageRequestBinding
private val screenWidth = Resources.getSystem().displayMetrics.widthPixels
var thread: ThreadRecord? = null
@ -38,7 +41,7 @@ class MessageRequestView : LinearLayout {
val senderDisplayName = getUserDisplayName(thread.recipient)
?: thread.recipient.address.toString()
binding.displayNameTextView.text = senderDisplayName
binding.timestampTextView.text = DateUtils.getDisplayFormattedTimeSpanString(context, Locale.getDefault(), thread.date)
binding.timestampTextView.text = dateUtil.format(thread.date)
val rawSnippet = thread.getDisplayBody(context)
val snippet = highlightMentions(rawSnippet, thread.threadId, context)
binding.snippetTextView.text = snippet

View File

@ -73,15 +73,6 @@ object BackupUtil {
return prefList
}
@JvmStatic
fun getLastBackupTimeString(context: Context, locale: Locale): String {
val timestamp = DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFileTime()
if (timestamp == null) {
return context.getString(R.string.BackupUtil_never)
}
return DateUtils.getDisplayFormattedTimeSpanString(context, locale, timestamp.time)
}
@JvmStatic
fun getLastBackup(context: Context): BackupFileRecord? {
return DatabaseComponent.get(context).lokiBackupFilesDatabase().getLastBackupFile()

View File

@ -0,0 +1,14 @@
package org.thoughtcrime.securesms.util
import javax.inject.Inject
import javax.inject.Singleton
interface Clock {
val currentTimeMillis: Long
}
@Singleton
class AndroidClock @Inject constructor(): Clock {
override val currentTimeMillis: Long
get() = System.currentTimeMillis()
}

View File

@ -0,0 +1,52 @@
package org.thoughtcrime.securesms.util
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import network.loki.messenger.R
import org.thoughtcrime.securesms.util.DateUtils.isSameDay
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class DateUtil @Inject constructor (
@ApplicationContext private val context: Context,
private val clock: Clock
) {
/**
* If the message was sent/received on the same day we show: 12 hour time + am/pm
* 12:43 pm
* If the message was sent/received on the same week we show: word abbreviation day of week + 12 hour time + am/pm
* Tue 12:34 pm
* If the message was sent/received in the current year we show, word abbreviation of month, date (day) and 12 hour time + am/pm
* Mar 9 12:34 pm
* If the message was sent or received outside of the current year, then we show dd/mm/yy
* 09/03/23
*/
@JvmOverloads
fun format(timestamp: Long, locale: Locale = Locale.getDefault()): String {
if (isWithin(timestamp, 1, TimeUnit.MINUTES)) {
return context.getString(R.string.DateUtils_just_now)
}
return when {
isToday(timestamp) -> context.hourFormat
isWithin(timestamp, 6, TimeUnit.DAYS) -> "EEE " + context.hourFormat
isThisYear(timestamp) -> "MMM d " + context.hourFormat
else -> "MMM d " + context.hourFormat + ", yyyy"
}.let { DateUtils.getFormattedDateTime(timestamp, it, locale) }
}
private fun isThisYear(millis: Long): Boolean = isSameYear(millis, clock.currentTimeMillis)
fun isToday(time: Long): Boolean = isSameDay(time, clock.currentTimeMillis)
fun isWithin(millis: Long, span: Long, unit: TimeUnit): Boolean =
clock.currentTimeMillis - millis <= unit.toMillis(span)
}
fun isSameYear(millis: Long, millisOther: Long): Boolean = Calendar.getInstance().run {
apply { timeInMillis = millis }.get(Calendar.YEAR) == apply { timeInMillis = millisOther }.get(Calendar.YEAR)
}
private val Context.hourFormat: String get() = DateUtils.getHourFormat(this)

View File

@ -45,7 +45,7 @@ public class DateUtils extends android.text.format.DateUtils {
private static final SimpleDateFormat DAY_PRECISION_DATE_FORMAT = new SimpleDateFormat("yyyyMMdd");
private static final SimpleDateFormat HOUR_PRECISION_DATE_FORMAT = new SimpleDateFormat("yyyyMMddHH");
private static boolean isWithin(final long millis, final long span, final TimeUnit unit) {
static boolean isWithin(final long millis, final long span, final TimeUnit unit) {
return System.currentTimeMillis() - millis <= unit.toMillis(span);
}
@ -66,20 +66,6 @@ public class DateUtils extends android.text.format.DateUtils {
return (DateFormat.is24HourFormat(c)) ? "HH:mm" : "hh:mm a";
}
public static String getDisplayFormattedTimeSpanString(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 (isToday(timestamp)) {
return getFormattedDateTime(timestamp, getHourFormat(c), locale);
} else if (isWithin(timestamp, 6, TimeUnit.DAYS)) {
return getFormattedDateTime(timestamp, "EEE " + getHourFormat(c), locale);
} else if (isWithin(timestamp, 365, TimeUnit.DAYS)) {
return getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c), locale);
} else {
return getFormattedDateTime(timestamp, "MMM d " + getHourFormat(c) + ", yyyy", locale);
}
}
public static SimpleDateFormat getDetailedDateFormatter(Context context, Locale locale) {
String dateFormatPattern;

View File

@ -0,0 +1,147 @@
package org.thoughtcrime.securesms.util
import android.app.Application
import androidx.test.core.app.ApplicationProvider
import org.junit.Assert.*
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito
import org.mockito.kotlin.whenever
import org.robolectric.ParameterizedRobolectricTestRunner
import org.robolectric.annotation.Config
import org.robolectric.shadows.ShadowDateFormat
import org.robolectric.shadows.ShadowSettings
import java.util.Locale
import java.util.TimeZone
import java.util.Calendar
import java.util.Calendar.JANUARY
import java.util.Calendar.NOVEMBER
import java.util.Calendar.DECEMBER
@RunWith(ParameterizedRobolectricTestRunner::class)
@Config(application = TestApplication::class, shadows = [ShadowDateFormat::class])
class DateUtilTest(
private val expected: String,
private val locale: Locale,
private val then: Long,
private val now: Long,
private val is24HourTime: Boolean
) {
private val clock = Mockito.mock(Clock::class.java)
private val context = ApplicationProvider.getApplicationContext<Application>()
private val dateUtil = DateUtil(context, clock)
@Test
fun test() {
whenever(clock.currentTimeMillis).thenReturn(now)
ShadowSettings.set24HourTimeFormat(is24HourTime)
val timestamp = dateUtil.format(then, locale)
assertEquals(expected, timestamp)
}
companion object {
private val zone = TimeZone.getTimeZone("GMT")
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{index}: myMethod({0}, {1})")
fun parameters(): List<Array<Any>> = listOf(
timespan(
millis(2001, JANUARY),
millis(2001, JANUARY),
assertion(Locale.US, "Now", "Now"),
assertion(Locale.CHINA, "Now", "Now"),
assertion(Locale.ENGLISH, "Now", "Now")
),
timespan(
millis(2001, JANUARY),
millis(2001, JANUARY),
assertion(Locale.US, "Now", "Now"),
),
timespan(
millis(2001, DECEMBER, 30),
millis(2001, DECEMBER, 30, second = 59),
assertion(Locale.US, "Now", "Now"),
),
timespan(
millis(2001, DECEMBER, 30),
millis(2001, DECEMBER, 30, minute = 1),
assertion(Locale.US, "Now", "Now")
),
timespan(
millis(2001, DECEMBER, 30),
millis(2001, DECEMBER, 30, minute = 1, second = 1),
assertion(Locale.US, "10:30 AM", "10:30"),
assertion(Locale.JAPAN, "10:30 午前", "10:30")
),
timespan(
millis(2001, DECEMBER, 30),
millis(2001, DECEMBER, 30, minute = 1, second = 1),
assertion(Locale.US, "10:30 AM", "10:30")
),
timespan(
millis(2001, DECEMBER, 30, hour = 12, minute = 15),
millis(2001, DECEMBER, 30, hour = 12, minute = 16, second = 1),
assertion(Locale.US, "10:45 PM", "22:45")
),
timespan(
millis(2001, DECEMBER, 25),
millis(2001, DECEMBER, 30),
assertion(Locale.US, "Tue 10:30 AM", "Tue 10:30")
),
timespan(
millis(2001, NOVEMBER, 25),
millis(2001, DECEMBER, 10),
assertion(Locale.US, "Nov 25 10:30 AM", "Nov 25 10:30")
),
timespan(
millis(2001, JANUARY),
millis(2003, JANUARY),
assertion(Locale.US, "Jan 1 10:30 AM, 2001", "Jan 1 10:30, 2001")
),
).flatten()
private fun assertion(
locale: Locale,
expected: String,
expected24HourTime: String
): Timespan.() -> List<Array<Any>> = {
listOf(
assertion(locale, expected, false),
assertion(locale, expected24HourTime, true)
)
}
private fun timespan(
start: Locale.() -> Long,
end: Locale.() -> Long,
vararg assertions: Timespan.() -> List<Array<Any>>
): List<Array<Any>> = Timespan(start, end).run { assertions.map { it() } }.flatten()
private fun millis(
year: Int,
month: Int = 0,
date: Int = 1,
hour: Int = 0,
minute: Int = 0,
second: Int = 0
): (Locale) -> Long = {
Calendar.getInstance(zone, it)
.apply { set(year, month, date, hour, minute, second) }
.timeInMillis
}
class Timespan(
val start: (Locale) -> Long,
val end: (Locale) -> Long
) {
fun assertion(
locale: Locale,
expected: String,
is24HourTime: Boolean
): Array<Any> = arrayOf(expected, locale, start(locale), end(locale), is24HourTime)
}
}
}
class TestApplication : Application()

View File

@ -0,0 +1 @@
sdk=29