From aedefc90100dd8a48ae6ebedbbe89c2c1bfb3e01 Mon Sep 17 00:00:00 2001 From: soraefir Date: Thu, 3 Sep 2026 21:18:29 +0200 Subject: [PATCH] Partial Sync --- .../android/main/BillsListViewActivity.kt | 2 +- .../persistence/CowspentServerSyncHelper.kt | 72 ++++++++++++----- .../util/VersatileProjectSyncClient.kt | 78 +++++++++++++++---- 3 files changed, 117 insertions(+), 35 deletions(-) diff --git a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt index a657ce4..6b8a4b8 100644 --- a/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt +++ b/app/src/main/java/net/helcel/cowspent/android/main/BillsListViewActivity.kt @@ -761,7 +761,7 @@ class BillsListViewActivity : val proj = withContext(Dispatchers.IO) { db.getProject(selectedProjectId) } if (proj != null && !proj.isLocal) { db.cowspentServerSyncHelper.addCallbackPull(syncCallBack) - db.cowspentServerSyncHelper.scheduleSync(false, selectedProjectId) + db.cowspentServerSyncHelper.scheduleSync(false, selectedProjectId, manual) } else viewModel.isRefreshing = false } } else viewModel.isRefreshing = false diff --git a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt index 2f288e4..94f3563 100644 --- a/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt +++ b/app/src/main/java/net/helcel/cowspent/persistence/CowspentServerSyncHelper.kt @@ -93,14 +93,14 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen callbacksPull.add(callback) } - fun scheduleSync(onlyLocalChanges: Boolean, projId: Long): SyncTask? { - Log.d(TAG, "Sync requested (${if (onlyLocalChanges) "onlyLocalChanges" else "full"}; ${if (syncActive) "sync active" else "sync NOT active"}) ...") + 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"}; forceFullSync=$forceFullSync) ...") updateNetworkStatus() if (isSyncPossible && (!syncActive || onlyLocalChanges)) { val project = dbHelper.getProject(projId) if (project != null) { Log.d(TAG, "... starting now") - val syncTask = SyncTask(onlyLocalChanges, project) + val syncTask = SyncTask(onlyLocalChanges, project, forceFullSync) syncTask.addCallbacks(callbacksPush) callbacksPush = ArrayList() 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 = ArrayList() private var nextcloudClient: NextcloudClient? = 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() dbPaymentModes.filter { it.remoteId < 0 }.forEach { paymentModesRemoteIdToId[it.remoteId] = it.id } - val billsResponse = client!!.getBills(project) val isIHM = project.type == ProjectType.IHATEMONEY - val serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp - val remoteBills: List = if (isIHM) { - billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId) + var serverSyncTimestamp = project.lastSyncedTimestamp + val remoteBills = mutableListOf() + val remoteAllBillIds = mutableListOf() + + 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 { - billsResponse.getBillsCospend(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId) - } - val remoteAllBillIds: List = if (isIHM) { - remoteBills.map { it.remoteId } - } else { - billsResponse.allBillIds + Log.d(TAG, "Starting full sync for project ${project.remoteId}") + val billsResponse = client!!.getBills(project) + serverSyncTimestamp = if (isIHM) 0L else billsResponse.syncTimestamp + if (isIHM) { + remoteBills.addAll(billsResponse.getBillsIHM(project.id, memberRemoteIdToId, categoriesRemoteIdToId, paymentModesRemoteIdToId)) + 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 localBills = dbHelper.getBillsOfProject(project.id) - val localBillsByRemoteId = localBills.associateBy { it.remoteId } for (remoteBill in remoteBills) { 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) { if (!remoteAllBillIds.contains(localBill.remoteId)) { dbHelper.deleteBill(localBill.id) @@ -792,7 +828,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen Log.d(TAG, "Delete local bill : $localBill") } } - } else { + } else if (remoteAllBillIds.isNotEmpty()) { for (localBill in localBills) { if (!remoteBillsByRemoteId.containsKey(localBill.remoteId)) { dbHelper.deleteBill(localBill.id) diff --git a/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt b/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt index 3f232bb..829c801 100644 --- a/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt +++ b/app/src/main/java/net/helcel/cowspent/util/VersatileProjectSyncClient.kt @@ -679,22 +679,66 @@ class VersatileProjectSyncClient( } @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 username: String? var password: String? var bearerToken: String? var useOcsApiRequest: Boolean + + val paramKeys: MutableList = ArrayList() + val paramValues: MutableList = ArrayList() + if (ProjectType.COSPEND == project.type) { 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)) { username = this.username 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 + 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( requestServer( target, METHOD_GET, null, null, @@ -703,14 +747,6 @@ class VersatileProjectSyncClient( useOcsApiRequest ) } else if (canAccessProjectWithSSO(project)) { - val paramKeys: MutableList = ArrayList() - val paramValues: MutableList = ArrayList() - if (cospendVersionGT161) { - paramKeys.add("lastChanged") - } else { - paramKeys.add("lastchanged") - } - paramValues.add(tsLastSync.toString()) return if (cospendVersionGT161) { target = "/ocs/v2.php/apps/cospend/api/v1/projects/" + project.remoteId + "/bills" ServerResponse.BillsResponse(requestServerWithSSO(nextcloudAPI!!, target, METHOD_GET, paramKeys, paramValues, true), true) @@ -720,10 +756,20 @@ class VersatileProjectSyncClient( } } else { useOcsApiRequest = cospendVersionGT161 - target = if (cospendVersionGT161) - project.getRequestBaseUrl(true) + "/api/v1/public/projects/" + project.remoteId + "/" + getEncodedPassword(project.password) + "/bills?lastChanged=" + tsLastSync + val baseUrl = project.getRequestBaseUrl(useOcsApiRequest) + target = if (useOcsApiRequest) + "$baseUrl/api/v1/public/projects/${project.remoteId}/${getEncodedPassword(project.password)}/bills" 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( requestServer( target, METHOD_GET, null, null,