From c865ed0cdcc4450041e993a8c412136993e58a91 Mon Sep 17 00:00:00 2001 From: Doug Melton Date: Thu, 1 May 2025 21:05:54 -0700 Subject: [PATCH] Improve handling of 12/24 hour timestamps on configuration change. This fixes an edge case seen on ConversationFragment, where if the device time format is switched between 12/24 hour format while the app is running, the old time format will still be displayed when the app is resumed. This is due to a design flaw in `DateTimeFormatter.ofLocalizedTime`, where the time format is statically cached and not updated upon configuration change. The `LocalTime.formatHours()` extension method was updated to no longer rely on the misbehaving `ofLocalTime` method. In addition, `ConversationMessaageComputeWorkers.recomputeFormattedDate` was designed to skip recomputing non-relative timestamps. This works in most cases but not this specific edge case. A `force: Boolean` flag was added to force all items to be updated. And the `force = true` flag was passed upon `onResume` of the fragment. Closes #14121 --- .../conversation/v2/ConversationFragment.kt | 23 ++++++++++++++----- .../ConversationMessageComputeWorkers.kt | 5 ++-- .../securesms/util/JavaTimeExtensions.kt | 13 ++++------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt index 5ac8d785e8..05fe536b17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationFragment.kt @@ -665,6 +665,11 @@ class ConversationFragment : outState.putBoolean(SAVED_STATE_IS_SEARCH_REQUESTED, isSearchRequested) } + override fun onStart() { + super.onStart() + recomputeMessageDates(forceUpdate = true) + } + override fun onResume() { super.onResume() @@ -1173,12 +1178,7 @@ class ConversationFragment : getVoiceNoteMediaController().voiceNotePlaybackState.observe(viewLifecycleOwner, inputPanel.playbackStateObserver) - val conversationUpdateTick = ConversationUpdateTick { - disposables += ConversationMessageComputeWorkers.recomputeFormattedDate( - requireContext(), - adapter.currentList.filterIsInstance() - ).observeOn(AndroidSchedulers.mainThread()).subscribeBy { adapter.updateTimestamps() } - } + val conversationUpdateTick = ConversationUpdateTick { recomputeMessageDates() } viewLifecycleOwner.lifecycle.addObserver(conversationUpdateTick) @@ -1188,6 +1188,17 @@ class ConversationFragment : } } + private fun recomputeMessageDates(forceUpdate: Boolean = false) { + disposables += ConversationMessageComputeWorkers + .recomputeFormattedDate( + context = requireContext(), + items = adapter.currentList.filterIsInstance(), + forceUpdate = forceUpdate + ) + .observeOn(AndroidSchedulers.mainThread()) + .subscribeBy { adapter.updateTimestamps() } + } + private fun initializeInlineSearch() { inlineQueryController.onOrientationChange(resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/ConversationMessageComputeWorkers.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/ConversationMessageComputeWorkers.kt index 5c38bb2941..9a6ada4e10 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/ConversationMessageComputeWorkers.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/computed/ConversationMessageComputeWorkers.kt @@ -21,13 +21,14 @@ object ConversationMessageComputeWorkers { fun recomputeFormattedDate( context: Context, - items: List + items: List, + forceUpdate: Boolean = false ): Single { return Single.fromCallable { var hasUpdatedProperties = false for (item in items) { val oldDate = item.conversationMessage.computedProperties.formattedDate - if (oldDate.isRelative) { + if (oldDate.isRelative || forceUpdate) { val newDate = ConversationMessage.getFormattedDate(context, item.conversationMessage.messageRecord) item.conversationMessage.computedProperties.formattedDate = newDate hasUpdatedProperties = true diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt b/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt index 316e9f4416..a5a0cf0389 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/JavaTimeExtensions.kt @@ -1,7 +1,6 @@ package org.thoughtcrime.securesms.util import android.content.Context -import android.os.Build import android.text.format.DateFormat import java.time.DayOfWeek import java.time.Instant @@ -13,7 +12,6 @@ import java.time.ZoneId import java.time.ZoneOffset import java.time.ZonedDateTime import java.time.format.DateTimeFormatter -import java.time.format.FormatStyle import java.time.temporal.WeekFields import java.util.Locale import java.util.concurrent.TimeUnit @@ -91,14 +89,13 @@ fun Long.toLocalTime(zoneId: ZoneId = ZoneId.systemDefault()): LocalTime { } /** - * Formats [LocalTime] as localized time. For example, "8:00 AM" + * Formats [LocalTime] as localized time. For example, "1:45 PM" or "13:45" */ fun LocalTime.formatHours(context: Context): String { - return if (Build.VERSION.SDK_INT >= 26 || !DateFormat.is24HourFormat(context)) { - DateTimeFormatter.ofLocalizedTime(FormatStyle.SHORT).format(this) - } else { - DateTimeFormatter.ofPattern("HH:mm", Locale.getDefault()).format(this) - } + // We have to create our own pattern here, since the formatter instance returned by DateTimeFormatter.ofLocalizedTime() is looked up lazily, is immutable, + // and is not updated when the system's 24-hour time setting changes. + val pattern = if (DateFormat.is24HourFormat(context)) "HH:mm" else "h:mm a" + return DateTimeFormatter.ofPattern(pattern, Locale.getDefault()).format(this) } /**