Synchronize VALARMs

pull/1767/head
Alex Baker 2 years ago
parent 09da1dcc42
commit 3b2b16f0c6

@ -5,6 +5,8 @@ import at.bitfire.ical4android.Task.Companion.tasksFromReader
import com.todoroo.astrid.data.Task
import kotlinx.coroutines.runBlocking
import org.tasks.caldav.iCalendar.Companion.applyRemote
import org.tasks.caldav.iCalendar.Companion.reminders
import org.tasks.data.Alarm
import org.tasks.data.CaldavTask
import org.tasks.preferences.Preferences
import org.tasks.time.DateTime
@ -44,6 +46,9 @@ object TestUtilities {
return task
}
val String.alarms: List<Alarm>
get() = fromResource(this).reminders
fun setup(path: String): Triple<Task, CaldavTask, at.bitfire.ical4android.Task> {
val task = Task()
val vtodo = readFile(path)

@ -0,0 +1,32 @@
@file:Suppress("ClassName")
package com.todoroo.astrid.service
import org.tasks.caldav.iCalendar
import org.tasks.caldav.iCalendar.Companion.reminders
import org.tasks.data.AlarmDao
import org.tasks.data.TaskDao
import org.tasks.data.UpgraderDao
import javax.inject.Inject
class Upgrade_12_4 @Inject constructor(
private val alarmDao: AlarmDao,
private val taskDao: TaskDao,
private val upgraderDao: UpgraderDao,
) {
internal suspend fun syncExistingAlarms() {
val existingAlarms = alarmDao.getActiveAlarms().map { it.task }
upgraderDao.tasksWithVtodos().forEach { caldav ->
val remoteTask = caldav.vtodo?.let(iCalendar::fromVtodo) ?: return@forEach
remoteTask
.reminders
.onEach { alarm -> alarm.task = caldav.id }
.let { alarms -> alarmDao.insert(alarms) }
}
taskDao.touch(existingAlarms)
}
companion object {
const val VERSION = 120400
}
}

@ -59,6 +59,7 @@ class Upgrader @Inject constructor(
private val upgraderDao: UpgraderDao,
private val upgrade_11_3: Lazy<Upgrade_11_3>,
private val upgrade_11_12_3: Lazy<Upgrade_11_12_3>,
private val upgrade_12_4: Lazy<Upgrade_12_4>,
) {
fun upgrade(from: Int, to: Int) {
@ -95,13 +96,14 @@ class Upgrader @Inject constructor(
}
}
run(from, Upgrade_11_12_3.VERSION) {
with(upgrade_11_12_3.get()) {
migrateDefaultReminderPreference()
}
upgrade_11_12_3.get().migrateDefaultReminderPreference()
}
run(from, V11_13) {
preferences.setString(R.string.p_completion_ringtone, "")
}
run(from, Upgrade_12_4.VERSION) {
upgrade_12_4.get().syncExistingAlarms()
}
preferences.setBoolean(R.string.p_just_updated, true)
}
preferences.setCurrentVersion(to)

@ -12,9 +12,17 @@ import at.bitfire.dav4jvm.exception.DavException
import at.bitfire.dav4jvm.exception.HttpException
import at.bitfire.dav4jvm.exception.ServiceUnavailableException
import at.bitfire.dav4jvm.exception.UnauthorizedException
import at.bitfire.dav4jvm.property.*
import at.bitfire.dav4jvm.property.CalendarColor
import at.bitfire.dav4jvm.property.CalendarData
import at.bitfire.dav4jvm.property.CurrentUserPrincipal
import at.bitfire.dav4jvm.property.CurrentUserPrivilegeSet
import at.bitfire.dav4jvm.property.DisplayName
import at.bitfire.dav4jvm.property.GetCTag
import at.bitfire.dav4jvm.property.GetETag
import at.bitfire.dav4jvm.property.GetETag.Companion.fromResponse
import at.bitfire.dav4jvm.property.SyncToken
import at.bitfire.ical4android.ICalendar.Companion.prodId
import com.todoroo.astrid.alarms.AlarmService
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task
import com.todoroo.astrid.helper.UUIDHelper
@ -32,18 +40,26 @@ import org.tasks.Strings.isNullOrEmpty
import org.tasks.analytics.Firebase
import org.tasks.billing.Inventory
import org.tasks.caldav.iCalendar.Companion.fromVtodo
import org.tasks.caldav.property.*
import org.tasks.caldav.iCalendar.Companion.reminders
import org.tasks.caldav.property.Invite
import org.tasks.caldav.property.OCAccess
import org.tasks.caldav.property.OCInvite
import org.tasks.caldav.property.OCOwnerPrincipal
import org.tasks.caldav.property.OCUser
import org.tasks.caldav.property.PropertyUtils.register
import org.tasks.caldav.property.ShareAccess
import org.tasks.caldav.property.ShareAccess.Companion.READ
import org.tasks.caldav.property.ShareAccess.Companion.READ_WRITE
import org.tasks.caldav.property.ShareAccess.Companion.SHARED_OWNER
import org.tasks.data.*
import org.tasks.caldav.property.Sharee
import org.tasks.data.CaldavAccount
import org.tasks.data.CaldavAccount.Companion.ERROR_UNAUTHORIZED
import org.tasks.data.CaldavAccount.Companion.SERVER_OPEN_XCHANGE
import org.tasks.data.CaldavAccount.Companion.SERVER_OWNCLOUD
import org.tasks.data.CaldavAccount.Companion.SERVER_SABREDAV
import org.tasks.data.CaldavAccount.Companion.SERVER_TASKS
import org.tasks.data.CaldavAccount.Companion.SERVER_UNKNOWN
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavCalendar.Companion.ACCESS_OWNER
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_ONLY
import org.tasks.data.CaldavCalendar.Companion.ACCESS_READ_WRITE
@ -53,6 +69,10 @@ import org.tasks.data.CaldavCalendar.Companion.INVITE_DECLINED
import org.tasks.data.CaldavCalendar.Companion.INVITE_INVALID
import org.tasks.data.CaldavCalendar.Companion.INVITE_NO_RESPONSE
import org.tasks.data.CaldavCalendar.Companion.INVITE_UNKNOWN
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.PrincipalAccess
import org.tasks.data.PrincipalDao
import timber.log.Timber
import java.io.IOException
import java.net.ConnectException
@ -60,7 +80,6 @@ import java.net.SocketTimeoutException
import java.net.UnknownHostException
import java.security.KeyManagementException
import java.security.NoSuchAlgorithmException
import java.util.*
import javax.inject.Inject
import javax.net.ssl.SSLException
@ -75,6 +94,7 @@ class CaldavSynchronizer @Inject constructor(
private val provider: CaldavClientProvider,
private val iCal: iCalendar,
private val principalDao: PrincipalDao,
private val alarmService: AlarmService,
) {
suspend fun sync(account: CaldavAccount) {
Thread.currentThread().contextClassLoader = context.classLoader
@ -254,8 +274,11 @@ class CaldavSynchronizer @Inject constructor(
Timber.e("Invalid VCALENDAR: %s", fileName)
return
}
val caldavTask = caldavDao.getTask(caldavCalendar.uuid!!, fileName)
var caldavTask = caldavDao.getTask(caldavCalendar.uuid!!, fileName)
iCal.fromVtodo(caldavCalendar, caldavTask, remote, vtodo, fileName, eTag)
caldavTask = caldavTask ?: caldavDao.getTask(caldavCalendar.uuid!!, fileName)
?: continue
alarmService.synchronizeAlarms(caldavTask.task, remote.reminders.toMutableSet())
}
}
caldavDao

@ -3,6 +3,7 @@ package org.tasks.caldav
import at.bitfire.ical4android.DateUtils.ical4jTimeZone
import at.bitfire.ical4android.Task
import at.bitfire.ical4android.Task.Companion.tasksFromReader
import at.bitfire.ical4android.util.TimeApiExtensions.toDuration
import com.todoroo.andlib.utility.DateUtilities
import com.todoroo.astrid.dao.TaskDao
import com.todoroo.astrid.data.Task.Companion.HIDE_UNTIL_SPECIFIC_DAY
@ -11,17 +12,44 @@ import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY
import com.todoroo.astrid.data.Task.Companion.URGENCY_SPECIFIC_DAY_TIME
import com.todoroo.astrid.helper.UUIDHelper
import com.todoroo.astrid.service.TaskCreator
import net.fortuna.ical4j.model.Date
import net.fortuna.ical4j.model.DateTime
import net.fortuna.ical4j.model.Parameter
import net.fortuna.ical4j.model.ParameterList
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.parameter.RelType
import net.fortuna.ical4j.model.property.*
import net.fortuna.ical4j.model.parameter.Related
import net.fortuna.ical4j.model.parameter.Related.*
import net.fortuna.ical4j.model.property.Action
import net.fortuna.ical4j.model.property.Completed
import net.fortuna.ical4j.model.property.DateProperty
import net.fortuna.ical4j.model.property.Description
import net.fortuna.ical4j.model.property.DtStart
import net.fortuna.ical4j.model.property.Due
import net.fortuna.ical4j.model.property.Geo
import net.fortuna.ical4j.model.property.RelatedTo
import net.fortuna.ical4j.model.property.Repeat
import net.fortuna.ical4j.model.property.Status
import net.fortuna.ical4j.model.property.Trigger
import net.fortuna.ical4j.model.property.XProperty
import org.tasks.Strings.isNullOrEmpty
import org.tasks.caldav.GeoUtils.equalish
import org.tasks.caldav.GeoUtils.toGeo
import org.tasks.caldav.GeoUtils.toLikeString
import org.tasks.data.*
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.data.Alarm.Companion.TYPE_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START
import org.tasks.data.AlarmDao
import org.tasks.data.CaldavCalendar
import org.tasks.data.CaldavDao
import org.tasks.data.CaldavTask
import org.tasks.data.Geofence
import org.tasks.data.LocationDao
import org.tasks.data.Place
import org.tasks.data.TagDao
import org.tasks.data.TagData
import org.tasks.data.TagDataDao
import org.tasks.date.DateTimeUtils.newDateTime
import org.tasks.date.DateTimeUtils.toDateTime
import org.tasks.jobs.WorkManager
@ -37,6 +65,9 @@ import timber.log.Timber
import java.io.ByteArrayOutputStream
import java.io.StringReader
import java.text.ParseException
import java.time.Duration
import java.time.Instant
import java.time.temporal.TemporalAmount
import java.util.*
import javax.inject.Inject
import kotlin.math.max
@ -52,7 +83,9 @@ class iCalendar @Inject constructor(
private val taskCreator: TaskCreator,
private val tagDao: TagDao,
private val taskDao: TaskDao,
private val caldavDao: CaldavDao) {
private val caldavDao: CaldavDao,
private val alarmDao: AlarmDao,
) {
suspend fun setPlace(taskId: Long, geo: Geo?) {
if (geo == null) {
@ -136,6 +169,42 @@ class iCalendar @Inject constructor(
if (localGeo == null || !localGeo.equalish(remoteModel.geoPosition)) {
remoteModel.geoPosition = localGeo
}
remoteModel.alarms.removeAll(remoteModel.alarms.filtered)
remoteModel.alarms.addAll(
alarmDao
.getAlarms(task.id)
.mapNotNull {
val trigger = when (it.type) {
TYPE_DATE_TIME ->
Trigger(getDateTime(it.time))
TYPE_REL_START,
TYPE_REL_END ->
Trigger(
ParameterList().apply {
add(if (it.type == TYPE_REL_END) END else START)
},
Duration.ofMillis(it.time)
)
else -> return@mapNotNull null
}
VAlarm().apply {
with(properties) {
add(trigger)
add(Action.DISPLAY)
add(Description("Default Tasks.org description"))
if (it.repeat > 0) {
add(Repeat(it.repeat))
add(
net.fortuna.ical4j.model.property.Duration(
Duration.ofMillis(it.interval)
)
)
}
}
}
}
)
}
suspend fun fromVtodo(
@ -177,6 +246,8 @@ class iCalendar @Inject constructor(
private const val MOZ_SNOOZE_TIME = "X-MOZ-SNOOZE-TIME"
private const val MOZ_LASTACK = "X-MOZ-LASTACK"
private const val HIDE_SUBTASKS = "1"
// VALARM extensions: https://datatracker.ietf.org/doc/html/rfc9074
private val IGNORE_ALARM = DateTime("19760401T005545Z")
private val IS_PARENT = { r: RelatedTo ->
r.parameters.getParameter<RelType>(Parameter.RELTYPE).let {
it === RelType.PARENT || it == null || it.value.isNullOrBlank()
@ -210,8 +281,7 @@ class iCalendar @Inject constructor(
}
}
@JvmStatic
fun getLocal(property: DateProperty): Long =
private fun getLocal(property: DateProperty): Long =
org.tasks.time.DateTime.from(property.date)?.toLocal()?.millis ?: 0
fun fromVtodo(vtodo: String): Task? {
@ -389,11 +459,41 @@ class iCalendar @Inject constructor(
snooze = task.reminderSnooze
}
val List<VAlarm>.filtered: List<VAlarm>
get() =
filter { it.action == Action.DISPLAY || it.action == Action.AUDIO }
.filterNot { it.trigger.dateTime == IGNORE_ALARM }
val Task.reminders: List<Alarm>
get() = alarms.filtered.mapNotNull {
val (type, time) = when {
it.trigger.date != null ->
Pair(TYPE_DATE_TIME, getLocal(it.trigger))
it.trigger.duration != null ->
Pair(
if (it.trigger.parameters.getParameter<Related>(RELATED) == END) {
TYPE_REL_END
} else {
TYPE_REL_START
},
it.trigger.duration.toMillis()
)
else -> return@mapNotNull null
}
Alarm(0L, time, type, it.repeat?.count ?: 0, it.duration?.toMillis() ?: 0)
}
private fun getDateTime(timestamp: Long): DateTime {
val tz = ical4jTimeZone(TimeZone.getDefault().id)
val dateTime = DateTime(if (tz != null) timestamp else org.tasks.time.DateTime(timestamp).toUTC().millis)
dateTime.timeZone = tz
return dateTime
}
private fun net.fortuna.ical4j.model.property.Duration.toMillis() =
duration.toMillis()
private fun TemporalAmount.toMillis(): Long =
toDuration(Instant.EPOCH).toSeconds() * 1_000
}
}

@ -65,7 +65,8 @@ class Alarm : Parcelable {
override fun describeContents() = 0
override fun toString(): String {
return "Alarm(id=$id, task=$task, time=${printTimestamp(time)}, type=$type, repeat=$repeat, interval=$interval)"
val timestamp = if (type == TYPE_DATE_TIME) printTimestamp(time) else time
return "Alarm(id=$id, task=$task, time=$timestamp, type=$type, repeat=$repeat, interval=$interval)"
}
override fun equals(other: Any?): Boolean {

@ -3,9 +3,12 @@ package org.tasks.caldav
import com.todoroo.astrid.data.Task
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.tasks.TestUtilities.alarms
import org.tasks.TestUtilities.vtodo
import org.tasks.data.Alarm
import org.tasks.time.DateTime
import java.util.*
@ -77,4 +80,17 @@ class AppleRemindersTests {
fun highPriority() {
assertEquals(Task.Priority.HIGH, vtodo("apple/priority_high.txt").priority)
}
@Test
fun dateTimeReminder() {
assertEquals(
listOf(Alarm(0, 1642568400000)),
"apple/date_time_reminder.txt".alarms
)
}
@Test
fun ignoreLocationReminders() {
assertTrue("apple/geofence_arrival.txt".alarms.isEmpty())
}
}

@ -6,11 +6,18 @@ import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Ignore
import org.junit.Test
import org.tasks.TestUtilities.alarms
import org.tasks.TestUtilities.setup
import org.tasks.TestUtilities.vtodo
import org.tasks.caldav.iCalendar.Companion.applyLocal
import org.tasks.data.Alarm
import org.tasks.data.Alarm.Companion.TYPE_DATE_TIME
import org.tasks.data.Alarm.Companion.TYPE_REL_END
import org.tasks.data.Alarm.Companion.TYPE_REL_START
import org.tasks.time.DateTime
import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MINUTES
class ThunderbirdTests {
private val defaultTimeZone = TimeZone.getDefault()
@ -110,6 +117,62 @@ class ThunderbirdTests {
vtodo("thunderbird/start_date_time.txt").hideUntil)
}
@Test
fun dateTimeReminder() {
assertEquals(
listOf(Alarm(task = 0, time = 1642791600000, type = TYPE_DATE_TIME)),
"thunderbird/date_time_reminder.txt".alarms
)
}
@Test
fun reminderBeforeStart() {
assertEquals(
listOf(Alarm(task = 0, time = -TimeUnit.HOURS.toMillis(1), type = TYPE_REL_START)),
"thunderbird/reminder_before_start.txt".alarms
)
}
@Test
fun reminderAfterStart() {
assertEquals(
listOf(Alarm(task = 0, time = MINUTES.toMillis(15), type = TYPE_REL_START)),
"thunderbird/reminder_after_start.txt".alarms
)
}
@Test
fun reminderBeforeEnd() {
assertEquals(
listOf(Alarm(task = 0, time = -MINUTES.toMillis(15), type = TYPE_REL_END)),
"thunderbird/reminder_before_end.txt".alarms
)
}
@Test
fun reminderAfterEnd() {
assertEquals(
listOf(Alarm(task = 0, time = MINUTES.toMillis(15), type = TYPE_REL_END)),
"thunderbird/reminder_after_end.txt".alarms
)
}
@Test
fun reminderAtStart() {
assertEquals(
listOf(Alarm(task = 0, time = 0, type = TYPE_REL_START)),
"thunderbird/reminder_at_start.txt".alarms
)
}
@Test
fun reminder50Days() {
assertEquals(
listOf(Alarm(task = 0, time = -TimeUnit.DAYS.toMillis(50), type = TYPE_REL_START)),
"thunderbird/reminder_50_days.txt".alarms
)
}
@Test
@Ignore
fun dontCrashOnMultipleTasks() {

@ -0,0 +1,39 @@
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
PRODID:-//Apple Inc.//iOS 12.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/Chicago
BEGIN:DAYLIGHT
DTSTART:20070311T020000
RRULE:FREQ=YEARLY;BYMONTH=3;BYDAY=2SU
TZNAME:CDT
TZOFFSETFROM:-0600
TZOFFSETTO:-0500
END:DAYLIGHT
BEGIN:STANDARD
DTSTART:20071104T020000
RRULE:FREQ=YEARLY;BYMONTH=11;BYDAY=1SU
TZNAME:CST
TZOFFSETFROM:-0500
TZOFFSETTO:-0600
END:STANDARD
END:VTIMEZONE
BEGIN:VTODO
CREATED:20220119T045003Z
DTSTAMP:20220119T045013Z
DTSTART;TZID=America/Chicago:20220118T230000
DUE;TZID=America/Chicago:20220118T230000
LAST-MODIFIED:20220119T045013Z
STATUS:NEEDS-ACTION
SUMMARY:Test
UID:A11D558C-3BC3-45FE-B95E-8B1F0023CB17
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Reminder
TRIGGER;VALUE=DATE-TIME:20220119T050000Z
UID:2CBB42D2-039E-4F6B-BFDF-761632A15305
X-WR-ALARMUID:2CBB42D2-039E-4F6B-BFDF-761632A15305
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,25 @@
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
PRODID:-//Apple Inc.//iOS 12.1//EN
VERSION:2.0
BEGIN:VTODO
CREATED:20220119T050820Z
DTSTAMP:20220119T050843Z
LAST-MODIFIED:20220119T050843Z
STATUS:NEEDS-ACTION
SUMMARY:Test
UID:5F1A1AD2-6412-4F61-AE56-7D0EE8CE4CE5
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Reminder
TRIGGER;VALUE=DATE-TIME:19760401T005545Z
UID:9EF67428-7209-4EA4-9029-4049F577F4FF
X-APPLE-PROXIMITY:ARRIVE
X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS=1600 Amphitheatre Pkwy\\
nMountain View CA 94043\\nUnited States;X-APPLE-RADIUS=141.1748408176572
;X-APPLE-REFERENCEFRAME=1;X-TITLE=Google Mountain View - Headquarters:ge
o:37.422338,-122.084370
X-WR-ALARMUID:9EF67428-7209-4EA4-9029-4049F577F4FF
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,25 @@
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
PRODID:-//Apple Inc.//iOS 12.1//EN
VERSION:2.0
BEGIN:VTODO
CREATED:20220119T050930Z
DTSTAMP:20220119T050938Z
LAST-MODIFIED:20220119T050937Z
STATUS:NEEDS-ACTION
SUMMARY:Test
UID:E41BC37A-6261-4172-96F6-1B8FF9F74380
BEGIN:VALARM
ACTION:DISPLAY
DESCRIPTION:Reminder
TRIGGER;VALUE=DATE-TIME:19760401T005545Z
UID:9E6BDF76-8FA4-4B22-AE14-FD2A43BC4D18
X-APPLE-PROXIMITY:DEPART
X-APPLE-STRUCTURED-LOCATION;VALUE=URI;X-ADDRESS=1600 Amphitheatre Pkwy\\
nMountain View CA 94043\\nUnited States;X-APPLE-RADIUS=141.1748408176572
;X-APPLE-REFERENCEFRAME=1;X-TITLE=Google Mountain View - Headquarters:ge
o:37.422338,-122.084370
X-WR-ALARMUID:9E6BDF76-8FA4-4B22-AE14-FD2A43BC4D18
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,17 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTODO
CREATED:20220121T053659Z
LAST-MODIFIED:20220121T053753Z
DTSTAMP:20220121T053753Z
UID:e7cd55dc-9519-2a45-8630-4b632a4ca297
SUMMARY:Test
PERCENT-COMPLETE:0
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DATE-TIME:20220121T190000Z
DESCRIPTION:Default Mozilla Description
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,35 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/Chicago
BEGIN:DAYLIGHT
TZOFFSETFROM:-0600
TZOFFSETTO:-0500
TZNAME:CDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0500
TZOFFSETTO:-0600
TZNAME:CST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
END:STANDARD
END:VTIMEZONE
BEGIN:VTODO
CREATED:20220206T055100Z
LAST-MODIFIED:20220206T055130Z
DTSTAMP:20220206T055130Z
UID:796d08f4-dd49-4749-94ba-6ce2ff0aae22
SUMMARY:Test
DTSTART;TZID=America/Chicago:20230201T230000
PERCENT-COMPLETE:0
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DURATION:-P50D
DESCRIPTION:Default Mozilla Description
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,37 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/Chicago
BEGIN:DAYLIGHT
TZOFFSETFROM:-0600
TZOFFSETTO:-0500
TZNAME:CDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0500
TZOFFSETTO:-0600
TZNAME:CST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
END:STANDARD
END:VTIMEZONE
BEGIN:VTODO
CREATED:20220121T054934Z
LAST-MODIFIED:20220121T055843Z
DTSTAMP:20220121T055843Z
UID:5b3a42d5-e99c-2f47-87d5-8ba0e2b753ab
SUMMARY:Test
DUE;TZID=America/Chicago:20220121T130000
PERCENT-COMPLETE:0
X-MOZ-GENERATION:3
SEQUENCE:1
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DURATION;RELATED=END:PT15M
DESCRIPTION:Default Mozilla Description
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,37 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/Chicago
BEGIN:DAYLIGHT
TZOFFSETFROM:-0600
TZOFFSETTO:-0500
TZNAME:CDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0500
TZOFFSETTO:-0600
TZNAME:CST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
END:STANDARD
END:VTIMEZONE
BEGIN:VTODO
CREATED:20220121T054934Z
LAST-MODIFIED:20220121T055642Z
DTSTAMP:20220121T055642Z
UID:5b3a42d5-e99c-2f47-87d5-8ba0e2b753ab
SUMMARY:Test
DTSTART;TZID=America/Chicago:20220121T130000
DUE;TZID=America/Chicago:20220121T130000
PERCENT-COMPLETE:0
X-MOZ-GENERATION:1
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DURATION:PT15M
DESCRIPTION:Default Mozilla Description
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,35 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/Chicago
BEGIN:DAYLIGHT
TZOFFSETFROM:-0600
TZOFFSETTO:-0500
TZNAME:CDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0500
TZOFFSETTO:-0600
TZNAME:CST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
END:STANDARD
END:VTIMEZONE
BEGIN:VTODO
CREATED:20220205T213726Z
LAST-MODIFIED:20220205T213750Z
DTSTAMP:20220205T213750Z
UID:7d5eca48-9faa-294c-8155-052a4e49f342
SUMMARY:Test
DTSTART;TZID=America/Chicago:20220205T160000
PERCENT-COMPLETE:0
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DURATION:PT0S
DESCRIPTION:Default Mozilla Description
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,37 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/Chicago
BEGIN:DAYLIGHT
TZOFFSETFROM:-0600
TZOFFSETTO:-0500
TZNAME:CDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0500
TZOFFSETTO:-0600
TZNAME:CST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
END:STANDARD
END:VTIMEZONE
BEGIN:VTODO
CREATED:20220121T054934Z
LAST-MODIFIED:20220121T055751Z
DTSTAMP:20220121T055751Z
UID:5b3a42d5-e99c-2f47-87d5-8ba0e2b753ab
SUMMARY:Test
DUE;TZID=America/Chicago:20220121T130000
PERCENT-COMPLETE:0
X-MOZ-GENERATION:2
SEQUENCE:1
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DURATION;RELATED=END:-PT15M
DESCRIPTION:Default Mozilla Description
END:VALARM
END:VTODO
END:VCALENDAR

@ -0,0 +1,36 @@
BEGIN:VCALENDAR
PRODID:-//Mozilla.org/NONSGML Mozilla Calendar V1.1//EN
VERSION:2.0
BEGIN:VTIMEZONE
TZID:America/Chicago
BEGIN:DAYLIGHT
TZOFFSETFROM:-0600
TZOFFSETTO:-0500
TZNAME:CDT
DTSTART:19700308T020000
RRULE:FREQ=YEARLY;BYDAY=2SU;BYMONTH=3
END:DAYLIGHT
BEGIN:STANDARD
TZOFFSETFROM:-0500
TZOFFSETTO:-0600
TZNAME:CST
DTSTART:19701101T020000
RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=11
END:STANDARD
END:VTIMEZONE
BEGIN:VTODO
CREATED:20220121T054934Z
LAST-MODIFIED:20220121T054956Z
DTSTAMP:20220121T054956Z
UID:5b3a42d5-e99c-2f47-87d5-8ba0e2b753ab
SUMMARY:Test
DTSTART;TZID=America/Chicago:20220121T130000
DUE;TZID=America/Chicago:20220121T130000
PERCENT-COMPLETE:0
BEGIN:VALARM
ACTION:DISPLAY
TRIGGER;VALUE=DURATION:-PT1H
DESCRIPTION:Default Mozilla Description
END:VALARM
END:VTODO
END:VCALENDAR
Loading…
Cancel
Save