1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
|
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.ical4android
import androidx.annotation.IntRange
import net.fortuna.ical4j.data.CalendarOutputter
import net.fortuna.ical4j.data.ParserException
import net.fortuna.ical4j.model.*
import net.fortuna.ical4j.model.Calendar
import net.fortuna.ical4j.model.TimeZone
import net.fortuna.ical4j.model.component.VAlarm
import net.fortuna.ical4j.model.component.VToDo
import net.fortuna.ical4j.model.property.*
import java.io.IOException
import java.io.OutputStream
import java.io.Reader
import java.net.URI
import java.net.URISyntaxException
import java.util.*
import java.util.logging.Level
class Task: ICalendar() {
var createdAt: Long? = null
var lastModified: Long? = null
var summary: String? = null
var location: String? = null
var geoPosition: Geo? = null
var description: String? = null
var color: Int? = null
var url: String? = null
var organizer: Organizer? = null
@IntRange(from = 0, to = 9)
var priority: Int = Priority.UNDEFINED.level
var classification: Clazz? = null
var status: Status? = null
var dtStart: DtStart? = null
var due: Due? = null
var duration: Duration? = null
var completedAt: Completed? = null
@IntRange(from = 0, to = 100)
var percentComplete: Int? = null
var rRule: RRule? = null
val rDates = LinkedList<RDate>()
val exDates = LinkedList<ExDate>()
val categories = LinkedList<String>()
var relatedTo = LinkedList<RelatedTo>()
val unknownProperties = LinkedList<Property>()
val alarms = LinkedList<VAlarm>()
companion object {
/**
* Parses an iCalendar resource, applies [ICalPreprocessor] to increase compatibility
* and extracts the VTODOs.
*
* @param reader where the iCalendar is taken from
*
* @return array of filled [Task] data objects (may have size 0)
*
* @throws ParserException when the iCalendar can't be parsed
* @throws IllegalArgumentException when the iCalendar resource contains an invalid value
* @throws IOException on I/O errors
*/
@UsesThreadContextClassLoader
fun tasksFromReader(reader: Reader): List<Task> {
val ical = fromReader(reader)
val vToDos = ical.getComponents<VToDo>(Component.VTODO)
return vToDos.mapTo(LinkedList()) { this.fromVToDo(it) }
}
private fun fromVToDo(todo: VToDo): Task {
val t = Task()
if (todo.uid != null)
t.uid = todo.uid.value
else {
Ical4Android.log.warning("Received VTODO without UID, generating new one")
t.generateUID()
}
// sequence must only be null for locally created, not-yet-synchronized events
t.sequence = 0
for (prop in todo.properties)
when (prop) {
is Sequence -> t.sequence = prop.sequenceNo
is Created -> t.createdAt = prop.dateTime.time
is LastModified -> t.lastModified = prop.dateTime.time
is Summary -> t.summary = prop.value
is Location -> t.location = prop.value
is Geo -> t.geoPosition = prop
is Description -> t.description = prop.value
is Color -> t.color = Css3Color.fromString(prop.value)?.argb
is Url -> t.url = prop.value
is Organizer -> t.organizer = prop
is Priority -> t.priority = prop.level
is Clazz -> t.classification = prop
is Status -> t.status = prop
is Due -> { t.due = prop }
is Duration -> t.duration = prop
is DtStart -> { t.dtStart = prop }
is Completed -> { t.completedAt = prop }
is PercentComplete -> t.percentComplete = prop.percentage
is RRule -> t.rRule = prop
is RDate -> t.rDates += prop
is ExDate -> t.exDates += prop
is Categories ->
for (category in prop.categories)
t.categories += category
is RelatedTo -> t.relatedTo.add(prop)
is Uid, is ProdId, is DtStamp -> { /* don't save these as unknown properties */ }
else -> t.unknownProperties += prop
}
t.alarms.addAll(todo.alarms)
// There seem to be many invalid tasks out there because of some defect clients, do some validation.
val dtStart = t.dtStart
val due = t.due
if (dtStart != null && due != null) {
if (DateUtils.isDate(dtStart) && DateUtils.isDateTime(due)) {
Ical4Android.log.warning("DTSTART is DATE but DUE is DATE-TIME, rewriting DTSTART to DATE-TIME")
t.dtStart = DtStart(DateTime(dtStart.value, due.timeZone))
} else if (DateUtils.isDateTime(dtStart) && DateUtils.isDate(due)) {
Ical4Android.log.warning("DTSTART is DATE-TIME but DUE is DATE, rewriting DUE to DATE-TIME")
t.due = Due(DateTime(due.value, dtStart.timeZone))
}
if (due.date <= dtStart.date) {
Ical4Android.log.warning("Found invalid DUE <= DTSTART; dropping DTSTART")
t.dtStart = null
}
}
if (t.duration != null && t.dtStart == null) {
Ical4Android.log.warning("Found DURATION without DTSTART; ignoring")
t.duration = null
}
return t
}
}
@UsesThreadContextClassLoader
fun write(os: OutputStream) {
Ical4Android.checkThreadContextClassLoader()
val ical = Calendar()
ical.properties += Version.VERSION_2_0
ical.properties += prodId
val vTodo = VToDo(true /* generates DTSTAMP */)
ical.components += vTodo
val props = vTodo.properties
uid?.let { props += Uid(uid) }
sequence?.let {
if (it != 0)
props += Sequence(it)
}
createdAt?.let { props += Created(DateTime(it)) }
lastModified?.let { props += LastModified(DateTime(it)) }
summary?.let { props += Summary(it) }
location?.let { props += Location(it) }
geoPosition?.let { props += it }
description?.let { props += Description(it) }
color?.let { props += Color(null, Css3Color.nearestMatch(it).name) }
url?.let {
try {
props += Url(URI(it))
} catch (e: URISyntaxException) {
Ical4Android.log.log(Level.WARNING, "Ignoring invalid task URL: $url", e)
}
}
organizer?.let { props += it }
if (priority != Priority.UNDEFINED.level)
props += Priority(priority)
classification?.let { props += it }
status?.let { props += it }
rRule?.let { props += it }
rDates.forEach { props += it }
exDates.forEach { props += it }
if (categories.isNotEmpty())
props += Categories(TextList(categories.toTypedArray()))
props.addAll(relatedTo)
props.addAll(unknownProperties)
// remember used time zones
val usedTimeZones = HashSet<TimeZone>()
due?.let {
props += it
it.timeZone?.let(usedTimeZones::add)
}
duration?.let(props::add)
dtStart?.let {
props += it
it.timeZone?.let(usedTimeZones::add)
}
completedAt?.let {
props += it
it.timeZone?.let(usedTimeZones::add)
}
percentComplete?.let { props += PercentComplete(it) }
if (alarms.isNotEmpty())
vTodo.alarms.addAll(alarms)
// determine earliest referenced date
val earliest = arrayOf(
dtStart?.date,
due?.date,
completedAt?.date
).filterNotNull().minOrNull()
// add VTIMEZONE components
for (tz in usedTimeZones)
ical.components += minifyVTimeZone(tz.vTimeZone, earliest)
softValidate(ical)
CalendarOutputter(false).output(ical, os)
}
fun isAllDay(): Boolean {
return dtStart?.let { DateUtils.isDate(it) } ?:
due?.let { DateUtils.isDate(it) } ?:
true
}
}
|