Skip to content

Commit

Permalink
Added Configurable Date Format For App (#3554)
Browse files Browse the repository at this point in the history
* added date format configurable and applied on sync time in navigation, setting screen and insight screen

* Resolve CI build issue.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Run spotless fix

Signed-off-by: Lentumunai-Mark <[email protected]>

* Resolve failing tests.

Signed-off-by: Lentumunai-Mark <[email protected]>

* Add tests for uncovered functionality.

Signed-off-by: Lentumunai-Mark <[email protected]>

---------

Signed-off-by: Lentumunai-Mark <[email protected]>
Co-authored-by: Lentumunai-Mark <[email protected]>
Co-authored-by: Benjamin Mwalimu <[email protected]>
  • Loading branch information
3 people authored Oct 17, 2024
1 parent fa7a8bd commit f29d85c
Show file tree
Hide file tree
Showing 12 changed files with 188 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import org.smartregister.fhircore.engine.configuration.ConfigType
import org.smartregister.fhircore.engine.configuration.Configuration
import org.smartregister.fhircore.engine.configuration.event.EventWorkflow
import org.smartregister.fhircore.engine.domain.model.LauncherType
import org.smartregister.fhircore.engine.util.extension.DEFAULT_FORMAT_SDF_DD_MM_YYYY

@Serializable
data class ApplicationConfiguration(
Expand Down Expand Up @@ -57,6 +58,7 @@ data class ApplicationConfiguration(
id = null,
),
val codingSystems: List<CodingSystemConfig> = emptyList(),
var dateFormat: String = DEFAULT_FORMAT_SDF_DD_MM_YYYY,
) : Configuration()

enum class SyncStrategy {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.hl7.fhir.r4.model.DateTimeType
import org.hl7.fhir.r4.model.DateType
import org.ocpsoft.prettytime.PrettyTime
import org.smartregister.fhircore.engine.R
import timber.log.Timber

const val SDF_DD_MMM_YYYY = "dd-MMM-yyyy"
const val SDF_YYYY_MM_DD = "yyyy-MM-dd"
Expand All @@ -39,6 +40,9 @@ const val SDF_YYYY = "yyyy"
const val SDF_D_MMM_YYYY_WITH_COMA = "d MMM, yyyy"
const val SDFHH_MM = "HH:mm"
const val SDF_E_MMM_DD_YYYY = "E, MMM dd yyyy"
const val DEFAULT_FORMAT_SDF_DD_MM_YYYY = "EEE, MMM dd - hh:mm a"
const val SDF_YYYY_MMM_DD_HH_MM_SS = "yyyy-MM-dd HH:mm:ss"
const val MMM_D_HH_MM_AA = "MMM d, hh:mm aa"

fun yesterday(): Date = DateTimeType.now().apply { add(Calendar.DATE, -1) }.value

Expand Down Expand Up @@ -149,3 +153,60 @@ fun calculateAge(date: Date, context: Context, localDateNow: LocalDate = LocalDa

private fun Context.abbreviateString(resourceId: Int, content: Int) =
if (content > 0) "$content${this.getString(resourceId).lowercase().abbreviate()} " else ""

fun formatDate(timeMillis: Long, desireFormat: String): String {
return try {
// Try formatting with the provided format
val format = SimpleDateFormat(desireFormat, Locale.getDefault())
val date = Date(timeMillis)
format.format(date)
} catch (e: Exception) {
// If formatting fails, fall back to the default format
val defaultFormat = SimpleDateFormat(DEFAULT_FORMAT_SDF_DD_MM_YYYY, Locale.getDefault())
val date = Date(timeMillis)
defaultFormat.format(date)
}
}

/**
* Reformats a given date string from its current format to a specified desired format. If the date
* string cannot be parsed in the provided current format, the method will return the original date
* string as a fallback.
*
* @param inputDateString The date string that needs to be reformatted.
* @param currentFormat The format in which the input date string is provided (e.g., "dd-MM-yyyy").
* @param desiredFormat The format in which the output date string should be returned (default:
* "yyyy-MM-dd HH:mm:ss"). If no desired format is specified, it defaults to "yyyy-MM-dd
* HH:mm:ss".
* @return A string representing the date in the desired format if parsing succeeds, or the original
* input date string if parsing fails.
*
* Example usage:
* ```
* val reformattedDate = reformatDate("08-10-2024 15:30", "dd-MM-yyyy HH:mm", "yyyy-MM-dd HH:mm:ss")
* println(reformattedDate) // Output: "2024-10-08 15:30:00"
*
* val invalidDate = reformatDate("InvalidDate", "dd-MM-yyyy", "yyyy-MM-dd")
* println(invalidDate) // Output: "InvalidDate"
* ```
*/
fun reformatDate(
inputDateString: String,
currentFormat: String,
desiredFormat: String,
): String {
return try {
// Create a SimpleDateFormat with the current format of the input date
val inputDateFormat = SimpleDateFormat(currentFormat, Locale.getDefault())
// Parse the input date string into a Date object
val date = inputDateFormat.parse(inputDateString)
// Create a SimpleDateFormat for the desired format
val outputDateFormat = SimpleDateFormat(desiredFormat, Locale.getDefault())
// Format the date into the desired format and return the result
outputDateFormat.format(date ?: Date())
} catch (e: Exception) {
// In case of any exception, return the original input date string
Timber.e(e)
inputDateString
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -137,4 +137,26 @@ class DateTimeExtensionTest : RobolectricTest() {
fun isTodayWithDateYesterdayShouldReturnFalse() {
assertFalse(yesterday().isToday())
}

@Test
fun testReformatDateWithValidDate() {
val inputDateString = "2022-02-02"
val currentFormat = "yyyy-MM-dd"
val desiredFormat = "dd/MM/yyyy"

val result = reformatDate(inputDateString, currentFormat, desiredFormat)

assertEquals("02/02/2022", result)
}

@Test
fun testReformatDateWithInvalidDateFormat() {
val inputDateString = "02/02/2022"
val currentFormat = "yyyy-MM-dd"
val desiredFormat = "dd/MM/yyyy"

val result = reformatDate(inputDateString, currentFormat, desiredFormat)

assertEquals(inputDateString, result)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.smartregister.fhircore.engine.util.extension.DEFAULT_FORMAT_SDF_DD_MM_YYYY
import org.smartregister.fhircore.quest.ui.usersetting.INSIGHT_UNSYNCED_DATA
import org.smartregister.fhircore.quest.ui.usersetting.UserSettingInsightScreen

Expand Down Expand Up @@ -118,6 +119,7 @@ class UserSettingInsightScreenTest {
unsyncedResourcesFlow = unsyncedResourcesFlow,
navController = rememberNavController(),
onRefreshRequest = {},
dateFormat = DEFAULT_FORMAT_SDF_DD_MM_YYYY,
)
}
this.activity = activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,5 +85,6 @@
],
"logGpsLocation": [
"QUESTIONNAIRE"
]
],
"dateFormat": "MMM d, hh:mm aa"
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,10 @@ import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.extension.countUnSyncedResources
import org.smartregister.fhircore.engine.util.extension.extractLogicalIdUuid
import org.smartregister.fhircore.engine.util.extension.fetchLanguages
import org.smartregister.fhircore.engine.util.extension.formatDate
import org.smartregister.fhircore.engine.util.extension.getActivity
import org.smartregister.fhircore.engine.util.extension.isDeviceOnline
import org.smartregister.fhircore.engine.util.extension.reformatDate
import org.smartregister.fhircore.engine.util.extension.refresh
import org.smartregister.fhircore.engine.util.extension.setAppLocale
import org.smartregister.fhircore.engine.util.extension.showToast
Expand Down Expand Up @@ -157,7 +159,7 @@ constructor(
appTitle = applicationConfiguration.appTitle,
currentLanguage = loadCurrentLanguage(),
username = secureSharedPreference.retrieveSessionUsername() ?: "",
lastSyncTime = retrieveLastSyncTimestamp() ?: "",
lastSyncTime = getSyncTime(),
languages = configurationRegistry.fetchLanguages(),
navigationConfiguration = navigationConfiguration,
registerCountMap = registerCountMap,
Expand All @@ -166,6 +168,47 @@ constructor(

countRegisterData()
}
// todo - if we can move this method to somewhere else where it can be accessed easily on multiple
// view models
/**
* Retrieves the last sync time from shared preferences and returns it in a formatted way. This
* method handles both cases:
* 1. The time stored as a timestamp in milliseconds (preferred).
* 2. Backward compatibility where the time is stored in a formatted string.
*
* @return A formatted sync time string.
*/
fun getSyncTime(): String {
var result = ""

// First, check if we have any previously stored sync time in SharedPreferences.
retrieveLastSyncTimestamp()?.let { storedDate ->

// Try to treat the stored time as a timestamp (in milliseconds).
runCatching {
// Attempt to convert the stored date to Long (i.e., millis format) and format it.
result =
formatDate(
timeMillis = storedDate.toLong(),
desireFormat = applicationConfiguration.dateFormat,
)
}
.onFailure {
// If conversion to Long fails, it's likely that the stored date is in a formatted string
// (backward compatibility).
// Reformat the stored date using the provided SYNC_TIMESTAMP_OUTPUT_FORMAT.
result =
reformatDate(
inputDateString = storedDate,
currentFormat = SYNC_TIMESTAMP_OUTPUT_FORMAT,
desiredFormat = applicationConfiguration.dateFormat,
)
}
}

// Return the result (either formatted time in millis or re-formatted backward-compatible date).
return result
}

fun countRegisterData() {
viewModelScope.launch {
Expand Down Expand Up @@ -205,7 +248,7 @@ constructor(
if (event.state is CurrentSyncJobStatus.Succeeded) {
sharedPreferencesHelper.write(
SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name,
formatLastSyncTimestamp(event.state.timestamp),
event.state.timestamp.toInstant().toEpochMilli().toString(),
)
retrieveAppMainUiState()
viewModelScope.launch { retrieveAppMainUiState() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@ class UserInsightScreenFragment : Fragment() {
location = userSettingViewModel.practitionerLocation(),
appVersionCode = userSettingViewModel.appVersionCode.toString(),
appVersion = userSettingViewModel.appVersionName,
buildDate = userSettingViewModel.buildDate,
buildDate = userSettingViewModel.getBuildDate(),
unsyncedResourcesFlow = userSettingViewModel.unsyncedResourcesMutableSharedFlow,
navController = findNavController(),
onRefreshRequest = { userSettingViewModel.fetchUnsyncedResources() },
dateFormat = userSettingViewModel.getDateFormat(),
)
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class UserSettingFragment : Fragment(), OnSyncListener {
userSettingViewModel.progressBarState.observeAsState(Pair(false, 0)).value,
isDebugVariant = BuildConfig.DEBUG,
mainNavController = findNavController(),
lastSyncTime = userSettingViewModel.retrieveLastSyncTimestamp(),
lastSyncTime = appMainViewModel.getSyncTime(),
showProgressIndicatorFlow = userSettingViewModel.showProgressIndicatorFlow,
dataMigrationVersion = userSettingViewModel.retrieveDataMigrationVersion(),
enableManualSync =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ import kotlinx.coroutines.flow.MutableSharedFlow
import org.smartregister.fhircore.engine.R
import org.smartregister.fhircore.engine.ui.theme.DividerColor
import org.smartregister.fhircore.engine.ui.theme.LoginDarkColor
import org.smartregister.fhircore.engine.util.extension.DEFAULT_FORMAT_SDF_DD_MM_YYYY
import org.smartregister.fhircore.engine.util.extension.formatDate

const val USER_INSIGHT_TOP_APP_BAR = "userInsightToAppBar"
const val INSIGHT_UNSYNCED_DATA = "insightUnsyncedData"
Expand All @@ -93,6 +95,7 @@ fun UserSettingInsightScreen(
unsyncedResourcesFlow: MutableSharedFlow<List<Pair<String, Int>>>,
navController: NavController,
onRefreshRequest: () -> Unit,
dateFormat: String = DEFAULT_FORMAT_SDF_DD_MM_YYYY,
) {
val unsyncedResources = unsyncedResourcesFlow.collectAsState(initial = listOf()).value

Expand Down Expand Up @@ -235,7 +238,8 @@ fun UserSettingInsightScreen(
(if (Build.DEVICE.isNullOrEmpty()) "-" else Build.DEVICE),
stringResource(R.string.os_version) to
(if (Build.VERSION.BASE_OS.isNullOrEmpty()) "-" else Build.VERSION.BASE_OS),
stringResource(R.string.device_date) to (formatTimestamp(Build.TIME).ifEmpty { "-" }),
stringResource(R.string.device_date) to
(formatDate(Build.TIME, desireFormat = dateFormat).ifEmpty { "-" }),
)
InsightInfoView(
title = stringResource(id = R.string.device_info),
Expand Down Expand Up @@ -365,6 +369,7 @@ fun UserSettingInsightScreenPreview() {
unsyncedResourcesFlow = MutableSharedFlow(),
navController = rememberNavController(),
onRefreshRequest = {},
dateFormat = DEFAULT_FORMAT_SDF_DD_MM_YYYY,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,13 @@ import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.SecureSharedPreference
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.extension.SDF_YYYY_MMM_DD_HH_MM_SS
import org.smartregister.fhircore.engine.util.extension.countUnSyncedResources
import org.smartregister.fhircore.engine.util.extension.fetchLanguages
import org.smartregister.fhircore.engine.util.extension.getActivity
import org.smartregister.fhircore.engine.util.extension.isDeviceOnline
import org.smartregister.fhircore.engine.util.extension.launchActivityWithNoBackStackHistory
import org.smartregister.fhircore.engine.util.extension.reformatDate
import org.smartregister.fhircore.engine.util.extension.refresh
import org.smartregister.fhircore.engine.util.extension.setAppLocale
import org.smartregister.fhircore.engine.util.extension.showToast
Expand Down Expand Up @@ -89,7 +91,6 @@ constructor(

val appVersionCode = BuildConfig.VERSION_CODE
val appVersionName = BuildConfig.VERSION_NAME
val buildDate = BuildConfig.BUILD_DATE

fun retrieveUsername(): String? = secureSharedPreference.retrieveSessionUsername()

Expand Down Expand Up @@ -204,6 +205,15 @@ constructor(

fun enabledDeviceToDeviceSync(): Boolean = applicationConfiguration.deviceToDeviceSync != null

fun getDateFormat() = applicationConfiguration.dateFormat

fun getBuildDate() =
reformatDate(
inputDateString = BuildConfig.BUILD_DATE,
currentFormat = SDF_YYYY_MMM_DD_HH_MM_SS,
desiredFormat = applicationConfiguration.dateFormat,
)

fun fetchUnsyncedResources() {
viewModelScope.launch {
withContext(dispatcherProvider.io()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,14 +124,15 @@ class AppMainActivityTest : ActivityRobolectricTest() {
}

@Test
fun testOnSyncWithSyncStateSucceded() {
fun testOnSyncWithSyncStateSucceeded() {
// Arrange
val viewModel = appMainActivity.appMainViewModel
val stateSucceded = CurrentSyncJobStatus.Succeeded(OffsetDateTime.now())
appMainActivity.onSync(stateSucceded)

Assert.assertEquals(
viewModel.formatLastSyncTimestamp(timestamp = stateSucceded.timestamp),
viewModel.retrieveLastSyncTimestamp(),
viewModel.getSyncTime(),
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,13 +71,16 @@ import org.smartregister.fhircore.engine.util.DispatcherProvider
import org.smartregister.fhircore.engine.util.SecureSharedPreference
import org.smartregister.fhircore.engine.util.SharedPreferenceKey
import org.smartregister.fhircore.engine.util.SharedPreferencesHelper
import org.smartregister.fhircore.engine.util.extension.formatDate
import org.smartregister.fhircore.engine.util.extension.isDeviceOnline
import org.smartregister.fhircore.engine.util.extension.reformatDate
import org.smartregister.fhircore.engine.util.extension.showToast
import org.smartregister.fhircore.engine.util.test.HiltActivityForTest
import org.smartregister.fhircore.quest.app.fakes.Faker
import org.smartregister.fhircore.quest.navigation.MainNavigationScreen
import org.smartregister.fhircore.quest.navigation.NavigationArg
import org.smartregister.fhircore.quest.robolectric.RobolectricTest
import org.smartregister.fhircore.quest.ui.main.AppMainViewModel.Companion.SYNC_TIMESTAMP_OUTPUT_FORMAT
import org.smartregister.fhircore.quest.ui.shared.models.QuestionnaireSubmission

@HiltAndroidTest
Expand Down Expand Up @@ -185,7 +188,7 @@ class AppMainViewModelTest : RobolectricTest() {
)
Assert.assertEquals(
appMainViewModel.formatLastSyncTimestamp(syncFinishedTimestamp),
sharedPreferencesHelper.read(SharedPreferenceKey.LAST_SYNC_TIMESTAMP.name, null),
appMainViewModel.getSyncTime(),
)
coVerify { appMainViewModel.retrieveAppMainUiState() }
}
Expand Down Expand Up @@ -314,6 +317,33 @@ class AppMainViewModelTest : RobolectricTest() {
}
}

@Test
fun testGetSyncTimeWithMillisTimestamp() {
// Mocking a timestamp in milliseconds
val mockTimestamp = OffsetDateTime.now().toInstant().toEpochMilli().toString()
every { appMainViewModel.retrieveLastSyncTimestamp() } returns mockTimestamp
every { appMainViewModel.applicationConfiguration.dateFormat } returns "yyyy-MM-dd HH:mm:ss"
val syncTime = appMainViewModel.getSyncTime()
val expectedFormattedDate =
formatDate(mockTimestamp.toLong(), appMainViewModel.applicationConfiguration.dateFormat)
Assert.assertEquals(expectedFormattedDate, syncTime)
}

@Test
fun testGetSyncTimeWithFormattedDate() {
val mockFormattedDate = "2023-10-10 10:10:10"
every { appMainViewModel.retrieveLastSyncTimestamp() } returns mockFormattedDate
every { appMainViewModel.applicationConfiguration.dateFormat } returns "yyyy-MM-dd"
val syncTime = appMainViewModel.getSyncTime()
val expectedReformattedDate =
reformatDate(
inputDateString = mockFormattedDate,
currentFormat = SYNC_TIMESTAMP_OUTPUT_FORMAT,
desiredFormat = appMainViewModel.applicationConfiguration.dateFormat,
)
Assert.assertEquals(expectedReformattedDate, syncTime)
}

@Test
@kotlinx.coroutines.ExperimentalCoroutinesApi
fun testOnSubmitQuestionnaireShouldNeverUpdateTaskStatusWhenQuestionnaireTaskIdIsNull() =
Expand Down

0 comments on commit f29d85c

Please sign in to comment.