@@ -24,33 +24,42 @@ import java.time.format.DateTimeFormatter
2424import java.time.format.DateTimeParseException
2525import java.time.temporal.Temporal
2626
27+ /* *
28+ * Maps contact events (like birthdays and anniversaries) to vCard properties.
29+ *
30+ * Android stores the events as date/date-time strings, so we have to parse these strings.
31+ * Unfortunately, the format is not specified in the ContactsContract ("as the user entered it"):
32+ * https://developer.android.com/reference/android/provider/ContactsContract.CommonDataKinds.Event?hl=en#START_DATE
33+ *
34+ * At least we know the formats used by AOSP Contacts:
35+ * https://android.googlesource.com/platform/packages/apps/Contacts/+/c326c157541978c180be4e3432327eceb1e66637/src/com/android/contacts/util/CommonDateUtils.java#25
36+ * so we support at least these formats.
37+ */
2738object EventHandler : DataRowHandler() {
2839
29- // CommonDateUtils: https://android.googlesource.com/platform/packages/apps/Contacts/+/c326c157541978c180be4e3432327eceb1e66637/src/com/android/contacts/util/CommonDateUtils.java#25
30-
3140 /* *
32- * Date formats for full date with time. Converts to [OffsetDateTime] .
41+ * Date formats for full date with time (taken from Android's CommonDateUtils) .
3342 */
3443 private val fullDateTimeFormats = listOf (
35- // Provided by Android's CommonDateUtils
3644 DateTimeFormatter .ofPattern(" yyyy-MM-dd'T'HH:mm:ss.SSSXXX" ),
3745 // "yyyy-MM-dd'T'HH:mm:ssXXX"
3846 DateTimeFormatter .ISO_OFFSET_DATE_TIME ,
3947 )
4048
4149 /* *
42- * Date format for full date without time. Converts to [LocalDate] .
50+ * Date format for full date without time (taken from Android's CommonDateUtils) .
4351 */
4452 private val fullDateFormat = DateTimeFormatter .ofPattern(" yyyy-MM-dd" )
4553
4654
4755 override fun forMimeType () = Event .CONTENT_ITEM_TYPE
4856
4957 /* *
50- * Tries to parse a date string into a [Temporal] object using multiple acceptable formats.
51- * Returns the parsed [Temporal] if successful, or `null` if none of the formats match.
52- * @param dateString The date string to parse.
53- * @return If format is:
58+ * Tries to parse a contact event date string into a [Temporal] object using multiple acceptable formats.
59+ *
60+ * @param dateString The contact event date string to parse.
61+ *
62+ * @return The parsed [Temporal] if successful, or `null` if none of the formats match. If format is:
5463 * - `yyyy-MM-dd'T'HH:mm:ss.SSS'Z'` or `yyyy-MM-dd'T'HH:mm:ssXXX` ([fullDateTimeFormats]) -> [OffsetDateTime]
5564 * - `yyyy-MM-dd` ([fullDateFormat]) -> [LocalDate]
5665 * - else -> `null`
@@ -77,11 +86,14 @@ object EventHandler : DataRowHandler() {
7786 }
7887
7988 /* *
80- * Tries to parse a date string into a [PartialDate] object.
81- * Returns the parsed [PartialDate] if successful, or `null` if parsing fails.
89+ * Tries to parse a contact event date string into a [PartialDate] object, covering the cases
90+ * from Android's CommonDateUtils:
91+ *
92+ * - `--MM-dd`
93+ * - `--MM-dd'T'HH:mm:ss.SSS'Z'`
8294 *
83- * Does some preprocessing to handle 'Z' suffix and strip nanoseconds, both not supported by
84- * [PartialDate.parse].
95+ * Does some preprocessing to handle the 'Z' suffix and strip nanoseconds
96+ * (both not supported by [PartialDate.parse]) .
8597 *
8698 * @param dateString The date string to parse.
8799 * @return The parsed [PartialDate] or `null` if parsing fails.
@@ -94,11 +106,10 @@ object EventHandler : DataRowHandler() {
94106 val withoutZ = if (dateString.endsWith(' Z' )) {
95107 // 'Z' is not supported for suffix in PartialDate, replace with actual offset
96108 dateString.removeSuffix(" Z" ) + " +00:00"
97- } else {
109+ } else
98110 dateString
99- }
100111
101- // PartialDates .parse() does not accept fractions of seconds, so strip them if present
112+ // PartialDate .parse() does not accept fractions of seconds, so strip them if present
102113 val subSecondsRegex = " \\ .\\ d+" .toRegex() // 2025-12-05T010203.456+00:30
103114 // ^^^^ (number of digits may vary)
104115 val subSecondsMatch = subSecondsRegex.find(withoutZ)
0 commit comments