Loading app/build.gradle.kts +1 −1 Original line number Diff line number Diff line Loading @@ -30,7 +30,7 @@ android { base.archivesName = "davx5-ose-$versionName" minSdk = 24 // Android 7.0 minSdk = 31 // Android 12 - canScheduleExactAlarms minimum requirement targetSdk = 36 // Android 16 // whether the build supports and allows to use custom certificates Loading app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt +3 −0 Original line number Diff line number Diff line Loading @@ -112,6 +112,9 @@ class AccountRepository @Inject constructor( // set up automatic sync (processes inserted services) automaticSyncManager.get().updateAutomaticSync(account) // Schedule sync AccountHelper.scheduleSyncWithDelay(context) } catch(e: InvalidAccountException) { logger.log(Level.SEVERE, "Couldn't access account settings", e) return null Loading app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt +7 −4 Original line number Diff line number Diff line Loading @@ -764,11 +764,10 @@ abstract class SyncManager<LocalType: LocalResource, out CollectionType: LocalCo */ private fun handleException(e: Throwable, local: LocalResource?, remote: HttpUrl?) { var message: String var isNetworkAvailable = true val isNetworkAvailable = SystemUtils.isNetworkAvailable(context) when (e) { is IOException -> { logger.log(Level.WARNING, "I/O error", e) isNetworkAvailable = SystemUtils.isNetworkAvailable(context) if (isNetworkAvailable) { syncResult.numIoExceptions++ } Loading @@ -777,13 +776,17 @@ abstract class SyncManager<LocalType: LocalResource, out CollectionType: LocalCo is UnauthorizedException -> { logger.log(Level.SEVERE, "Not authorized anymore", e) if (isNetworkAvailable) { syncResult.numAuthExceptions++ } message = context.getString(R.string.sync_error_authentication_failed) } is HttpException, is DavException -> { logger.log(Level.SEVERE, "HTTP/DAV exception", e) if (isNetworkAvailable) { syncResult.numHttpExceptions++ } message = context.getString(R.string.sync_error_http_dav, e.localizedMessage) } Loading app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt +2 −0 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.accountmanager.utils.AccountHelper import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow Loading Loading @@ -234,6 +235,7 @@ class AccountSettingsModel @AssistedInject constructor( fun updateCredentials(credentials: Credentials) = CoroutineScope(defaultDispatcher).launch { accountSettings.credentials(credentials) AccountHelper.syncMailAccounts(context) reload() } Loading app/src/main/kotlin/foundation/e/accountmanager/sync/OneTimeSyncModel.kt 0 → 100644 +96 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 eFoundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.accountmanager.sync import android.accounts.AccountManager import android.content.Context import androidx.work.WorkManager import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings.Companion.KEY_SETTINGS_VERSION import at.bitfire.davdroid.sync.worker.SyncWorkerManager import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject class OneTimeSyncModel @Inject constructor( private val accountRepository: AccountRepository, private val serviceRepository: DavServiceRepository, @ApplicationContext val context: Context, private val syncWorkerManager: SyncWorkerManager ) { fun requestSync() { CoroutineScope(Dispatchers.Default).launch { val workManager = WorkManager.getInstance(context) val accountManager = AccountManager.get(context) val workNames = mutableListOf<String>() val allAccounts = accountRepository.getAll() val startTime = System.currentTimeMillis() for (service in serviceRepository.getAll()) { val account = retry(maxRetries = 3, delayMs = 2000) { allAccounts.find { it.name == service.accountName } } var version: String? = null retry(maxRetries = 3, delayMs = 2000) { account?.let { version = accountManager.getUserData(it, KEY_SETTINGS_VERSION) } } if (version != null) { val (name, _) = RefreshCollectionsWorker.enqueue(context, service.id) workNames.add(name) } } for (name in workNames) { waitForUniqueWorkToFinish(workManager, name) } val elapsed = System.currentTimeMillis() - startTime if (elapsed < 3_000) delay(3_000 - elapsed) for (account in allAccounts) { syncWorkerManager.enqueueOneTimeAllAuthorities(account, manual = true) } } } private suspend fun <T> retry(maxRetries: Int, delayMs: Long, block: suspend () -> T?): T? { repeat(maxRetries - 1) { block()?.let { return it } delay(delayMs) } return block() } private suspend fun waitForUniqueWorkToFinish(workManager: WorkManager, uniqueWorkName: String) { while (true) { val infos = workManager.getWorkInfosForUniqueWork(uniqueWorkName).get() if (infos.all { it.state.isFinished }) break delay(500) } } } No newline at end of file Loading
app/build.gradle.kts +1 −1 Original line number Diff line number Diff line Loading @@ -30,7 +30,7 @@ android { base.archivesName = "davx5-ose-$versionName" minSdk = 24 // Android 7.0 minSdk = 31 // Android 12 - canScheduleExactAlarms minimum requirement targetSdk = 36 // Android 16 // whether the build supports and allows to use custom certificates Loading
app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt +3 −0 Original line number Diff line number Diff line Loading @@ -112,6 +112,9 @@ class AccountRepository @Inject constructor( // set up automatic sync (processes inserted services) automaticSyncManager.get().updateAutomaticSync(account) // Schedule sync AccountHelper.scheduleSyncWithDelay(context) } catch(e: InvalidAccountException) { logger.log(Level.SEVERE, "Couldn't access account settings", e) return null Loading
app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt +7 −4 Original line number Diff line number Diff line Loading @@ -764,11 +764,10 @@ abstract class SyncManager<LocalType: LocalResource, out CollectionType: LocalCo */ private fun handleException(e: Throwable, local: LocalResource?, remote: HttpUrl?) { var message: String var isNetworkAvailable = true val isNetworkAvailable = SystemUtils.isNetworkAvailable(context) when (e) { is IOException -> { logger.log(Level.WARNING, "I/O error", e) isNetworkAvailable = SystemUtils.isNetworkAvailable(context) if (isNetworkAvailable) { syncResult.numIoExceptions++ } Loading @@ -777,13 +776,17 @@ abstract class SyncManager<LocalType: LocalResource, out CollectionType: LocalCo is UnauthorizedException -> { logger.log(Level.SEVERE, "Not authorized anymore", e) if (isNetworkAvailable) { syncResult.numAuthExceptions++ } message = context.getString(R.string.sync_error_authentication_failed) } is HttpException, is DavException -> { logger.log(Level.SEVERE, "HTTP/DAV exception", e) if (isNetworkAvailable) { syncResult.numHttpExceptions++ } message = context.getString(R.string.sync_error_http_dav, e.localizedMessage) } Loading
app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt +2 −0 Original line number Diff line number Diff line Loading @@ -26,6 +26,7 @@ import dagger.assisted.AssistedFactory import dagger.assisted.AssistedInject import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.qualifiers.ApplicationContext import foundation.e.accountmanager.utils.AccountHelper import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.MutableStateFlow Loading Loading @@ -234,6 +235,7 @@ class AccountSettingsModel @AssistedInject constructor( fun updateCredentials(credentials: Credentials) = CoroutineScope(defaultDispatcher).launch { accountSettings.credentials(credentials) AccountHelper.syncMailAccounts(context) reload() } Loading
app/src/main/kotlin/foundation/e/accountmanager/sync/OneTimeSyncModel.kt 0 → 100644 +96 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 eFoundation * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <https://www.gnu.org/licenses/>. * */ package foundation.e.accountmanager.sync import android.accounts.AccountManager import android.content.Context import androidx.work.WorkManager import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings.Companion.KEY_SETTINGS_VERSION import at.bitfire.davdroid.sync.worker.SyncWorkerManager import dagger.hilt.android.qualifiers.ApplicationContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.launch import javax.inject.Inject class OneTimeSyncModel @Inject constructor( private val accountRepository: AccountRepository, private val serviceRepository: DavServiceRepository, @ApplicationContext val context: Context, private val syncWorkerManager: SyncWorkerManager ) { fun requestSync() { CoroutineScope(Dispatchers.Default).launch { val workManager = WorkManager.getInstance(context) val accountManager = AccountManager.get(context) val workNames = mutableListOf<String>() val allAccounts = accountRepository.getAll() val startTime = System.currentTimeMillis() for (service in serviceRepository.getAll()) { val account = retry(maxRetries = 3, delayMs = 2000) { allAccounts.find { it.name == service.accountName } } var version: String? = null retry(maxRetries = 3, delayMs = 2000) { account?.let { version = accountManager.getUserData(it, KEY_SETTINGS_VERSION) } } if (version != null) { val (name, _) = RefreshCollectionsWorker.enqueue(context, service.id) workNames.add(name) } } for (name in workNames) { waitForUniqueWorkToFinish(workManager, name) } val elapsed = System.currentTimeMillis() - startTime if (elapsed < 3_000) delay(3_000 - elapsed) for (account in allAccounts) { syncWorkerManager.enqueueOneTimeAllAuthorities(account, manual = true) } } } private suspend fun <T> retry(maxRetries: Int, delayMs: Long, block: suspend () -> T?): T? { repeat(maxRetries - 1) { block()?.let { return it } delay(delayMs) } return block() } private suspend fun waitForUniqueWorkToFinish(workManager: WorkManager, uniqueWorkName: String) { while (true) { val infos = workManager.getWorkInfosForUniqueWork(uniqueWorkName).get() if (infos.all { it.state.isFinished }) break delay(500) } } } No newline at end of file