aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-android/app/src/main/kotlin/org/meshbay/client/phonesync/CalendarSource.kt
blob: bcef08c384e2525c003f072e6014b911003e335d (plain) (blame)
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)
    }
}