Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/bitfireAT/ical4android.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorRicki Hirner <hirner@bitfire.at>2020-01-20 23:58:48 +0300
committerRicki Hirner <hirner@bitfire.at>2020-01-20 23:58:48 +0300
commit640fc4111999851ca282ccfa3aa02245014e7164 (patch)
tree57e85eb4c1b0d68c405d432f0b3901b4f29889b6
parentbe6d515db8721008bdc93a9a6668f51b4a79502e (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
-rw-r--r--src/main/java/at/bitfire/ical4android/AndroidEvent.kt3
-rw-r--r--src/main/java/at/bitfire/ical4android/AndroidTask.kt10
-rw-r--r--src/main/java/at/bitfire/ical4android/ICalendar.kt91
-rw-r--r--src/test/java/at/bitfire/ical4android/ICalendarTest.kt77
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