mirror of
https://github.com/bitwarden/android.git
synced 2026-03-11 20:54:58 -05:00
Replace ZonedDateTime with Instant (#6554)
This commit is contained in:
@@ -6,28 +6,31 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
|
||||
/**
|
||||
* Used to serialize and deserialize [ZonedDateTime].
|
||||
* Used to serialize and deserialize [Instant].
|
||||
*/
|
||||
class ZonedDateTimeSerializer : KSerializer<ZonedDateTime> {
|
||||
class InstantSerializer : KSerializer<Instant> {
|
||||
private val dateTimeFormatterDeserialization = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd'T'HH:mm:ss[.][:][SSSSSSS][SSSSSS][SSSSS][SSSS][SSS][SS][S]XXX")
|
||||
|
||||
private val dateTimeFormatterSerialization =
|
||||
DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX")
|
||||
private val dateTimeFormatterSerialization: DateTimeFormatter = DateTimeFormatter
|
||||
.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSX")
|
||||
.withZone(ZoneOffset.UTC)
|
||||
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor(serialName = "ZonedDateTime", kind = PrimitiveKind.STRING)
|
||||
get() = PrimitiveSerialDescriptor(serialName = "Instant", kind = PrimitiveKind.STRING)
|
||||
|
||||
override fun deserialize(decoder: Decoder): ZonedDateTime =
|
||||
decoder.decodeString().let { dateString ->
|
||||
ZonedDateTime.parse(dateString, dateTimeFormatterDeserialization)
|
||||
}
|
||||
override fun deserialize(decoder: Decoder): Instant =
|
||||
ZonedDateTime
|
||||
.parse(decoder.decodeString(), dateTimeFormatterDeserialization)
|
||||
.toInstant()
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ZonedDateTime) {
|
||||
override fun serialize(encoder: Encoder, value: Instant) {
|
||||
encoder.encodeString(dateTimeFormatterSerialization.format(value))
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.bitwarden.core.di
|
||||
|
||||
import com.bitwarden.core.data.serializer.ZonedDateTimeSerializer
|
||||
import com.bitwarden.core.data.serializer.InstantSerializer
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
@@ -30,7 +30,7 @@ object CoreModule {
|
||||
// We allow for nullable values to have keys missing in the JSON response.
|
||||
explicitNulls = false
|
||||
serializersModule = SerializersModule {
|
||||
contextual(ZonedDateTimeSerializer())
|
||||
contextual(InstantSerializer())
|
||||
}
|
||||
|
||||
// Respect model default property values.
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.bitwarden.core.util
|
||||
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.Instant
|
||||
|
||||
/**
|
||||
* Returns a [Boolean] indicating whether this [Instant] is five or more minutes old.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun Instant.isOverFiveMinutesOld(
|
||||
clock: Clock = Clock.systemDefaultZone(),
|
||||
): Boolean =
|
||||
Duration
|
||||
.between(this, clock.instant())
|
||||
.toMinutes() > 5
|
||||
@@ -1,8 +1,6 @@
|
||||
package com.bitwarden.core.util
|
||||
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
private const val NANOS_PER_TICK = 100L
|
||||
private const val TICKS_PER_SECOND = 1000000000L / NANOS_PER_TICK
|
||||
@@ -13,32 +11,31 @@ private const val TICKS_PER_SECOND = 1000000000L / NANOS_PER_TICK
|
||||
private const val YEAR_OFFSET = -62135596800L
|
||||
|
||||
/**
|
||||
* Returns the [ZonedDateTime] of the binary [Long] [value]. This is needed to remain consistent
|
||||
* Returns the [Instant] of the binary [Long] [value]. This is needed to remain consistent
|
||||
* with how `DateTime`s were stored when using C#.
|
||||
*
|
||||
* This functionality is based on the https://stackoverflow.com/questions/65315060/how-to-convert-net-datetime-tobinary-to-java-date
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun getZoneDateTimeFromBinaryLong(value: Long): ZonedDateTime {
|
||||
fun getInstantFromBinaryLong(value: Long): Instant {
|
||||
// Shift the bits to eliminate the "Kind" property since we know it was stored as UTC and leave
|
||||
// us with ticks
|
||||
val ticks = value and (1L shl 62) - 1
|
||||
val instant = Instant.ofEpochSecond(
|
||||
return Instant.ofEpochSecond(
|
||||
ticks / TICKS_PER_SECOND + YEAR_OFFSET,
|
||||
ticks % TICKS_PER_SECOND * NANOS_PER_TICK,
|
||||
)
|
||||
return ZonedDateTime.ofInstant(instant, ZoneOffset.UTC)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the [ZonedDateTime] [value] converted to a binary [Long]. This is needed to remain
|
||||
* Returns the [Instant] [value] converted to a binary [Long]. This is needed to remain
|
||||
* consistent with how `DateTime`s were stored when using C#.
|
||||
*
|
||||
* This functionality is based on the https://stackoverflow.com/questions/65315060/how-to-convert-net-datetime-tobinary-to-java-date
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun getBinaryLongFromZoneDateTime(value: ZonedDateTime): Long {
|
||||
fun getBinaryLongFromInstant(value: Instant): Long {
|
||||
val nanoAdjustment = value.nano / NANOS_PER_TICK
|
||||
val ticks = (value.toEpochSecond() - YEAR_OFFSET) * TICKS_PER_SECOND + nanoAdjustment
|
||||
val ticks = (value.epochSecond - YEAR_OFFSET) * TICKS_PER_SECOND + nanoAdjustment
|
||||
return 1L shl 62 or ticks
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.bitwarden.core.util
|
||||
|
||||
import java.time.Clock
|
||||
import java.time.Duration
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
/**
|
||||
* Returns a [Boolean] indicating whether this [ZonedDateTime] is five or more minutes old.
|
||||
*/
|
||||
@Suppress("MagicNumber")
|
||||
fun ZonedDateTime.isOverFiveMinutesOld(
|
||||
clock: Clock = Clock.systemDefaultZone(),
|
||||
): Boolean =
|
||||
Duration
|
||||
.between(this.toInstant(), clock.instant())
|
||||
.toMinutes() > 5
|
||||
@@ -0,0 +1,70 @@
|
||||
package com.bitwarden.core.data.serializer
|
||||
|
||||
import com.bitwarden.core.di.CoreModule
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
class InstantSerializerTest {
|
||||
private val json = CoreModule.providesJson()
|
||||
|
||||
@Test
|
||||
fun `properly deserializes raw JSON to Instant`() {
|
||||
assertEquals(
|
||||
InstantData(
|
||||
dataAsInstant = Instant.ofEpochSecond(1_696_612_948L, 440_000_000L),
|
||||
),
|
||||
json.decodeFromString<InstantData>(
|
||||
"""
|
||||
{
|
||||
"dataAsInstant": "2023-10-06T17:22:28.44Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `properly deserializes raw JSON with nano seconds to Instant`() {
|
||||
assertEquals(
|
||||
InstantData(
|
||||
dataAsInstant = Instant.ofEpochSecond(1_690_906_383L, 502_391_000L),
|
||||
),
|
||||
json.decodeFromString<InstantData>(
|
||||
"""
|
||||
{
|
||||
"dataAsInstant": "2023-08-01T16:13:03.502391Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `properly serializes external model back to raw JSON`() {
|
||||
assertEquals(
|
||||
json.parseToJsonElement(
|
||||
"""
|
||||
{
|
||||
"dataAsInstant": "2023-10-06T17:22:28.440Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
json.encodeToJsonElement(
|
||||
InstantData(
|
||||
dataAsInstant = Instant.ofEpochSecond(1_696_612_948L, 440_000_000L),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class InstantData(
|
||||
@Serializable(InstantSerializer::class)
|
||||
@SerialName("dataAsInstant")
|
||||
val dataAsInstant: Instant,
|
||||
)
|
||||
@@ -1,99 +1 @@
|
||||
package com.bitwarden.core.data.serializer
|
||||
|
||||
import com.bitwarden.core.di.CoreModule
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.encodeToJsonElement
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZoneId
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class ZonedDateTimeSerializerTest {
|
||||
private val json = CoreModule.providesJson()
|
||||
|
||||
@Test
|
||||
fun `properly deserializes raw JSON to ZonedDateTime`() {
|
||||
assertEquals(
|
||||
ZonedDateTimeData(
|
||||
dataAsZonedDateTime = ZonedDateTime.of(
|
||||
2023,
|
||||
10,
|
||||
6,
|
||||
17,
|
||||
22,
|
||||
28,
|
||||
440000000,
|
||||
ZoneOffset.UTC,
|
||||
),
|
||||
),
|
||||
json.decodeFromString<ZonedDateTimeData>(
|
||||
"""
|
||||
{
|
||||
"dataAsZonedDateTime": "2023-10-06T17:22:28.44Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `properly deserializes raw JSON with nano seconds to ZonedDateTime`() {
|
||||
assertEquals(
|
||||
ZonedDateTimeData(
|
||||
dataAsZonedDateTime = ZonedDateTime.of(
|
||||
2023,
|
||||
8,
|
||||
1,
|
||||
16,
|
||||
13,
|
||||
3,
|
||||
502391000,
|
||||
ZoneOffset.UTC,
|
||||
),
|
||||
),
|
||||
json.decodeFromString<ZonedDateTimeData>(
|
||||
"""
|
||||
{
|
||||
"dataAsZonedDateTime": "2023-08-01T16:13:03.502391Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `properly serializes external model back to raw JSON`() {
|
||||
assertEquals(
|
||||
json.parseToJsonElement(
|
||||
"""
|
||||
{
|
||||
"dataAsZonedDateTime": "2023-10-06T17:22:28.440Z"
|
||||
}
|
||||
""",
|
||||
),
|
||||
json.encodeToJsonElement(
|
||||
ZonedDateTimeData(
|
||||
dataAsZonedDateTime = ZonedDateTime.of(
|
||||
2023,
|
||||
10,
|
||||
6,
|
||||
17,
|
||||
22,
|
||||
28,
|
||||
440000000,
|
||||
ZoneId.of("UTC"),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
private data class ZonedDateTimeData(
|
||||
@Serializable(ZonedDateTimeSerializer::class)
|
||||
@SerialName("dataAsZonedDateTime")
|
||||
val dataAsZonedDateTime: ZonedDateTime,
|
||||
)
|
||||
|
||||
@@ -6,9 +6,8 @@ import org.junit.jupiter.api.Test
|
||||
import java.time.Clock
|
||||
import java.time.Instant
|
||||
import java.time.ZoneOffset
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class ZonedDateTimeExtensionsTest {
|
||||
class InstantExtensionsTest {
|
||||
|
||||
private val fixedClock: Clock = Clock.fixed(
|
||||
Instant.parse("2023-10-27T12:00:00Z"),
|
||||
@@ -17,25 +16,25 @@ class ZonedDateTimeExtensionsTest {
|
||||
|
||||
@Test
|
||||
fun `isOverFiveMinutesOld returns true when time is old`() {
|
||||
val time = ZonedDateTime.parse("2022-09-13T00:00Z")
|
||||
val time = Instant.parse("2022-09-13T00:00:00Z")
|
||||
assertTrue(time.isOverFiveMinutesOld(fixedClock))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverFiveMinutesOld returns false when time is now`() {
|
||||
val time = ZonedDateTime.parse("2023-10-27T11:55:00Z")
|
||||
val time = Instant.parse("2023-10-27T11:55:00Z")
|
||||
assertFalse(time.isOverFiveMinutesOld(fixedClock))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverFiveMinutesOld returns false when time is now minus 5 minutes`() {
|
||||
val time = ZonedDateTime.now(fixedClock).minusMinutes(5)
|
||||
val time = fixedClock.instant().minusSeconds(300)
|
||||
assertFalse(time.isOverFiveMinutesOld(fixedClock))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOverFiveMinutesOld returns true when time is now minus 6 minutes`() {
|
||||
val time = ZonedDateTime.now(fixedClock).minusMinutes(6)
|
||||
val time = fixedClock.instant().minusSeconds(360)
|
||||
assertTrue(time.isOverFiveMinutesOld(fixedClock))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.bitwarden.core.util
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.Instant
|
||||
|
||||
class InstantUtilsTest {
|
||||
@Test
|
||||
fun `getInstantFromBinaryLong should correctly convert a Long to an Instant`() {
|
||||
val binaryLong = 5250087787086431044L
|
||||
val expectedInstant = Instant.parse("2024-01-06T22:27:45.904314Z")
|
||||
assertEquals(expectedInstant, getInstantFromBinaryLong(binaryLong))
|
||||
|
||||
val a = getInstantFromBinaryLong(binaryLong)
|
||||
val b = getBinaryLongFromInstant(a)
|
||||
assertEquals(binaryLong, b)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBinaryLongFromInstant should correctly convert an Instant to a Long`() {
|
||||
val instant = Instant.parse("2024-01-06T22:27:45.904314Z")
|
||||
val expectedBinaryLong = 5250087787086431044L
|
||||
assertEquals(expectedBinaryLong, getBinaryLongFromInstant(instant))
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
package com.bitwarden.core.util
|
||||
|
||||
import org.junit.jupiter.api.Assertions.assertEquals
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.time.ZonedDateTime
|
||||
|
||||
class ZonedDateTimeUtilsTest {
|
||||
@Test
|
||||
fun `getZoneDateTimeFromBinaryLong should correctly convert a Long to a ZonedDateTime`() {
|
||||
val binaryLong = 5250087787086431044L
|
||||
val expectedDateTime = ZonedDateTime.parse("2024-01-06T22:27:45.904314Z")
|
||||
assertEquals(expectedDateTime, getZoneDateTimeFromBinaryLong(binaryLong))
|
||||
|
||||
val a = getZoneDateTimeFromBinaryLong(binaryLong)
|
||||
val b = getBinaryLongFromZoneDateTime(a)
|
||||
assertEquals(binaryLong, b)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getBinaryLongFromZoneDateTime should correctly convert a ZonedDateTime to a Long`() {
|
||||
val dateTime = ZonedDateTime.parse("2024-01-06T22:27:45.904314Z")
|
||||
val expectedBinaryLong = 5250087787086431044L
|
||||
assertEquals(expectedBinaryLong, getBinaryLongFromZoneDateTime(dateTime))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user