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() 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>() 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>() val organizers = HashMap() 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() 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) } }