1
0
mirror of https://github.com/etesync/android synced 2024-12-23 15:18:14 +00:00

Sync manager: add etebase support (pushing changes)

This commit is contained in:
Tom Hacohen 2020-08-26 10:57:35 +03:00
parent 6302ab42de
commit f8c0eaca35
8 changed files with 221 additions and 42 deletions

View File

@ -76,9 +76,12 @@ class EtebaseLocalCache private constructor(context: Context, username: String)
} }
} }
fun collectionGet(colMgr: CollectionManager, colUid: String): CachedCollection { fun collectionGet(colMgr: CollectionManager, colUid: String): CachedCollection? {
val colDir = File(colsDir, colUid) val colDir = File(colsDir, colUid)
val colFile = File(colDir, "col") val colFile = File(colDir, "col")
if (!colFile.exists()) {
return null
}
val content = colFile.readBytes() val content = colFile.readBytes()
return colMgr.cacheLoad(content).let { return colMgr.cacheLoad(content).let {
CachedCollection(it, it.meta) CachedCollection(it, it.meta)
@ -110,6 +113,18 @@ class EtebaseLocalCache private constructor(context: Context, username: String)
} }
} }
fun itemGet(itemMgr: ItemManager, colUid: String, itemUid: String): CachedItem? {
val itemsDir = getCollectionItemsDir(colUid)
val itemFile = File(itemsDir, itemUid)
if (!itemFile.exists()) {
return null
}
val content = itemFile.readBytes()
return itemMgr.cacheLoad(content).let {
CachedItem(it, it.meta)
}
}
fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) { fun itemSet(itemMgr: ItemManager, colUid: String, item: Item) {
val itemsDir = getCollectionItemsDir(colUid) val itemsDir = getCollectionItemsDir(colUid)
val itemFile = File(itemsDir, item.uid) val itemFile = File(itemsDir, item.uid)

View File

@ -52,7 +52,7 @@ class LocalContact : AndroidContact, LocalAddress {
override// The same now override// The same now
val uuid: String? val uuid: String?
get() = fileName get() = contact?.uid
override val isLocalOnly: Boolean override val isLocalOnly: Boolean
get() = TextUtils.isEmpty(eTag) get() = TextUtils.isEmpty(eTag)
@ -88,9 +88,11 @@ class LocalContact : AndroidContact, LocalAddress {
addressBook.provider?.update(rawContactSyncURI(), values, null, null) addressBook.provider?.update(rawContactSyncURI(), values, null, null)
} }
override fun clearDirty(eTag: String) { override fun clearDirty(eTag: String?) {
val values = ContentValues(3) val values = ContentValues(3)
values.put(AndroidContact.COLUMN_ETAG, eTag) if (eTag != null) {
values.put(AndroidContact.COLUMN_ETAG, eTag)
}
values.put(ContactsContract.RawContacts.DIRTY, 0) values.put(ContactsContract.RawContacts.DIRTY, 0)
if (LocalContact.HASH_HACK) { if (LocalContact.HASH_HACK) {
@ -105,15 +107,16 @@ class LocalContact : AndroidContact, LocalAddress {
this.eTag = eTag this.eTag = eTag
} }
override fun prepareForUpload() { override fun prepareForUpload(fileName_: String?) {
val uid = UUID.randomUUID().toString() val uid = UUID.randomUUID().toString()
val values = ContentValues(2) val values = ContentValues(2)
values.put(AndroidContact.COLUMN_FILENAME, uid) val fileName = fileName_ ?: uid
values.put(AndroidContact.COLUMN_FILENAME, fileName)
values.put(AndroidContact.COLUMN_UID, uid) values.put(AndroidContact.COLUMN_UID, uid)
addressBook.provider?.update(rawContactSyncURI(), values, null, null) addressBook.provider?.update(rawContactSyncURI(), values, null, null)
fileName = uid this.fileName = fileName
} }
override fun populateData(mimeType: String, row: ContentValues) { override fun populateData(mimeType: String, row: ContentValues) {

View File

@ -38,7 +38,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
private var saveAsDirty = false // When true, the resource will be saved as dirty private var saveAsDirty = false // When true, the resource will be saved as dirty
private var fileName: String? = null override var fileName: String? = null
var eTag: String? = null var eTag: String? = null
var weAreOrganizer = true var weAreOrganizer = true
@ -58,7 +58,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
override// Now the same override// Now the same
val uuid: String? val uuid: String?
get() = fileName get() = event?.uid
constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?) : super(calendar, event) { constructor(calendar: AndroidCalendar<*>, event: Event, fileName: String?, eTag: String?) : super(calendar, event) {
this.fileName = fileName this.fileName = fileName
@ -133,7 +133,7 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
/* custom queries */ /* custom queries */
override fun prepareForUpload() { override fun prepareForUpload(fileName_: String?) {
var uid: String? = null var uid: String? = null
val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null) val c = calendar.provider.query(eventSyncURI(), arrayOf(COLUMN_UID), null, null, null)
if (c.moveToNext()) if (c.moveToNext())
@ -142,14 +142,14 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
uid = UUID.randomUUID().toString() uid = UUID.randomUUID().toString()
c.close() c.close()
val newFileName = uid
val fileName = fileName_ ?: uid
val values = ContentValues(2) val values = ContentValues(2)
values.put(Events._SYNC_ID, newFileName) values.put(Events._SYNC_ID, fileName)
values.put(COLUMN_UID, uid) values.put(COLUMN_UID, uid)
calendar.provider.update(eventSyncURI(), values, null, null) calendar.provider.update(eventSyncURI(), values, null, null)
fileName = newFileName this.fileName = fileName
val event = this.event val event = this.event
if (event != null) if (event != null)
@ -162,10 +162,12 @@ class LocalEvent : AndroidEvent, LocalResource<Event> {
calendar.provider.update(eventSyncURI(), values, null, null) calendar.provider.update(eventSyncURI(), values, null, null)
} }
override fun clearDirty(eTag: String) { override fun clearDirty(eTag: String?) {
val values = ContentValues(2) val values = ContentValues(2)
values.put(CalendarContract.Events.DIRTY, 0) values.put(CalendarContract.Events.DIRTY, 0)
values.put(COLUMN_ETAG, eTag) if (eTag != null) {
values.put(COLUMN_ETAG, eTag)
}
if (event != null) if (event != null)
values.put(COLUMN_SEQUENCE, event?.sequence) values.put(COLUMN_SEQUENCE, event?.sequence)
calendar.provider.update(eventSyncURI(), values, null, null) calendar.provider.update(eventSyncURI(), values, null, null)

View File

@ -120,13 +120,15 @@ class LocalGroup : AndroidGroup, LocalAddress {
return values return values
} }
override fun clearDirty(eTag: String) { override fun clearDirty(eTag: String?) {
val id = requireNotNull(id) val id = requireNotNull(id)
val values = ContentValues(2) val values = ContentValues(2)
values.put(Groups.DIRTY, 0) values.put(Groups.DIRTY, 0)
this.eTag = eTag this.eTag = eTag
values.put(AndroidGroup.COLUMN_ETAG, eTag) if (eTag != null) {
values.put(AndroidGroup.COLUMN_ETAG, eTag)
}
update(values) update(values)
// update cached group memberships // update cached group memberships
@ -154,15 +156,16 @@ class LocalGroup : AndroidGroup, LocalAddress {
batch.commit() batch.commit()
} }
override fun prepareForUpload() { override fun prepareForUpload(fileName_: String?) {
val uid = UUID.randomUUID().toString() val uid = UUID.randomUUID().toString()
val values = ContentValues(2) val values = ContentValues(2)
values.put(AndroidGroup.COLUMN_FILENAME, uid) val fileName = fileName_ ?: uid
values.put(AndroidGroup.COLUMN_FILENAME, fileName)
values.put(AndroidGroup.COLUMN_UID, uid) values.put(AndroidGroup.COLUMN_UID, uid)
update(values) update(values)
fileName = uid this.fileName = fileName
} }
override fun resetDeleted() { override fun resetDeleted() {

View File

@ -10,6 +10,7 @@ package com.etesync.syncadapter.resource
interface LocalResource<in TData: Any> { interface LocalResource<in TData: Any> {
val uuid: String? val uuid: String?
val fileName: String?
/** True if doesn't exist on server yet, false otherwise. */ /** True if doesn't exist on server yet, false otherwise. */
val isLocalOnly: Boolean val isLocalOnly: Boolean
@ -19,9 +20,10 @@ interface LocalResource<in TData: Any> {
fun delete(): Int fun delete(): Int
fun prepareForUpload() // FIXME: The null is for legacy
fun prepareForUpload(fileName: String?)
fun clearDirty(eTag: String) fun clearDirty(eTag: String?)
fun resetDeleted() fun resetDeleted()
} }

View File

@ -31,7 +31,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
private var saveAsDirty = false // When true, the resource will be saved as dirty private var saveAsDirty = false // When true, the resource will be saved as dirty
private var fileName: String? = null override var fileName: String? = null
var eTag: String? = null var eTag: String? = null
override val content: String override val content: String
@ -49,7 +49,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
override// Now the same override// Now the same
val uuid: String? val uuid: String?
get() = fileName get() = task?.uid
constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?) constructor(taskList: AndroidTaskList<*>, task: Task, fileName: String?, eTag: String?)
: super(taskList, task) { : super(taskList, task) {
@ -96,7 +96,7 @@ class LocalTask : AndroidTask, LocalResource<Task> {
/* custom queries */ /* custom queries */
override fun prepareForUpload() { override fun prepareForUpload(fileName_: String?) {
var uid: String? = null var uid: String? = null
val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null) val c = taskList.provider.client.query(taskSyncURI(), arrayOf(COLUMN_UID), null, null, null)
if (c.moveToNext()) if (c.moveToNext())
@ -106,12 +106,13 @@ class LocalTask : AndroidTask, LocalResource<Task> {
c.close() c.close()
val fileName = fileName_ ?: uid
val values = ContentValues(2) val values = ContentValues(2)
values.put(TaskContract.Tasks._SYNC_ID, uid) values.put(TaskContract.Tasks._SYNC_ID, fileName)
values.put(COLUMN_UID, uid) values.put(COLUMN_UID, uid)
taskList.provider.client.update(taskSyncURI(), values, null, null) taskList.provider.client.update(taskSyncURI(), values, null, null)
fileName = uid this.fileName = fileName
val task = this.task val task = this.task
if (task != null) if (task != null)
task.uid = uid task.uid = uid
@ -123,10 +124,12 @@ class LocalTask : AndroidTask, LocalResource<Task> {
taskList.provider.client.update(taskSyncURI(), values, null, null) taskList.provider.client.update(taskSyncURI(), values, null, null)
} }
override fun clearDirty(eTag: String) { override fun clearDirty(eTag: String?) {
val values = ContentValues(2) val values = ContentValues(2)
values.put(TaskContract.Tasks._DIRTY, 0) values.put(TaskContract.Tasks._DIRTY, 0)
values.put(COLUMN_ETAG, eTag) if (eTag != null) {
values.put(COLUMN_ETAG, eTag)
}
if (task != null) if (task != null)
values.put(COLUMN_SEQUENCE, task?.sequence) values.put(COLUMN_SEQUENCE, task?.sequence)
taskList.provider.client.update(taskSyncURI(), values, null, null) taskList.provider.client.update(taskSyncURI(), values, null, null)

View File

@ -134,8 +134,8 @@ constructor(context: Context, account: Account, settings: AccountSettings, extra
} }
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class) @Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
override fun createLocalEntries() { override fun prepareLocal() {
super.createLocalEntries() super.prepareLocal()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
createInviteAttendeesNotification() createInviteAttendeesNotification()

View File

@ -118,7 +118,7 @@ constructor(protected val context: Context, protected val account: Account, prot
etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings) etebase = EtebaseLocalCache.getEtebase(context, httpClient.okHttpClient, settings)
colMgr = etebase.collectionManager colMgr = etebase.collectionManager
synchronized(etebaseLocalCache) { synchronized(etebaseLocalCache) {
cachedCollection = etebaseLocalCache.collectionGet(colMgr, journalUid) cachedCollection = etebaseLocalCache.collectionGet(colMgr, journalUid)!!
} }
itemMgr = colMgr.getItemManager(cachedCollection.col) itemMgr = colMgr.getItemManager(cachedCollection.col)
} }
@ -199,6 +199,27 @@ constructor(protected val context: Context, protected val account: Account, prot
etebaseLocalCache.collectionLoadStoken(cachedCollection.col.uid) etebaseLocalCache.collectionLoadStoken(cachedCollection.col.uid)
} }
// Push local changes // Push local changes
var chunkPushItems: List<Item>
do {
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_prepare_local
Logger.log.info("Sync phase: " + context.getString(syncPhase))
prepareLocal()
/* Create push items out of local changes. */
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_create_local_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
chunkPushItems = createPushItems()
if (Thread.interrupted())
throw InterruptedException()
syncPhase = R.string.sync_phase_push_entries
Logger.log.info("Sync phase: " + context.getString(syncPhase))
pushItems(chunkPushItems)
} while (chunkPushItems.size == MAX_PUSH)
do { do {
if (Thread.interrupted()) if (Thread.interrupted())
@ -239,6 +260,11 @@ constructor(protected val context: Context, protected val account: Account, prot
} catch (e: SSLHandshakeException) { } catch (e: SSLHandshakeException) {
syncResult.stats.numIoExceptions++ syncResult.stats.numIoExceptions++
notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account)
notificationManager.notify(syncErrorTitle, context.getString(syncPhase))
} catch (e: FileNotFoundException) {
notificationManager.setThrowable(e) notificationManager.setThrowable(e)
val detailsIntent = notificationManager.detailsIntent val detailsIntent = notificationManager.detailsIntent
detailsIntent.putExtra(KEY_ACCOUNT, account) detailsIntent.putExtra(KEY_ACCOUNT, account)
@ -395,7 +421,7 @@ constructor(protected val context: Context, protected val account: Account, prot
Logger.log.info("Fetched items. Done=${ret.isDone}") Logger.log.info("Fetched items. Done=${ret.isDone}")
return ret return ret
} else { } else {
Logger.log.info("Skipping fetch because local lastUid == remoteLastUid (${remoteCTag})") Logger.log.info("Skipping fetch because local stoken == lastStoken (${remoteCTag})")
return null return null
} }
} }
@ -527,8 +553,127 @@ constructor(protected val context: Context, protected val account: Account, prot
} }
} }
private fun pushItems(chunkPushItems_: List<Item>) {
var chunkPushItems = chunkPushItems_
// upload dirty contacts
var pushed = 0
try {
if (!chunkPushItems.isEmpty()) {
val items = chunkPushItems
itemMgr.batch(items.toTypedArray())
// Persist the items
synchronized(etebaseLocalCache) {
val colUid = cachedCollection.col.uid
for (item in items) {
etebaseLocalCache.itemSet(itemMgr, colUid, item)
}
}
pushed += items.size
}
} finally {
// FIXME: A bit fragile, we assume the order in createPushItems
var left = pushed
for (local in localDeleted!!) {
if (pushed-- <= 0) {
break
}
local.delete()
}
if (left > 0) {
localDeleted = localDeleted?.drop(left)
chunkPushItems = chunkPushItems.drop(left - pushed)
}
left = pushed
var i = 0
for (local in localDirty) {
if (pushed-- <= 0) {
break
}
Logger.log.info("Added/changed resource with filename: " + local.fileName)
local.clearDirty(chunkPushItems[i].etag)
i++
}
if (left > 0) {
localDirty = localDirty.drop(left)
chunkPushItems.drop(left)
}
if (pushed > 0) {
Logger.log.severe("Unprocessed localentries left, this should never happen!")
}
}
}
private fun itemUpdateMtime(item: Item) {
val meta = item.meta
meta.setMtime(System.currentTimeMillis())
item.meta = meta
}
private fun createPushItems(): List<Item> {
val ret = LinkedList<Item>()
val colUid = cachedCollection.col.uid
synchronized(etebaseLocalCache) {
for (local in localDeleted!!) {
val item = etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!)!!.item
itemUpdateMtime(item)
item.delete()
ret.add(item)
if (ret.size == MAX_PUSH) {
return ret
}
}
}
synchronized(etebaseLocalCache) {
for (local in localDirty) {
val cacheItem = if (local.fileName != null) etebaseLocalCache.itemGet(itemMgr, colUid, local.fileName!!)!! else null
val item: Item
if (cacheItem != null) {
item = cacheItem.item
itemUpdateMtime(item)
} else {
val meta = ItemMetadata()
meta.name = local.uuid
meta.setMtime(System.currentTimeMillis())
item = itemMgr.create(meta, "")
local.prepareForUpload(item.uid)
}
try {
item.setContent(local.content)
} catch (e: Exception) {
Logger.log.warning("Failed creating local entry ${local.uuid}")
if (local is LocalContact) {
Logger.log.warning("Contact with title ${local.contact?.displayName}")
} else if (local is LocalEvent) {
Logger.log.warning("Event with title ${local.event?.summary}")
} else if (local is LocalTask) {
Logger.log.warning("Task with title ${local.task?.summary}")
}
throw e
}
ret.add(item)
if (ret.size == MAX_PUSH) {
return ret
}
}
}
return ret
}
@Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class) @Throws(CalendarStorageException::class, ContactsStorageException::class, IOException::class)
protected open fun createLocalEntries() { private fun createLocalEntries() {
localEntries = LinkedList() localEntries = LinkedList()
// Not saving, just creating a fake one until we load it from a local db // Not saving, just creating a fake one until we load it from a local db
@ -581,7 +726,7 @@ constructor(protected val context: Context, protected val account: Account, prot
/** /**
*/ */
@Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class) @Throws(CalendarStorageException::class, ContactsStorageException::class, FileNotFoundException::class)
private fun prepareLocal() { protected open fun prepareLocal() {
localDeleted = processLocallyDeleted() localDeleted = processLocallyDeleted()
localDirty = localCollection!!.findDirty(MAX_PUSH) localDirty = localCollection!!.findDirty(MAX_PUSH)
// This is done after fetching the local dirty so all the ones we are using will be prepared // This is done after fetching the local dirty so all the ones we are using will be prepared
@ -598,7 +743,8 @@ constructor(protected val context: Context, protected val account: Account, prot
val localList = localCollection!!.findDeleted() val localList = localCollection!!.findDeleted()
val ret = ArrayList<T>(localList.size) val ret = ArrayList<T>(localList.size)
if (journalEntity.isReadOnly) { val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == "ro"))
if (readOnly) {
for (local in localList) { for (local in localList) {
Logger.log.info("Restoring locally deleted resource on a read only collection: ${local.uuid}") Logger.log.info("Restoring locally deleted resource on a read only collection: ${local.uuid}")
local.resetDeleted() local.resetDeleted()
@ -612,8 +758,11 @@ constructor(protected val context: Context, protected val account: Account, prot
if (local.uuid != null) { if (local.uuid != null) {
Logger.log.info(local.uuid + " has been deleted locally -> deleting from server") Logger.log.info(local.uuid + " has been deleted locally -> deleting from server")
} else { } else {
Logger.log.fine("Entry deleted before ever syncing - genarting a UUID") if (isLegacy) {
local.prepareForUpload() // It's done later for non-legacy
Logger.log.fine("Entry deleted before ever syncing - genarting a UUID")
local.prepareForUpload(null)
}
} }
ret.add(local) ret.add(local)
@ -627,20 +776,22 @@ constructor(protected val context: Context, protected val account: Account, prot
@Throws(CalendarStorageException::class, ContactsStorageException::class) @Throws(CalendarStorageException::class, ContactsStorageException::class)
protected open fun prepareDirty() { protected open fun prepareDirty() {
if (journalEntity.isReadOnly) { val readOnly = (isLegacy && journalEntity.isReadOnly) || (!isLegacy && (cachedCollection.col.accessLevel == "ro"))
if (readOnly) {
for (local in localDirty) { for (local in localDirty) {
Logger.log.info("Restoring locally modified resource on a read only collection: ${local.uuid}") Logger.log.info("Restoring locally modified resource on a read only collection: ${local.uuid}")
if (local.uuid == null) { if (local.uuid == null) {
// If it was only local, delete. // If it was only local, delete.
local.delete() local.delete()
} else { } else {
local.clearDirty(local.uuid!!) local.clearDirty(null)
} }
numDiscarded++ numDiscarded++
} }
localDirty = LinkedList() localDirty = LinkedList()
} else { } else if (isLegacy) {
// It's done later for non-legacy
// assign file names and UIDs to new entries // assign file names and UIDs to new entries
Logger.log.info("Looking for local entries without a uuid") Logger.log.info("Looking for local entries without a uuid")
for (local in localDirty) { for (local in localDirty) {
@ -649,7 +800,7 @@ constructor(protected val context: Context, protected val account: Account, prot
} }
Logger.log.fine("Found local record without file name; generating file name/UID if necessary") Logger.log.fine("Found local record without file name; generating file name/UID if necessary")
local.prepareForUpload() local.prepareForUpload(null)
} }
} }
} }