Partial Sync

This commit is contained in:
2026-09-03 22:00:27 +02:00
parent 492230faa2
commit aedefc9010
3 changed files with 117 additions and 35 deletions
@@ -761,7 +761,7 @@ class BillsListViewActivity :
val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) } val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) }
if (proj != null && !proj.isLocal) { if (proj != null && !proj.isLocal) {
db.cowspentServerSyncHelper.addCallbackPull(syncCallBack) db.cowspentServerSyncHelper.addCallbackPull(syncCallBack)
db.cowspentServerSyncHelper.scheduleSync(false, selectedProjectId) db.cowspentServerSyncHelper.scheduleSync(false, selectedProjectId, manual)
} else viewModel.isRefreshing = false } else viewModel.isRefreshing = false
} }
} else viewModel.isRefreshing = false } else viewModel.isRefreshing = false
@@ -93,14 +93,14 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
callbacksPull.add(callback) callbacksPull.add(callback)
} }
fun scheduleSync(onlyLocalChanges: Boolean, projId: Long): SyncTask? { fun scheduleSync(onlyLocalChanges: Boolean, projId: Long, forceFullSync: Boolean = false): SyncTask? {
Log.d(TAG, "Sync requested (${if (onlyLocalChanges) "onlyLocalChanges" else "full"}; ${if (syncActive) "sync active" else "sync NOT active"}) ...") Log.d(TAG, "Sync requested (${if (onlyLocalChanges) "onlyLocalChanges" else "full"}; ${if (syncActive) "sync active" else "sync NOT active"}; forceFullSync=$forceFullSync) ...")
updateNetworkStatus() updateNetworkStatus()
if (isSyncPossible && (!syncActive || onlyLocalChanges)) { if (isSyncPossible && (!syncActive || onlyLocalChanges)) {
val project = dbHelper.getProject(projId) val project = dbHelper.getProject(projId)
if (project != null) { if (project != null) {
Log.d(TAG, "... starting now") Log.d(TAG, "... starting now")
val syncTask = SyncTask(onlyLocalChanges, project) val syncTask = SyncTask(onlyLocalChanges, project, forceFullSync)
syncTask.addCallbacks(callbacksPush) syncTask.addCallbacks(callbacksPush)
callbacksPush = ArrayList() callbacksPush = ArrayList()
if (!onlyLocalChanges) { if (!onlyLocalChanges) {
@@ -138,7 +138,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
} }
} }
inner class SyncTask(private val onlyLocalChanges: Boolean, private val project: DBProject) { inner class SyncTask(private val onlyLocalChanges: Boolean, private val project: DBProject, private val forceFullSync: Boolean = false) {
private val callbacks: MutableList<ICallback> = ArrayList() private val callbacks: MutableList<ICallback> = ArrayList()
private var nextcloudClient: NextcloudClient? = null private var nextcloudClient: NextcloudClient? = null
private var client: VersatileProjectSyncClient? = null private var client: VersatileProjectSyncClient? = null
@@ -726,23 +726,59 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val paymentModesRemoteIdToId = dbPaymentModes.associate { it.remoteId to it.id }.toMutableMap() val paymentModesRemoteIdToId = dbPaymentModes.associate { it.remoteId to it.id }.toMutableMap()
dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = it.id } dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = it.id }
val billsResponse = client!!.getBills(project)
val isIHM = project.type == ProjectType.IHATEMONEY val isIHM = project.type == ProjectType.IHATEMONEY
val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp var serverSyncTimestamp = project.lastSyncedTimestamp
val remoteBills: List<DBBill> = if (isIHM) { val remoteBills = mutableListOf<DBBill>()
billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId) val remoteAllBillIds = mutableListOf<Long>()
val localBills = dbHelper.getBillsOfProject(project.id)
val localBillsByRemoteId = localBills.associateBy { it.remoteId }
if (project.type == ProjectType.COSPEND && !forceFullSync && localBills.isNotEmpty()) {
Log.d(TAG, "Starting partial sync for project ${project.remoteId}")
var offset = 0
val limit = 50
var allMatched = false
while (!allMatched) {
val partialResponse = client!!.getBills(project, offset, limit, true, 0)
val partialRemoteBills = partialResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)
if (partialRemoteBills.isEmpty()) break
remoteBills.addAll(partialRemoteBills)
var frameMatched = true
for (rb in partialRemoteBills) {
val lb = localBillsByRemoteId[rb.remoteId]
if (lb == null || hasChanged(lb, rb)) {
frameMatched = false
break
}
}
if (offset == 0 && partialResponse.syncTimestamp > 0) {
serverSyncTimestamp = partialResponse.syncTimestamp
}
if (frameMatched) {
allMatched = true
} else {
offset += limit
}
}
} else { } else {
billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId) Log.d(TAG, "Starting full sync for project ${project.remoteId}")
} val billsResponse = client!!.getBills(project)
val remoteAllBillIds: List<Long> = if (isIHM) { serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp
remoteBills.map { it.remoteId } if (isIHM) {
} else { remoteBills.addAll(billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId))
billsResponse.allBillIds remoteAllBillIds.addAll(remoteBills.map { it.remoteId })
} else {
remoteBills.addAll(billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId))
remoteAllBillIds.addAll(billsResponse.allBillIds)
}
} }
val remoteBillsByRemoteId = remoteBills.associateBy { it.remoteId } val remoteBillsByRemoteId = remoteBills.associateBy { it.remoteId }
val localBills = dbHelper.getBillsOfProject(project.id)
val localBillsByRemoteId = localBills.associateBy { it.remoteId }
for (remoteBill in remoteBills) { for (remoteBill in remoteBills) {
if (!localBillsByRemoteId.containsKey(remoteBill.remoteId)) { if (!localBillsByRemoteId.containsKey(remoteBill.remoteId)) {
@@ -783,7 +819,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
} }
} }
if (project.type == ProjectType.COSPEND || project.type == ProjectType.IHATEMONEY) { if (remoteAllBillIds.isNotEmpty() && (project.type == ProjectType.COSPEND || project.type == ProjectType.IHATEMONEY)) {
for (localBill in localBills) { for (localBill in localBills) {
if (!remoteAllBillIds.contains(localBill.remoteId)) { if (!remoteAllBillIds.contains(localBill.remoteId)) {
dbHelper.deleteBill(localBill.id) dbHelper.deleteBill(localBill.id)
@@ -792,7 +828,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
Log.d(TAG, "Delete local bill : $localBill") Log.d(TAG, "Delete local bill : $localBill")
} }
} }
} else { } else if (remoteAllBillIds.isNotEmpty()) {
for (localBill in localBills) { for (localBill in localBills) {
if (!remoteBillsByRemoteId.containsKey(localBill.remoteId)) { if (!remoteBillsByRemoteId.containsKey(localBill.remoteId)) {
dbHelper.deleteBill(localBill.id) dbHelper.deleteBill(localBill.id)
@@ -679,22 +679,66 @@ class VersatileProjectSyncClient(
} }
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class) @Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
fun getBills(project: DBProject): ServerResponse.BillsResponse { fun getBills(
project: DBProject,
offset: Int? = null,
limit: Int? = null,
reverse: Boolean? = null,
deleted: Int? = null
): ServerResponse.BillsResponse {
var target: String var target: String
var username: String? var username: String?
var password: String? var password: String?
var bearerToken: String? var bearerToken: String?
var useOcsApiRequest: Boolean var useOcsApiRequest: Boolean
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
if (ProjectType.COSPEND == project.type) { if (ProjectType.COSPEND == project.type) {
val tsLastSync = project.lastSyncedTimestamp val tsLastSync = project.lastSyncedTimestamp
if (offset == null) {
if (cospendVersionGT161) {
paramKeys.add("lastChanged")
} else {
paramKeys.add("lastchanged")
}
paramValues.add(tsLastSync.toString())
} else {
paramKeys.add("offset")
paramValues.add(offset.toString())
if (limit != null) {
paramKeys.add("limit")
paramValues.add(limit.toString())
}
if (reverse != null) {
paramKeys.add("reverse")
paramValues.add(reverse.toString())
}
if (deleted != null) {
paramKeys.add("deleted")
paramValues.add(deleted.toString())
}
}
if (canAccessProjectWithNCLogin(project)) { if (canAccessProjectWithNCLogin(project)) {
username = this.username username = this.username
password = this.password password = this.password
target = if (cospendVersionGT161)
project.getRequestBaseUrl(true) + "/api/v1/projects/" + project.remoteId + "/bills?lastChanged=" + tsLastSync
else
project.getRequestBaseUrl(false) + "/api-priv/projects/" + project.remoteId + "/bills?lastchanged=" + tsLastSync
useOcsApiRequest = cospendVersionGT161 useOcsApiRequest = cospendVersionGT161
val baseUrl = project.getRequestBaseUrl(useOcsApiRequest)
target = if (useOcsApiRequest)
"$baseUrl/api/v1/projects/${project.remoteId}/bills"
else
"$baseUrl/api-priv/projects/${project.remoteId}/bills"
if (paramKeys.isNotEmpty()) {
target += "?"
for (i in paramKeys.indices) {
if (i > 0) target += "&"
target += "${paramKeys[i]}=${paramValues[i]}"
}
}
return ServerResponse.BillsResponse( return ServerResponse.BillsResponse(
requestServer( requestServer(
target, METHOD_GET, null, null, target, METHOD_GET, null, null,
@@ -703,14 +747,6 @@ class VersatileProjectSyncClient(
useOcsApiRequest useOcsApiRequest
) )
} else if (canAccessProjectWithSSO(project)) { } else if (canAccessProjectWithSSO(project)) {
val paramKeys: MutableList<String> = ArrayList()
val paramValues: MutableList<String> = ArrayList()
if (cospendVersionGT161) {
paramKeys.add("lastChanged")
} else {
paramKeys.add("lastchanged")
}
paramValues.add(tsLastSync.toString())
return if (cospendVersionGT161) { return if (cospendVersionGT161) {
target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills" target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills"
ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true) ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true)
@@ -720,10 +756,20 @@ class VersatileProjectSyncClient(
} }
} else { } else {
useOcsApiRequest = cospendVersionGT161 useOcsApiRequest = cospendVersionGT161
target = if (cospendVersionGT161) val baseUrl = project.getRequestBaseUrl(useOcsApiRequest)
project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastChanged=" + tsLastSync target = if (useOcsApiRequest)
"$baseUrl/api/v1/public/projects/${project.remoteId}/${getEncodedPassword(project.password)}/bills"
else else
project.getRequestBaseUrl(false) + "/apiv2/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastchanged=" + tsLastSync "$baseUrl/apiv2/projects/${project.remoteId}/${getEncodedPassword(project.password)}/bills"
if (paramKeys.isNotEmpty()) {
target += "?"
for (i in paramKeys.indices) {
if (i > 0) target += "&"
target += "${paramKeys[i]}=${paramValues[i]}"
}
}
return ServerResponse.BillsResponse( return ServerResponse.BillsResponse(
requestServer( requestServer(
target, METHOD_GET, null, null, target, METHOD_GET, null, null,