Use formatter for LLS duration
This commit is contained in:
@@ -29,21 +29,18 @@ fun LocationConstraintsDialog(
|
|||||||
onSubmitClick = onRequestPermissions,
|
onSubmitClick = onRequestPermissions,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
submitText = stringResource(CommonStrings.action_continue),
|
submitText = stringResource(CommonStrings.action_continue),
|
||||||
cancelText = stringResource(CommonStrings.action_cancel),
|
|
||||||
)
|
)
|
||||||
LocationConstraintsDialogState.PermissionDenied -> ConfirmationDialog(
|
LocationConstraintsDialogState.PermissionDenied -> ConfirmationDialog(
|
||||||
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
|
content = stringResource(CommonStrings.error_missing_location_auth_android, appName),
|
||||||
onSubmitClick = onOpenAppSettings,
|
onSubmitClick = onOpenAppSettings,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
submitText = stringResource(CommonStrings.action_continue),
|
submitText = stringResource(CommonStrings.action_continue),
|
||||||
cancelText = stringResource(CommonStrings.action_cancel),
|
|
||||||
)
|
)
|
||||||
LocationConstraintsDialogState.LocationServiceDisabled -> ConfirmationDialog(
|
LocationConstraintsDialogState.LocationServiceDisabled -> ConfirmationDialog(
|
||||||
content = stringResource(CommonStrings.error_location_service_disabled_android),
|
content = stringResource(CommonStrings.error_location_service_disabled_android),
|
||||||
onSubmitClick = onOpenLocationSettings,
|
onSubmitClick = onOpenLocationSettings,
|
||||||
onDismiss = onDismiss,
|
onDismiss = onDismiss,
|
||||||
submitText = stringResource(CommonStrings.action_continue),
|
submitText = stringResource(CommonStrings.action_continue),
|
||||||
cancelText = stringResource(CommonStrings.action_cancel),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,14 +8,8 @@
|
|||||||
package io.element.android.features.location.impl.share
|
package io.element.android.features.location.impl.share
|
||||||
|
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
import kotlin.time.Duration.Companion.hours
|
|
||||||
import kotlin.time.Duration.Companion.minutes
|
|
||||||
|
|
||||||
enum class LiveLocationDuration(
|
data class LiveLocationDuration(
|
||||||
val duration: Duration,
|
val duration: Duration,
|
||||||
val label: String,
|
val formatted: String
|
||||||
) {
|
)
|
||||||
FifteenMinutes(15.minutes, "15 minutes"),
|
|
||||||
OneHour(1.hours, "1 hour"),
|
|
||||||
EightHours(8.hours, "8 hours")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import io.element.android.features.messages.api.MessageComposerContext
|
|||||||
import io.element.android.libraries.architecture.Presenter
|
import io.element.android.libraries.architecture.Presenter
|
||||||
import io.element.android.libraries.core.extensions.flatMap
|
import io.element.android.libraries.core.extensions.flatMap
|
||||||
import io.element.android.libraries.core.meta.BuildMeta
|
import io.element.android.libraries.core.meta.BuildMeta
|
||||||
|
import io.element.android.libraries.dateformatter.api.DurationFormatter
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
import io.element.android.libraries.featureflag.api.FeatureFlagService
|
||||||
import io.element.android.libraries.featureflag.api.FeatureFlags
|
import io.element.android.libraries.featureflag.api.FeatureFlags
|
||||||
import io.element.android.libraries.matrix.api.MatrixClient
|
import io.element.android.libraries.matrix.api.MatrixClient
|
||||||
@@ -43,7 +44,12 @@ import io.element.android.libraries.matrix.api.room.location.AssetType
|
|||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
import io.element.android.libraries.textcomposer.model.MessageComposerMode
|
||||||
import io.element.android.services.analytics.api.AnalyticsService
|
import io.element.android.services.analytics.api.AnalyticsService
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
private val LIVE_LOCATION_DURATIONS = listOf(15.minutes, 1.hours, 8.hours)
|
||||||
|
|
||||||
@AssistedInject
|
@AssistedInject
|
||||||
class ShareLocationPresenter(
|
class ShareLocationPresenter(
|
||||||
@@ -56,6 +62,7 @@ class ShareLocationPresenter(
|
|||||||
private val buildMeta: BuildMeta,
|
private val buildMeta: BuildMeta,
|
||||||
private val featureFlagService: FeatureFlagService,
|
private val featureFlagService: FeatureFlagService,
|
||||||
private val client: MatrixClient,
|
private val client: MatrixClient,
|
||||||
|
private val durationFormatter: DurationFormatter,
|
||||||
) : Presenter<ShareLocationState> {
|
) : Presenter<ShareLocationState> {
|
||||||
@AssistedFactory
|
@AssistedFactory
|
||||||
fun interface Factory {
|
fun interface Factory {
|
||||||
@@ -105,7 +112,10 @@ class ShareLocationPresenter(
|
|||||||
ShareLocationEvent.ShowLiveLocationDurationPicker -> {
|
ShareLocationEvent.ShowLiveLocationDurationPicker -> {
|
||||||
val constraintsResult = checkLocationConstraints(permissionsState, locationActions)
|
val constraintsResult = checkLocationConstraints(permissionsState, locationActions)
|
||||||
dialogState = if (constraintsResult is LocationConstraintsCheck.Success) {
|
dialogState = if (constraintsResult is LocationConstraintsCheck.Success) {
|
||||||
ShareLocationState.Dialog.LiveLocationDuration
|
val durations = LIVE_LOCATION_DURATIONS.map {
|
||||||
|
LiveLocationDuration(duration = it, formatted = durationFormatter.format(it))
|
||||||
|
}
|
||||||
|
ShareLocationState.Dialog.LiveLocationDurations(durations.toImmutableList())
|
||||||
} else {
|
} else {
|
||||||
Constraints(constraintsResult.toDialogState())
|
Constraints(constraintsResult.toDialogState())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ package io.element.android.features.location.impl.share
|
|||||||
|
|
||||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
|
||||||
data class ShareLocationState(
|
data class ShareLocationState(
|
||||||
val currentUser: MatrixUser,
|
val currentUser: MatrixUser,
|
||||||
@@ -23,6 +24,6 @@ data class ShareLocationState(
|
|||||||
sealed interface Dialog {
|
sealed interface Dialog {
|
||||||
data object None : Dialog
|
data object None : Dialog
|
||||||
data class Constraints(val state: LocationConstraintsDialogState) : Dialog
|
data class Constraints(val state: LocationConstraintsDialogState) : Dialog
|
||||||
data object LiveLocationDuration : Dialog
|
data class LiveLocationDurations(val durations: ImmutableList<LiveLocationDuration>) : Dialog
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameterProvider
|
|||||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||||
import io.element.android.libraries.matrix.api.core.UserId
|
import io.element.android.libraries.matrix.api.core.UserId
|
||||||
import io.element.android.libraries.matrix.api.user.MatrixUser
|
import io.element.android.libraries.matrix.api.user.MatrixUser
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
private const val APP_NAME = "ApplicationName"
|
private const val APP_NAME = "ApplicationName"
|
||||||
|
|
||||||
@@ -49,7 +52,13 @@ class ShareLocationStateProvider : PreviewParameterProvider<ShareLocationState>
|
|||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
),
|
),
|
||||||
aShareLocationState(
|
aShareLocationState(
|
||||||
dialogState = ShareLocationState.Dialog.LiveLocationDuration,
|
dialogState = ShareLocationState.Dialog.LiveLocationDurations(
|
||||||
|
persistentListOf(
|
||||||
|
LiveLocationDuration(15.minutes, "15 minutes"),
|
||||||
|
LiveLocationDuration(1.hours, "1 hour"),
|
||||||
|
LiveLocationDuration(8.hours, "8 hours"),
|
||||||
|
)
|
||||||
|
),
|
||||||
trackUserPosition = true,
|
trackUserPosition = true,
|
||||||
hasLocationPermission = true,
|
hasLocationPermission = true,
|
||||||
canShareLiveLocation = true,
|
canShareLiveLocation = true,
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ import io.element.android.libraries.designsystem.theme.components.Text
|
|||||||
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
import io.element.android.libraries.designsystem.theme.components.TopAppBar
|
||||||
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
import io.element.android.libraries.matrix.ui.model.getAvatarData
|
||||||
import io.element.android.libraries.ui.strings.CommonStrings
|
import io.element.android.libraries.ui.strings.CommonStrings
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import org.maplibre.compose.camera.CameraMoveReason
|
import org.maplibre.compose.camera.CameraMoveReason
|
||||||
import org.maplibre.compose.camera.CameraState
|
import org.maplibre.compose.camera.CameraState
|
||||||
import org.maplibre.compose.camera.rememberCameraState
|
import org.maplibre.compose.camera.rememberCameraState
|
||||||
@@ -83,7 +84,8 @@ fun ShareLocationView(
|
|||||||
onOpenLocationSettings = { state.eventSink(ShareLocationEvent.OpenLocationSettings) },
|
onOpenLocationSettings = { state.eventSink(ShareLocationEvent.OpenLocationSettings) },
|
||||||
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
|
onDismiss = { state.eventSink(ShareLocationEvent.DismissDialog) },
|
||||||
)
|
)
|
||||||
ShareLocationState.Dialog.LiveLocationDuration -> LiveLocationDurationDialog(
|
is ShareLocationState.Dialog.LiveLocationDurations -> LiveLocationDurationDialog(
|
||||||
|
durations = dialogState.durations,
|
||||||
onSelectDuration = { duration ->
|
onSelectDuration = { duration ->
|
||||||
state.eventSink(ShareLocationEvent.StartLiveLocationShare(duration))
|
state.eventSink(ShareLocationEvent.StartLiveLocationShare(duration))
|
||||||
context.toast("Not implemented yet!")
|
context.toast("Not implemented yet!")
|
||||||
@@ -252,6 +254,7 @@ private fun ShareLiveLocationItem(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun LiveLocationDurationDialog(
|
private fun LiveLocationDurationDialog(
|
||||||
|
durations: ImmutableList<LiveLocationDuration>,
|
||||||
onSelectDuration: (Duration) -> Unit,
|
onSelectDuration: (Duration) -> Unit,
|
||||||
onDismiss: () -> Unit,
|
onDismiss: () -> Unit,
|
||||||
) {
|
) {
|
||||||
@@ -259,14 +262,14 @@ private fun LiveLocationDurationDialog(
|
|||||||
ListDialog(
|
ListDialog(
|
||||||
title = "Choose how long to share your live location.",
|
title = "Choose how long to share your live location.",
|
||||||
submitText = stringResource(CommonStrings.action_continue),
|
submitText = stringResource(CommonStrings.action_continue),
|
||||||
onSubmit = { onSelectDuration(LiveLocationDuration.entries[selectedIndex].duration) },
|
onSubmit = { onSelectDuration(durations[selectedIndex].duration) },
|
||||||
onDismissRequest = onDismiss,
|
onDismissRequest = onDismiss,
|
||||||
applyPaddingToContents = false,
|
applyPaddingToContents = false,
|
||||||
verticalArrangement = Arrangement.Top
|
verticalArrangement = Arrangement.Top
|
||||||
) {
|
) {
|
||||||
itemsIndexed(LiveLocationDuration.entries) { index, duration ->
|
itemsIndexed(durations) { index, duration ->
|
||||||
RadioButtonListItem(
|
RadioButtonListItem(
|
||||||
headline = duration.label,
|
headline = duration.formatted,
|
||||||
selected = index == selectedIndex,
|
selected = index == selectedIndex,
|
||||||
onSelect = { selectedIndex = index },
|
onSelect = { selectedIndex = index },
|
||||||
compactLayout = true,
|
compactLayout = true,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import com.google.common.truth.Truth.assertThat
|
|||||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
|
import io.element.android.libraries.dateformatter.test.FakeDurationFormatter
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.timeline.Timeline
|
import io.element.android.libraries.matrix.api.timeline.Timeline
|
||||||
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
import io.element.android.libraries.matrix.test.FakeMatrixClient
|
||||||
@@ -46,6 +47,7 @@ class DefaultShareLocationEntryPointTest {
|
|||||||
buildMeta = aBuildMeta(),
|
buildMeta = aBuildMeta(),
|
||||||
featureFlagService = FakeFeatureFlagService(),
|
featureFlagService = FakeFeatureFlagService(),
|
||||||
client = FakeMatrixClient(),
|
client = FakeMatrixClient(),
|
||||||
|
durationFormatter = FakeDurationFormatter(),
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
analyticsService = FakeAnalyticsService(),
|
analyticsService = FakeAnalyticsService(),
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ import io.element.android.features.location.impl.aPermissionsState
|
|||||||
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
import io.element.android.features.location.impl.common.actions.FakeLocationActions
|
||||||
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
import io.element.android.features.location.impl.common.permissions.FakePermissionsPresenter
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
import io.element.android.features.location.impl.common.permissions.PermissionsEvents
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsPresenter
|
|
||||||
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
import io.element.android.features.location.impl.common.permissions.PermissionsState
|
||||||
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
import io.element.android.features.location.impl.common.ui.LocationConstraintsDialogState
|
||||||
import io.element.android.features.messages.test.FakeMessageComposerContext
|
import io.element.android.features.messages.test.FakeMessageComposerContext
|
||||||
|
import io.element.android.libraries.dateformatter.test.FakeDurationFormatter
|
||||||
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
import io.element.android.libraries.featureflag.test.FakeFeatureFlagService
|
||||||
import io.element.android.libraries.matrix.api.core.EventId
|
import io.element.android.libraries.matrix.api.core.EventId
|
||||||
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
import io.element.android.libraries.matrix.api.room.JoinedRoom
|
||||||
@@ -54,13 +54,13 @@ class ShareLocationPresenterTest {
|
|||||||
private val fakeFeatureFlagService = FakeFeatureFlagService()
|
private val fakeFeatureFlagService = FakeFeatureFlagService()
|
||||||
private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID)
|
private val fakeMatrixClient = FakeMatrixClient(sessionId = A_USER_ID)
|
||||||
|
|
||||||
|
private val durationFormatter = FakeDurationFormatter()
|
||||||
|
|
||||||
private fun createShareLocationPresenter(
|
private fun createShareLocationPresenter(
|
||||||
joinedRoom: JoinedRoom = FakeJoinedRoom(),
|
joinedRoom: JoinedRoom = FakeJoinedRoom(),
|
||||||
locationActions: FakeLocationActions = fakeLocationActions,
|
locationActions: FakeLocationActions = fakeLocationActions,
|
||||||
): ShareLocationPresenter = ShareLocationPresenter(
|
): ShareLocationPresenter = ShareLocationPresenter(
|
||||||
permissionsPresenterFactory = object : PermissionsPresenter.Factory {
|
permissionsPresenterFactory = { fakePermissionsPresenter },
|
||||||
override fun create(permissions: List<String>): PermissionsPresenter = fakePermissionsPresenter
|
|
||||||
},
|
|
||||||
room = joinedRoom,
|
room = joinedRoom,
|
||||||
timelineMode = Timeline.Mode.Live,
|
timelineMode = Timeline.Mode.Live,
|
||||||
analyticsService = fakeAnalyticsService,
|
analyticsService = fakeAnalyticsService,
|
||||||
@@ -69,6 +69,7 @@ class ShareLocationPresenterTest {
|
|||||||
buildMeta = fakeBuildMeta,
|
buildMeta = fakeBuildMeta,
|
||||||
featureFlagService = fakeFeatureFlagService,
|
featureFlagService = fakeFeatureFlagService,
|
||||||
client = fakeMatrixClient,
|
client = fakeMatrixClient,
|
||||||
|
durationFormatter = durationFormatter,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -306,7 +307,7 @@ class ShareLocationPresenterTest {
|
|||||||
initialState.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker)
|
initialState.eventSink(ShareLocationEvent.ShowLiveLocationDurationPicker)
|
||||||
val durationDialogState = awaitItem()
|
val durationDialogState = awaitItem()
|
||||||
|
|
||||||
assertThat(durationDialogState.dialogState).isEqualTo(ShareLocationState.Dialog.LiveLocationDuration)
|
assertThat(durationDialogState.dialogState).isInstanceOf(ShareLocationState.Dialog.LiveLocationDurations::class.java)
|
||||||
cancelAndIgnoreRemainingEvents()
|
cancelAndIgnoreRemainingEvents()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,19 @@ package io.element.android.libraries.dateformatter.api
|
|||||||
import java.util.Locale
|
import java.util.Locale
|
||||||
import kotlin.time.Duration
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats a duration in a localized, human-readable way.
|
||||||
|
* Uses the largest appropriate unit (hours, minutes, or seconds).
|
||||||
|
*
|
||||||
|
* Examples (in English):
|
||||||
|
* - 2 hours 30 minutes → "3 hours" (rounded)
|
||||||
|
* - 45 minutes → "45 minutes"
|
||||||
|
* - 30 seconds → "30 seconds"
|
||||||
|
*/
|
||||||
|
interface DurationFormatter {
|
||||||
|
fun format(duration: Duration): String
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convert milliseconds to human readable duration.
|
* Convert milliseconds to human readable duration.
|
||||||
* Hours in 1 digit or more.
|
* Hours in 1 digit or more.
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import android.icu.text.MeasureFormat
|
||||||
|
import android.icu.text.MeasureFormat.FormatWidth
|
||||||
|
import android.icu.util.Measure
|
||||||
|
import android.icu.util.MeasureUnit
|
||||||
|
import android.text.format.DateUtils
|
||||||
|
import dev.zacsweers.metro.AppScope
|
||||||
|
import dev.zacsweers.metro.ContributesBinding
|
||||||
|
import dev.zacsweers.metro.SingleIn
|
||||||
|
import dev.zacsweers.metro.binding
|
||||||
|
import io.element.android.libraries.dateformatter.api.DurationFormatter
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.time.Duration
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Formats durations in a localized, human-readable way using Android's MeasureFormat.
|
||||||
|
*
|
||||||
|
* Uses WIDE format for readability (e.g., "5 hours", "3 minutes", "10 seconds").
|
||||||
|
* Rounds to the nearest unit for cleaner display.
|
||||||
|
*/
|
||||||
|
@SingleIn(AppScope::class)
|
||||||
|
@ContributesBinding(AppScope::class, binding = binding<DurationFormatter>())
|
||||||
|
class DefaultDurationFormatter(
|
||||||
|
localeChangeObserver: LocaleChangeObserver,
|
||||||
|
locale: Locale,
|
||||||
|
) : DurationFormatter, LocaleChangeListener {
|
||||||
|
init {
|
||||||
|
localeChangeObserver.addListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache formatter, recreate only on locale change
|
||||||
|
private var formatter: MeasureFormat = MeasureFormat.getInstance(locale, FormatWidth.WIDE)
|
||||||
|
|
||||||
|
override fun onLocaleChange() {
|
||||||
|
formatter = MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun format(duration: Duration): String {
|
||||||
|
val millis = duration.inWholeMilliseconds
|
||||||
|
|
||||||
|
return when {
|
||||||
|
duration >= 1.hours -> {
|
||||||
|
// Round to nearest hour (add 30 minutes before dividing)
|
||||||
|
val hours = ((millis + 30 * DateUtils.MINUTE_IN_MILLIS) / DateUtils.HOUR_IN_MILLIS).toInt()
|
||||||
|
formatter.format(Measure(hours, MeasureUnit.HOUR))
|
||||||
|
}
|
||||||
|
duration >= 1.minutes -> {
|
||||||
|
// Round to nearest minute (add 30 seconds before dividing)
|
||||||
|
val minutes = ((millis + 30 * DateUtils.SECOND_IN_MILLIS) / DateUtils.MINUTE_IN_MILLIS).toInt()
|
||||||
|
formatter.format(Measure(minutes, MeasureUnit.MINUTE))
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
// Round to nearest second (add 500ms before dividing)
|
||||||
|
val seconds = ((millis + 500) / DateUtils.SECOND_IN_MILLIS).toInt()
|
||||||
|
formatter.format(Measure(seconds, MeasureUnit.SECOND))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.impl
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
|
import com.google.common.truth.Truth.assertThat
|
||||||
|
import org.junit.Test
|
||||||
|
import org.junit.runner.RunWith
|
||||||
|
import org.robolectric.annotation.Config
|
||||||
|
import java.util.Locale
|
||||||
|
import kotlin.time.Duration.Companion.hours
|
||||||
|
import kotlin.time.Duration.Companion.milliseconds
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
@RunWith(AndroidJUnit4::class)
|
||||||
|
@Config(qualifiers = "en", sdk = [Build.VERSION_CODES.TIRAMISU])
|
||||||
|
class DefaultDurationFormatterTest {
|
||||||
|
private fun createDurationFormatter(): DefaultDurationFormatter {
|
||||||
|
return DefaultDurationFormatter(
|
||||||
|
localeChangeObserver = {},
|
||||||
|
locale = Locale.US,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test zero duration`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(0.seconds)).isEqualTo("0 seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 1 second`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(1.seconds)).isEqualTo("1 second")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 30 seconds`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(30.seconds)).isEqualTo("30 seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 59 seconds`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(59.seconds)).isEqualTo("59 seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 1 minute`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(1.minutes)).isEqualTo("1 minute")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 1 minute 29 seconds rounds to 1 minute`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(1.minutes + 29.seconds)).isEqualTo("1 minute")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 1 minute 30 seconds rounds to 2 minutes`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(1.minutes + 30.seconds)).isEqualTo("2 minutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 45 minutes`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(45.minutes)).isEqualTo("45 minutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 59 minutes`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(59.minutes)).isEqualTo("59 minutes")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 1 hour`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(1.hours)).isEqualTo("1 hour")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 1 hour 29 minutes rounds to 1 hour`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(1.hours + 29.minutes)).isEqualTo("1 hour")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 1 hour 30 minutes rounds to 2 hours`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(1.hours + 30.minutes)).isEqualTo("2 hours")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 2 hours 30 minutes rounds to 3 hours`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(2.hours + 30.minutes)).isEqualTo("3 hours")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 5 hours`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(5.hours)).isEqualTo("5 hours")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test 24 hours`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(24.hours)).isEqualTo("24 hours")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test rounding at seconds threshold - 499ms rounds to 0 seconds`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(499.milliseconds)).isEqualTo("0 seconds")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `test rounding at seconds threshold - 500ms rounds to 1 second`() {
|
||||||
|
val formatter = createDurationFormatter()
|
||||||
|
assertThat(formatter.format(500.milliseconds)).isEqualTo("1 second")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
/*
|
||||||
|
* Copyright (c) 2025 Element Creations Ltd.
|
||||||
|
*
|
||||||
|
* SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial.
|
||||||
|
* Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package io.element.android.libraries.dateformatter.test
|
||||||
|
|
||||||
|
import io.element.android.libraries.dateformatter.api.DurationFormatter
|
||||||
|
import kotlin.time.Duration
|
||||||
|
|
||||||
|
class FakeDurationFormatter(
|
||||||
|
private val formatLambda: (Duration) -> String = { it.toString() },
|
||||||
|
) : DurationFormatter {
|
||||||
|
override fun format(duration: Duration): String {
|
||||||
|
return formatLambda(duration)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user