You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
etesync-android/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt

303 lines
14 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/*
* Copyright © 2013 2015 Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package com.etesync.syncadapter.resource
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.ContentUris
import android.content.ContentValues
import android.net.Uri
import android.os.RemoteException
import android.provider.CalendarContract
import android.provider.CalendarContract.*
import at.bitfire.ical4android.*
import com.etebase.client.CollectionAccessLevel
import com.etesync.syncadapter.CachedCollection
import com.etesync.syncadapter.log.Logger
import com.etesync.syncadapter.model.JournalEntity
import com.etesync.syncadapter.resource.LocalEvent.Companion.COLUMN_UID
import org.apache.commons.lang3.StringUtils
import java.util.*
import java.util.logging.Level
class LocalCalendar private constructor(
account: Account,
provider: ContentProviderClient,
id: Long
): AndroidCalendar<LocalEvent>(account, provider, LocalEvent.Factory, id), LocalCollection<LocalEvent> {
companion object {
val defaultColor = -0x743cb6 // light green 500 - should be "8BC349"?
fun parseColor(color_: String?): Int {
if (color_.isNullOrBlank()) {
return defaultColor
}
val color = color_.replaceFirst("^#".toRegex(), "")
if (color.length == 8) {
return (color.substring(0, 6).toLong(16) or (color.substring(6, 8).toLong(16) shl 24)).toInt()
} else if (color.length == 6) {
return (color.toLong(16) or (0xFF000000)).toInt()
} else {
return defaultColor
}
}
val COLUMN_CTAG = Calendars.CAL_SYNC1
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)
}
fun create(account: Account, provider: ContentProviderClient, cachedCollection: CachedCollection): Uri {
val values = valuesFromCachedCollection(cachedCollection, 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)
}
fun findByName(account: Account, provider: ContentProviderClient, factory: Factory, name: String): LocalCalendar?
= AndroidCalendar.find(account, provider, factory, Calendars.NAME + "==?", arrayOf(name)).firstOrNull()
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) {
Logger.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
}
private fun valuesFromCachedCollection(cachedCollection: CachedCollection, withColor: Boolean): ContentValues {
val values = ContentValues()
val col = cachedCollection.col
val meta = cachedCollection.meta
values.put(Calendars.NAME, col.uid)
values.put(Calendars.CALENDAR_DISPLAY_NAME, meta.name)
if (withColor)
values.put(Calendars.CALENDAR_COLOR, parseColor(meta.color))
if (col.accessLevel == CollectionAccessLevel.ReadOnly)
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)
}
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 val url: String?
get() = name
fun update(journalEntity: JournalEntity, updateColor: Boolean) =
update(valuesFromCollectionInfo(journalEntity, updateColor))
fun update(cachedCollection: CachedCollection, updateColor: Boolean) =
update(valuesFromCachedCollection(cachedCollection, updateColor))
override fun findDeleted() =
queryEvents("${Events.DELETED}!=0 AND ${Events.ORIGINAL_ID} IS NULL", null)
override fun findDirty(limit: Int?): List<LocalEvent> {
val dirty = LinkedList<LocalEvent>()
val sortOrder = if (limit != null) "${Events._ID} ASC LIMIT $limit" else null
// get dirty events which are required to have an increased SEQUENCE value
for (localEvent in queryEvents("${Events.DIRTY}!=0 AND ${Events.DELETED}==0 AND ${Events.ORIGINAL_ID} IS NULL", null, sortOrder)) {
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
}
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(COLUMN_UID + " =? ", arrayOf(uid)).firstOrNull()
override fun findByFilename(filename: String): LocalEvent?
= queryEvents(Events._SYNC_ID + " =? ", arrayOf(filename)).firstOrNull()
fun processDirtyExceptions() {
// process deleted exceptions
Logger.log.info("Processing deleted exceptions")
try {
val cursor = provider.query(
syncAdapterURI(Events.CONTENT_URI),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null)
while (cursor != null && cursor.moveToNext()) {
Logger.log.fine("Found deleted exception, removing; then re-schuling original event")
val id = cursor.getLong(0)
// can't be null (by definition)
val originalID = cursor.getLong(1) // can't be null (by query)
// get original event's SEQUENCE
val cursor2 = provider.query(
syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)),
arrayOf(LocalEvent.COLUMN_SEQUENCE), null, null, null)
val originalSequence = if (cursor2 == null || cursor2.isNull(0)) 0 else cursor2.getInt(0)
cursor2!!.close()
val batch = BatchOperation(provider)
// re-schedule original event and set it to DIRTY
batch.enqueue(
BatchOperation.CpoBuilder.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(LocalEvent.COLUMN_SEQUENCE, originalSequence + 1)
.withValue(Events.DIRTY, 1)
)
// remove exception
batch.enqueue(
BatchOperation.CpoBuilder.newDelete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
)
batch.commit()
}
cursor!!.close()
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't process locally modified exception", e)
}
// process dirty exceptions
Logger.log.info("Processing dirty exceptions")
try {
val cursor = provider.query(
syncAdapterURI(Events.CONTENT_URI),
arrayOf(Events._ID, Events.ORIGINAL_ID, LocalEvent.COLUMN_SEQUENCE),
Events.DIRTY + "!=0 AND " + Events.ORIGINAL_ID + " IS NOT NULL", null, null)
while (cursor != null && cursor.moveToNext()) {
Logger.log.fine("Found dirty exception, increasing SEQUENCE to re-schedule")
val id = cursor.getLong(0)
// can't be null (by definition)
val originalID = cursor.getLong(1) // can't be null (by query)
val sequence = if (cursor.isNull(2)) 0 else cursor.getInt(2)
val batch = BatchOperation(provider)
// original event to DIRTY
batch.enqueue(BatchOperation.CpoBuilder.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, originalID)))
.withValue(Events.DIRTY, 1)
)
// increase SEQUENCE and set DIRTY to 0
batch.enqueue(BatchOperation.CpoBuilder.newUpdate(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, id)))
.withValue(LocalEvent.COLUMN_SEQUENCE, sequence + 1)
.withValue(Events.DIRTY, 0)
)
batch.commit()
}
cursor!!.close()
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't process locally modified exception", e)
}
}
override fun count(): Long {
try {
val cursor = provider.query(
syncAdapterURI(Events.CONTENT_URI), null,
Events.CALENDAR_ID + "=?", arrayOf(id.toString()), null)
try {
return cursor?.count?.toLong()!!
} finally {
cursor?.close()
}
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't query calendar events", e)
}
}
/** Fix all of the etags of all of the non-dirty events to be non-null.
* Currently set to all ones.. */
@Throws(CalendarStorageException::class)
fun fixEtags() {
val newEtag = "1111111111111111111111111111111111111111111111111111111111111111"
val where = Events.CALENDAR_ID + "=? AND " + Events.DIRTY + "=0 AND " + LocalEvent.COLUMN_ETAG + " IS NULL"
val whereArgs = arrayOf(id.toString())
val values = ContentValues(1)
values.put(LocalEvent.COLUMN_ETAG, newEtag)
try {
val fixed = provider.update(syncAdapterURI(Events.CONTENT_URI),
values, where, whereArgs)
Logger.log.info("Fixed entries: " + fixed.toString())
} catch (e: RemoteException) {
throw CalendarStorageException("Couldn't fix etags", e)
}
}
object Factory: AndroidCalendarFactory<LocalCalendar> {
override fun newInstance(account: Account, provider: ContentProviderClient, id: Long) =
LocalCalendar(account, provider, id)
}
}