Skip to content

Commit 9f189cc

Browse files
authored
uplift(release): fix: relative date format crash (#11154) (#11217)
2 parents ac57bee + 7609b65 commit 9f189cc

8 files changed

Lines changed: 98 additions & 56 deletions

File tree

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatter.kt

Lines changed: 64 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,58 @@ import android.text.format.DateUtils.FORMAT_SHOW_DATE
99
import android.text.format.DateUtils.FORMAT_SHOW_TIME
1010
import android.text.format.DateUtils.FORMAT_SHOW_WEEKDAY
1111
import android.text.format.DateUtils.FORMAT_SHOW_YEAR
12-
import java.text.SimpleDateFormat
13-
import java.util.Calendar
14-
import java.util.Calendar.DAY_OF_WEEK
15-
import java.util.Calendar.YEAR
16-
import java.util.Locale
1712
import kotlin.time.Clock
18-
import kotlin.time.ExperimentalTime
1913
import kotlin.time.Instant
14+
import kotlinx.datetime.LocalDateTime
15+
import kotlinx.datetime.TimeZone
16+
import kotlinx.datetime.daysUntil
17+
import kotlinx.datetime.format.char
18+
import kotlinx.datetime.toLocalDateTime
2019
import net.thunderbird.core.preference.display.visualSettings.message.list.MessageListDateTimeFormat
2120

2221
/**
2322
* Formatter to describe timestamps as a time relative to now.
23+
*
24+
* @param context The context to use for formatting dates.
25+
* @param clock The clock to use for getting the current time.
2426
*/
25-
class RelativeDateTimeFormatter
26-
@OptIn(ExperimentalTime::class)
27-
constructor(
27+
class RelativeDateTimeFormatter(
2828
private val context: Context,
2929
private val clock: Clock,
3030
) {
31+
/**
32+
* Format a date using the given [dateTimeFormat].
33+
*
34+
* @param timestamp The timestamp to format.
35+
* @param dateTimeFormat The date format to use.
36+
*/
37+
fun formatDate(
38+
timestamp: Long,
39+
dateTimeFormat: MessageListDateTimeFormat,
40+
): String {
41+
val timeZone = TimeZone.currentSystemDefault()
42+
val now = clock.now().toLocalDateTime(timeZone)
43+
44+
return formatDate(
45+
timestamp = timestamp,
46+
dateTimeFormat = dateTimeFormat,
47+
now = now,
48+
timeZone = timeZone,
49+
)
50+
}
51+
52+
private fun formatDate(
53+
timestamp: Long,
54+
dateTimeFormat: MessageListDateTimeFormat,
55+
now: LocalDateTime,
56+
timeZone: TimeZone,
57+
): String {
58+
val date = Instant.fromEpochMilliseconds(timestamp).toLocalDateTime(timeZone)
3159

32-
fun formatDate(timestamp: Long, dateTimeFormat: MessageListDateTimeFormat): String {
33-
@OptIn(ExperimentalTime::class)
34-
val now = clock.now().toCalendar()
35-
val date = timestamp.toCalendar()
3660
return when (dateTimeFormat) {
3761
MessageListDateTimeFormat.Contextual -> {
3862
val flags = when {
39-
date.isToday() -> FORMAT_SHOW_TIME
63+
date.isSameDayAs(now) -> FORMAT_SHOW_TIME
4064
date.isWithinPastSevenDaysOf(now) -> FORMAT_SHOW_WEEKDAY or FORMAT_ABBREV_WEEKDAY
4165
date.isSameYearAs(now) -> FORMAT_SHOW_DATE or FORMAT_ABBREV_MONTH
4266
else -> FORMAT_SHOW_DATE or FORMAT_SHOW_YEAR or FORMAT_NUMERIC_DATE
@@ -48,30 +72,37 @@ constructor(
4872
DateUtils.formatDateRange(context, timestamp, timestamp, flags)
4973
}
5074
MessageListDateTimeFormat.ISO -> {
51-
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
52-
sdf.format(java.util.Date(timestamp))
75+
isoDateTimeFormat.format(date)
5376
}
5477
}
5578
}
56-
}
5779

58-
private fun Long.toCalendar(): Calendar {
59-
val calendar = Calendar.getInstance()
60-
calendar.timeInMillis = this
61-
return calendar
62-
}
80+
private fun LocalDateTime.isSameDayAs(other: LocalDateTime): Boolean {
81+
return date == other.date
82+
}
6383

64-
@OptIn(ExperimentalTime::class)
65-
private fun Instant.toCalendar(): Calendar {
66-
val calendar = Calendar.getInstance()
67-
calendar.timeInMillis = this.toEpochMilliseconds()
68-
return calendar
69-
}
84+
private fun LocalDateTime.isWithinPastSevenDaysOf(other: LocalDateTime): Boolean {
85+
val daysUntil = date.daysUntil(other.date)
86+
return daysUntil in 1 until DAYS_PER_WEEK
87+
}
7088

71-
private fun Calendar.isToday() = DateUtils.isToday(this.timeInMillis)
89+
private fun LocalDateTime.isSameYearAs(other: LocalDateTime): Boolean {
90+
return year == other.year
91+
}
7292

73-
private fun Calendar.isWithinPastSevenDaysOf(other: Calendar) = this.before(other) &&
74-
DateUtils.WEEK_IN_MILLIS > other.timeInMillis - this.timeInMillis &&
75-
this[DAY_OF_WEEK] != other[DAY_OF_WEEK]
93+
private companion object {
94+
private const val DAYS_PER_WEEK = 7
7695

77-
private fun Calendar.isSameYearAs(other: Calendar) = this[YEAR] == other[YEAR]
96+
val isoDateTimeFormat = LocalDateTime.Format {
97+
year()
98+
char('-')
99+
monthNumber()
100+
char('-')
101+
day()
102+
char(' ')
103+
hour()
104+
char(':')
105+
minute()
106+
}
107+
}
108+
}

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/KoinModule.kt

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ val messageListUiModule = module {
1818
messageHelper = get(),
1919
messageListPreferencesManager = get(),
2020
outboxFolderManager = get(),
21-
relativeDateTimeFormatter = get(),
2221
featureFlagProvider = get(),
2322
contactLetterBitmapCreator = get(),
2423
)

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/LegacyMessageListFragment.kt

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import com.fsck.k9.ui.changelog.RecentChangesActivity
6868
import com.fsck.k9.ui.changelog.RecentChangesViewModel
6969
import com.fsck.k9.ui.choosefolder.ChooseFolderActivity
7070
import com.fsck.k9.ui.choosefolder.ChooseFolderResultContract
71+
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
7172
import com.fsck.k9.ui.messagelist.MessageListFragmentBridgeContract.MessageListFragmentListener
7273
import com.fsck.k9.ui.messagelist.MessageListFragmentBridgeContract.MessageListFragmentListener.Companion.MAX_PROGRESS
7374
import com.fsck.k9.ui.messagelist.debug.AuthDebugActions
@@ -80,10 +81,8 @@ import java.util.concurrent.Future
8081
import kotlin.time.ExperimentalTime
8182
import kotlinx.collections.immutable.persistentSetOf
8283
import kotlinx.coroutines.Dispatchers
83-
import kotlinx.coroutines.flow.distinctUntilChanged
8484
import kotlinx.coroutines.flow.drop
8585
import kotlinx.coroutines.flow.launchIn
86-
import kotlinx.coroutines.flow.map
8786
import kotlinx.coroutines.flow.onEach
8887
import kotlinx.coroutines.launch
8988
import net.jcip.annotations.GuardedBy
@@ -183,6 +182,7 @@ class LegacyMessageListFragment :
183182

184183
private val contactRepository: ContactRepository by inject()
185184
private val avatarMonogramCreator: AvatarMonogramCreator by inject()
185+
private val relativeDateTimeFormatter: RelativeDateTimeFormatter by inject()
186186

187187
private val chooseFolderForMoveLauncher: ActivityResultLauncher<ChooseFolderResultContract.Input> =
188188
registerForActivityResult(ChooseFolderResultContract(ChooseFolderActivity.Action.MOVE)) { result ->
@@ -318,18 +318,25 @@ class LegacyMessageListFragment :
318318

319319
adapter = createMessageListAdapter()
320320

321+
var observedDateTimeFormat = messageListSettings.dateTimeFormat
321322
generalSettingsManager.getSettingsFlow()
322323
/**
323324
* Skips the first emitted item from the settings flow,
324-
* since the initial value of `showingThreadedList` is taken
325-
* from the fragment's arguments rather than the flow.
325+
* since initial display settings are taken from fragment state and current preferences.
326326
*/
327327
.drop(1)
328-
.map { it.display.inboxSettings.isThreadedViewEnabled }
329-
.distinctUntilChanged()
330-
.onEach {
331-
showingThreadedList = it
332-
loadMessageList(forceUpdate = true)
328+
.onEach { generalSettings ->
329+
val isThreadedViewEnabled = generalSettings.display.inboxSettings.isThreadedViewEnabled
330+
if (showingThreadedList != isThreadedViewEnabled) {
331+
showingThreadedList = isThreadedViewEnabled
332+
loadMessageList(forceUpdate = true)
333+
}
334+
335+
val dateTimeFormat = generalSettings.display.visualSettings.messageListSettings.dateTimeFormat
336+
if (observedDateTimeFormat != dateTimeFormat) {
337+
observedDateTimeFormat = dateTimeFormat
338+
adapter.refreshFormattedDates()
339+
}
333340
}
334341
.launchIn(lifecycleScope)
335342

@@ -405,6 +412,9 @@ class LegacyMessageListFragment :
405412
featureFlagProvider = featureFlagProvider,
406413
contactRepository = contactRepository,
407414
avatarMonogramCreator = avatarMonogramCreator,
415+
formatDate = { timestamp ->
416+
relativeDateTimeFormatter.formatDate(timestamp, messageListSettings.dateTimeFormat)
417+
},
408418
).apply {
409419
activeMessage = this@LegacyMessageListFragment.activeMessage
410420
}

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListAdapter.kt

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ class MessageListAdapter internal constructor(
4848
private val featureFlagProvider: FeatureFlagProvider,
4949
private val contactRepository: ContactRepository,
5050
private val avatarMonogramCreator: AvatarMonogramCreator,
51+
private val formatDate: (Long) -> String,
5152
) : RecyclerView.Adapter<MessageListViewHolder>() {
5253

5354
val colors: MessageViewHolderColors = MessageViewHolderColors.resolveColors(theme)
@@ -170,6 +171,10 @@ class MessageListAdapter internal constructor(
170171
return viewItems[position].viewType
171172
}
172173

174+
fun refreshFormattedDates() {
175+
notifyItemRangeChanged(0, itemCount)
176+
}
177+
173178
private fun getItem(position: Int): MessageListItem = (viewItems[position] as MessageListViewItem.Message).item
174179

175180
fun getItemById(uniqueId: Long): MessageListItem? {
@@ -263,18 +268,19 @@ class MessageListAdapter internal constructor(
263268

264269
TYPE_MESSAGE -> {
265270
val messageListItem = getItem(position)
271+
val formattedMessageListItem = messageListItem.withFormattedDate()
266272
val result = featureFlagProvider.provide(UseComposeForMessageListItems)
267273
if (result.isEnabled()) {
268274
val messageViewHolder = holder as ComposableMessageViewHolder
269275
messageViewHolder.bind(
270-
item = messageListItem,
276+
item = formattedMessageListItem,
271277
isActive = isActiveMessage(messageListItem),
272278
isSelected = isSelected(messageListItem),
273279
)
274280
} else {
275281
val messageViewHolder = holder as MessageViewHolder
276282
messageViewHolder.bind(
277-
messageListItem = messageListItem,
283+
messageListItem = formattedMessageListItem,
278284
isActive = isActiveMessage(messageListItem),
279285
isSelected = isSelected(messageListItem),
280286
)
@@ -301,6 +307,10 @@ class MessageListAdapter internal constructor(
301307
item.messageUid == activeMessage.uid
302308
}
303309

310+
private fun MessageListItem.withFormattedDate(): MessageListItem {
311+
return copy(displayMessageDateTime = formatDate(messageDate))
312+
}
313+
304314
fun isSelected(item: MessageListItem): Boolean {
305315
return item.uniqueId in selected
306316
}

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListItemMapper.kt

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ class MessageListItemMapper(
1515
private val account: LegacyAccount,
1616
private val messageListPreferencesManager: MessageListPreferencesManager,
1717
private val outboxFolderManager: OutboxFolderManager,
18-
private val formatDate: (Long) -> String,
1918
private val contactLetterBitmapCreator: ContactLetterBitmapCreator?,
2019
) : MessageMapper<MessageListItem> {
2120

@@ -39,7 +38,6 @@ class MessageListItemMapper(
3938
} else {
4039
messageHelper.getSenderDisplayName(displayAddress)
4140
}
42-
val displayMessageDateTime = formatDate(message.messageDate)
4341

4442
return MessageListItem(
4543
account,
@@ -49,7 +47,7 @@ class MessageListItemMapper(
4947
message.internalDate,
5048
displayName,
5149
displayAddress,
52-
displayMessageDateTime,
50+
displayMessageDateTime = "undefined",
5351
previewText,
5452
isMessageEncrypted,
5553
message.isRead,

legacy/ui/legacy/src/main/java/com/fsck/k9/ui/messagelist/MessageListLoader.kt

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import com.fsck.k9.helper.MessageHelper
66
import com.fsck.k9.mailstore.LocalStoreProvider
77
import com.fsck.k9.mailstore.MessageColumns
88
import com.fsck.k9.search.getLegacyAccounts
9-
import com.fsck.k9.ui.helper.RelativeDateTimeFormatter
109
import net.thunderbird.core.android.account.LegacyAccount
1110
import net.thunderbird.core.android.account.LegacyAccountManager
1211
import net.thunderbird.core.android.account.SortType
@@ -27,7 +26,6 @@ class MessageListLoader(
2726
private val messageHelper: MessageHelper,
2827
private val messageListPreferencesManager: MessageListPreferencesManager,
2928
private val outboxFolderManager: OutboxFolderManager,
30-
private val relativeDateTimeFormatter: RelativeDateTimeFormatter,
3129
private val featureFlagProvider: FeatureFlagProvider,
3230
private val contactLetterBitmapCreator: ContactLetterBitmapCreator,
3331
) {
@@ -65,12 +63,6 @@ class MessageListLoader(
6563
account,
6664
messageListPreferencesManager,
6765
outboxFolderManager,
68-
formatDate = { formatTime ->
69-
relativeDateTimeFormatter.formatDate(
70-
formatTime,
71-
messageListPreferencesManager.getConfig().dateTimeFormat,
72-
)
73-
},
7466
contactLetterBitmapCreator = contactLetterBitmapCreator.takeIf {
7567
featureFlagProvider.provide(MessageListFeatureFlags.UseComposeForMessageListItems).isEnabled()
7668
},

legacy/ui/legacy/src/test/java/com/fsck/k9/ui/helper/RelativeDateTimeFormatterTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,7 @@ class RelativeDateTimeFormatterTest : RobolectricTest() {
178178

179179
assertThat(displayDate).isEqualTo("2019-12-31 23:59")
180180
}
181+
181182
private fun setClockTo(time: String) {
182183
val dateTime = LocalDateTime.parse(time)
183184
val timeInMillis = dateTime.toEpochMillis()

legacy/ui/legacy/src/test/java/com/fsck/k9/ui/messagelist/MessageListAdapterTest.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,7 @@ class MessageListAdapterTest : RobolectricTest() {
442442
featureFlagProvider = FakeFeatureFlagProvider(),
443443
avatarMonogramCreator = mock(),
444444
contactRepository = mock(),
445+
formatDate = { "12:34" },
445446
)
446447
}
447448

0 commit comments

Comments
 (0)