Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Unverified Commit 3851e70f authored by Ricki Hirner's avatar Ricki Hirner Committed by GitHub
Browse files

EventBuilder: fix date/time formatting (#15)

* EventBuilder: support all Android date/time formats; fix formatting exception of Instants

* Remove obsolete tests
parent dd940bc0
Loading
Loading
Loading
Loading
+0 −27
Original line number Diff line number Diff line
@@ -182,31 +182,4 @@ class AndroidContactTest {
        assertTrue(os.toString().contains("ADR;LABEL=My ^'Label^'\\nLine 2:;;Street \"Address\";;;;"))
    }

    @Test
    fun testBirthdayWithoutYear() {
        val vcard = Contact()
        vcard.displayName = "Mya Contact"
        vcard.birthDay = Birthday(PartialDate.parse("-04-16"))

        val contact = AndroidContact(addressBook, vcard, null, null)
        contact.add()

        val contact2 = addressBook.findContactById(contact.id!!)
        try {
            val vcard2 = contact2.getContact()
            assertEquals(vcard.displayName, vcard2.displayName)
            assertEquals(vcard.birthDay, vcard2.birthDay)
        } finally {
            contact2.delete()
        }
    }

    /*@Test
    fun testToURIScheme() {
        assertEquals("testp+csfgh-ewt4345.2qiuz4", AndroidContact.toURIScheme("02 34test#ä{☺}ö p[]ß+csfgh()-e_wt4\\345.2qiuz4"))
        assertEquals("CyanogenModForum", AndroidContact.toURIScheme("CyanogenMod Forum"))
        assertEquals("CyanogenModForum", AndroidContact.toURIScheme("CyanogenMod_Forum"))
    }*/


}
+34 −10
Original line number Diff line number Diff line
@@ -14,7 +14,9 @@ import ezvcard.property.Birthday
import ezvcard.util.PartialDate
import org.junit.Assert.assertEquals
import org.junit.Test
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime

