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" /> + + + + + + + + + + + + +