diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt index fe009751..92586d38 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalAddressBook.kt @@ -199,7 +199,7 @@ class LocalAddressBook( accountManager.removeAccount(account, null, null) } - fun findAll(): List = queryContacts(RawContacts.DELETED + "== 0", null) + override fun findAll(): List = queryContacts(RawContacts.DELETED + "== 0", null) /** * Returns an array of local contacts/groups which have been deleted locally. (DELETED != 0). diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt index b1ffceb2..4218854a 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCalendar.kt @@ -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 net.fortuna.ical4j.model.component.VTimeZone - import org.apache.commons.lang3.StringUtils - import java.io.FileNotFoundException -import java.util.LinkedList +import java.util.* +import java.util.logging.Level -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 private constructor( + account: Account, + provider: ContentProviderClient, + id: Long +): AndroidCalendar(account, provider, LocalEvent.Factory, id), LocalCollection { -class LocalCalendar protected constructor(account: Account, provider: ContentProviderClient, id: Long) : AndroidCalendar(account, provider, LocalEvent.Factory.INSTANCE, id), LocalCollection { + companion object { + val defaultColor = -0x743cb6 // light green 500 - override val deleted: Array - @Throws(CalendarStorageException::class) - get() = queryEvents(Events.DELETED + "!=0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array + val COLUMN_CTAG = Calendars.CAL_SYNC1 - override val withoutFileName: Array - @Throws(CalendarStorageException::class) - get() = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array + 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) - val all: Array - @Throws(CalendarStorageException::class) - get() = queryEvents(null, null) as Array + // 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) - 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 - @Throws(CalendarStorageException::class, FileNotFoundException::class) - get() { - val dirty = LinkedList() - for (event in queryEvents(Events.DIRTY + "!=0 AND " + Events.DELETED + "==0 AND " + Events.ORIGINAL_ID + " IS NULL", null) as Array) { - if (event.event.sequence == null) - event.event.sequence = 0 - else if (event.weAreOrganizer) - event.event.sequence++ - dirty.add(event) - } - - return dirty.toTypedArray() + return AndroidCalendar.create(account, provider, values) } - override fun eventBaseInfoColumns(): Array { - 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() + + 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) - fun update(journalEntity: JournalEntity, updateColor: Boolean) { - update(valuesFromCollectionInfo(journalEntity, updateColor)) + 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 { + val dirty = LinkedList() + + // 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 findByUid(uid: String): LocalEvent? { - val ret = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)) as Array - return if (ret != null && ret.size > 0) { - ret[0] - } else null - } + override fun findWithoutFileName(): List + = queryEvents(Events._SYNC_ID + " IS NULL AND " + Events.ORIGINAL_ID + " IS NULL", null) + + override fun findAll(): List + = queryEvents(null, null) + + override fun findByUid(uid: String): LocalEvent? + = queryEvents(Events._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull() - @Throws(CalendarStorageException::class) 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 { - return arrayOfNulls(size) as Array - } - - 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 { + object Factory: AndroidCalendarFactory { - val defaultColor = -0x743cb6 // light green 500 - - val COLUMN_CTAG = Calendars.CAL_SYNC1 - - 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) } } diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt index 82ec31b3..fc2c77b1 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalCollection.kt @@ -12,6 +12,7 @@ interface LocalCollection> { fun findDeleted(): List fun findDirty(): List fun findWithoutFileName(): List + fun findAll(): List fun findByUid(uid: String): T? diff --git a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt index ff3d05dd..35b1adde 100644 --- a/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt +++ b/app/src/main/java/com/etesync/syncadapter/resource/LocalTaskList.kt @@ -9,82 +9,92 @@ package com.etesync.syncadapter.resource import android.accounts.Account -import android.content.ContentProviderClient -import android.content.ContentResolver import android.content.ContentValues import android.content.Context -import android.database.Cursor import android.net.Uri import android.os.Build import android.os.RemoteException - -import com.etesync.syncadapter.model.CollectionInfo - -import org.dmfs.provider.tasks.TaskContract.TaskLists -import org.dmfs.provider.tasks.TaskContract.Tasks - -import java.io.FileNotFoundException - import at.bitfire.ical4android.AndroidTaskList import at.bitfire.ical4android.AndroidTaskListFactory import at.bitfire.ical4android.CalendarStorageException import at.bitfire.ical4android.TaskProvider +import com.etesync.syncadapter.model.JournalEntity +import org.dmfs.tasks.contract.TaskContract.TaskLists +import org.dmfs.tasks.contract.TaskContract.Tasks -class LocalTaskList protected constructor(account: Account, provider: TaskProvider, id: Long) : AndroidTaskList(account, provider, LocalTask.Factory.INSTANCE, id), LocalCollection { +class LocalTaskList private constructor( + account: Account, + provider: TaskProvider, + id: Long +): AndroidTaskList(account, provider, LocalTask.Factory, id), LocalCollection { + companion object { + val defaultColor = -0x3c1592 // "DAVdroid green" - override val deleted: Array - @Throws(CalendarStorageException::class) - get() = queryTasks(Tasks._DELETED + "!=0", null) as Array - - override val withoutFileName: Array - @Throws(CalendarStorageException::class) - get() = queryTasks(Tasks._SYNC_ID + " IS NULL", null) as Array - - override// sequence has not been assigned yet (i.e. this task was just locally created) - val dirty: Array - @Throws(CalendarStorageException::class, FileNotFoundException::class) - get() { - val tasks = queryTasks(Tasks._DIRTY + "!=0 AND " + Tasks._DELETED + "== 0", null) as Array - for (task in tasks) { - if (task.task.sequence == null) - task.task.sequence = 0 - else - task.task.sequence++ + fun tasksProviderAvailable(context: Context): Boolean { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) + return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null + else { + val provider = TaskProvider.acquire(context, TaskProvider.ProviderName.OpenTasks) + provider?.use { return true } + return false } - return tasks } + fun create(account: Account, provider: TaskProvider, journalEntity: JournalEntity): Uri { + val values = valuesFromCollectionInfo(journalEntity, true) + values.put(TaskLists.OWNER, account.name) + values.put(TaskLists.SYNC_ENABLED, 1) + values.put(TaskLists.VISIBLE, 1) + return create(account, provider, values) + } + + private fun valuesFromCollectionInfo(journalEntity: JournalEntity, withColor: Boolean): ContentValues { + val info = journalEntity.info + val values = ContentValues(3) + values.put(TaskLists._SYNC_ID, info.uid) + values.put(TaskLists.LIST_NAME, if (info.displayName.isNullOrBlank()) info.uid else info.displayName) + + if (withColor) + values.put(TaskLists.LIST_COLOR, info.color ?: defaultColor) + + return values + } - override fun taskBaseInfoColumns(): Array { - return BASE_INFO_COLUMNS } - @Throws(CalendarStorageException::class) - fun update(info: CollectionInfo, updateColor: Boolean) { - update(valuesFromCollectionInfo(info, updateColor)) + fun update(journalEntity: JournalEntity, updateColor: Boolean) = + update(valuesFromCollectionInfo(journalEntity, updateColor)) + + override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null) + + override fun findDirty(): List { + val tasks = queryTasks("${Tasks._DIRTY}!=0", null) + for (localTask in tasks) { + val task = requireNotNull(localTask.task) + val sequence = task.sequence + if (sequence == null) // sequence has not been assigned yet (i.e. this task was just locally created) + task.sequence = 0 + else + task.sequence = sequence + 1 + } + return tasks } - @Throws(CalendarStorageException::class) - override fun findByUid(uid: String): LocalTask? { - val ret = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)) as Array - return if (ret != null && ret.size > 0) { - ret[0] - } else null - } + override fun findWithoutFileName(): List + = queryTasks(Tasks._SYNC_ID + " IS NULL", null) + + override fun findByUid(uid: String): LocalTask? + = queryTasks(Tasks._SYNC_ID + " =? ", arrayOf(uid)).firstOrNull() - @Throws(CalendarStorageException::class) override fun count(): Long { - val where = Tasks.LIST_ID + "=?" - val whereArgs = arrayOf(id.toString()) - try { val cursor = provider.client.query( - syncAdapterURI(provider.tasksUri()), null, - where, whereArgs, null) + TaskProvider.syncAdapterUri(provider.tasksUri()), null, + Tasks.LIST_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) @@ -92,78 +102,10 @@ class LocalTaskList protected constructor(account: Account, provider: TaskProvid } + object Factory: AndroidTaskListFactory { - class Factory : AndroidTaskListFactory { + override fun newInstance(account: Account, provider: TaskProvider, id: Long) = + LocalTaskList(account, provider, id) - override fun newInstance(account: Account, provider: TaskProvider, id: Long): AndroidTaskList { - return LocalTaskList(account, provider, id) - } - - override fun newArray(size: Int): Array { - return arrayOfNulls(size) as Array - } - - companion object { - val INSTANCE = Factory() - } } - - companion object { - - val defaultColor = -0x3c1592 // "DAVdroid green" - - val COLUMN_CTAG = TaskLists.SYNC_VERSION - - internal var BASE_INFO_COLUMNS = arrayOf(Tasks._ID, Tasks._SYNC_ID, LocalTask.COLUMN_ETAG) - - @Throws(CalendarStorageException::class) - fun create(account: Account, provider: TaskProvider, info: CollectionInfo): Uri { - val values = valuesFromCollectionInfo(info, true) - values.put(TaskLists.OWNER, account.name) - values.put(TaskLists.SYNC_ENABLED, 1) - values.put(TaskLists.VISIBLE, 1) - return AndroidTaskList.create(account, provider, values) - } - - private fun valuesFromCollectionInfo(info: CollectionInfo, withColor: Boolean): ContentValues { - val values = ContentValues() - values.put(TaskLists._SYNC_ID, info.uid) - values.put(TaskLists.LIST_NAME, info.displayName) - - if (withColor) - values.put(TaskLists.LIST_COLOR, if (info.color != null) info.color else defaultColor) - - return values - } - - // helpers - - fun tasksProviderAvailable(context: Context): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) - return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null - else { - val provider = TaskProvider.acquire(context.contentResolver, TaskProvider.ProviderName.OpenTasks) - try { - return provider != null - } finally { - provider?.close() - } - } - } - - - // HELPERS - - @Throws(RemoteException::class) - fun onRenameAccount(resolver: ContentResolver, oldName: String, newName: String) { - val client = resolver.acquireContentProviderClient(TaskProvider.ProviderName.OpenTasks.authority) - if (client != null) { - val values = ContentValues(1) - values.put(Tasks.ACCOUNT_NAME, newName) - client.update(Tasks.getContentUri(TaskProvider.ProviderName.OpenTasks.authority), values, Tasks.ACCOUNT_NAME + "=?", arrayOf(oldName)) - client.release() - } - } - } - }