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
|
package org.meshbay.client.phonesync
import android.Manifest
import android.content.Context
import android.provider.CalendarContract.Attendees
import android.provider.CalendarContract.Calendars
import android.provider.CalendarContract.Events
import android.provider.CalendarContract.Reminders
import java.io.File
import java.security.MessageDigest
import java.util.TimeZone
/**
* The phone's calendars, as one iCalendar file (Ics.kt) every calendar
* application imports.
*
* Every calendar the person can write to is in it: their own, local ones and
* those of the accounts on the phone. Calendars they only subscribe to (public
* holidays, birthdays from contacts) are left out: they are someone else's and
* come back by subscribing again. Like contacts, every run writes the whole of
* it and sends it only when it differs from the last copy the node took
* (`marker` is its SHA-256, without the DTSTAMP lines that change every run).
*/
class CalendarSource(private val context: Context) : DocSource {
override val permissions = arrayOf(Manifest.permission.READ_CALENDAR)
override val suffix = "calendar"
override fun export(marker: String?, dir: File, now: Long, zone: TimeZone): Export? {
val resolver = context.contentResolver
val calendars = HashMap<Long, String>()
resolver.query(Calendars.CONTENT_URI,
arrayOf(Calendars._ID, Calendars.CALENDAR_DISPLAY_NAME, Calendars.CALENDAR_ACCESS_LEVEL),
"${Calendars.CALENDAR_ACCESS_LEVEL} >= ?", arrayOf(Calendars.CAL_ACCESS_CONTRIBUTOR.toString()),
null)?.use { c -> while (c.moveToNext()) calendars[c.getLong(0)] = c.getString(1) ?: "" }
if (calendars.isEmpty()) return null
val reminders = HashMap<Long, MutableList<Int>>()
resolver.query(Reminders.CONTENT_URI, arrayOf(Reminders.EVENT_ID, Reminders.MINUTES), null, null, null)
?.use { c -> while (c.moveToNext()) reminders.getOrPut(c.getLong(0)) { ArrayList() } += c.getInt(1) }
val attendees = HashMap<Long, MutableList<Attendee>>()
val organizers = HashMap<Long, String>()
resolver.query(Attendees.CONTENT_URI,
arrayOf(Attendees.EVENT_ID, Attendees.ATTENDEE_EMAIL, Attendees.ATTENDEE_NAME,
Attendees.ATTENDEE_RELATIONSHIP), null, null, null)?.use { c ->
while (c.moveToNext()) {
val email = c.getString(1)?.takeIf { it.contains('@') } ?: continue
if (c.getInt(3) == Attendees.RELATIONSHIP_ORGANIZER) organizers[c.getLong(0)] = email
else attendees.getOrPut(c.getLong(0)) { ArrayList() } += Attendee(email, c.getString(2))
}
}
val cols = arrayOf(
Events._ID, Events.CALENDAR_ID, Events.UID_2445, Events.TITLE, Events.DESCRIPTION,
Events.EVENT_LOCATION, Events.DTSTART, Events.DTEND, Events.DURATION, Events.ALL_DAY,
Events.EVENT_TIMEZONE, Events.RRULE, Events.RDATE, Events.EXDATE, Events.ORIGINAL_ID,
Events.ORIGINAL_INSTANCE_TIME, Events.ORIGINAL_ALL_DAY, Events.STATUS,
)
val ids = calendars.keys.joinToString(",")
val events = ArrayList<CalEvent>()
resolver.query(Events.CONTENT_URI, cols, "${Events.DELETED} = 0 AND ${Events.CALENDAR_ID} IN ($ids)",
null, "${Events._ID} ASC")?.use { c ->
fun str(i: Int) = if (c.isNull(i)) null else c.getString(i)
fun long(i: Int) = if (c.isNull(i)) null else c.getLong(i)
while (c.moveToNext()) {
val id = c.getLong(0)
events += CalEvent(
id, calendars[c.getLong(1)] ?: "", str(2), str(3), str(4), str(5),
c.getLong(6), long(7), str(8), c.getInt(9) == 1, str(10),
str(11), str(12), str(13), long(14), long(15), c.getInt(16) == 1,
if (c.isNull(17)) null else c.getInt(17),
reminders[id].orEmpty(), organizers[id], attendees[id].orEmpty(),
)
}
}
if (events.isEmpty()) return null
val file = File(dir, "calendar.ics")
val digest = MessageDigest.getInstance("SHA-256")
file.bufferedWriter(Charsets.UTF_8).use { out ->
Ics.write(events, now) { l ->
out.write(l); out.write("\r\n")
if (!l.startsWith("DTSTAMP:")) digest.update((l + "\n").toByteArray(Charsets.UTF_8))
}
}
val sha = digest.digest().joinToString("") { "%02x".format(it) }
if (sha == marker) { file.delete(); return null }
return Export(file, DocPlan.nameFor("calendar", "ics", now, zone), "text/calendar", sha, events.size)
}
}
|