|
|
|
@ -13,82 +13,114 @@ import android.content.ContentProviderClient
|
|
|
|
|
import android.content.ContentProviderOperation
|
|
|
|
|
import android.content.ContentUris
|
|
|
|
|
import android.content.ContentValues
|
|
|
|
|
import android.database.Cursor
|
|
|
|
|
import android.net.Uri
|
|
|
|
|
import android.os.RemoteException
|
|
|
|
|
import android.provider.CalendarContract
|
|
|
|
|
import android.provider.CalendarContract.Calendars
|
|
|
|
|
import android.provider.CalendarContract.Events
|
|
|
|
|
import android.provider.CalendarContract.Reminders
|
|
|
|
|
import android.text.TextUtils
|
|
|
|
|
|
|
|
|
|
import android.provider.CalendarContract.*
|
|
|
|
|
import at.bitfire.ical4android.*
|
|
|
|
|
import com.etesync.syncadapter.App
|
|
|
|
|
import com.etesync.syncadapter.model.CollectionInfo
|
|
|
|
|
import com.etesync.syncadapter.model.JournalEntity
|
|
|
|
|
import org.apache.commons.lang3.StringUtils
|
|
|
|
|
import java.io.FileNotFoundException
|
|
|
|
|
import java.util.*
|
|
|
|
|
import java.util.logging.Level
|
|
|
|
|
|
|
|
|
|
import net.fortuna.ical4j.model.component.VTimeZone
|
|
|
|
|
class LocalCalendar private constructor(
|
|
|
|
|
account: Account,
|
|
|
|
|
provider: ContentProviderClient,
|
|
|
|
|
id: Long
|
|
|
|
|
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
|
|
|
|
|
|
|
|
|
|
import org.apache.commons.lang3.StringUtils
|
|
|
|
|
companion object {
|
|
|
|
|
val defaultColor = -0x743cb6 // light green 500
|
|
|
|
|
|
|
|
|
|
import java.io.FileNotFoundException
|
|
|
|
|
import java.util.LinkedList
|
|
|
|
|
|
|
|
|
|
import at.bitfire.ical4android.AndroidCalendar
|
|
|
|
|
import at.bitfire.ical4android.AndroidCalendarFactory
|
|
|
|
|
import at.bitfire.ical4android.BatchOperation
|
|
|
|
|
import at.bitfire.ical4android.CalendarStorageException
|
|
|
|
|
import at.bitfire.ical4android.DateUtils
|
|
|
|
|
|
|
|
|
|
class LocalCalendar protected constructor(account: Account, provider: ContentProviderClient, id: Long) : AndroidCalendar(account, provider, LocalEvent.Factory.INSTANCE, id), LocalCollection<LocalEvent> {
|
|
|
|
|
|
|
|
|
|
override val deleted: Array<LocalEvent>
|
|
|
|
|
@Throws(CalendarStorageException::class)
|
|
|
|
|
get() = queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>
|
|
|
|
|
|
|
|
|
|
override val withoutFileName: Array<LocalEvent>
|
|
|
|
|
@Throws(CalendarStorageException::class)
|
|
|
|
|
get() = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
val all: Array<LocalEvent>
|
|
|
|
|
@Throws(CalendarStorageException::class)
|
|
|
|
|
get() = queryEvents(null, null) as Array<LocalEvent>
|
|
|
|
|
|
|
|
|
|
override// get dirty events which are required to have an increased SEQUENCE value
|
|
|
|
|
// sequence has not been assigned yet (i.e. this event was just locally created)
|
|
|
|
|
val dirty: Array<LocalEvent>
|
|
|
|
|
@Throws(CalendarStorageException::class, FileNotFoundException::class)
|
|
|
|
|
get() {
|
|
|
|
|
val dirty = LinkedList<LocalEvent>()
|
|
|
|
|
for (event in queryEvents(Events.DIRTY + "!=0 AND " + Events.DELETED + "==0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array<LocalEvent>) {
|
|
|
|
|
if (event.event.sequence == null)
|
|
|
|
|
event.event.sequence = 0
|
|
|
|
|
else if (event.weAreOrganizer)
|
|
|
|
|
event.event.sequence++
|
|
|
|
|
dirty.add(event)
|
|
|
|
|
}
|
|
|
|
|
val COLUMN_CTAG = Calendars.CAL_SYNC1
|
|
|
|
|
|
|
|
|
|
fun create(account: Account, provider: ContentProviderClient, journalEntity: JournalEntity): Uri {
|
|
|
|
|
val values = valuesFromCollectionInfo(journalEntity, true)
|
|
|
|
|
|
|
|
|
|
return dirty.toTypedArray()
|
|
|
|
|
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
|
|
|
|
|
values.put(Calendars.ACCOUNT_NAME, account.name)
|
|
|
|
|
values.put(Calendars.ACCOUNT_TYPE, account.type)
|
|
|
|
|
values.put(Calendars.OWNER_ACCOUNT, account.name)
|
|
|
|
|
|
|
|
|
|
// flag as visible & synchronizable at creation, might be changed by user at any time
|
|
|
|
|
values.put(Calendars.VISIBLE, 1)
|
|
|
|
|
values.put(Calendars.SYNC_EVENTS, 1)
|
|
|
|
|
|
|
|
|
|
return AndroidCalendar.create(account, provider, values)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun eventBaseInfoColumns(): Array<String> {
|
|
|
|
|
return BASE_INFO_COLUMNS
|
|
|
|
|
}
|
|
|
|
|
fun findByName(account: Account, provider: ContentProviderClient, factory: Factory, name: String): LocalCalendar?
|
|
|
|
|
= AndroidCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name)).firstOrNull()
|
|
|
|
|
|
|
|
|
|
@Throws(CalendarStorageException::class)
|
|
|
|
|
fun update(journalEntity: JournalEntity, updateColor: Boolean) {
|
|
|
|
|
update(valuesFromCollectionInfo(journalEntity, updateColor))
|
|
|
|
|
private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues {
|
|
|
|
|
val info = journalEntity.info
|
|
|
|
|
val values = ContentValues()
|
|
|
|
|
values.put(Calendars.NAME, info.uid)
|
|
|
|
|
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.displayName)
|
|
|
|
|
|
|
|
|
|
if (withColor)
|
|
|
|
|
values.put(Calendars.CALENDAR_COLOR, if (info.color != null) info.color else defaultColor)
|
|
|
|
|
|
|
|
|
|
if (journalEntity.isReadOnly)
|
|
|
|
|
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
|
|
|
|
|
else {
|
|
|
|
|
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
|
|
|
|
|
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
|
|
|
|
|
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
info.timeZone?.let { tzData ->
|
|
|
|
|
try {
|
|
|
|
|
val timeZone = DateUtils.parseVTimeZone(tzData)
|
|
|
|
|
timeZone.timeZoneId?.let { tzId ->
|
|
|
|
|
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(tzId.value))
|
|
|
|
|
}
|
|
|
|
|
} catch(e: IllegalArgumentException) {
|
|
|
|
|
App.log.log(Level.WARNING, "Couldn't parse calendar default time zone", e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
|
|
|
|
|
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ","))
|
|
|
|
|
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Throws(CalendarStorageException::class)
|
|
|
|
|
override fun findByUid(uid: String): LocalEvent? {
|
|
|
|
|
val ret = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)) as Array<LocalEvent>
|
|
|
|
|
return if (ret != null && ret.size > 0) {
|
|
|
|
|
ret[0]
|
|
|
|
|
} else null
|
|
|
|
|
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
|
|
|
|
|
update(valuesFromCollectionInfo(journalEntity, updateColor))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
override fun findDeleted() =
|
|
|
|
|
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
|
|
|
|
|
|
|
|
|
|
override fun findDirty(): List<LocalEvent> {
|
|
|
|
|
val dirty = LinkedList<LocalEvent>()
|
|
|
|
|
|
|
|
|
|
// get dirty events which are required to have an increased SEQUENCE value
|
|
|
|
|
for (localEvent in queryEvents("${Events.DIRTY}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)) {
|
|
|
|
|
val event = localEvent.event!!
|
|
|
|
|
val sequence = event.sequence
|
|
|
|
|
if (event.sequence == null) // sequence has not been assigned yet (i.e. this event was just locally created)
|
|
|
|
|
event.sequence = 0
|
|
|
|
|
else if (localEvent.weAreOrganizer)
|
|
|
|
|
event.sequence = sequence!! + 1
|
|
|
|
|
dirty += localEvent
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return dirty
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Throws(CalendarStorageException::class)
|
|
|
|
|
override fun findWithoutFileName(): List<LocalEvent>
|
|
|
|
|
= queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null)
|
|
|
|
|
|
|
|
|
|
override fun findAll(): List<LocalEvent>
|
|
|
|
|
= queryEvents(null, null)
|
|
|
|
|
|
|
|
|
|
override fun findByUid(uid: String): LocalEvent?
|
|
|
|
|
= queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull()
|
|
|
|
|
|
|
|
|
|
fun processDirtyExceptions() {
|
|
|
|
|
// process deleted exceptions
|
|
|
|
|
App.log.info("Processing deleted exceptions")
|
|
|
|
@ -163,19 +195,15 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Throws(CalendarStorageException::class)
|
|
|
|
|
override fun count(): Long {
|
|
|
|
|
val where = Events.CALENDAR_ID + "=?"
|
|
|
|
|
val whereArgs = arrayOf(id.toString())
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
val cursor = provider.query(
|
|
|
|
|
syncAdapterURI(Events.CONTENT_URI), null,
|
|
|
|
|
where, whereArgs, null)
|
|
|
|
|
Events.CALENDAR_ID + "=?", arrayOf(id.toString()), null)
|
|
|
|
|
try {
|
|
|
|
|
return cursor.count.toLong()
|
|
|
|
|
return cursor?.count.toLong()
|
|
|
|
|
} finally {
|
|
|
|
|
cursor.close()
|
|
|
|
|
cursor?.close()
|
|
|
|
|
}
|
|
|
|
|
} catch (e: RemoteException) {
|
|
|
|
|
throw CalendarStorageException("Couldn't query calendar events", e)
|
|
|
|
@ -183,20 +211,6 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class Factory : AndroidCalendarFactory {
|
|
|
|
|
|
|
|
|
|
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long): AndroidCalendar {
|
|
|
|
|
return LocalCalendar(account, provider, id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
override fun newArray(size: Int): Array<AndroidCalendar?> {
|
|
|
|
|
return arrayOfNulls<LocalCalendar>(size) as Array<AndroidCalendar?>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
val INSTANCE = Factory()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Fix all of the etags of all of the non-dirty events to be non-null.
|
|
|
|
|
* Currently set to all ones.. */
|
|
|
|
@ -218,67 +232,9 @@ class LocalCalendar protected constructor(account: Account, provider: ContentPro
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
companion object {
|
|
|
|
|
|
|
|
|
|
val defaultColor = -0x743cb6 // light green 500
|
|
|
|
|
|
|
|
|
|
val COLUMN_CTAG = Calendars.CAL_SYNC1
|
|
|
|
|
object Factory: AndroidCalendarFactory<LocalCalendar> {
|
|
|
|
|
|
|
|
|
|
internal var BASE_INFO_COLUMNS = arrayOf(Events._ID, Events._SYNC_ID, LocalEvent.COLUMN_ETAG)
|
|
|
|
|
|
|
|
|
|
@Throws(CalendarStorageException::class)
|
|
|
|
|
fun create(account: Account, provider: ContentProviderClient, journalEntity: JournalEntity): Uri {
|
|
|
|
|
val values = valuesFromCollectionInfo(journalEntity, true)
|
|
|
|
|
|
|
|
|
|
// ACCOUNT_NAME and ACCOUNT_TYPE are required (see docs)! If it's missing, other apps will crash.
|
|
|
|
|
values.put(Calendars.ACCOUNT_NAME, account.name)
|
|
|
|
|
values.put(Calendars.ACCOUNT_TYPE, account.type)
|
|
|
|
|
values.put(Calendars.OWNER_ACCOUNT, account.name)
|
|
|
|
|
|
|
|
|
|
// flag as visible & synchronizable at creation, might be changed by user at any time
|
|
|
|
|
values.put(Calendars.VISIBLE, 1)
|
|
|
|
|
values.put(Calendars.SYNC_EVENTS, 1)
|
|
|
|
|
|
|
|
|
|
return AndroidCalendar.create(account, provider, values)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Throws(FileNotFoundException::class, CalendarStorageException::class)
|
|
|
|
|
fun findByName(account: Account, provider: ContentProviderClient, factory: AndroidCalendarFactory, name: String): LocalCalendar? {
|
|
|
|
|
val ret = LocalCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name))
|
|
|
|
|
if (ret.size == 1) {
|
|
|
|
|
return ret[0]
|
|
|
|
|
} else {
|
|
|
|
|
App.log.severe("No calendar found for name $name")
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues {
|
|
|
|
|
val info = journalEntity.info
|
|
|
|
|
val values = ContentValues()
|
|
|
|
|
values.put(Calendars.NAME, info.uid)
|
|
|
|
|
values.put(Calendars.CALENDAR_DISPLAY_NAME, info.displayName)
|
|
|
|
|
|
|
|
|
|
if (withColor)
|
|
|
|
|
values.put(Calendars.CALENDAR_COLOR, if (info.color != null) info.color else defaultColor)
|
|
|
|
|
|
|
|
|
|
if (journalEntity.isReadOnly)
|
|
|
|
|
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_READ)
|
|
|
|
|
else {
|
|
|
|
|
values.put(Calendars.CALENDAR_ACCESS_LEVEL, Calendars.CAL_ACCESS_OWNER)
|
|
|
|
|
values.put(Calendars.CAN_MODIFY_TIME_ZONE, 1)
|
|
|
|
|
values.put(Calendars.CAN_ORGANIZER_RESPOND, 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!TextUtils.isEmpty(info.timeZone)) {
|
|
|
|
|
val timeZone = DateUtils.parseVTimeZone(info.timeZone)
|
|
|
|
|
if (timeZone != null && timeZone.timeZoneId != null)
|
|
|
|
|
values.put(Calendars.CALENDAR_TIME_ZONE, DateUtils.findAndroidTimezoneID(timeZone.timeZoneId.value))
|
|
|
|
|
}
|
|
|
|
|
values.put(Calendars.ALLOWED_REMINDERS, Reminders.METHOD_ALERT)
|
|
|
|
|
values.put(Calendars.ALLOWED_AVAILABILITY, StringUtils.join(intArrayOf(Reminders.AVAILABILITY_TENTATIVE, Reminders.AVAILABILITY_FREE, Reminders.AVAILABILITY_BUSY), ","))
|
|
|
|
|
values.put(Calendars.ALLOWED_ATTENDEE_TYPES, StringUtils.join(intArrayOf(CalendarContract.Attendees.TYPE_OPTIONAL, CalendarContract.Attendees.TYPE_REQUIRED, CalendarContract.Attendees.TYPE_RESOURCE), ", "))
|
|
|
|
|
return values
|
|
|
|
|
}
|
|
|
|
|
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) =
|
|
|
|
|
LocalCalendar(account, provider, id)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|