Commit 7fdcb371 authored by Ricki Hirner's avatar Ricki Hirner

Rewrite sync algorithm, prepare for WebDAV collection sync

parent 2f9f4f1d
......@@ -22,6 +22,7 @@ import at.bitfire.davdroid.model.Credentials
import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.*
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.model.SyncState
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalCalendar
import at.bitfire.davdroid.resource.LocalTaskList
......@@ -374,7 +375,7 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.let { client ->
try {
val addrBook = LocalAddressBook(context, account, client)
val url = addrBook.getURL()
val url = addrBook.url
Logger.log.fine("Migrating address book $url")
// insert CardDAV service
......@@ -504,16 +505,16 @@ class AccountSettings @Throws(InvalidAccountException::class) constructor(
// until now, ContactsContract.Settings.UNGROUPED_VISIBLE was not set explicitly
val values = ContentValues()
values.put(ContactsContract.Settings.UNGROUPED_VISIBLE, 1)
addr.updateSettings(values)
addr.settings = values
val url = accountManager.getUserData(account, "addressbook_url")
if (!url.isNullOrEmpty())
addr.setURL(url)
addr.url = url
accountManager.setUserData(account, "addressbook_url", null)
val cTag = accountManager.getUserData (account, "addressbook_ctag")
if (!cTag.isNullOrEmpty())
addr.setCTag(cTag)
addr.lastSyncState = SyncState(SyncState.Type.CTAG, cTag)
accountManager.setUserData(account, "addressbook_ctag", null)
} finally {
if (Build.VERSION.SDK_INT >= 24)
......
/*
* Copyright © 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 at.bitfire.davdroid.model
import at.bitfire.davdroid.log.Logger
import java.util.logging.Level
data class SyncState(
val type: Type,
val value: String
) {
companion object {
fun fromString(s: String): SyncState? {
val pos = s.indexOf(':')
if (pos == -1)
return null
return try {
SyncState(
Type.valueOf(s.substring(0, pos)),
s.substring(pos + 1)
)
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't restore SyncState", e)
null
}
}
}
enum class Type { CTAG, SYNC_TOKEN }
override fun toString() =
"${type.name}:${value}"
}
\ No newline at end of file
/*
* Copyright © 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 at.bitfire.davdroid.resource
interface LocalAddress: LocalResource {
fun resetDeleted()
}
\ No newline at end of file
......@@ -8,28 +8,33 @@
package at.bitfire.davdroid.resource
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
import java.io.FileNotFoundException
import at.bitfire.davdroid.model.SyncState
interface LocalCollection<out T: LocalResource> {
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getDeleted(): List<T>
var lastSyncState: SyncState?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getWithoutFileName(): List<T>
/**
* Unique collection ID. Used to distinguish collections in Android notifications.
*/
val uid: String
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
fun getDirty(): List<T>
fun findDeleted(): List<T>
fun findDirty(): List<T>
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getAll(): List<T>
fun findByName(name: String): T?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun getCTag(): String?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun setCTag(cTag: String?)
/**
* Marks all entries which are not dirty with the given flags only.
* @return number of marked entries
**/
fun markNotDirty(flags: Int): Int
/**
* Removes all entries with are not dirty and are marked with exactly the given flags.
* @return number of removed entries
*/
fun removeNotDirtyMarked(flags: Int): Int
}
......@@ -15,56 +15,58 @@ import android.provider.CalendarContract.Events
import at.bitfire.davdroid.BuildConfig
import at.bitfire.ical4android.*
import net.fortuna.ical4j.model.property.ProdId
import java.io.FileNotFoundException
import java.util.*
class LocalEvent: AndroidEvent, LocalResource {
companion object {
init {
iCalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x")
ICalendar.prodId = ProdId("+//IDN bitfire.at//DAVdroid/" + BuildConfig.VERSION_NAME + " ical4j/2.x")
}
val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
const val COLUMN_ETAG = CalendarContract.Events.SYNC_DATA1
const val COLUMN_FLAGS = CalendarContract.Events.SYNC_DATA2
const val COLUMN_SEQUENCE = CalendarContract.Events.SYNC_DATA3
}
override var fileName: String? = null
private set
override var eTag: String? = null
override var flags: Int = 0
private set
var weAreOrganizer = true
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?): super(calendar, event) {
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?, flags: Int): super(calendar, event) {
this.fileName = fileName
this.eTag = eTag
this.flags = flags
}
private constructor(calendar: AndroidCalendar<*>, id: Long, baseInfo: ContentValues?): super(calendar, id, baseInfo) {
baseInfo?.let {
fileName = it.getAsString(Events._SYNC_ID)
eTag = it.getAsString(COLUMN_ETAG)
}
private constructor(calendar: AndroidCalendar<*>, values: ContentValues): super(calendar, values) {
fileName = values.getAsString(Events._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG)
flags = values.getAsInteger(COLUMN_FLAGS)
}
/* process LocalEvent-specific fields */
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun populateEvent(row: ContentValues) {
super.populateEvent(row)
val event = requireNotNull(event)
fileName = row.getAsString(Events._SYNC_ID)
eTag = row.getAsString(COLUMN_ETAG)
event.uid = row.getAsString(Events.UID_2445)
flags = row.getAsInteger(COLUMN_FLAGS)
event.uid = row.getAsString(Events.UID_2445)
event.sequence = row.getAsInteger(COLUMN_SEQUENCE)
val isOrganizer = row.getAsInteger(Events.IS_ORGANIZER)
weAreOrganizer = isOrganizer != null && isOrganizer != 0
}
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun buildEvent(recurrence: Event?, builder: ContentProviderOperation.Builder) {
super.buildEvent(recurrence, builder)
val event = requireNotNull(event)
......@@ -76,6 +78,7 @@ class LocalEvent: AndroidEvent, LocalResource {
.withValue(COLUMN_SEQUENCE, eventToBuild.sequence)
.withValue(CalendarContract.Events.DIRTY, 0)
.withValue(CalendarContract.Events.DELETED, 0)
.withValue(LocalEvent.COLUMN_FLAGS, flags)
if (buildException)
builder .withValue(Events.ORIGINAL_SYNC_ID, fileName)
......@@ -85,57 +88,48 @@ class LocalEvent: AndroidEvent, LocalResource {
}
/* custom queries */
@Throws(CalendarStorageException::class)
override fun prepareForUpload() {
try {
var uid: String? = null
calendar.provider.query(eventSyncURI(), arrayOf(Events.UID_2445), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
if (uid == null)
uid = UUID.randomUUID().toString()
val newFileName = "$uid.ics"
override fun assignNameAndUID() {
var uid: String? = null
calendar.provider.query(eventSyncURI(), arrayOf(Events.UID_2445), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
uid = cursor.getString(0)
}
if (uid == null)
uid = UUID.randomUUID().toString()
val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName)
values.put(Events.UID_2445, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
val newFileName = "$uid.ics"
fileName = newFileName
event!!.uid = uid
val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName)
values.put(Events.UID_2445, uid)
calendar.provider.update(eventSyncURI(), values, null, null)
} catch(e: Exception) {
throw CalendarStorageException("Couldn't update UID", e)
}
fileName = newFileName
event!!.uid = uid
}
@Throws(CalendarStorageException::class)
override fun clearDirty(eTag: String?) {
try {
val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
values.put(COLUMN_SEQUENCE, event!!.sequence)
calendar.provider.update(eventSyncURI(), values, null, null)
this.eTag = eTag
} catch (e: Exception) {
throw CalendarStorageException("Couldn't update UID", e)
}
}
val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0)
values.put(COLUMN_ETAG, eTag)
values.put(COLUMN_SEQUENCE, event!!.sequence)
calendar.provider.update(eventSyncURI(), values, null, null)
this.eTag = eTag
}
object Factory: AndroidEventFactory<LocalEvent> {
override fun updateFlags(flags: Int) {
val values = ContentValues(1)
values.put(COLUMN_FLAGS, flags)
calendar.provider.update(eventSyncURI(), values, null, null)
override fun newInstance(calendar: AndroidCalendar<*>, id: Long, baseInfo: ContentValues?) =
LocalEvent(calendar, id, baseInfo)
this.flags = flags
}
override fun newInstance(calendar: AndroidCalendar<*>, event: Event) =
LocalEvent(calendar, event, null, null)
object Factory: AndroidEventFactory<LocalEvent> {
override fun fromProvider(calendar: AndroidCalendar<*>, values: ContentValues) =
LocalEvent(calendar, values)
}
}
......@@ -8,23 +8,32 @@
package at.bitfire.davdroid.resource
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.vcard4android.ContactsStorageException
interface LocalResource {
val id: Long?
companion object {
/**
* Resource is present on remote server. This flag is used to identify resources
* which are not present on the remote server anymore and can be deleted at the end
* of the synchronization.
*/
const val FLAG_REMOTELY_PRESENT = 1
}
var fileName: String?
var eTag: String?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun delete(): Int
/**
* Unique ID which identifies the resource in the local storage. May be null if the
* resource has not been saved yet.
*/
val id: Long?
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun prepareForUpload()
val fileName: String?
var eTag: String?
val flags: Int
@Throws(CalendarStorageException::class, ContactsStorageException::class)
fun assignNameAndUID()
fun clearDirty(eTag: String?)
fun updateFlags(flags: Int)
fun delete(): Int
}
\ No newline at end of file
......@@ -13,36 +13,39 @@ import android.content.ContentValues
import android.provider.CalendarContract.Events
import at.bitfire.ical4android.*
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.text.ParseException
import java.util.*
class LocalTask: AndroidTask, LocalResource {
companion object {
val COLUMN_ETAG = Tasks.SYNC_VERSION
val COLUMN_SEQUENCE = Tasks.SYNC3
const val COLUMN_ETAG = Tasks.SYNC_VERSION
const val COLUMN_FLAGS = Tasks.SYNC1
const val COLUMN_SEQUENCE = Tasks.SYNC3
}
override var fileName: String? = null
override var eTag: String? = null
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?): super(taskList, task) {
override var flags = 0
private set
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?, flags: Int)
: super(taskList, task) {
this.fileName = fileName
this.eTag = eTag
this.flags = flags
}
private constructor(taskList: AndroidTaskList<*>, id: Long, baseInfo: ContentValues?): super(taskList, id) {
baseInfo?.let {
fileName = it.getAsString(Events._SYNC_ID)
eTag = it.getAsString(COLUMN_ETAG)
}
private constructor(taskList: AndroidTaskList<*>, values: ContentValues): super(taskList) {
id = values.getAsLong(Tasks._ID)
fileName = values.getAsString(Tasks._SYNC_ID)
eTag = values.getAsString(COLUMN_ETAG)
}
/* process LocalTask-specific fields */
@Throws(ParseException::class)
override fun populateTask(values: ContentValues) {
super.populateTask(values)
......@@ -53,7 +56,6 @@ class LocalTask: AndroidTask, LocalResource {
task.sequence = values.getAsInteger(COLUMN_SEQUENCE)
}
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun buildTask(builder: ContentProviderOperation.Builder, update: Boolean) {
super.buildTask(builder, update)
val task = requireNotNull(task)
......@@ -66,8 +68,7 @@ class LocalTask: AndroidTask, LocalResource {
/* custom queries */
@Throws(CalendarStorageException::class)
override fun prepareForUpload() {
override fun assignNameAndUID() {
try {
val uid = UUID.randomUUID().toString()
val newFileName = uid + ".ics"
......@@ -85,7 +86,6 @@ class LocalTask: AndroidTask, LocalResource {
}
}
@Throws(CalendarStorageException::class)
override fun clearDirty(eTag: String?) {
try {
val values = ContentValues(2)
......@@ -101,14 +101,19 @@ class LocalTask: AndroidTask, LocalResource {
}
}
override fun updateFlags(flags: Int) {
if (id != null) {
val values = ContentValues(1)
values.put(COLUMN_FLAGS, flags)
taskList.provider.client.update(taskSyncURI(), values, null, null)
}
object Factory: AndroidTaskFactory<LocalTask> {
override fun newInstance(calendar: AndroidTaskList<*>, id: Long, baseInfo: ContentValues?) =
LocalTask(calendar, id, baseInfo)
this.flags = flags
}
override fun newInstance(calendar: AndroidTaskList<*>, task: Task) =
LocalTask(calendar, task, null, null)
object Factory: AndroidTaskFactory<LocalTask> {
override fun fromProvider(taskList: AndroidTaskList<*>, values: ContentValues) =
LocalTask(taskList, values)
}
}
......@@ -16,14 +16,15 @@ import android.content.Context
import android.net.Uri
import android.os.Build
import at.bitfire.davdroid.DavUtils
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.model.CollectionInfo
import at.bitfire.davdroid.model.SyncState
import at.bitfire.ical4android.AndroidTaskList
import at.bitfire.ical4android.AndroidTaskListFactory
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.ical4android.TaskProvider
import org.dmfs.tasks.contract.TaskContract.TaskLists
import org.dmfs.tasks.contract.TaskContract.Tasks
import java.io.FileNotFoundException
import java.util.logging.Level
class LocalTaskList private constructor(
account: Account,
......@@ -33,17 +34,8 @@ class LocalTaskList private constructor(
companion object {
val defaultColor = 0xFFC3EA6E.toInt() // "DAVdroid green"
private const val defaultColor = 0xFFC3EA6E.toInt() // "DAVdroid green"
val COLUMN_CTAG = TaskLists.SYNC_VERSION
val BASE_INFO_COLUMNS = arrayOf(
Tasks._ID,
Tasks._SYNC_ID,
LocalTask.COLUMN_ETAG
)
@JvmStatic
fun tasksProviderAvailable(context: Context): Boolean {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
return context.packageManager.resolveContentProvider(TaskProvider.ProviderName.OpenTasks.authority, 0) != null
......@@ -54,8 +46,6 @@ class LocalTaskList private constructor(
}
}
@JvmStatic
@Throws(CalendarStorageException::class)
fun create(account: Account, provider: TaskProvider, info: CollectionInfo): Uri {
val values = valuesFromCollectionInfo(info, true)
values.put(TaskLists.OWNER, account.name)
......@@ -64,7 +54,6 @@ class LocalTaskList private constructor(
return create(account, provider, values)
}
@JvmStatic
@Throws(Exception::class)
fun onRenameAccount(resolver: ContentResolver, oldName: String, newName: String) {
var client: ContentProviderClient? = null
......@@ -96,27 +85,36 @@ class LocalTaskList private constructor(
}
override fun taskBaseInfoColumns() = BASE_INFO_COLUMNS
override var lastSyncState: SyncState?
get() {
try {
provider.client.query(taskListSyncUri(), arrayOf(TaskLists.SYNC_VERSION),
null, null, null)?.use { cursor ->
if (cursor.moveToNext())
cursor.getString(0)?.let {
return SyncState.fromString(it)
}
}
} catch (e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't read sync state", e)
}
return null
}
set(state) {
val values = ContentValues(1)
values.put(TaskLists.SYNC_VERSION, state?.toString())
provider.client.update(taskListSyncUri(), values, null, null)
}
@Throws(CalendarStorageException::class)
fun update(info: CollectionInfo, updateColor: Boolean) {
update(valuesFromCollectionInfo(info, updateColor))
}
@Throws(CalendarStorageException::class)
override fun getAll() = queryTasks(null, null)
@Throws(CalendarStorageException::class)
override fun getDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
@Throws(CalendarStorageException::class)
override fun getWithoutFileName() = queryTasks("${Tasks._SYNC_ID} IS NULL", null)
override fun findDeleted() = queryTasks("${Tasks._DELETED}!=0", null)
@Throws(FileNotFoundException::class, CalendarStorageException::class)
override fun getDirty(): List<LocalTask> {
override fun findDirty(): List<LocalTask> {
val tasks = queryTasks("${Tasks._DIRTY}!=0", null)
for (localTask in tasks) {
val task = requireNotNull(localTask.task)
......@@ -129,30 +127,21 @@ class LocalTaskList private constructor(
return tasks
}
override fun findByName(name: String) =
queryTasks("${Tasks._SYNC_ID}=?", arrayOf(name)).firstOrNull()
@Throws(CalendarStorageException::class)
override fun getCTag(): String? =
try {
provider.client.query(taskListSyncUri(), arrayOf(COLUMN_CTAG), null, null, null)?.use { cursor ->
if (cursor.moveToNext())
return cursor.getString(0)
}
null
} catch(e: Exception) {
throw CalendarStorageException("Couldn't read local (last known) CTag", e)
}
@Throws(CalendarStorageException::class)
override fun setCTag(cTag: String?) {
try {
val values = ContentValues(1)
values.put(COLUMN_CTAG, cTag)
provider.client.update(taskListSyncUri(), values, null, null)
} catch (e: Exception) {
throw CalendarStorageException("Couldn't write local (last known) CTag", e)
}
override fun markNotDirty(flags: Int): Int {
val values = ContentValues(1)
values.put(LocalTask.COLUMN_FLAGS, flags)
return provider.client.update(tasksSyncUri(), values, "${Tasks._DIRTY}=0", null)
}
override fun removeNotDirtyMarked(flags: Int) =
provider.client.delete(tasksSyncUri(),
"${Tasks._DIRTY}=0 AND ${LocalTask.COLUMN_FLAGS}=?",
arrayOf(flags.toString()))
object Factory: AndroidTaskListFactory<LocalTaskList> {
......@@ -161,4 +150,4 @@ class LocalTaskList private constructor(
}
}
}
\ No newline at end of file
......@@ -52,10 +52,10 @@ class AccountAuthenticatorService: Service(), OnAccountsUpdateListener {
.map { LocalAddressBook(context, it, null) }
.forEach {
try {
if (!accountNames.contains(it.getMainAccount().name))
if (!accountNames.contains(it.mainAccount.name))
it.delete()
} catch(e: ContactsStorageException) {
Logger.log.log(Level.SEVERE, "Couldn't get address book main account", e)
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't delete address book account", e)
}
}
......
......@@ -21,7 +21,6 @@ import at.bitfire.davdroid.model.ServiceDB
import at.bitfire.davdroid.model.ServiceDB.Collections
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.ISettings
import at.bitfire.vcard4android.ContactsStorageException
import java.util.logging.Level
class AddressBooksSyncAdapterService: SyncAdapterService() {
......@@ -29,7 +28,7 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
override fun syncAdapter() = AddressBooksSyncAdapter(this)
protected class AddressBooksSyncAdapter(
class AddressBooksSyncAdapter(
context: Context
): SyncAdapter(context) {
......@@ -103,8 +102,8 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
val remote = remoteAddressBooks(service)
// delete/update local address books
for (addressBook in LocalAddressBook.find(context, provider, account)) {
val url = addressBook.getURL()
for (addressBook in LocalAddressBook.findAll(context, provider, account)) {
val url = addressBook.url
val info = remote[url]
if (info == null) {
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
......@@ -114,7 +113,7 @@ class AddressBooksSyncAdapterService: SyncAdapterService() {
try {
Logger.log.log(Level.FINE, "Updating local address book $url", info)
addressBook.update(info)
} catch(e: ContactsStorageException) {
} catch(e: Exception) {
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
}
// we already have a local address book for this remote collection, don't take into consideration anymore
......