diff options
author | Ricki Hirner <hirner@bitfire.at> | 2020-01-20 23:58:48 +0300 |
---|---|---|
committer | Ricki Hirner <hirner@bitfire.at> | 2020-01-20 23:58:48 +0300 |
commit | 640fc4111999851ca282ccfa3aa02245014e7164 (patch) | |
tree | 57e85eb4c1b0d68c405d432f0b3901b4f29889b6 | |
parent | be6d515db8721008bdc93a9a6668f51b4a79502e (diff) |
Events/tasks: better handling of VALARM1.0
* handle alarms with REL=END even when it's not supported by the storage backend (like in case of events)
* handle alarms with VALUE=DATE-TIME instead of VALUE=DURATION
* round seconds to minutes
4 files changed, 167 insertions, 14 deletions
diff --git a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt index 4f62d08..54531a4 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidEvent.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidEvent.kt @@ -664,7 +664,8 @@ abstract class AndroidEvent( else -> Reminders.METHOD_DEFAULT } - val minutes = ICalendar.alarmMinBefore(alarm) + val (_, minutes) = ICalendar.vAlarmToMin(alarm, event!!, false) ?: return + builder .withValue(Reminders.METHOD, method) .withValue(Reminders.MINUTES, minutes) diff --git a/src/main/java/at/bitfire/ical4android/AndroidTask.kt b/src/main/java/at/bitfire/ical4android/AndroidTask.kt index 15e3add..d7a7635 100644 --- a/src/main/java/at/bitfire/ical4android/AndroidTask.kt +++ b/src/main/java/at/bitfire/ical4android/AndroidTask.kt @@ -312,8 +312,10 @@ abstract class AndroidTask( } protected open fun insertAlarms(batch: BatchOperation) { - for (alarm in requireNotNull(task).alarms) { - val alarmRef = when (alarm.trigger.getParameter(Parameter.RELATED)) { + val task = requireNotNull(task) + for (alarm in task.alarms) { + val (alarmRef, minutes) = ICalendar.vAlarmToMin(alarm, task, true) ?: continue + val ref = when (alarmRef) { Related.END -> Alarm.ALARM_REFERENCE_DUE_DATE else /* Related.START is the default value */ -> @@ -334,8 +336,8 @@ abstract class AndroidTask( val builder = ContentProviderOperation.newInsert(taskList.tasksPropertiesSyncUri()) .withValue(Alarm.TASK_ID, id) .withValue(Alarm.MIMETYPE, Alarm.CONTENT_ITEM_TYPE) - .withValue(Alarm.MINUTES_BEFORE, ICalendar.alarmMinBefore(alarm)) - .withValue(Alarm.REFERENCE, alarmRef) + .withValue(Alarm.MINUTES_BEFORE, minutes) + .withValue(Alarm.REFERENCE, ref) .withValue(Alarm.MESSAGE, alarm.description?.value ?: alarm.summary) .withValue(Alarm.ALARM_TYPE, alarmType) diff --git a/src/main/java/at/bitfire/ical4android/ICalendar.kt b/src/main/java/at/bitfire/ical4android/ICalendar.kt index e5c8298..dc725e3 100644 --- a/src/main/java/at/bitfire/ical4android/ICalendar.kt +++ b/src/main/java/at/bitfire/ical4android/ICalendar.kt @@ -12,9 +12,9 @@ import net.fortuna.ical4j.data.CalendarBuilder import net.fortuna.ical4j.data.ParserException import net.fortuna.ical4j.model.Calendar import net.fortuna.ical4j.model.Date -import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Parameter import net.fortuna.ical4j.model.component.* -import net.fortuna.ical4j.model.property.DateProperty +import net.fortuna.ical4j.model.parameter.Related import net.fortuna.ical4j.model.property.ProdId import net.fortuna.ical4j.model.property.TzUrl import net.fortuna.ical4j.validate.ValidationException @@ -23,6 +23,9 @@ import java.io.StringReader import java.util.* import java.util.logging.Level import java.util.logging.Logger +import kotlin.math.round +import kotlin.math.roundToInt +import kotlin.math.roundToLong open class ICalendar { @@ -195,15 +198,87 @@ open class ICalendar { // misc. iCalendar helpers - internal fun alarmMinBefore(alarm: VAlarm): Int { - var minutes = 0 - alarm.trigger?.duration?.let { duration -> + /** + * Calculates the minutes before/after an event/task a certain alarm occurs. + * + * @param alarm the alarm to calculate the minutes from + * @param eventEndRef reference [VEvent] or [VToDo] to take start/end time from (required for calculations) + * @param allowRelEnd *true*: caller accepts minutes related to the end; + * *false*: caller only accepts minutes related to the start + * + * @return Pair of values: + * + * 1. whether the minutes are related to the start or end (always [Related.START] if [allowRelEnd] is *false*) + * 2. number of minutes before start/end (negative value means number of minutes *after* start/end) + * + * May be *null* if the minutes can't be calculated. + */ + fun vAlarmToMin(alarm: VAlarm, reference: ICalendar, allowRelEnd: Boolean): Pair<Related, Int>? { + val trigger = alarm.trigger ?: return null + + var minutes = 0 // minutes before/after the event + var related = trigger.getParameter(Parameter.RELATED) as? Related ?: Related.START + + val alarmDur = trigger.duration + val alarmTime = trigger.dateTime + + if (alarmDur != null) { + // TRIGGER value is a DURATION + // negative value in TRIGGER means positive value in Reminders.MINUTES and vice versa - minutes = -(((duration.weeks * 7 + duration.days) * 24 + duration.hours) * 60 + duration.minutes + duration.seconds/60) - if (duration.isNegative) + minutes = -(((alarmDur.weeks * 7 + alarmDur.days) * 24 + alarmDur.hours) * 60 + alarmDur.minutes + (alarmDur.seconds/60.0).roundToInt()) + // duration.weeks etc. always contain positive values → evaluate duration.isNegative + if (alarmDur.isNegative) minutes *= -1 + + // DURATION triggers may have RELATED=END (default: RELATED=START), which may not be useful for caller + if (related == Related.END && !allowRelEnd) { + // Related.END is not accepted by caller (for instance because the calendar storage doesn't support it) + + val start = when (reference) { + is Event -> reference.dtStart?.date?.time + is Task -> reference.dtStart?.date?.time + else -> null + } + if (start == null) { + Constants.log.warning("iCalendar with RELATED=END VALARM doesn't have start time (required for calculation), ignoring") + return null + } + + val end = when (reference) { + is Event -> reference.dtEnd?.date?.time + is Task -> reference.due?.date?.time + else -> null + } + if (end == null) { + Constants.log.warning("iCalendar with RELATED=END VALARM doesn't have end time, ignoring") + return null + } + val durMin = ((end - start)/60000.0).roundToInt() // ms → min + + // move alarm towards end + related = Related.START + minutes -= durMin + } + + } else if (alarmTime != null) { + // TRIGGER value is a DATE-TIME, calculate minutes from start time + val start = if (reference is Event) + reference.dtStart?.date?.time + else if (reference is Task) + reference.dtStart?.date?.time + else + null + if (start == null) { + Constants.log.warning("iCalendar with DATE-TIME VALARM doesn't have start time (required for calculation), ignoring") + return null + } + + related = Related.START + minutes = ((start - alarmTime.time)/60000.0).roundToInt() // ms → min } - return minutes + + return Pair(related, minutes) } } diff --git a/src/test/java/at/bitfire/ical4android/ICalendarTest.kt b/src/test/java/at/bitfire/ical4android/ICalendarTest.kt index 09dd323..c0c7340 100644 --- a/src/test/java/at/bitfire/ical4android/ICalendarTest.kt +++ b/src/test/java/at/bitfire/ical4android/ICalendarTest.kt @@ -8,6 +8,13 @@ package at.bitfire.ical4android +import net.fortuna.ical4j.model.DateTime +import net.fortuna.ical4j.model.Dur +import net.fortuna.ical4j.model.component.VAlarm +import net.fortuna.ical4j.model.parameter.Related +import net.fortuna.ical4j.model.property.DtEnd +import net.fortuna.ical4j.model.property.DtStart +import net.fortuna.ical4j.model.property.Due import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Test @@ -50,4 +57,72 @@ class ICalendarTest { "END:VCALENDAR")) } -} + @Test + fun testVAlarmToMin() { + run { + // TRIGGER;REL=START:-PT1D1H1M29S (round down) + val (ref, min) = ICalendar.vAlarmToMin( + VAlarm(Dur(1, 1, 1, 29).negate()), + ICalendar(), false)!! + assertEquals(Related.START, ref) + assertEquals(60*24 + 60 + 1, min) + } + + run { + // TRIGGER;REL=START:PT1D1H1M30S (round up; alarm *after* start) + val (ref, min) = ICalendar.vAlarmToMin( + VAlarm(Dur(1, 1, 1, 30)), + ICalendar(), false)!! + assertEquals(Related.START, ref) + assertEquals(-(60 * 24 + 60 + 1 + 1), min) + } + + run { + // TRIGGER;REL=END:-PT1D1H1M30S (caller accepts Related.END) + val alarm = VAlarm(Dur(1, 1, 1, 30).negate()) + alarm.trigger.parameters.add(Related.END) + val (ref, min) = ICalendar.vAlarmToMin(alarm, ICalendar(), true)!! + assertEquals(Related.END, ref) + assertEquals(60 * 24 + 60 + 1 + 1, min) + } + + run { + // event with TRIGGER;REL=END:-PT1D1H1M30S (caller doesn't accept Related.END) + val alarm = VAlarm(Dur(1, 1, 1, 30).negate()) + alarm.trigger.parameters.add(Related.END) + val event = Event() + val currentTime = java.util.Date().time + event.dtStart = DtStart(DateTime(currentTime)) + event.dtEnd = DtEnd(DateTime(currentTime + 90*1000)) // 90 sec (should be rounded up to 2 min) later + val (ref, min) = ICalendar.vAlarmToMin(alarm, event, false)!! + assertEquals(Related.START, ref) + assertEquals(60 * 24 + 60 + 1 + 1 /* duration of event: */ - 2, min) + } + + run { + // task with TRIGGER;REL=END:-PT1D1H1M30S (caller doesn't accept Related.END; alarm *after* end) + val alarm = VAlarm(Dur(1, 1, 1, 30)) + alarm.trigger.parameters.add(Related.END) + val task = Task() + val currentTime = java.util.Date().time + task.dtStart = DtStart(DateTime(currentTime)) + task.due = Due(DateTime(currentTime + 90*1000)) // 90 sec (should be rounded up to 2 min) later + val (ref, min) = ICalendar.vAlarmToMin(alarm, task, false)!! + assertEquals(Related.START, ref) + assertEquals(-(60 * 24 + 60 + 1 + 1) /* duration of event: */ - 2, min) + } + + run { + // TRIGGER;VALUE=DATE-TIME:<xxxx> + val event = Event() + val currentTime = java.util.Date().time + event.dtStart = DtStart(DateTime(currentTime)) + val alarm = VAlarm(DateTime(currentTime - 89*1000)) // 89 sec (should be rounded down to 1 min) before event + alarm.trigger.parameters.add(Related.END) // not useful for DATE-TIME values, should be ignored + val (ref, min) = ICalendar.vAlarmToMin(alarm, event, false)!! + assertEquals(Related.START, ref) + assertEquals(1, min) + } + } + +}
\ No newline at end of file |