diff --git a/app/.gitignore b/app/.gitignore
index 6eeec870def36743f89ef0fb28d39e024627195a..ccccca9c48ffadc5b788ebe3a93d96b4d7820e3e 100644
--- a/app/.gitignore
+++ b/app/.gitignore
@@ -1,2 +1,3 @@
build
target
+ose
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 208a90be9c2e5583757a516907a6342eaec62670..70193ab13adfe9c04b1565cf001eccde5620b8b0 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -30,7 +30,7 @@ android {
setProperty("archivesBaseName", "davx5-ose-$versionName")
- minSdk = 24 // Android 7.0
+ minSdk = 31 // Android 12 - canScheduleExactAlarms minimum requirement
targetSdk = 36 // Android 16
buildConfigField("boolean", "customCertsUI", "true")
@@ -179,6 +179,15 @@ android {
}
}
}
+
+ splits {
+ abi {
+ isEnable = true
+ reset()
+ include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
+ isUniversalApk = false
+ }
+ }
}
fun retrieveKey(keyName: String): String {
@@ -272,7 +281,7 @@ dependencies {
// See: https://codeberg.org/UnifiedPush/android-connector/src/commit/28cb0d622ed0a972996041ab9cc85b701abc48c6/connector/build.gradle#L56-L59
exclude(group = "com.google.crypto.tink", module = "tink")
}
- implementation(libs.unifiedpush.fcm)
+ //implementation(libs.unifiedpush.fcm)
// force some versions for compatibility with our minSdk level (see version catalog for details)
implementation(libs.commons.codec)
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 6cf7d65d7f83ab2d8d3aee8a8a2be26ab47216bf..50d0409501e22946670c9f32bcfec015f5b888e4 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -78,7 +78,6 @@
android:exported="true">
-
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt
index 282a3baba2f3ff9336dc75f08be6f637d0712a6c..0420512b99a9f99c861ee90d4d8dfc35ed5fbe66 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt
@@ -108,6 +108,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
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt
index 5d48e1da45d09dc0804e24408cfc99866ce21d97..1cf93cea8f03021f6e343b5cd341c98050049d3f 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/servicedetection/RefreshCollectionsWorker.kt
@@ -37,8 +37,11 @@ import dagger.assisted.AssistedInject
import foundation.e.accountmanager.AccountTypes
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.runInterruptible
+import java.io.IOException
+import java.io.InterruptedIOException
import java.util.logging.Level
import java.util.logging.Logger
+import kotlin.coroutines.cancellation.CancellationException
/**
* Refreshes list of home sets and their respective collections of a service type (CardDAV or CalDAV).
@@ -195,18 +198,30 @@ class RefreshCollectionsWorker @AssistedInject constructor(
settingsIntent
)
return Result.failure()
- } catch(e: Exception) {
- logger.log(Level.SEVERE, "Couldn't refresh collection list", e)
+ } catch (e: Exception) {
+ when (e) {
+ is CancellationException -> throw e
- val debugIntent = DebugInfoActivity.IntentBuilder(applicationContext)
- .withCause(e)
- .withAccount(account)
- .build()
- notifyRefreshError(
- applicationContext.getString(R.string.refresh_collections_worker_refresh_couldnt_refresh),
- debugIntent
- )
- return Result.failure()
+ is IOException -> {
+ logger.log(Level.WARNING, "I/O issue while refreshing collection list", e)
+ return Result.failure()
+ }
+
+ else -> {
+ logger.log(Level.SEVERE, "Couldn't refresh collection list", e)
+
+ val debugIntent = DebugInfoActivity.IntentBuilder(applicationContext)
+ .withCause(e)
+ .withAccount(account)
+ .build()
+
+ notifyRefreshError(
+ applicationContext.getString(R.string.refresh_collections_worker_refresh_couldnt_refresh),
+ debugIntent
+ )
+ return Result.failure()
+ }
+ }
}
// update push registrations
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt
index d26e1a1a73e365ecd281eb96cb0ea6f418907050..b114c024bdcf204a3a0b009572e0b6f3a9e66914 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt
@@ -390,7 +390,7 @@ class AccountSettings @AssistedInject constructor(
* - n>0: entries more than n days in the past won't be synchronized
*/
const val KEY_TIME_RANGE_PAST_DAYS = "time_range_past_days"
- const val DEFAULT_TIME_RANGE_PAST_DAYS = 90
+ const val DEFAULT_TIME_RANGE_PAST_DAYS = 365
/**
* Whether a default alarm shall be assigned to received events/tasks which don't have an alarm.
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt
index f2ae8898a3a0e276448ab63f959a746356014201..722f955a4d3191b82350b91d2f22ed363512d586 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncManager.kt
@@ -734,11 +734,10 @@ abstract class SyncManager, out CollectionType: L
*/
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++
}
@@ -747,13 +746,17 @@ abstract class SyncManager, out CollectionType: L
is UnauthorizedException -> {
logger.log(Level.SEVERE, "Not authorized anymore", e)
- syncResult.numAuthExceptions++
+ 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)
- syncResult.numHttpExceptions++
+ if (isNetworkAvailable) {
+ syncResult.numHttpExceptions++
+ }
message = context.getString(R.string.sync_error_http_dav, e.localizedMessage)
}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterImpl.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterImpl.kt
index 2b956065dd1321b397e8ab2e93a687de6a487bba..270457b5643c6ba267c971f7eb8bfe0fba1d9992 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterImpl.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncAdapterImpl.kt
@@ -117,15 +117,6 @@ class SyncAdapterImpl @Inject constructor(
logger.fine("Starting OneTimeSyncWorker for $account $authority and waiting for it")
val workerName = syncWorkerManager.enqueueOneTime(account, dataType = SyncDataType.Companion.fromAuthority(authority), fromUpload = upload)
- // Android 14+ does not handle pending sync state correctly.
- // As a defensive workaround, we can cancel specifically this still pending sync only
- // See: https://github.com/bitfireAT/davx5-ose/issues/1458
- if (Build.VERSION.SDK_INT >= 34) {
- logger.fine("Android 14+ bug: Canceling forever pending sync adapter framework sync request for " +
- "account=$account authority=$authority upload=$upload")
- syncFrameworkIntegration.cancelSync(account, authority, extras)
- }
-
/* Because we are not allowed to observe worker state on a background thread, we can not
use it to block the sync adapter. Instead we use a Flow to get notified when the sync
has finished. */
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt
index 539ea2bec844c1cd8dd30dd83bcab446f480ef71..2e05e7bdaabc4b5024be4e209ea4b03794565bed 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/adapter/SyncFrameworkIntegration.kt
@@ -183,38 +183,34 @@ class SyncFrameworkIntegration @Inject constructor(
* @return flow emitting true if any of the given data types has a sync pending, false otherwise
*/
@OptIn(ExperimentalCoroutinesApi::class)
- fun isSyncPending(account: Account, dataTypes: Iterable): Flow =
- if (Build.VERSION.SDK_INT >= 34) {
- // On Android 14+ pending sync checks always return true (bug), so we don't need to check.
- // See: https://github.com/bitfireAT/davx5-ose/issues/1458
- flowOf(false)
- } else {
- val authorities = dataTypes.flatMap { it.possibleAuthorities() }
-
- // Use address book accounts if needed
- val accountsFlow = if (dataTypes.contains(SyncDataType.CONTACTS))
- localAddressBookStore.get().getAddressBookAccountsFlow(account)
- else
- flowOf(listOf(account))
-
- // Observe sync pending state for the given accounts and authorities
- accountsFlow.flatMapLatest { accounts ->
- callbackFlow {
- // Observe sync pending state
- val listener = ContentResolver.addStatusChangeListener(
- ContentResolver.SYNC_OBSERVER_TYPE_PENDING
- ) {
+ fun isSyncPending(account: Account, dataTypes: Iterable): Flow {
+ val authorities = dataTypes.flatMap { it.possibleAuthorities() }
+
+ val accountsFlow = if (dataTypes.contains(SyncDataType.CONTACTS))
+ localAddressBookStore.get().getAddressBookAccountsFlow(account)
+ else
+ flowOf(listOf(account))
+
+ return accountsFlow.flatMapLatest { accounts ->
+ callbackFlow {
+ val listener = ContentResolver.addStatusChangeListener(
+ ContentResolver.SYNC_OBSERVER_TYPE_PENDING
+ ) {
+ runCatching {
trySend(anyPendingSync(accounts, authorities))
}
+ }
- // Emit initial value
- trySend(anyPendingSync(accounts, authorities))
+ // Emit initial value
+ runCatching { trySend(anyPendingSync(accounts, authorities)) }
- // Clean up listener on close
- awaitClose { ContentResolver.removeStatusChangeListener(listener) }
+ awaitClose {
+ ContentResolver.removeStatusChangeListener(listener)
}
- }.distinctUntilChanged()
- }
+ }
+ }.distinctUntilChanged()
+ }
+
/**
* Check if any of the given accounts and authorities have a sync pending.
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt
index 9114462200848c4999a33703bfa89cb0d1a1b05f..39f63b2e0064ef7bab844fd57f108b5a1bdf3cd6 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsDrawerHandler.kt
@@ -5,6 +5,7 @@
package at.bitfire.davdroid.ui
import android.content.ActivityNotFoundException
+import android.content.ComponentName
import android.content.Context
import android.content.Intent
import androidx.annotation.StringRes
@@ -21,8 +22,10 @@ import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.filled.EditCalendar
import androidx.compose.material.icons.filled.Feedback
import androidx.compose.material.icons.filled.Info
+import androidx.compose.material.icons.filled.Policy
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Storage
import androidx.compose.material3.HorizontalDivider
@@ -54,6 +57,8 @@ import androidx.core.net.toUri
import at.bitfire.davdroid.BuildConfig
import at.bitfire.davdroid.R
import at.bitfire.davdroid.ui.webdav.WebdavMountsActivity
+import foundation.e.accountmanager.ui.PrivacyPolicyActivity
+import foundation.e.accountmanager.utils.AppConstants
import kotlinx.coroutines.launch
import java.net.URI
@@ -119,6 +124,14 @@ abstract class AccountsDrawerHandler {
}
)
+ MenuEntry(
+ icon = Icons.Default.Policy,
+ title = stringResource(R.string.privacy_policy_title_nav),
+ onClick = {
+ context.startActivity(Intent(context, PrivacyPolicyActivity::class.java))
+ }
+ )
+
if (isBeta)
MenuEntry(
icon = Icons.Default.Feedback,
@@ -157,6 +170,18 @@ abstract class AccountsDrawerHandler {
context.startActivity(Intent(context, WebdavMountsActivity::class.java))
}
)
+ MenuEntry(
+ icon = Icons.Default.EditCalendar,
+ title = stringResource(R.string.navigation_drawer_open_webcalmanager),
+ onClick = {
+ val intent = Intent(Intent.ACTION_MAIN)
+ intent.component = ComponentName(
+ AppConstants.WEBCAL_MANAGER_PACKAGE,
+ AppConstants.WEBCAL_MANAGER_ACTIVITY
+ )
+ context.startActivity(intent)
+ }
+ )
}
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt
index 3a2ad77454e7550cba090fde5fb3754ae2bda36d..c91142a76579168d6a6545cc87a04b14ef31956e 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/OseAccountsDrawerHandler.kt
@@ -37,6 +37,7 @@ open class OseAccountsDrawerHandler @Inject constructor(): AccountsDrawerHandler
ImportantEntries(snackbarHostState)
// News
+ /*
MenuHeading(R.string.navigation_drawer_news_updates)
MenuEntry(
icon = painterResource(R.drawable.mastodon),
@@ -45,11 +46,13 @@ open class OseAccountsDrawerHandler @Inject constructor(): AccountsDrawerHandler
uriHandler.openUri(Social.fediverseUrl.toString())
}
)
+ */
// Tools
Tools()
// Support the project
+ /*
MenuHeading(R.string.navigation_drawer_support_project)
Contribute(onContribute = {
uriHandler.openUri(
@@ -66,9 +69,11 @@ open class OseAccountsDrawerHandler @Inject constructor(): AccountsDrawerHandler
uriHandler.openUri(Social.discussionsUrl.toString())
}
)
+ */
// External links
+ /*
MenuHeading(R.string.navigation_drawer_external_links)
MenuEntry(
icon = Icons.Default.Home,
@@ -125,6 +130,7 @@ open class OseAccountsDrawerHandler @Inject constructor(): AccountsDrawerHandler
)
}
)
+ */
}
@Composable
diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt
index 539d202d69d54e63a2016dc9befb768a6d812f34..84146d0c8c78c6d08d9e5ac32cab3e7a0f2a837e 100644
--- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt
+++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsModel.kt
@@ -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
@@ -234,6 +235,7 @@ class AccountSettingsModel @AssistedInject constructor(
fun updateCredentials(credentials: Credentials) = CoroutineScope(defaultDispatcher).launch {
accountSettings.credentials(credentials)
+ AccountHelper.syncMailAccounts(context)
reload()
}
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/AccountTypes.kt b/app/src/main/kotlin/foundation/e/accountmanager/AccountTypes.kt
index 50e1ec65eb6956d2d6bd0fbcd9f49ecaf203a25b..fdce88f3fc22216b8c3b17e45f6aac8cc4309715 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/AccountTypes.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/AccountTypes.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/Authority.kt b/app/src/main/kotlin/foundation/e/accountmanager/Authority.kt
index 71ba6de4caffc6e403d57867b7a1c98503d302d1..324ec3063dd3103e89093d5f1818e080d7af2155 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/Authority.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/Authority.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/auth/AccountReceiver.kt b/app/src/main/kotlin/foundation/e/accountmanager/auth/AccountReceiver.kt
index d212a690853fb3da998ddf47a5df361709e09a00..8603d6117617a6e81911733ba3ee9264130a6882 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/auth/AccountReceiver.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/auth/AccountReceiver.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/network/OAuthMurena.kt b/app/src/main/kotlin/foundation/e/accountmanager/network/OAuthMurena.kt
index e053bb7dd0286502c9bf8c633f4266ad6c010f10..eb6273e1babcddb2b6574f152699b19f3188f76d 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/network/OAuthMurena.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/network/OAuthMurena.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/pref/AuthStatePrefUtils.kt b/app/src/main/kotlin/foundation/e/accountmanager/pref/AuthStatePrefUtils.kt
index db5f3c71dfeab6d714747d4e3e8eaf10d0c5cb8a..ff70c93c0cfa614251df00af69edfdc7d091f069 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/pref/AuthStatePrefUtils.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/pref/AuthStatePrefUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/sync/OneTimeSyncModel.kt b/app/src/main/kotlin/foundation/e/accountmanager/sync/OneTimeSyncModel.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b065c6992834e1e2644fe5f7c1b918081a87830b
--- /dev/null
+++ b/app/src/main/kotlin/foundation/e/accountmanager/sync/OneTimeSyncModel.kt
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2025 e Foundation
+ *
+ * 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 .
+ *
+ */
+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()
+ 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 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/sync/OneTimeSyncModelEntryPoint.kt b/app/src/main/kotlin/foundation/e/accountmanager/sync/OneTimeSyncModelEntryPoint.kt
new file mode 100644
index 0000000000000000000000000000000000000000..b2dae8f5cbb3b46af6f5bf4fb24bd8d02ec351dc
--- /dev/null
+++ b/app/src/main/kotlin/foundation/e/accountmanager/sync/OneTimeSyncModelEntryPoint.kt
@@ -0,0 +1,28 @@
+/*
+ * Copyright (C) 2025 e Foundation
+ *
+ * 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 .
+ *
+ */
+package foundation.e.accountmanager.sync
+
+import dagger.hilt.EntryPoint
+import dagger.hilt.InstallIn
+import dagger.hilt.components.SingletonComponent
+
+@EntryPoint
+@InstallIn(SingletonComponent::class)
+interface OneTimeSyncModelEntryPoint {
+ fun syncModel(): OneTimeSyncModel
+}
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/sync/SyncBroadcastReceiver.kt b/app/src/main/kotlin/foundation/e/accountmanager/sync/SyncBroadcastReceiver.kt
new file mode 100644
index 0000000000000000000000000000000000000000..c39faba72fe1b9de8a11007fe5115202e1e25930
--- /dev/null
+++ b/app/src/main/kotlin/foundation/e/accountmanager/sync/SyncBroadcastReceiver.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2025 e Foundation
+ *
+ * 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 .
+ *
+ */
+package foundation.e.accountmanager.sync
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.util.Log
+import dagger.hilt.android.EntryPointAccessors
+
+class SyncBroadcastReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context, intent: Intent?) {
+ val entryPoint = EntryPointAccessors.fromApplication(context)
+ val syncModel = entryPoint.syncModel()
+ syncModel.requestSync()
+ }
+}
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/sync/account/MurenaAccountAuthenticatorService.kt b/app/src/main/kotlin/foundation/e/accountmanager/sync/account/MurenaAccountAuthenticatorService.kt
index 731b8eaaacfdb4b60c0c136ca2469df219ea4d92..ee35c4a693fe5df7fb81ae2a0aeedcc04c519bd8 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/sync/account/MurenaAccountAuthenticatorService.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/sync/account/MurenaAccountAuthenticatorService.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/sync/account/MurenaAddressBookAuthenticatorService.kt b/app/src/main/kotlin/foundation/e/accountmanager/sync/account/MurenaAddressBookAuthenticatorService.kt
index 3527e4d3eeb272716cac5fb28ca2d292990468f6..6ca351412fe064707383e17142e5768ce56d0cfd 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/sync/account/MurenaAddressBookAuthenticatorService.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/sync/account/MurenaAddressBookAuthenticatorService.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/ui/PrivacyPolicyActivity.kt b/app/src/main/kotlin/foundation/e/accountmanager/ui/PrivacyPolicyActivity.kt
new file mode 100644
index 0000000000000000000000000000000000000000..09894800e2edfd9a408bf7a36df18b6da963aa11
--- /dev/null
+++ b/app/src/main/kotlin/foundation/e/accountmanager/ui/PrivacyPolicyActivity.kt
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2025 e Foundation
+ *
+ * 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 .
+ *
+ */
+package foundation.e.accountmanager.ui
+
+import android.os.Bundle
+import androidx.activity.ComponentActivity
+import foundation.e.accountmanager.utils.AppConstants
+import foundation.e.accountmanager.utils.WebViewUtils
+
+
+class PrivacyPolicyActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ WebViewUtils.openCustomTab(this, AppConstants.PRIVACY_POLICY_URL)
+ finishAfterTransition() // Finish the activity after launching the custom tab
+ }
+}
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/ui/components/ESwitch.kt b/app/src/main/kotlin/foundation/e/accountmanager/ui/components/ESwitch.kt
index 6a678300c291bb2603e209040d9e58ff62da9ac6..26754c4c12db7f240e792dde96c954004c027f7e 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/ui/components/ESwitch.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/ui/components/ESwitch.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaAccountDetailsPageContent.kt b/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaAccountDetailsPageContent.kt
index 11726a069180a21ca5ed70ac7a751cb30bebc9c8..731bd336040e32eb1cc6c7c58632c3508efe7377 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaAccountDetailsPageContent.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaAccountDetailsPageContent.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogin.kt b/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogin.kt
index 56ceec360ff613f1e550ff6a7c182665ee476e1f..651d9a2fb383e6e4b998e54045b9208f9d54f02c 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogin.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogin.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLoginModel.kt b/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLoginModel.kt
index dbe8ea322df2386659b394dc07449827a130ae39..5d59c33aa071eba01f18e89b4f4fb4f5b3a626eb 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLoginModel.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLoginModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogoutActivity.kt b/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogoutActivity.kt
index 5d37d606b40a03632e159f09704ec479e8438aa4..64862d040f47ad73ce23b4ed8c8c981b465bc5d6 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogoutActivity.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/ui/setup/MurenaLogoutActivity.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/utils/AccountHelper.kt b/app/src/main/kotlin/foundation/e/accountmanager/utils/AccountHelper.kt
index 9f93a6eb79853f955aba3fb750d3a87580f40299..9ae52d232c96bb04cc2a78ab4d7830a93220e1c8 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/utils/AccountHelper.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/utils/AccountHelper.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
@@ -19,10 +19,15 @@ package foundation.e.accountmanager.utils
import android.accounts.Account
import android.accounts.AccountManager
+import android.app.AlarmManager
+import android.app.PendingIntent
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.os.Build
import foundation.e.accountmanager.AccountTypes
+import foundation.e.accountmanager.sync.SyncBroadcastReceiver
+import java.util.concurrent.TimeUnit
object AccountHelper {
private const val MAIL_PACKAGE = "foundation.e.mail"
@@ -52,4 +57,21 @@ object AccountHelper {
intent.action = ACTION_PREFIX + "create"
context.sendBroadcast(intent)
}
+
+ fun scheduleSyncWithDelay(context: Context) {
+ val intent = Intent(context, SyncBroadcastReceiver::class.java)
+ val pendingIntent = PendingIntent.getBroadcast(
+ context,
+ 0,
+ intent,
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
+ val triggerTime = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(AppConstants.SYNC_DELAY_SECONDS)
+
+ if (alarmManager.canScheduleExactAlarms()) {
+ alarmManager.setExact(AlarmManager.RTC_WAKEUP, triggerTime, pendingIntent)
+ }
+ }
}
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/utils/AppConstants.kt b/app/src/main/kotlin/foundation/e/accountmanager/utils/AppConstants.kt
new file mode 100644
index 0000000000000000000000000000000000000000..99474967a1a750f5c49eac8ca47843f9c8ae2b36
--- /dev/null
+++ b/app/src/main/kotlin/foundation/e/accountmanager/utils/AppConstants.kt
@@ -0,0 +1,26 @@
+/*
+ * Copyright (C) 2025 e Foundation
+ *
+ * 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 .
+ *
+ */
+package foundation.e.accountmanager.utils
+
+object AppConstants {
+ const val WEBCAL_MANAGER_PACKAGE = "foundation.e.webcalendarmanager"
+ const val WEBCAL_MANAGER_ACTIVITY = "at.bitfire.icsdroid.ui.views.CalendarListActivity"
+ const val PRIVACY_POLICY_URL = "https://e.foundation/legal-notice-privacy/#account-manager"
+
+ const val SYNC_DELAY_SECONDS = 5L
+}
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/utils/SystemUtils.kt b/app/src/main/kotlin/foundation/e/accountmanager/utils/SystemUtils.kt
index 1739bf2bfb727c57fb181781c7f6f43b88c1f9fd..c211e6990abbcc405ac39b084b1e0cb4f8ab16dc 100644
--- a/app/src/main/kotlin/foundation/e/accountmanager/utils/SystemUtils.kt
+++ b/app/src/main/kotlin/foundation/e/accountmanager/utils/SystemUtils.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2025 eFoundation
+ * Copyright (C) 2025 e Foundation
*
* 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
diff --git a/app/src/main/kotlin/foundation/e/accountmanager/utils/WebViewUtils.kt b/app/src/main/kotlin/foundation/e/accountmanager/utils/WebViewUtils.kt
new file mode 100644
index 0000000000000000000000000000000000000000..9a9f0b1891f2b2de5c7d7bf103163d019728eb59
--- /dev/null
+++ b/app/src/main/kotlin/foundation/e/accountmanager/utils/WebViewUtils.kt
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2025 e Foundation
+ *
+ * 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 .
+ *
+ */
+package foundation.e.accountmanager.utils
+
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import androidx.browser.customtabs.CustomTabsIntent
+
+object WebViewUtils {
+ fun openCustomTab(context: Context, url: String) {
+ val packageManager = context.packageManager
+ val resolveInfo = packageManager.queryIntentActivities(
+ Intent(Intent.ACTION_VIEW, Uri.parse(url)),
+ PackageManager.MATCH_DEFAULT_ONLY
+ )
+
+ if (resolveInfo.isNotEmpty()) {
+ val customTabsIntent = CustomTabsIntent.Builder()
+ .setShowTitle(true).build()
+ customTabsIntent.intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ customTabsIntent.launchUrl(context, Uri.parse(url))
+ } else {
+ // Fallback to default browser
+ val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
+ context.startActivity(intent)
+ }
+ }
+}
diff --git a/app/src/main/res/values/e_strings.xml b/app/src/main/res/values/e_strings.xml
index 1356803cbb23f1ba96265b65ed9607d8fc00418a..5c277d71731b2617486d897f95bba969d6b087fc 100644
--- a/app/src/main/res/values/e_strings.xml
+++ b/app/src/main/res/values/e_strings.xml
@@ -29,4 +29,8 @@
Use a specific server
Collapse
Expand
+
+ "Account Manager's Privacy Policy"
+ "Privacy Policy"
+ Web Calendar Manager
diff --git a/app/src/ose/AndroidManifest.xml b/app/src/ose/AndroidManifest.xml
index f645872f4340bd3dc30d04ad328f0b0727dc63c1..4289641faa63a547d0da7b6c9e62a3a48431f1cd 100644
--- a/app/src/ose/AndroidManifest.xml
+++ b/app/src/ose/AndroidManifest.xml
@@ -9,6 +9,7 @@
+
@@ -227,6 +228,32 @@
android:resource="@xml/e_sync_e_notes" />
+
+
+
+
+
+
+
+
+
+
+
+
+