class EventBuilderTest {

@@ -27,7 +29,17 @@ class EventBuilderTest {


    @Test
    fun testStartDate_FullDate() {
    fun testStartDate_Date_Instant() {
        EventBuilder(Uri.EMPTY, null, Contact().apply {
            anniversary = Anniversary(Instant.ofEpochSecond(1683924316))
        }, false).build().also { result ->
            assertEquals(1, result.size)
            assertEquals("2023-05-12T20:45:16.000Z", result[0].values[CommonDataKinds.Event.START_DATE])
        }
    }

    @Test
    fun testStartDate_Date_LocalDate() {
        EventBuilder(Uri.EMPTY, null, Contact().apply {
            anniversary = Anniversary(
                LocalDate.of(1984, 8, 20)
@@ -35,13 +47,24 @@ class EventBuilderTest {
        }, false).build().also { result ->
            assertEquals(1, result.size)
            assertEquals("1984-08-20", result[0].values[CommonDataKinds.Event.START_DATE])
            assertEquals(CommonDataKinds.Event.TYPE_ANNIVERSARY, result[0].values[CommonDataKinds.Event.TYPE])
        }
    }

    @Test
    fun testStartDate_Date_LocalDateTime() {
        EventBuilder(Uri.EMPTY, null, Contact().apply {
            anniversary = Anniversary(
                LocalDateTime.of(1984, 8, 20, 12, 30, 51)
            )
        }, false).build().also { result ->
            assertEquals(1, result.size)
            assertEquals("1984-08-20T12:30:51.000Z", result[0].values[CommonDataKinds.Event.START_DATE])
        }
    }


    @Test
    fun testStartDate_PartialDate() {
    fun testStartDate_PartialDate_NoYear() {
        EventBuilder(Uri.EMPTY, null, Contact().apply {
            anniversary = Anniversary(PartialDate.builder()
                .date(20)
@@ -53,16 +76,17 @@ class EventBuilderTest {
        }
    }


    @Test
    fun testBirthday_FullDate() {
    fun testStartDate_PartialDate_NoYear_ButHour() {
        EventBuilder(Uri.EMPTY, null, Contact().apply {
            anniversary = Anniversary(
                LocalDate.of(1984, 8, 20)
            )
        }, false).build().also { result ->
            anniversary = Anniversary(PartialDate.builder()
                .date(20)
                .month(8)
                .hour(14)
                .build())
        }, true).build().also { result ->
            assertEquals(1, result.size)
            assertEquals("1984-08-20", result[0].values[CommonDataKinds.Event.START_DATE])
            assertEquals("--08-20T14:00:00.000Z", result[0].values[CommonDataKinds.Event.START_DATE])
        }
    }

+91 −14
Original line number Diff line number Diff line
@@ -10,14 +10,30 @@ import at.bitfire.vcard4android.BatchOperation
import at.bitfire.vcard4android.Constants
import at.bitfire.vcard4android.Contact
import ezvcard.property.DateOrTimeProperty
import java.text.SimpleDateFormat
import ezvcard.util.PartialDate
import java.time.Instant
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.LocalTime
import java.time.ZoneOffset
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.*
import java.util.LinkedList
import java.util.Locale
import java.util.logging.Level

class EventBuilder(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readOnly: Boolean)
    : DataRowBuilder(Factory.mimeType(), dataRowUri, rawContactId, contact, readOnly) {

    companion object {
        const val DATE_AND_TIME_FORMAT = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"
        const val FULL_DATE_FORMAT = "yyyy-MM-dd"
        const val NO_YEAR_DATE_FORMAT = "--MM-dd"
        //const val NO_YEAR_DATE_AND_TIME_FORMAT = "--MM-dd'T'HH:mm:ss.SSS'Z'"

        const val TIME_PART = "'T'HH:mm:ss.SSS'Z'"
    }

    override fun build(): List<BatchOperation.CpoBuilder> {
        val result = LinkedList<BatchOperation.CpoBuilder>()

@@ -36,26 +52,43 @@ class EventBuilder(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readO
        return result
    }

    fun buildEvent(dateOrTime: DateOrTimeProperty?, typeCode: Int, label: String? = null): BatchOperation.CpoBuilder? {
    private fun buildEvent(dateOrTime: DateOrTimeProperty?, typeCode: Int, label: String? = null): BatchOperation.CpoBuilder? {
        if (dateOrTime == null)
            return null

        val dateStr: String = when {
        // See here for formats supported by AOSP Contacts:
        // https://android.googlesource.com/platform/packages/apps/Contacts/+/refs/tags/android-13.0.0_r49/src/com/android/contacts/util/CommonDateUtils.java

        val androidStr: String? =
            when {
                dateOrTime.date != null -> {
                val format = DateTimeFormatter.ofPattern("yyyy-MM-dd", Locale.ROOT)
                format.format(dateOrTime.date)
                    val date = dateOrTime.date
                    when (date) {
                        is Instant -> {
                            val utc = ZonedDateTime.ofInstant(date, ZoneOffset.UTC)
                            DateTimeFormatter.ofPattern(DATE_AND_TIME_FORMAT, Locale.US).format(utc)
                        }
                        is LocalDate ->
                            DateTimeFormatter.ofPattern(FULL_DATE_FORMAT, Locale.US).format(date)
                        is LocalDateTime ->
                            DateTimeFormatter.ofPattern(DATE_AND_TIME_FORMAT, Locale.US).format(date)
                        else ->
                            null
                    }
                }
                dateOrTime.partialDate != null ->
                dateOrTime.partialDate.toISO8601(true)      // AOSP Contacts app expects this format ("--06-01")
            else -> {
                Constants.log.log(Level.WARNING, "Ignoring date/time without (partial) date", dateOrTime)
                return null
                    partialDateToAndroid(dateOrTime.partialDate)
                else ->
                    null
            }
        if (androidStr == null) {
            Constants.log.log(Level.WARNING, "Ignoring date/time without supported (partial) date", dateOrTime)
            return null
        }

        val builder = newDataRow()
            .withValue(Event.TYPE, typeCode)
            .withValue(Event.START_DATE, dateStr)
            .withValue(Event.START_DATE, androidStr)

        if (label != null)
            builder.withValue(Event.LABEL, label)
@@ -63,6 +96,50 @@ class EventBuilder(dataRowUri: Uri, rawContactId: Long?, contact: Contact, readO
        return builder
    }

    private fun partialDateToAndroid(partialDate: PartialDate): String? {
        // possible values: see RFC 6350 4.3.4 DATE-AND-OR-TIME
        val dateStr =
            if (partialDate.hasDateComponent()) {
                if (partialDate.month != null && partialDate.date != null) {
                    if (partialDate.year == null) {
                        val date = LocalDate.of(
                            /* dummy, won't be used */ 2000,
                            partialDate.month,
                            partialDate.date
                        )
                        DateTimeFormatter.ofPattern(NO_YEAR_DATE_FORMAT, Locale.US).format(date)
                    } else /* partialDate.year != null */ {
                        val date = LocalDate.of(
                            partialDate.year,
                            partialDate.month,
                            partialDate.date
                        )
                        DateTimeFormatter.ofPattern(FULL_DATE_FORMAT, Locale.US).format(date)
                    }
                } else  // no month and/or day-of-month
                    null
            } else  // no date component
                null

        if (dateStr == null)
            return null

        val str = StringBuilder(dateStr)
        // we have a (partial) date, append time if possible
        if (partialDate.hasTimeComponent()) {
            val timeStr = DateTimeFormatter.ofPattern(TIME_PART).format(
                LocalTime.of(
                    partialDate.hour ?: 0,
                    partialDate.minute ?: 0,
                    partialDate.second ?: 0
                )
            )
            str.append(timeStr)
        }

        return str.toString()
    }


    object Factory: DataRowBuilder.Factory<EventBuilder> {
        override fun mimeType() = Event.CONTENT_ITEM_TYPE