Sync Flag lifecycle

This commit is contained in:
2026-09-10 00:26:55 +02:00
parent 06f31571a3
commit d6ee50bc34
@@ -14,6 +14,7 @@ import com.nextcloud.android.sso.api.NextcloudAPI
import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException
import com.nextcloud.android.sso.exceptions.TokenMismatchException import com.nextcloud.android.sso.exceptions.TokenMismatchException
import com.nextcloud.android.sso.helper.SingleAccountHelper import com.nextcloud.android.sso.helper.SingleAccountHelper
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
@@ -176,14 +177,22 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
} }
fun execute(): SyncTask { fun execute(): SyncTask {
deferred = scope.async {
syncActive = true syncActive = true
deferred = scope.async {
try {
val status = withContext(Dispatchers.IO) { val status = withContext(Dispatchers.IO) {
doWork() doWork()
} }
onPostExecute(status) onPostExecute(status)
syncActive = false
status status
} catch (e: CancellationException) {
syncActive = false
throw e
} catch (e: Exception) {
Log.e(TAG, "Sync failed for ${project.remoteId}", e)
syncActive = false
LoginStatus.CONNECTION_FAILED
}
} }
return this return this
} }
@@ -293,12 +302,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
null, null, null, null, null null, null, null, null, null
) )
} }
} catch (e: IOException) { } catch (e: Exception) {
if (e.message == "{\"message\": \"Internal Server Error\"}") { Log.e(TAG, "EDIT MEMBER FAILED for ${mToEdit.name}", e)
Log.d(TAG, "EDIT MEMBER FAILED : it does not exist remotely")
} else {
throw e
}
} }
} }
@@ -1048,6 +1053,8 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
for (localMember in dbHelper.getMembersOfProject(project.id, null)) { for (localMember in dbHelper.getMembersOfProject(project.id, null)) {
if (remoteMembersByRemoteId.containsKey(localMember.remoteId)) continue if (remoteMembersByRemoteId.containsKey(localMember.remoteId)) continue
if (localMember.state != DBBill.STATE_OK) continue
// A member still named by a bill cannot be removed without orphaning it. // A member still named by a bill cannot be removed without orphaning it.
if (dbHelper.getBillsOfMember(localMember.id).isEmpty() && if (dbHelper.getBillsOfMember(localMember.id).isEmpty() &&
dbHelper.getBillowersOfMember(localMember.id).isEmpty() dbHelper.getBillowersOfMember(localMember.id).isEmpty()
@@ -1514,12 +1521,19 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
fun execute(): SyncAccountProjectsTask { fun execute(): SyncAccountProjectsTask {
scope.launch { scope.launch {
syncAccountProjectsActive = true syncAccountProjectsActive = true
try {
val status = withContext(Dispatchers.IO) { val status = withContext(Dispatchers.IO) {
doWork() doWork()
} }
onPostExecute(status) onPostExecute(status)
} catch (e: CancellationException) {
throw e
} catch (e: Exception) {
Log.e(TAG, "Account projects sync failed", e)
} finally {
syncAccountProjectsActive = false syncAccountProjectsActive = false
} }
}
return this return this
} }
@@ -1547,20 +1561,25 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
val response = client.getAccountProjects(useOcsApi) val response = client.getAccountProjects(useOcsApi)
val remoteAccountProjects = response.getAccountProjects(url) val remoteAccountProjects = response.getAccountProjects(url)
val forgotten = forgottenAccountProjects(preferences)
dbHelper.clearAccountProjects() dbHelper.clearAccountProjects()
for (remoteAccountProject in remoteAccountProjects) { for (remoteAccountProject in remoteAccountProjects) {
dbHelper.addAccountProject(remoteAccountProject) dbHelper.addAccountProject(remoteAccountProject)
Log.v(TAG, "received account project $remoteAccountProject") Log.v(TAG, "received account project $remoteAccountProject")
val existingProj = localProjects.find { val existingProj = localProjects.find {
it.remoteId == remoteAccountProject.remoteId && it.remoteId == remoteAccountProject.remoteId &&
it.serverUrl?.replace("/+$".toRegex(), "") == remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + "/index.php/apps/cospend" it.serverUrl?.replace("/+$".toRegex(), "") == remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + COSPEND_PATH
} }
if (existingProj == null) { if (existingProj == null) {
if (accountProjectKey(remoteAccountProject.remoteId, remoteAccountProject.ncUrl) in forgotten) {
Log.d(TAG, "skipping ${remoteAccountProject.remoteId}, deleted on this device")
continue
}
val newProj = DBProject(0, val newProj = DBProject(0,
remoteAccountProject.remoteId, remoteAccountProject.remoteId,
"", "",
remoteAccountProject.name, remoteAccountProject.name,
remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + "/index.php/apps/cospend", remoteAccountProject.ncUrl.replace("/+$".toRegex(), "") + COSPEND_PATH,
"", "",
null, null,
ProjectType.COSPEND, ProjectType.COSPEND,
@@ -1896,6 +1915,34 @@ class CowspentServerSyncHelper private constructor(private val dbHelper: Cowspen
preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false) preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)
} }
/** The path a Cospend project's URL carries on top of its Nextcloud server URL. */
const val COSPEND_PATH = "/index.php/apps/cospend"
private const val FORGOTTEN_ACCOUNT_PROJECTS = "forgottenAccountProjects"
private fun trimSlashes(url: String) = url.replace("/+$".toRegex(), "")
/** Names one project an account offers, by server and remote id, with no local row needed. */
private fun accountProjectKey(remoteId: String, ncUrl: String) = "${trimSlashes(ncUrl)}|$remoteId"
/** The same key for a stored project, or null when it is not one an account can offer. */
private fun accountProjectKey(project: DBProject): String? {
val url = trimSlashes(project.serverUrl.orEmpty())
if (!url.endsWith(COSPEND_PATH)) return null
return accountProjectKey(project.remoteId, url.removeSuffix(COSPEND_PATH))
}
private fun forgottenAccountProjects(preferences: SharedPreferences): Set<String> =
preferences.getStringSet(FORGOTTEN_ACCOUNT_PROJECTS, emptySet()).orEmpty()
fun forgetAccountProject(context: Context, project: DBProject) {
val key = accountProjectKey(project) ?: return
val preferences = PreferenceManager.getDefaultSharedPreferences(context)
preferences.edit {
putStringSet(FORGOTTEN_ACCOUNT_PROJECTS, forgottenAccountProjects(preferences) + key)
}
}
fun getNextcloudAccountServerUrl(context: Context): String { fun getNextcloudAccountServerUrl(context: Context): String {
val preferences = PreferenceManager.getDefaultSharedPreferences(context) val preferences = PreferenceManager.getDefaultSharedPreferences(context)
return if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) { return if (preferences.getBoolean(AccountActivity.SETTINGS_USE_SSO, false)) {