Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/src/main/kotlin/at/bitfire/ical4android/DmfsTask.kt
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@ import android.content.ContentValues
import android.net.Uri
import android.os.RemoteException
import androidx.annotation.CallSuper
import at.bitfire.ical4android.util.AndroidTimeUtils
import at.bitfire.synctools.storage.BatchOperation.CpoBuilder
import at.bitfire.synctools.storage.LocalStorageException
import at.bitfire.synctools.storage.TasksBatchOperation
import at.bitfire.synctools.storage.toContentValues
import at.bitfire.synctools.util.AndroidTimeUtils
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Parameter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,24 @@ import at.bitfire.synctools.mapping.calendar.processor.AvailabilityProcessor
import at.bitfire.synctools.mapping.calendar.processor.CategoriesProcessor
import at.bitfire.synctools.mapping.calendar.processor.ColorProcessor
import at.bitfire.synctools.mapping.calendar.processor.DescriptionProcessor
import at.bitfire.synctools.mapping.calendar.processor.DurationProcessor
import at.bitfire.synctools.mapping.calendar.processor.EndTimeProcessor
import at.bitfire.synctools.mapping.calendar.processor.LocationProcessor
import at.bitfire.synctools.mapping.calendar.processor.MutatorsProcessor
import at.bitfire.synctools.mapping.calendar.processor.OrganizerProcessor
import at.bitfire.synctools.mapping.calendar.processor.OriginalInstanceTimeProcessor
import at.bitfire.synctools.mapping.calendar.processor.RecurrenceFieldsProcessor
import at.bitfire.synctools.mapping.calendar.processor.RemindersProcessor
import at.bitfire.synctools.mapping.calendar.processor.SequenceProcessor
import at.bitfire.synctools.mapping.calendar.processor.StartTimeProcessor
import at.bitfire.synctools.mapping.calendar.processor.StatusProcessor
import at.bitfire.synctools.mapping.calendar.processor.TimeFieldsProcessor
import at.bitfire.synctools.mapping.calendar.processor.TitleProcessor
import at.bitfire.synctools.mapping.calendar.processor.UidProcessor
import at.bitfire.synctools.mapping.calendar.processor.UnknownPropertiesProcessor
import at.bitfire.synctools.mapping.calendar.processor.UrlProcessor
import at.bitfire.synctools.storage.calendar.EventAndExceptions
import net.fortuna.ical4j.model.DateList
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
import net.fortuna.ical4j.model.parameter.Value
import net.fortuna.ical4j.model.property.ExDate
import net.fortuna.ical4j.model.property.RecurrenceId
Expand All @@ -48,15 +51,19 @@ class LegacyAndroidEventProcessor(
private val accountName: String
) {

private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()

private val fieldProcessors: Array<AndroidEventFieldProcessor> = arrayOf(
// event row fields
MutatorsProcessor(), // for PRODID
UidProcessor(),
OriginalInstanceTimeProcessor(),
OriginalInstanceTimeProcessor(tzRegistry),
TitleProcessor(),
LocationProcessor(),
TimeFieldsProcessor(),
RecurrenceFieldsProcessor(),
StartTimeProcessor(tzRegistry),
EndTimeProcessor(tzRegistry),
DurationProcessor(tzRegistry),
RecurrenceFieldsProcessor(tzRegistry),
DescriptionProcessor(),
ColorProcessor(),
AccessLevelProcessor(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ import android.content.Entity
import android.provider.CalendarContract.Events
import androidx.annotation.VisibleForTesting
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.AndroidTimeUtils
import at.bitfire.ical4android.util.DateUtils
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime
import at.bitfire.ical4android.util.TimeApiExtensions.toLocalDate
import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
import at.bitfire.synctools.util.AndroidTimeUtils
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.property.DtEnd
import net.fortuna.ical4j.model.property.DtStart
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ package at.bitfire.synctools.mapping.calendar.builder
import android.content.Entity
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.AndroidTimeUtils
import at.bitfire.synctools.util.AndroidTimeUtils
import net.fortuna.ical4j.model.DateList
import net.fortuna.ical4j.model.property.RDate
import java.util.logging.Logger
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ package at.bitfire.synctools.mapping.calendar.builder
import android.content.Entity
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.AndroidTimeUtils
import at.bitfire.ical4android.util.DateUtils
import at.bitfire.synctools.util.AndroidTimeUtils
import java.time.ZoneId

class StartTimeBuilder: AndroidEntityBuilder {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* This file is part of bitfireAT/synctools which is released under GPLv3.
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package at.bitfire.synctools.mapping.calendar.processor

import at.bitfire.synctools.util.AndroidTimeUtils
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.TimeZoneRegistry
import net.fortuna.ical4j.util.TimeZones
import java.time.ZoneId

/**
* Converts timestamps from the [android.provider.CalendarContract.Events.DTSTART] or [android.provider.CalendarContract.Events.DTEND]
* fields into other representations.
*
* @param timestamp value of the DTSTART/DTEND field (timestamp in milliseconds)
* @param timeZone value of the respective timezone field ([android.provider.CalendarContract.Events.EVENT_TIMEZONE] / [android.provider.CalendarContract.Events.EVENT_END_TIMEZONE])
* @param allDay whether [android.provider.CalendarContract.Events.ALL_DAY] is non-null and not zero
*/
class AndroidTimeField(
private val timestamp: Long,
private val timeZone: String?,
private val allDay: Boolean,
private val tzRegistry: TimeZoneRegistry
) {

/** ID of system default timezone */
private val defaultTzId by lazy { ZoneId.systemDefault().id }

/**
* Converts the given Android date/time into an ical4j date property.
*
* @return `Date` in case of an all-day event, `DateTime` in case of a non-all-day event
*/
fun asIcal4jDate(): Date {
if (allDay)
return Date(timestamp)

// non-all-day
val tzId = timeZone
?: defaultTzId // safe fallback (should never be used because the calendar provider requires EVENT_TIMEZONE)

/* The resolved timezone may be null if there is no ical4j timezone for tzId, which can happen in rare cases
(for instance if Android already knows about a new timezone ID or alias that doesn't exist in our
ical4j version yet).

In this case, we use the system default timezone ID as fallback and hope that we have a VTIMEZONE for it.
If we also don't have a VTIMEZONE for the default timezone, we fall back to a UTC DATE-TIME without timezone. */

val timezone = if (tzId == AndroidTimeUtils.TZID_UTC || tzId == TimeZones.UTC_ID || tzId == TimeZones.IBM_UTC_ID)
null // indicates UTC
else
(tzRegistry.getTimeZone(tzId) ?: tzRegistry.getTimeZone(defaultTzId))

return DateTime(timestamp).also { dateTime ->
if (timezone == null)
dateTime.isUtc = true
else
dateTime.timeZone = timezone
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
/*
* This file is part of bitfireAT/synctools which is released under GPLv3.
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package at.bitfire.synctools.mapping.calendar.processor

import android.content.Entity
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDate
import at.bitfire.ical4android.util.TimeApiExtensions.toIcal4jDateTime
import at.bitfire.ical4android.util.TimeApiExtensions.toZonedDateTime
import at.bitfire.synctools.util.AndroidTimeUtils
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.TimeZoneRegistry
import net.fortuna.ical4j.model.property.DtEnd
import java.time.Duration
import java.time.Instant
import java.time.Period
import java.time.ZoneOffset

class DurationProcessor(
private val tzRegistry: TimeZoneRegistry
): AndroidEventFieldProcessor {

override fun process(from: Entity, main: Entity, to: Event) {
val values = from.entityValues

/* Skip if:
- DTEND is set – we don't need to process DURATION anymore.
- DURATION is not set – then usually DTEND is set; however it's also OK to have neither DTEND nor DURATION in a VEVENT. */
if (values.getAsLong(Events.DTEND) != null)
return
val durStr = values.getAsString(Events.DURATION) ?: return
val duration = AndroidTimeUtils.parseDuration(durStr)

// Skip in case of zero or negative duration (analogous to DTEND being before DTSTART).
if ((duration is Duration && (duration.isZero || duration.isNegative)) ||
(duration is Period && (duration.isZero || duration.isNegative)))
return

/* Some servers have problems with DURATION. For maximum compatibility, we always generate DTEND instead of DURATION.
(After all, the constraint that non-recurring events have a DTEND while recurring events use DURATION is Android-specific.)
So we have to calculate DTEND from DTSTART and its timezone plus DURATION. */

val tsStart = values.getAsLong(Events.DTSTART) ?: return
val allDay = (values.getAsInteger(Events.ALL_DAY) ?: 0) != 0

if (allDay) {
val startTimeUTC = Instant.ofEpochMilli(tsStart).atOffset(ZoneOffset.UTC)
val endDate = (startTimeUTC + duration).toLocalDate()

// DATE
to.dtEnd = DtEnd(endDate.toIcal4jDate())

} else {
// DATE-TIME
val startDateTime = AndroidTimeField(
timestamp = tsStart,
timeZone = values.getAsString(Events.EVENT_TIMEZONE),
allDay = false,
tzRegistry = tzRegistry
).asIcal4jDate() as DateTime

val start = startDateTime.toZonedDateTime()
val end = start + duration

to.dtEnd = DtEnd(end.toIcal4jDateTime())
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* This file is part of bitfireAT/synctools which is released under GPLv3.
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package at.bitfire.synctools.mapping.calendar.processor

import android.content.Entity
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.Event
import net.fortuna.ical4j.model.TimeZoneRegistry
import net.fortuna.ical4j.model.property.DtEnd
import java.util.logging.Logger

class EndTimeProcessor(
private val tzRegistry: TimeZoneRegistry
): AndroidEventFieldProcessor {

private val logger
get() = Logger.getLogger(javaClass.name)

override fun process(from: Entity, main: Entity, to: Event) {
val values = from.entityValues
val allDay = (values.getAsInteger(Events.ALL_DAY) ?: 0) != 0

// Skip if DTEND is not set – then usually DURATION is set; however it's also OK to have neither DTEND nor DURATION in a VEVENT.
val tsEnd = values.getAsLong(Events.DTEND) ?: return

// Also skip if DTEND is not after DTSTART (not allowed in iCalendar)
val tsStart = values.getAsLong(Events.DTSTART) ?: return
if (tsEnd <= tsStart) {
logger.warning("Ignoring DTEND=$tsEnd that is not after DTSTART=$tsStart")
return
}

// DATE or DATE-TIME according to allDay
val end = AndroidTimeField(
timestamp = tsEnd,
timeZone = values.getAsString(Events.EVENT_END_TIMEZONE)
?: values.getAsString(Events.EVENT_TIMEZONE), // if end timezone is not present, assume same as for start
allDay = allDay,
tzRegistry = tzRegistry
).asIcal4jDate()

to.dtEnd = DtEnd(end)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@ import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.DateUtils
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
import net.fortuna.ical4j.model.TimeZoneRegistry
import net.fortuna.ical4j.model.property.RecurrenceId
import net.fortuna.ical4j.util.TimeZones

class OriginalInstanceTimeProcessor: AndroidEventFieldProcessor {

private val tzRegistry by lazy { TimeZoneRegistryFactory.getInstance().createRegistry() }
class OriginalInstanceTimeProcessor(
private val tzRegistry: TimeZoneRegistry
): AndroidEventFieldProcessor {

override fun process(from: Entity, main: Entity, to: Event) {
// only applicable to exceptions, not to main events
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ package at.bitfire.synctools.mapping.calendar.processor
import android.content.Entity
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.Event
import at.bitfire.ical4android.util.AndroidTimeUtils
import at.bitfire.synctools.exception.InvalidLocalResourceException
import net.fortuna.ical4j.model.TimeZoneRegistryFactory
import at.bitfire.synctools.util.AndroidTimeUtils
import net.fortuna.ical4j.model.TimeZoneRegistry
import net.fortuna.ical4j.model.property.ExDate
import net.fortuna.ical4j.model.property.ExRule
import net.fortuna.ical4j.model.property.RDate
Expand All @@ -20,13 +20,13 @@ import java.util.LinkedList
import java.util.logging.Level
import java.util.logging.Logger

class RecurrenceFieldsProcessor: AndroidEventFieldProcessor {
class RecurrenceFieldsProcessor(
private val tzRegistry: TimeZoneRegistry
): AndroidEventFieldProcessor {

private val logger
get() = Logger.getLogger(javaClass.name)

private val tzRegistry = TimeZoneRegistryFactory.getInstance().createRegistry()

override fun process(from: Entity, main: Entity, to: Event) {
val values = from.entityValues

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* This file is part of bitfireAT/synctools which is released under GPLv3.
* Copyright © All Contributors. See the LICENSE and AUTHOR files in the root directory for details.
* SPDX-License-Identifier: GPL-3.0-or-later
*/

package at.bitfire.synctools.mapping.calendar.processor

import android.content.Entity
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.Event
import at.bitfire.synctools.exception.InvalidLocalResourceException
import net.fortuna.ical4j.model.TimeZoneRegistry
import net.fortuna.ical4j.model.property.DtStart

class StartTimeProcessor(
private val tzRegistry: TimeZoneRegistry
): AndroidEventFieldProcessor {

override fun process(from: Entity, main: Entity, to: Event) {
val values = from.entityValues
val allDay = (values.getAsInteger(Events.ALL_DAY) ?: 0) != 0

// DATE or DATE-TIME according to allDay
val start = AndroidTimeField(
timestamp = values.getAsLong(Events.DTSTART) ?: throw InvalidLocalResourceException("Missing DTSTART"),
timeZone = values.getAsString(Events.EVENT_TIMEZONE),
allDay = allDay,
tzRegistry = tzRegistry
).asIcal4jDate()

to.dtStart = DtStart(start)
}

}
Loading