diff --git a/lib/src/main/kotlin/at/bitfire/vcard4android/contactrow/EventHandler.kt b/lib/src/main/kotlin/at/bitfire/vcard4android/contactrow/EventHandler.kt index 66a581f7..e77070cb 100644 --- a/lib/src/main/kotlin/at/bitfire/vcard4android/contactrow/EventHandler.kt +++ b/lib/src/main/kotlin/at/bitfire/vcard4android/contactrow/EventHandler.kt @@ -8,6 +8,7 @@ package at.bitfire.vcard4android.contactrow import android.content.ContentValues import android.provider.ContactsContract.CommonDataKinds.Event +import androidx.annotation.VisibleForTesting import at.bitfire.vcard4android.Contact import at.bitfire.vcard4android.LabeledProperty import at.bitfire.vcard4android.Utils.trimToNull @@ -16,33 +17,67 @@ import ezvcard.property.Anniversary import ezvcard.property.Birthday import ezvcard.util.PartialDate import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter import java.time.format.DateTimeParseException -import java.util.logging.Level +import java.time.temporal.Temporal -object EventHandler: DataRowHandler() { +object EventHandler : DataRowHandler() { + + // source: https://cs.android.com/android/platform/superproject/main/+/main:packages/apps/Contacts/src/com/android/contacts/util/CommonDateUtils.java;drc=61197364367c9e404c7da6900658f1b16c42d0da;l=25 + private val acceptableFormats: List Temporal>> = listOf( + // Formats provided by Android's CommonDateUtils + DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX") to OffsetDateTime::parse, + DateTimeFormatter.ofPattern("yyyy-MM-dd") to LocalDate::parse, + // Additional common formats + DateTimeFormatter.ISO_OFFSET_DATE_TIME to OffsetDateTime::parse, // "yyyy-MM-dd'T'HH:mm:ssXXX" + ) override fun forMimeType() = Event.CONTENT_ITEM_TYPE + /** + * Tries to parse a date string into a [Temporal] object using multiple acceptable formats. + * Returns the parsed [Temporal] if successful, or `null` if none of the formats match. + * @param dateString The date string to parse. + * @return If format is: + * - `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` or `yyyy-MM-dd'T'HH:mm:ssXXX` -> [OffsetDateTime] + * - `yyyy-MM-dd` -> [LocalDate] + * - else -> `null` + */ + @VisibleForTesting + internal fun parseStartDate(dateString: String): Temporal? { + for ((formatter, parse) in acceptableFormats) { + try { + return parse(dateString, formatter) + } catch (_: DateTimeParseException) { + // ignore: given date is not valid + continue + } + } + + // could not parse date + return null + } + override fun handle(values: ContentValues, contact: Contact) { super.handle(values, contact) val dateStr = values.getAsString(Event.START_DATE) ?: return - var full: LocalDate? = null - var partial: PartialDate? = null - try { - full = LocalDate.parse(dateStr) - } catch(e: DateTimeParseException) { - try { - partial = PartialDate.parse(dateStr) - } catch (e: IllegalArgumentException) { - logger.log(Level.WARNING, "Couldn't parse birthday/anniversary date from database", e) - } + val full: Temporal? = parseStartDate(dateStr) + val partial: PartialDate? = if (full == null) try { + PartialDate.parse(dateStr) + } catch (_: IllegalArgumentException) { + null + } else { + null } if (full != null || partial != null) when (values.getAsInteger(Event.TYPE)) { Event.TYPE_ANNIVERSARY -> - contact.anniversary = if (full != null) Anniversary(full) else Anniversary(partial) + contact.anniversary = + if (full != null) Anniversary(full) else Anniversary(partial) + Event.TYPE_BIRTHDAY -> contact.birthDay = if (full != null) Birthday(full) else Birthday(partial) /* Event.TYPE_OTHER, diff --git a/lib/src/test/kotlin/at/bitfire/vcard4android/contactrow/EventHandlerTest.kt b/lib/src/test/kotlin/at/bitfire/vcard4android/contactrow/EventHandlerTest.kt index fba3dfc7..be797e13 100644 --- a/lib/src/test/kotlin/at/bitfire/vcard4android/contactrow/EventHandlerTest.kt +++ b/lib/src/test/kotlin/at/bitfire/vcard4android/contactrow/EventHandlerTest.kt @@ -19,10 +19,28 @@ import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.ZoneOffset @RunWith(RobolectricTestRunner::class) class EventHandlerTest { + @Test + fun test_parseStartDate_ISO_UTC_DateTime() { + assertEquals( + OffsetDateTime.of(1953, 10, 15, 23, 10, 0, 0, ZoneOffset.UTC), + EventHandler.parseStartDate("1953-10-15T23:10:00Z") + ) + } + + @Test + fun test_parseStartDate_ISO_Date() { + assertEquals( + LocalDate.of(1953, 10, 15), + EventHandler.parseStartDate("1953-10-15") + ) + } + @Test fun testStartDate_Empty() { val contact = Contact()