Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c72b3e779e |
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
plugins {
|
plugins {
|
||||||
id 'com.android.application' version '9.3.2'
|
id 'com.android.application' version '9.3.2'
|
||||||
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.10'
|
id 'org.jetbrains.kotlin.plugin.serialization' version '2.4.20'
|
||||||
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.10'
|
id 'org.jetbrains.kotlin.plugin.compose' version '2.4.10'
|
||||||
id 'jacoco'
|
id 'jacoco'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,6 @@ package net.helcel.cowspent.android.main
|
|||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.material.CircularProgressIndicator
|
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
@@ -72,43 +69,13 @@ fun EmptyProjectsState(onConfigureNextcloud: () -> Unit, onAddManually: () -> Un
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A full-height, centred state that a pull to refresh can still act on.
|
|
||||||
*
|
|
||||||
* PullRefresh reads the gesture from a nested scroll source, and a plain Column offers none - so
|
|
||||||
* on an empty list, which is exactly when a refresh is wanted most, the pull never fires. A
|
|
||||||
* single-item LazyColumn has nowhere to scroll but does provide that source.
|
|
||||||
*/
|
|
||||||
@Composable
|
|
||||||
private fun RefreshableFullScreenState(content: @Composable ColumnScope.() -> Unit) {
|
|
||||||
LazyColumn(modifier = Modifier.fillMaxSize()) {
|
|
||||||
item {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier.fillParentMaxSize().padding(16.dp),
|
|
||||||
verticalArrangement = Arrangement.Center,
|
|
||||||
horizontalAlignment = Alignment.CenterHorizontally,
|
|
||||||
content = content
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun LoadingBillsState() {
|
|
||||||
RefreshableFullScreenState {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
|
||||||
Text(
|
|
||||||
text = stringResource(R.string.error_loading),
|
|
||||||
style = MaterialTheme.typography.subtitle1,
|
|
||||||
color = MaterialTheme.colors.onSurface.copy(alpha = 0.6f)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyMembersState() {
|
fun EmptyMembersState() {
|
||||||
RefreshableFullScreenState {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.error_no_members).uppercase(),
|
text = stringResource(R.string.error_no_members).uppercase(),
|
||||||
style = MaterialTheme.typography.subtitle1,
|
style = MaterialTheme.typography.subtitle1,
|
||||||
@@ -127,7 +94,11 @@ fun EmptyMembersState() {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyBillsState() {
|
fun EmptyBillsState() {
|
||||||
RefreshableFullScreenState {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().padding(16.dp),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.error_no_bills).uppercase(),
|
text = stringResource(R.string.error_no_bills).uppercase(),
|
||||||
style = MaterialTheme.typography.subtitle1,
|
style = MaterialTheme.typography.subtitle1,
|
||||||
@@ -232,7 +203,11 @@ fun SectionHeader(title: String) {
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EmptyState() {
|
fun EmptyState() {
|
||||||
RefreshableFullScreenState {
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.error_no_bills).uppercase(),
|
text = stringResource(R.string.error_no_bills).uppercase(),
|
||||||
style = MaterialTheme.typography.subtitle1,
|
style = MaterialTheme.typography.subtitle1,
|
||||||
|
|||||||
@@ -409,10 +409,6 @@ fun BillsListScreen(
|
|||||||
when {
|
when {
|
||||||
viewModel.showNoProjects -> EmptyProjectsState(onAccountSwitcherClick, onAddProjectClick)
|
viewModel.showNoProjects -> EmptyProjectsState(onAccountSwitcherClick, onAddProjectClick)
|
||||||
viewModel.showNoMembers -> EmptyMembersState()
|
viewModel.showNoMembers -> EmptyMembersState()
|
||||||
// A large project takes a long time on its first sync. Until that finishes there
|
|
||||||
// is nothing stored for it yet, and reporting that as "no bills" tells the user
|
|
||||||
// the project is empty when it is still downloading.
|
|
||||||
viewModel.isRefreshing && viewModel.bills.isEmpty() -> LoadingBillsState()
|
|
||||||
viewModel.showNoBills -> EmptyBillsState()
|
viewModel.showNoBills -> EmptyBillsState()
|
||||||
viewModel.bills.isEmpty() -> EmptyState()
|
viewModel.bills.isEmpty() -> EmptyState()
|
||||||
else -> {
|
else -> {
|
||||||
|
|||||||
@@ -94,16 +94,14 @@ class BillsListViewActivity :
|
|||||||
private val syncCallBack = object : ICallback {
|
private val syncCallBack = object : ICallback {
|
||||||
override fun onFinish() {
|
override fun onFinish() {
|
||||||
mActionMode?.finish()
|
mActionMode?.finish()
|
||||||
viewModel.isRefreshing = false
|
|
||||||
refreshLists()
|
refreshLists()
|
||||||
|
viewModel.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onFinish(result: String, message: String) {}
|
override fun onFinish(result: String, message: String) {}
|
||||||
|
|
||||||
override fun onScheduled() {
|
override fun onScheduled() {
|
||||||
// Being queued is not being done. Clearing the indicator here stops the spinner while a
|
viewModel.isRefreshing = false
|
||||||
// large project is still downloading; synchronize() already releases it when nothing
|
|
||||||
// actually started.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -790,23 +788,11 @@ class BillsListViewActivity :
|
|||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
val remoteProjects = withContext(Dispatchers.IO) { db.projects }
|
val remoteProjects = withContext(Dispatchers.IO) { db.projects }
|
||||||
.filter { !it.isLocal && !it.isArchived }
|
.filter { !it.isLocal && !it.isArchived }
|
||||||
// Only the first one scheduled actually starts - the rest queue behind it - and it
|
|
||||||
// takes the screen's callback with it. So the selected project goes first: otherwise
|
|
||||||
// the spinner follows a project the user is not looking at, stops when that one
|
|
||||||
// finishes, and theirs is reported as empty while it is still waiting its turn.
|
|
||||||
.sortedByDescending { it.id == selectedProjectId }
|
|
||||||
|
|
||||||
viewModel.isRefreshing = true
|
viewModel.isRefreshing = true
|
||||||
db.cowspentServerSyncHelper.addCallbackPull(syncCallBack)
|
db.cowspentServerSyncHelper.addCallbackPull(syncCallBack)
|
||||||
|
|
||||||
val started = if (accountSyncDue) {
|
val started = if (accountSyncDue) {
|
||||||
// The pass is throttled by this stamp, so it has to be written here rather than
|
|
||||||
// left to the account sync: that only runs when an account is configured and only
|
|
||||||
// records itself on success, so without one the stamp stayed at zero and the pass
|
|
||||||
// repeated on every single resume, re-scheduling every project each time.
|
|
||||||
preferences.edit {
|
|
||||||
putLong(getString(R.string.pref_key_last_account_sync_timestamp), now)
|
|
||||||
}
|
|
||||||
if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) {
|
if (CowspentServerSyncHelper.isNextcloudAccountConfigured(applicationContext)) {
|
||||||
db.cowspentServerSyncHelper.runAccountProjectsSync()
|
db.cowspentServerSyncHelper.runAccountProjectsSync()
|
||||||
}
|
}
|
||||||
@@ -818,24 +804,14 @@ class BillsListViewActivity :
|
|||||||
} else {
|
} else {
|
||||||
val selectedProj = remoteProjects.find { it.id == selectedProjectId }
|
val selectedProj = remoteProjects.find { it.id == selectedProjectId }
|
||||||
val lastSync = preferences.getLong(lastProjectSyncKey(selectedProjectId), 0L)
|
val lastSync = preferences.getLong(lastProjectSyncKey(selectedProjectId), 0L)
|
||||||
// The pull writes the project cursor only after it has applied everything, so a cursor
|
val due = trigger == SyncTrigger.MANUAL ||
|
||||||
// still at zero means no sync ever finished and nothing is stored locally.
|
|
||||||
// Throttling that strands the user on an empty list with no spinner - which is what
|
|
||||||
// the back gesture does to a large project, since it destroys the activity while
|
|
||||||
// the sync is still running. IHateMoney never advances the cursor, so the interval
|
|
||||||
// stays in charge there.
|
|
||||||
val neverCompleted = selectedProj != null &&
|
|
||||||
selectedProj.type != ProjectType.IHATEMONEY &&
|
|
||||||
(selectedProj.lastSyncedTimestamp ?: 0L) == 0L
|
|
||||||
val due = trigger == SyncTrigger.MANUAL || neverCompleted ||
|
|
||||||
now - lastSync > SELECTED_PROJECT_SYNC_INTERVAL_MS
|
now - lastSync > SELECTED_PROJECT_SYNC_INTERVAL_MS
|
||||||
val fullSync = trigger == SyncTrigger.MANUAL &&
|
|
||||||
!partialRefreshPreferred(preferences, selectedProjectId, now)
|
|
||||||
if (selectedProj != null && due &&
|
if (selectedProj != null && due &&
|
||||||
db.cowspentServerSyncHelper.scheduleSync(false, selectedProj, fullSync) != null
|
db.cowspentServerSyncHelper.scheduleSync(
|
||||||
|
false, selectedProj, trigger == SyncTrigger.MANUAL
|
||||||
|
) != null
|
||||||
) {
|
) {
|
||||||
markProjectSynced(preferences, selectedProj.id, now)
|
markProjectSynced(preferences, selectedProj.id, now)
|
||||||
if (fullSync) markFullySynced(preferences, selectedProj.id, now)
|
|
||||||
1
|
1
|
||||||
} else 0
|
} else 0
|
||||||
}
|
}
|
||||||
@@ -847,30 +823,6 @@ class BillsListViewActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun lastProjectSyncKey(projectId: Long) = "lastProjectSyncTimestamp_$projectId"
|
private fun lastProjectSyncKey(projectId: Long) = "lastProjectSyncTimestamp_$projectId"
|
||||||
private fun lastFullSyncKey(projectId: Long) = "lastFullSyncTimestamp_$projectId"
|
|
||||||
|
|
||||||
private fun markFullySynced(preferences: SharedPreferences, projectId: Long, at: Long) {
|
|
||||||
preferences.edit { putLong(lastFullSyncKey(projectId), at) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether a pull to refresh should settle for the cheaper paged walk.
|
|
||||||
*
|
|
||||||
* A refresh by hand normally re-downloads the whole project, which is what makes it a usable
|
|
||||||
* repair when the local copy looks wrong. On a large project that is expensive to repeat, so
|
|
||||||
* with beta features on a refresh that follows a recent full one walks the recent pages
|
|
||||||
* instead - the common case of pulling twice in a row costs a page rather than the project.
|
|
||||||
*/
|
|
||||||
private fun partialRefreshPreferred(
|
|
||||||
preferences: SharedPreferences,
|
|
||||||
projectId: Long,
|
|
||||||
now: Long
|
|
||||||
): Boolean {
|
|
||||||
if (!preferences.getBoolean(getString(R.string.pref_key_beta_features), false)) return false
|
|
||||||
val lastFull = preferences.getLong(lastFullSyncKey(projectId), 0L)
|
|
||||||
if (lastFull == 0L) return false
|
|
||||||
return now - lastFull < SyncSettings.intervalMinutes(applicationContext) * 60 * 1000L
|
|
||||||
}
|
|
||||||
private fun markProjectSynced(preferences: SharedPreferences, projectId: Long, at: Long) {
|
private fun markProjectSynced(preferences: SharedPreferences, projectId: Long, at: Long) {
|
||||||
preferences.edit { putLong(lastProjectSyncKey(projectId), at) }
|
preferences.edit { putLong(lastProjectSyncKey(projectId), at) }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -448,26 +448,6 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
|
|
||||||
// --- Bills logic ---
|
// --- Bills logic ---
|
||||||
|
|
||||||
/**
|
|
||||||
* Runs [block] as one database transaction.
|
|
||||||
*
|
|
||||||
* Every write here commits on its own otherwise, which for a sync means a committed
|
|
||||||
* transaction per bill and another per ower - the dominant cost of a large project's first
|
|
||||||
* sync. Callers batch in chunks rather than wrapping everything: a sync interrupted halfway
|
|
||||||
* then keeps the chunks it already committed instead of rolling the lot back.
|
|
||||||
*/
|
|
||||||
fun <T> inTransaction(block: () -> T): T {
|
|
||||||
val db = writableDatabase
|
|
||||||
db.beginTransaction()
|
|
||||||
try {
|
|
||||||
val result = block()
|
|
||||||
db.setTransactionSuccessful()
|
|
||||||
return result
|
|
||||||
} finally {
|
|
||||||
db.endTransaction()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addBill(b: DBBill): Long {
|
fun addBill(b: DBBill): Long {
|
||||||
val db = writableDatabase
|
val db = writableDatabase
|
||||||
val values = ContentValues()
|
val values = ContentValues()
|
||||||
@@ -647,25 +627,11 @@ class CowspentSQLiteOpenHelper private constructor(val context: Context) :
|
|||||||
val cursor = db.query(table_bills, columnsBills, selection, selectionArgs, null, null, orderBy)
|
val cursor = db.query(table_bills, columnsBills, selection, selectionArgs, null, null, orderBy)
|
||||||
val bills: MutableList<DBBill> = ArrayList()
|
val bills: MutableList<DBBill> = ArrayList()
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
bills.add(getBillFromCursor(cursor))
|
val bill = getBillFromCursor(cursor)
|
||||||
|
bill.billOwers = getBillowersOfBill(bill.id)
|
||||||
|
bills.add(bill)
|
||||||
}
|
}
|
||||||
cursor.close()
|
cursor.close()
|
||||||
if (bills.isEmpty()) return bills
|
|
||||||
|
|
||||||
// Every ower of the matched bills in one query, keyed back to its bill.
|
|
||||||
//
|
|
||||||
// Fetching them per bill meant a project with thousands of bills issued thousands of
|
|
||||||
// queries on every list refresh - and that refresh runs on resume, on switching project,
|
|
||||||
// and after each sync. The bill ids are re-selected as a subquery rather than bound as
|
|
||||||
// arguments, which would blow past SQLite's limit on bound variables for a large project.
|
|
||||||
val owersByBill = getBillOwersCustom(
|
|
||||||
"$key_billId IN (SELECT $key_id FROM $table_bills WHERE $selection)",
|
|
||||||
selectionArgs,
|
|
||||||
null
|
|
||||||
).groupBy { it.billId }
|
|
||||||
for (bill in bills) {
|
|
||||||
bill.billOwers = owersByBill[bill.id].orEmpty()
|
|
||||||
}
|
|
||||||
return bills
|
return bills
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
private var syncActive = false
|
private var syncActive = false
|
||||||
private var syncAccountProjectsActive = false
|
private var syncAccountProjectsActive = false
|
||||||
|
|
||||||
|
private var callbacksPush: MutableList<ICallback> = ArrayList()
|
||||||
private var callbacksPull: MutableList<ICallback> = ArrayList()
|
private var callbacksPull: MutableList<ICallback> = ArrayList()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@@ -100,6 +101,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
if (project != null) {
|
if (project != null) {
|
||||||
Log.d(TAG, "... starting now")
|
Log.d(TAG, "... starting now")
|
||||||
val syncTask = SyncTask(onlyLocalChanges, project, forceFullSync)
|
val syncTask = SyncTask(onlyLocalChanges, project, forceFullSync)
|
||||||
|
syncTask.addCallbacks(callbacksPush)
|
||||||
|
callbacksPush = ArrayList()
|
||||||
if (!onlyLocalChanges) {
|
if (!onlyLocalChanges) {
|
||||||
syncTask.addCallbacks(callbacksPull)
|
syncTask.addCallbacks(callbacksPull)
|
||||||
callbacksPull = ArrayList()
|
callbacksPull = ArrayList()
|
||||||
@@ -111,8 +114,14 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
} else if (!onlyLocalChanges) {
|
} else if (!onlyLocalChanges) {
|
||||||
Log.d(TAG, "... scheduled")
|
Log.d(TAG, "... scheduled")
|
||||||
projectIdsToSync.add(projId)
|
projectIdsToSync.add(projId)
|
||||||
|
for (callback in callbacksPush) {
|
||||||
|
callback.onScheduled()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
Log.d(TAG, "... do nothing")
|
Log.d(TAG, "... do nothing")
|
||||||
|
for (callback in callbacksPush) {
|
||||||
|
callback.onScheduled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -799,9 +808,7 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
if (usePagedWalk) {
|
if (usePagedWalk) {
|
||||||
walkBillPages(idMaps, localBillsByRemoteId)?.let { return it }
|
walkBillPages(idMaps, localBillsByRemoteId)?.let { return it }
|
||||||
}
|
}
|
||||||
// A project with nothing stored has no cursor worth sending, so it asks for everything.
|
return fetchAllBills(idMaps)
|
||||||
val since = if (forceFullSync || localBillsByRemoteId.isEmpty()) 0L else null
|
|
||||||
return fetchAllBills(idMaps, since)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -876,14 +883,9 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
return RemoteBills(bills, emptyList(), syncTimestamp)
|
return RemoteBills(bills, emptyList(), syncTimestamp)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private fun fetchAllBills(idMaps: RemoteIdMaps): RemoteBills {
|
||||||
* One request for the whole collection. [since] 0 asks for everything; the project cursor
|
Log.d(TAG, "Starting full sync for project ${project.remoteId}")
|
||||||
* asks only for what changed. Either way the response carries the full id list, which is
|
val response = client!!.getBills(project)
|
||||||
* what lets vanished bills be removed locally.
|
|
||||||
*/
|
|
||||||
private fun fetchAllBills(idMaps: RemoteIdMaps, since: Long?): RemoteBills {
|
|
||||||
Log.d(TAG, "Fetching bills for ${project.remoteId} (since=${since ?: project.lastSyncedTimestamp})")
|
|
||||||
val response = client!!.getBills(project, since = since)
|
|
||||||
return if (project.type == ProjectType.IHATEMONEY) {
|
return if (project.type == ProjectType.IHATEMONEY) {
|
||||||
val bills = response.getBillsIHM(
|
val bills = response.getBillsIHM(
|
||||||
project.id, idMaps.members, idMaps.categories, idMaps.paymentModes
|
project.id, idMaps.members, idMaps.categories, idMaps.paymentModes
|
||||||
@@ -903,18 +905,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
private fun applyRemoteBills(
|
private fun applyRemoteBills(
|
||||||
remoteBills: List<DBBill>,
|
remoteBills: List<DBBill>,
|
||||||
localBillsByRemoteId: Map<Long, DBBill>
|
localBillsByRemoteId: Map<Long, DBBill>
|
||||||
) {
|
|
||||||
// Committed in chunks: one transaction per chunk turns a per-row commit into one write
|
|
||||||
// for the whole batch, while still leaving finished chunks on disk if the sync is cut
|
|
||||||
// short - a project part-way through is far better than one that rolled back.
|
|
||||||
remoteBills.chunked(BILL_APPLY_CHUNK).forEach { chunk ->
|
|
||||||
dbHelper.inTransaction { applyBillChunk(chunk, localBillsByRemoteId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun applyBillChunk(
|
|
||||||
remoteBills: List<DBBill>,
|
|
||||||
localBillsByRemoteId: Map<Long, DBBill>
|
|
||||||
) {
|
) {
|
||||||
for (remoteBill in remoteBills) {
|
for (remoteBill in remoteBills) {
|
||||||
val localBill = localBillsByRemoteId[remoteBill.remoteId]
|
val localBill = localBillsByRemoteId[remoteBill.remoteId]
|
||||||
@@ -976,10 +966,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
}
|
}
|
||||||
|
|
||||||
for (localBill in localBills) {
|
for (localBill in localBills) {
|
||||||
// A bill still waiting to be pushed has no remote id yet, so it is absent from the
|
|
||||||
// server list for the ordinary reason that the server has never seen it. Deleting
|
|
||||||
// it here would throw away a bill the user added while the sync was running.
|
|
||||||
if (localBill.state != DBBill.STATE_OK) continue
|
|
||||||
if (localBill.remoteId !in stillRemote) {
|
if (localBill.remoteId !in stillRemote) {
|
||||||
dbHelper.deleteBill(localBill.id)
|
dbHelper.deleteBill(localBill.id)
|
||||||
nbPulledDeletedBills++
|
nbPulledDeletedBills++
|
||||||
@@ -1815,9 +1801,6 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
|
|||||||
*/
|
*/
|
||||||
private const val UNCHANGED_RUN_TO_SETTLE = 25
|
private const val UNCHANGED_RUN_TO_SETTLE = 25
|
||||||
|
|
||||||
/** Bills written per transaction while applying a pull. */
|
|
||||||
private const val BILL_APPLY_CHUNK = 500
|
|
||||||
|
|
||||||
private var instance: CowspentServerSyncHelper? = null
|
private var instance: CowspentServerSyncHelper? = null
|
||||||
private val projectIdsToSync: MutableList<Long> = ArrayList()
|
private val projectIdsToSync: MutableList<Long> = ArrayList()
|
||||||
|
|
||||||
|
|||||||
@@ -689,18 +689,12 @@ class VersatileProjectSyncClient(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
|
@Throws(JSONException::class, IOException::class, TokenMismatchException::class, NextcloudHttpRequestFailedException::class)
|
||||||
/**
|
|
||||||
* [since] overrides the project cursor sent as lastChanged. Passing 0 asks the server for the
|
|
||||||
* whole project rather than the changes since the last sync; omitting it keeps the cursor, so
|
|
||||||
* the server returns only what has moved.
|
|
||||||
*/
|
|
||||||
fun getBills(
|
fun getBills(
|
||||||
project: DBProject,
|
project: DBProject,
|
||||||
offset: Int? = null,
|
offset: Int? = null,
|
||||||
limit: Int? = null,
|
limit: Int? = null,
|
||||||
reverse: Boolean? = null,
|
reverse: Boolean? = null,
|
||||||
deleted: Int? = null,
|
deleted: Int? = null
|
||||||
since: Long? = null
|
|
||||||
): ServerResponse.BillsResponse {
|
): ServerResponse.BillsResponse {
|
||||||
var target: String
|
var target: String
|
||||||
var username: String?
|
var username: String?
|
||||||
@@ -712,7 +706,7 @@ class VersatileProjectSyncClient(
|
|||||||
val paramValues: MutableList<String> = ArrayList()
|
val paramValues: MutableList<String> = ArrayList()
|
||||||
|
|
||||||
if (ProjectType.COSPEND == project.type) {
|
if (ProjectType.COSPEND == project.type) {
|
||||||
val tsLastSync = since ?: project.lastSyncedTimestamp
|
val tsLastSync = project.lastSyncedTimestamp
|
||||||
if (offset == null) {
|
if (offset == null) {
|
||||||
if (cospendVersionGT161) {
|
if (cospendVersionGT161) {
|
||||||
paramKeys.add("lastChanged")
|
paramKeys.add("lastChanged")
|
||||||
|
|||||||
Reference in New Issue
Block a user