diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/AccountSettingsTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/AccountSettingsTest.kt index 803f1ecc1..a9d98b686 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/AccountSettingsTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/AccountSettingsTest.kt @@ -6,6 +6,7 @@ package at.bitfire.davdroid.settings import android.accounts.AccountManager import android.content.Context +import at.bitfire.davdroid.TestUtils import at.bitfire.davdroid.sync.account.TestAccountAuthenticator import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule @@ -32,6 +33,7 @@ class AccountSettingsTest { @Before fun setUp() { hiltRule.inject() + TestUtils.setUpWorkManager(context) } @@ -50,7 +52,7 @@ class AccountSettingsTest { accountSettingsFactory.create(account, abortOnMissingMigration = true) val accountManager = AccountManager.get(context) - val version = accountManager.getUserData(account, AccountSettings.KEY_SETTINGS_VERSION).toIntOrNull() + val version = accountManager.getUserData(account, AccountSettings.KEY_SETTINGS_VERSION).toInt() assertEquals(AccountSettings.CURRENT_VERSION, version) } } diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19Test.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19Test.kt new file mode 100644 index 000000000..3aecf57d8 --- /dev/null +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19Test.kt @@ -0,0 +1,92 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.Context +import android.util.Log +import androidx.hilt.work.HiltWorkerFactory +import androidx.work.Configuration +import androidx.work.WorkManager +import androidx.work.testing.WorkManagerTestInitHelper +import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.AutomaticSyncManager +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.android.testing.HiltAndroidRule +import dagger.hilt.android.testing.HiltAndroidTest +import io.mockk.MockKAnnotations +import io.mockk.impl.annotations.InjectMockKs +import io.mockk.impl.annotations.MockK +import io.mockk.impl.annotations.SpyK +import io.mockk.mockkObject +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import javax.inject.Inject + +@HiltAndroidTest +class AccountSettingsMigration19Test { + + @Inject @ApplicationContext + @SpyK + lateinit var context: Context + + @MockK(relaxed = true) + lateinit var automaticSyncManager: AutomaticSyncManager + + @InjectMockKs + lateinit var migration: AccountSettingsMigration19 + + @Inject + lateinit var workerFactory: HiltWorkerFactory + + @get:Rule + val hiltRule = HiltAndroidRule(this) + + + @Before + fun setUp() { + hiltRule.inject() + + // Initialize WorkManager for instrumentation tests. + val config = Configuration.Builder() + .setMinimumLoggingLevel(Log.DEBUG) + .setWorkerFactory(workerFactory) + .build() + WorkManagerTestInitHelper.initializeTestWorkManager(context, config) + + MockKAnnotations.init(this) + } + + @After + fun tearDown() { + unmockkAll() + } + + + @Test + fun testMigrate_CancelsOldWorkersAndUpdatesAutomaticSync() { + val workManager = WorkManager.getInstance(context) + mockkObject(workManager) + + val account = Account("Some", "Test") + migration.migrate(account) + + val addressBookAuthority = context.getString(R.string.address_books_authority) + verify { + workManager.cancelUniqueWork("periodic-sync $addressBookAuthority Test/Some") + workManager.cancelUniqueWork("periodic-sync com.android.calendar Test/Some") + workManager.cancelUniqueWork("periodic-sync at.techbee.jtx.provider Test/Some") + workManager.cancelUniqueWork("periodic-sync org.dmfs.tasks Test/Some") + workManager.cancelUniqueWork("periodic-sync org.tasks.opentasks Test/Some") + + automaticSyncManager.updateAutomaticSync(account) + } + } + +} \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt index cfaf7e77f..1bd531e23 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/account/AccountsCleanupWorkerTest.kt @@ -5,8 +5,6 @@ import android.accounts.AccountManager import android.content.Context import android.os.Bundle import androidx.hilt.work.HiltWorkerFactory -import androidx.work.WorkerFactory -import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder import at.bitfire.davdroid.R import at.bitfire.davdroid.TestUtils @@ -141,7 +139,7 @@ class AccountsCleanupWorkerTest { @Test fun testCleanUpAddressBooks_keepsAddressBookWithAccount() { - TestAccountAuthenticator.provide() { account -> + TestAccountAuthenticator.provide { account -> // Create address book account _with_ corresponding account and verify val userData = Bundle(2).apply { putString(LocalAddressBook.USER_DATA_ACCOUNT_NAME, account.name) @@ -172,9 +170,4 @@ class AccountsCleanupWorkerTest { return db.serviceDao().get(serviceId)!! } - private fun workerFactory() = object : WorkerFactory() { - override fun createWorker(appContext: Context, workerClassName: String, workerParameters: WorkerParameters) = - accountsCleanupWorkerFactory.create(appContext, workerParameters) - } - } \ No newline at end of file diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt index 5d54056fa..455c83e2f 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorkerTest.kt @@ -6,7 +6,6 @@ package at.bitfire.davdroid.sync.worker import android.accounts.Account import android.content.Context -import android.provider.CalendarContract import androidx.test.platform.app.InstrumentationRegistry import androidx.work.ListenableWorker import androidx.work.WorkManager @@ -15,6 +14,7 @@ import androidx.work.WorkerParameters import androidx.work.testing.TestListenableWorkerBuilder import androidx.work.workDataOf import at.bitfire.davdroid.TestUtils +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.account.TestAccountAuthenticator import at.bitfire.davdroid.test.R import dagger.hilt.android.qualifiers.ApplicationContext @@ -66,7 +66,7 @@ class PeriodicSyncWorkerTest { // Run PeriodicSyncWorker as TestWorker val inputData = workDataOf( - BaseSyncWorker.INPUT_AUTHORITY to CalendarContract.AUTHORITY, + BaseSyncWorker.INPUT_DATA_TYPE to SyncDataType.EVENTS.toString(), BaseSyncWorker.INPUT_ACCOUNT_NAME to invalidAccount.name, BaseSyncWorker.INPUT_ACCOUNT_TYPE to invalidAccount.type ) diff --git a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManagerTest.kt b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManagerTest.kt index 8cbef7156..b28864a39 100644 --- a/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManagerTest.kt +++ b/app/src/androidTest/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManagerTest.kt @@ -6,10 +6,10 @@ package at.bitfire.davdroid.sync.worker import android.accounts.Account import android.content.Context -import android.provider.CalendarContract import androidx.hilt.work.HiltWorkerFactory import at.bitfire.davdroid.TestUtils import at.bitfire.davdroid.TestUtils.workScheduledOrRunning +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.account.TestAccountAuthenticator import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.testing.HiltAndroidRule @@ -59,10 +59,10 @@ class SyncWorkerManagerTest { @Test fun testEnqueueOneTime() { - val workerName = OneTimeSyncWorker.workerName(account, CalendarContract.AUTHORITY) + val workerName = OneTimeSyncWorker.workerName(account, SyncDataType.EVENTS) assertFalse(TestUtils.workScheduledOrRunningOrSuccessful(context, workerName)) - val returnedName = syncWorkerManager.enqueueOneTime(account, CalendarContract.AUTHORITY) + val returnedName = syncWorkerManager.enqueueOneTime(account, SyncDataType.EVENTS) assertEquals(workerName, returnedName) assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context, workerName)) } @@ -72,18 +72,18 @@ class SyncWorkerManagerTest { @Test fun enablePeriodic() { - syncWorkerManager.enablePeriodic(account, CalendarContract.AUTHORITY, 60, false).result.get() + syncWorkerManager.enablePeriodic(account, SyncDataType.EVENTS, 60, false).result.get() - val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY) + val workerName = PeriodicSyncWorker.workerName(account, SyncDataType.EVENTS) assertTrue(workScheduledOrRunning(context, workerName)) } @Test fun disablePeriodic() { - syncWorkerManager.enablePeriodic(account, CalendarContract.AUTHORITY, 60, false).result.get() - syncWorkerManager.disablePeriodic(account, CalendarContract.AUTHORITY).result.get() + syncWorkerManager.enablePeriodic(account, SyncDataType.EVENTS, 60, false).result.get() + syncWorkerManager.disablePeriodic(account, SyncDataType.EVENTS).result.get() - val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY) + val workerName = PeriodicSyncWorker.workerName(account, SyncDataType.EVENTS) assertFalse(workScheduledOrRunning(context, workerName)) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt index ea80ea29b..593dbdd28 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/push/PushNotificationManager.kt @@ -7,6 +7,7 @@ import android.content.Intent import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.ui.NotificationRegistry import at.bitfire.davdroid.ui.account.AccountActivity import dagger.hilt.android.qualifiers.ApplicationContext @@ -20,16 +21,16 @@ class PushNotificationManager @Inject constructor( /** * Generates the notification ID for a push notification. */ - private fun notificationId(account: Account, authority: String): Int { - return account.name.hashCode() + account.type.hashCode() + authority.hashCode() + private fun notificationId(account: Account, dataType: SyncDataType): Int { + return account.name.hashCode() + account.type.hashCode() + dataType.hashCode() } /** * Sends a notification to inform the user that a push notification has been received, the * sync has been scheduled, but it still has not run. */ - fun notify(account: Account, authority: String) { - notificationRegistry.notifyIfPossible(notificationId(account, authority)) { + fun notify(account: Account, dataType: SyncDataType) { + notificationRegistry.notifyIfPossible(notificationId(account, dataType)) { NotificationCompat.Builder(context, notificationRegistry.CHANNEL_STATUS) .setSmallIcon(R.drawable.ic_sync) .setContentTitle(context.getString(R.string.sync_notification_pending_push_title)) @@ -57,9 +58,9 @@ class PushNotificationManager @Inject constructor( * Once the sync has been started, the notification is no longer needed and can be dismissed. * It's safe to call this method even if the notification has not been shown. */ - fun dismiss(account: Account, authority: String) { + fun dismiss(account: Account, dataType: SyncDataType) { NotificationManagerCompat.from(context) - .cancel(notificationId(account, authority)) + .cancel(notificationId(account, dataType)) } } 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 7402f3c5c..b0cd373ba 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/repository/AccountRepository.kt @@ -8,7 +8,6 @@ import android.accounts.Account import android.accounts.AccountManager import android.accounts.OnAccountsUpdateListener import android.content.Context -import android.provider.CalendarContract import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Credentials @@ -19,14 +18,13 @@ import at.bitfire.davdroid.resource.LocalTaskList import at.bitfire.davdroid.servicedetection.DavResourceFinder import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.sync.AutomaticSyncManager +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.account.AccountsCleanupWorker import at.bitfire.davdroid.sync.account.SystemAccountUtils import at.bitfire.davdroid.sync.worker.SyncWorkerManager -import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod import dagger.Lazy import dagger.hilt.android.qualifiers.ApplicationContext @@ -82,52 +80,32 @@ class AccountRepository @Inject constructor( if (!SystemAccountUtils.createAccount(context, account, userData, credentials?.password)) return null - // add entries for account to service DB + // add entries for account to database logger.log(Level.INFO, "Writing account configuration to database", config) try { - val accountSettings = accountSettingsFactory.create(account) - val defaultSyncInterval = settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL) - - // Configure CardDAV service - val addrBookAuthority = context.getString(R.string.address_books_authority) if (config.cardDAV != null) { // insert CardDAV service val id = insertService(accountName, Service.TYPE_CARDDAV, config.cardDAV) // set initial CardDAV account settings and set sync intervals (enables automatic sync) + val accountSettings = accountSettingsFactory.create(account) accountSettings.setGroupMethod(groupMethod) - accountSettings.setSyncInterval(addrBookAuthority, defaultSyncInterval) // start CardDAV service detection (refresh collections) RefreshCollectionsWorker.enqueue(context, id) - } else - automaticSyncManager.disableAutomaticSync(account, addrBookAuthority) + } - // Configure CalDAV service if (config.calDAV != null) { // insert CalDAV service val id = insertService(accountName, Service.TYPE_CALDAV, config.calDAV) - // set default sync interval and enable sync regardless of permissions - accountSettings.setSyncInterval(CalendarContract.AUTHORITY, defaultSyncInterval) - - // if task provider present, set task sync interval and enable sync - val taskProvider = tasksAppManager.get().currentProvider() - if (taskProvider != null) { - accountSettings.setSyncInterval(taskProvider.authority, defaultSyncInterval) - // further changes will be handled by TasksWatcher on app start or when tasks app is (un)installed - logger.info("Tasks provider ${taskProvider.authority} found. Tasks sync enabled.") - } else - logger.info("No tasks provider found. Did not enable tasks sync.") - // start CalDAV service detection (refresh collections) RefreshCollectionsWorker.enqueue(context, id) - } else { - automaticSyncManager.disableAutomaticSync(account, CalendarContract.AUTHORITY) - for (provider in TaskProvider.ProviderName.entries) - automaticSyncManager.disableAutomaticSync(account, provider.authority) } + // set up automatic sync (processes inserted services) + automaticSyncManager.updateAutomaticSync(account) + } catch(e: InvalidAccountException) { logger.log(Level.SEVERE, "Couldn't access account settings", e) return null @@ -205,16 +183,6 @@ class AccountRepository @Inject constructor( if (accountManager.getAccountsByType(context.getString(R.string.account_type)).contains(newAccount)) throw IllegalArgumentException("Account with name \"$newName\" already exists") - // remember sync intervals - val oldSettings = accountSettingsFactory.create(oldAccount) - val authorities = mutableListOf( - context.getString(R.string.address_books_authority), - CalendarContract.AUTHORITY - ) - val tasksProvider = tasksAppManager.get().currentProvider() - tasksProvider?.authority?.let { authorities.add(it) } - val syncIntervals = authorities.map { Pair(it, oldSettings.getSyncInterval(it)) } - // rename account try { /* https://github.com/bitfireAT/davx5/issues/135 @@ -226,7 +194,7 @@ class AccountRepository @Inject constructor( 3. Now the services would be renamed, but they're not here anymore. */ AccountsCleanupWorker.lockAccountsCleanup() - // rename account + // rename account (also moves AccountSettings) val future = accountManager.renameAccount(oldAccount, newName, null, null) // wait for operation to complete @@ -241,9 +209,8 @@ class AccountRepository @Inject constructor( syncWorkerManager.cancelAllWork(oldAccount) // disable periodic syncs for old account - syncIntervals.forEach { (authority, _) -> - syncWorkerManager.disablePeriodic(oldAccount, authority) - } + for (dataType in SyncDataType.entries) + syncWorkerManager.disablePeriodic(oldAccount, dataType) // update account name references in database serviceRepository.renameAccount(oldName, newName) @@ -262,14 +229,8 @@ class AccountRepository @Inject constructor( // Couldn't update task lists, but this is not a fatal error (will be fixed at next sync) } - // restore sync intervals - val newSettings = accountSettingsFactory.create(newAccount) - for ((authority, interval) in syncIntervals) { - if (interval == null) - automaticSyncManager.disableAutomaticSync(newAccount, authority) - else - newSettings.setSyncInterval(authority, interval) - } + // update automatic sync + automaticSyncManager.updateAutomaticSync(newAccount) } finally { // release AccountsCleanupWorker mutex at the end of this async coroutine AccountsCleanupWorker.unlockAccountsCleanup() 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 b625f6ba7..6149b1421 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/AccountSettings.kt @@ -8,18 +8,15 @@ import android.accounts.AccountManager import android.content.Context import android.os.Bundle import android.os.Looper -import android.provider.CalendarContract import androidx.annotation.WorkerThread import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.settings.migration.AccountSettingsMigration import at.bitfire.davdroid.sync.AutomaticSyncManager -import at.bitfire.davdroid.sync.SyncFrameworkIntegration +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.account.setAndVerifyUserData -import at.bitfire.davdroid.sync.worker.SyncWorkerManager import at.bitfire.davdroid.util.trimToNull -import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -30,6 +27,7 @@ import java.util.Collections import java.util.logging.Level import java.util.logging.Logger import javax.inject.Provider +import kotlin.collections.mutableSetOf /** * Manages settings of an account. @@ -50,9 +48,7 @@ class AccountSettings @AssistedInject constructor( @ApplicationContext private val context: Context, private val logger: Logger, private val migrations: Map>, - private val settingsManager: SettingsManager, - private val syncFramework: SyncFrameworkIntegration, - private val syncWorkerManager: SyncWorkerManager + private val settingsManager: SettingsManager ) { @AssistedFactory @@ -135,65 +131,41 @@ class AccountSettings @AssistedInject constructor( // sync. settings /** - * Gets the currently set sync interval for this account in seconds. + * Gets the currently set sync interval for this account and data type in seconds. * - * @param authority authority to check (for instance: [CalendarContract.AUTHORITY]]) - * @return sync interval in seconds; *[SYNC_INTERVAL_MANUALLY]* if manual sync; *null* if not set + * @param dataType data type of desired sync interval + * @return sync interval in seconds, or `null` if not set (not applicable or only manual sync) */ - fun getSyncInterval(authority: String): Long? { - val addrBookAuthority = context.getString(R.string.address_books_authority) - - if (!syncFramework.isSyncable(account, authority) && authority != addrBookAuthority) - return null - - val key = when { - authority == addrBookAuthority -> - KEY_SYNC_INTERVAL_ADDRESSBOOKS - authority == CalendarContract.AUTHORITY -> - KEY_SYNC_INTERVAL_CALENDARS - TaskProvider.ProviderName.entries.any { it.authority == authority } -> - KEY_SYNC_INTERVAL_TASKS - else -> return null + fun getSyncInterval(dataType: SyncDataType): Long? { + val key = when (dataType) { + SyncDataType.CONTACTS -> KEY_SYNC_INTERVAL_ADDRESSBOOKS + SyncDataType.EVENTS -> KEY_SYNC_INTERVAL_CALENDARS + SyncDataType.TASKS -> KEY_SYNC_INTERVAL_TASKS + } + val seconds = accountManager.getUserData(account, key)?.toLong() + return when (seconds) { + null -> settingsManager.getLongOrNull(Settings.DEFAULT_SYNC_INTERVAL) // no setting → default value + SYNC_INTERVAL_MANUALLY -> null // manual sync + else -> seconds } - return accountManager.getUserData(account, key)?.toLong() } - fun getTasksSyncInterval() = accountManager.getUserData(account, KEY_SYNC_INTERVAL_TASKS)?.toLong() - /** - * Sets the sync interval and en- or disables periodic sync for the given account and authority. - * - * This method blocks until a worker as been created and enqueued (sync active) or removed - * (sync disabled), so it should not be called from the UI thread. + * Sets the sync interval for the given data type and updates the automatic sync. * - * @param authority sync authority (like [CalendarContract.AUTHORITY]) - * @param _seconds if [SYNC_INTERVAL_MANUALLY]: automatic sync will be disabled; - * otherwise (must be ≥ 15 min): automatic sync will be enabled and set to the given number of seconds + * @param dataType data type of the sync interval to set + * @param seconds sync interval in seconds; _null_ for no periodic sync */ - @WorkerThread - fun setSyncInterval(authority: String, _seconds: Long) { - val seconds = - if (_seconds != SYNC_INTERVAL_MANUALLY && _seconds < 60*15) - 60*15 - else - _seconds - - // Store (user defined) sync interval in account settings - val key = when { - authority == context.getString(R.string.address_books_authority) -> - KEY_SYNC_INTERVAL_ADDRESSBOOKS - authority == CalendarContract.AUTHORITY -> - KEY_SYNC_INTERVAL_CALENDARS - TaskProvider.ProviderName.entries.any { it.authority == authority } -> - KEY_SYNC_INTERVAL_TASKS - else -> { - logger.warning("Sync interval not applicable to authority $authority") - return - } + fun setSyncInterval(dataType: SyncDataType, seconds: Long?) { + val key = when (dataType) { + SyncDataType.CONTACTS -> KEY_SYNC_INTERVAL_ADDRESSBOOKS + SyncDataType.EVENTS -> KEY_SYNC_INTERVAL_CALENDARS + SyncDataType.TASKS -> KEY_SYNC_INTERVAL_TASKS } - accountManager.setAndVerifyUserData(account, key, seconds.toString()) + val newValue = if (seconds == null) SYNC_INTERVAL_MANUALLY else seconds + accountManager.setAndVerifyUserData(account, key, newValue.toString()) - automaticSyncManager.enableAutomaticSync(account, authority, seconds, getSyncWifiOnly()) + automaticSyncManager.updateAutomaticSync(account, dataType) } fun getSyncWifiOnly() = @@ -204,10 +176,7 @@ class AccountSettings @AssistedInject constructor( fun setSyncWiFiOnly(wiFiOnly: Boolean) { accountManager.setAndVerifyUserData(account, KEY_WIFI_ONLY, if (wiFiOnly) "1" else null) - - // update automatic sync (needs already updated wifi-only flag in AccountSettings) - for (authority in syncWorkerManager.syncAuthorities()) - automaticSyncManager.enableAutomaticSync(account, authority, getSyncInterval(authority), wiFiOnly) + automaticSyncManager.updateAutomaticSync(account) } fun getSyncWifiOnlySSIDs(): List? = @@ -248,7 +217,7 @@ class AccountSettings @AssistedInject constructor( } fun setTimeRangePastDays(days: Int?) = - accountManager.setAndVerifyUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString()) + accountManager.setAndVerifyUserData(account, KEY_TIME_RANGE_PAST_DAYS, (days ?: -1).toString()) /** * Takes the default alarm setting (in this order) from @@ -260,8 +229,8 @@ class AccountSettings @AssistedInject constructor( * non-full-day event without reminder. *null*: No default reminders shall be created. */ fun getDefaultAlarm() = - accountManager.getUserData(account, KEY_DEFAULT_ALARM)?.toInt() ?: - settingsManager.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 } + accountManager.getUserData(account, KEY_DEFAULT_ALARM)?.toInt() ?: + settingsManager.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 } /** * Sets the default alarm value in the local account settings, if the new value differs @@ -273,11 +242,11 @@ class AccountSettings @AssistedInject constructor( * start of every non-full-day event without reminder. *null*: No default reminders shall be created. */ fun setDefaultAlarm(minBefore: Int?) = - accountManager.setAndVerifyUserData(account, KEY_DEFAULT_ALARM, - if (minBefore == settingsManager.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 }) - null - else - minBefore?.toString()) + accountManager.setAndVerifyUserData(account, KEY_DEFAULT_ALARM, + if (minBefore == settingsManager.getIntOrNull(KEY_DEFAULT_ALARM)?.takeIf { it != -1 }) + null + else + minBefore?.toString()) fun getManageCalendarColors() = if (settingsManager.containsKey(KEY_MANAGE_CALENDAR_COLORS)) @@ -376,7 +345,7 @@ class AccountSettings @AssistedInject constructor( companion object { - const val CURRENT_VERSION = 18 + const val CURRENT_VERSION = 19 const val KEY_SETTINGS_VERSION = "version" const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks" @@ -432,7 +401,7 @@ class AccountSettings @AssistedInject constructor( "1" show only personal collections */ const val KEY_SHOW_ONLY_PERSONAL = "show_only_personal" - const val SYNC_INTERVAL_MANUALLY = -1L + internal const val SYNC_INTERVAL_MANUALLY = -1L /** Static property to remember which AccountSettings updates/migrations are currently running */ val currentlyUpdating = Collections.synchronizedSet(mutableSetOf()) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration11.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration11.kt index 3f037c1fa..530c118cd 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration11.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration11.kt @@ -6,8 +6,10 @@ package at.bitfire.davdroid.settings.migration import android.accounts.Account import android.accounts.AccountManager +import android.content.ContentResolver import android.content.Context import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.settings.AccountSettings.Companion.SYNC_INTERVAL_MANUALLY import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.account.setAndVerifyUserData import dagger.Binds @@ -24,7 +26,6 @@ import javax.inject.Inject * again when the tasks provider is switched. */ class AccountSettingsMigration11 @Inject constructor( - private val accountSettingsFactory: AccountSettings.Factory, @ApplicationContext private val context: Context, private val tasksAppManager: TasksAppManager ): AccountSettingsMigration { @@ -32,12 +33,22 @@ class AccountSettingsMigration11 @Inject constructor( override fun migrate(account: Account) { val accountManager: AccountManager = AccountManager.get(context) tasksAppManager.currentProvider()?.let { provider -> - val interval = accountSettingsFactory.create(account).getSyncInterval(provider.authority) + val interval = getSyncFrameworkInterval(account, provider.authority) if (interval != null) accountManager.setAndVerifyUserData(account, AccountSettings.KEY_SYNC_INTERVAL_TASKS, interval.toString()) } } + private fun getSyncFrameworkInterval(account: Account, authority: String): Long? { + if (ContentResolver.getIsSyncable(account, authority) <= 0) + return null + + return if (ContentResolver.getSyncAutomatically(account, authority)) + ContentResolver.getPeriodicSyncs(account, authority).firstOrNull()?.period ?: SYNC_INTERVAL_MANUALLY + else + SYNC_INTERVAL_MANUALLY + } + @Module @InstallIn(SingletonComponent::class) diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration14.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration14.kt index 134fa282a..c30f9eda2 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration14.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration14.kt @@ -10,6 +10,7 @@ import android.content.Context import android.provider.CalendarContract import at.bitfire.davdroid.R import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.ical4android.TaskProvider import dagger.Binds import dagger.Module @@ -22,8 +23,7 @@ import java.util.logging.Logger import javax.inject.Inject /** - * Disables all sync adapter periodic syncs for every authority. Then enables - * corresponding PeriodicSyncWorkers + * Disables all sync adapter periodic syncs for every authority. Then enables corresponding periodic sync workers. */ class AccountSettingsMigration14 @Inject constructor( private val accountSettingsFactory: AccountSettings.Factory, @@ -43,20 +43,22 @@ class AccountSettingsMigration14 @Inject constructor( TaskProvider.ProviderName.TasksOrg.authority ) - val accountSettings = accountSettingsFactory.create(account) - for (authority in authorities) { - // Enable PeriodicSyncWorker (WorkManager), with known intervals - enableWorkManager(account, authority, accountSettings) - // Disable periodic syncs (sync adapter framework) + // Disable periodic syncs (sync adapter framework) + for (authority in authorities) disableSyncFramework(account, authority) - } + + // Enable PeriodicSyncWorker (WorkManager), with known intervals + for (dataType in SyncDataType.entries) + enableWorkManager(account, dataType) } - private fun enableWorkManager(account: Account, authority: String, accountSettings: AccountSettings) { - val enabled = accountSettings.getSyncInterval(authority)?.let { syncInterval -> - accountSettings.setSyncInterval(authority, syncInterval) - } ?: false - logger.info("PeriodicSyncWorker for $account/$authority enabled=$enabled") + private fun enableWorkManager(account: Account, dataType: SyncDataType) { + val accountSettings = accountSettingsFactory.create(account) + val enabled: Boolean = accountSettings.getSyncInterval(dataType)?.let { syncInterval -> + accountSettings.setSyncInterval(dataType, syncInterval) + true + } == true + logger.info("PeriodicSyncWorker for $account/$dataType enabled=$enabled") } private fun disableSyncFramework(account: Account, authority: String) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration15.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration15.kt index 4f61d7a84..fa654e9b7 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration15.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration15.kt @@ -5,8 +5,7 @@ package at.bitfire.davdroid.settings.migration import android.accounts.Account -import at.bitfire.davdroid.settings.AccountSettings -import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import at.bitfire.davdroid.sync.AutomaticSyncManager import dagger.Binds import dagger.Module import dagger.hilt.InstallIn @@ -18,20 +17,15 @@ import javax.inject.Inject /** * Updates the periodic sync workers by re-setting the same sync interval. * - * The goal is to add the [BaseSyncWorker.commonTag] to all existing periodic sync workers so that they can be detected by - * the new [BaseSyncWorker.exists] and [at.bitfire.davdroid.ui.AccountsActivity.Model]. + * The goal is to add the [at.bitfire.davdroid.sync.worker.BaseSyncWorker.commonTag] to all existing periodic sync workers so that they + * can be detected correctly. */ class AccountSettingsMigration15 @Inject constructor( - private val accountSettingsFactory: AccountSettings.Factory, - private val syncWorkerManager: SyncWorkerManager + private val automaticSyncManager: AutomaticSyncManager ): AccountSettingsMigration { override fun migrate(account: Account) { - for (authority in syncWorkerManager.syncAuthorities()) { - val accountSettings = accountSettingsFactory.create(account) - val interval = accountSettings.getSyncInterval(authority) - accountSettings.setSyncInterval(authority, interval ?: AccountSettings.SYNC_INTERVAL_MANUALLY) - } + automaticSyncManager.updateAutomaticSync(account) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration16.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration16.kt index f9bb7adcf..1d77d7b56 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration16.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration16.kt @@ -8,6 +8,7 @@ import android.accounts.Account import android.content.Context import androidx.work.WorkManager import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.worker.SyncWorkerManager import dagger.Binds import dagger.Module @@ -32,24 +33,24 @@ class AccountSettingsMigration16 @Inject constructor( ): AccountSettingsMigration { override fun migrate(account: Account) { - val accountSettings = accountSettingsFactory.create(account) - for (authority in syncWorkerManager.syncAuthorities()) { - logger.info("Re-enqueuing periodic sync workers for $account/$authority, if necessary") + for (dataType in SyncDataType.entries) { + logger.info("Re-enqueuing periodic sync workers for $account/$dataType, if necessary") /* A maybe existing periodic worker references the old class name (even if it failed and/or is not active). So we need to explicitly disable and prune all workers. Just updating the worker is not enough – WorkManager will update the work details, but not the class name. */ - val disableOp = syncWorkerManager.disablePeriodic(account, authority) + val disableOp = syncWorkerManager.disablePeriodic(account, dataType) disableOp.result.get() // block until worker with old name is disabled val pruneOp = WorkManager.getInstance(context).pruneWork() pruneOp.result.get() // block until worker with old name is removed from DB - val interval = accountSettings.getSyncInterval(authority) - if (interval != null && interval != AccountSettings.SYNC_INTERVAL_MANUALLY) { + val accountSettings = accountSettingsFactory.create(account) + val interval = accountSettings.getSyncInterval(dataType) + if (interval != null) { // There's a sync interval for this account/authority; a periodic sync worker should be there, too. val onlyWifi = accountSettings.getSyncWifiOnly() - syncWorkerManager.enablePeriodic(account, authority, interval, onlyWifi) + syncWorkerManager.enablePeriodic(account, dataType, interval, onlyWifi) } } } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19.kt b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19.kt new file mode 100644 index 000000000..e2da25f73 --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/settings/migration/AccountSettingsMigration19.kt @@ -0,0 +1,60 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.settings.migration + +import android.accounts.Account +import android.content.Context +import android.provider.CalendarContract +import androidx.work.WorkManager +import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.AutomaticSyncManager +import at.bitfire.ical4android.TaskProvider +import dagger.Binds +import dagger.Module +import dagger.hilt.InstallIn +import dagger.hilt.android.qualifiers.ApplicationContext +import dagger.hilt.components.SingletonComponent +import dagger.multibindings.IntKey +import dagger.multibindings.IntoMap +import javax.inject.Inject + +/** + * Sync workers are now not per authority anymore, but per [at.bitfire.davdroid.sync.SyncDataType]. So we have to + * + * 1. cancel all current periodic sync workers (which have "authority" input data), + * 2. re-enqueue periodic sync workers (now with "data type" input data), if applicable. + */ +class AccountSettingsMigration19 @Inject constructor( + @ApplicationContext private val context: Context, + private val automaticSyncManager: AutomaticSyncManager +): AccountSettingsMigration { + + override fun migrate(account: Account) { + // cancel old workers + val workManager = WorkManager.getInstance(context) + val authorities = listOf( + context.getString(R.string.address_books_authority), + CalendarContract.AUTHORITY, + *TaskProvider.TASK_PROVIDERS.map { it.authority }.toTypedArray() + ) + for (authority in authorities) { + val oldWorkerName = "periodic-sync $authority ${account.type}/${account.name}" + workManager.cancelUniqueWork(oldWorkerName) + } + + // enqueue new workers + automaticSyncManager.updateAutomaticSync(account) + } + + + @Module + @InstallIn(SingletonComponent::class) + abstract class AccountSettingsMigrationModule { + @Binds @IntoMap + @IntKey(19) + abstract fun provide(impl: AccountSettingsMigration19): AccountSettingsMigration + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt index a4164107f..201427911 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/AutomaticSyncManager.kt @@ -5,9 +5,16 @@ package at.bitfire.davdroid.sync import android.accounts.Account +import android.content.Context +import android.provider.CalendarContract +import at.bitfire.davdroid.R +import at.bitfire.davdroid.db.Service +import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.sync.worker.SyncWorkerManager +import dagger.hilt.android.qualifiers.ApplicationContext import javax.inject.Inject +import javax.inject.Provider /** * Manages automatic synchronization, that is: @@ -15,20 +22,28 @@ import javax.inject.Inject * - synchronization in given intervals, and * - synchronization on local data changes. * + * Integrates with both the periodic sync workers and the sync framework. So this class should be used when + * the caller just wants to update the automatic sync, without needing to know about the underlying details. + * * Automatic synchronization stands in contrast to manual synchronization, which is only triggered by the user. */ class AutomaticSyncManager @Inject constructor( private val accountSettingsFactory: AccountSettings.Factory, + @ApplicationContext private val context: Context, + private val serviceRepository: DavServiceRepository, private val syncFramework: SyncFrameworkIntegration, + private val tasksAppManager: Provider, private val workerManager: SyncWorkerManager ) { /** * Disable automatic synchronization for the given account and data type. */ - fun disableAutomaticSync(account: Account, authority: String) { - workerManager.disablePeriodic(account, authority) - syncFramework.disableSyncAbility(account, authority) + private fun disableAutomaticSync(account: Account, dataType: SyncDataType) { + workerManager.disablePeriodic(account, dataType) + + for (authority in dataType.possibleAuthorities(context)) + syncFramework.disableSyncAbility(account, authority) } /** @@ -38,27 +53,77 @@ class AutomaticSyncManager @Inject constructor( * 2. Enables sync in the sync framework for the given data type and sets up periodic sync with the given interval. * * @param account the account to synchronize - * @param authority the authority to synchronize - * @param wifiOnly whether to synchronize only on Wi-Fi (default takes the account setting) - * @param seconds interval in seconds, or `null` to disable periodic sync (only sync on local data changes) + * @param dataType the data type to synchronize */ - fun enableAutomaticSync( + private fun enableAutomaticSync( account: Account, - authority: String, - seconds: Long?, - wifiOnly: Boolean = accountSettingsFactory.create(account).getSyncWifiOnly() + dataType: SyncDataType ) { - if (seconds != null) { + val accountSettings = accountSettingsFactory.create(account) + val syncInterval = accountSettings.getSyncInterval(dataType) + if (syncInterval != null) { // update sync workers (needs already updated sync interval in AccountSettings) - workerManager.enablePeriodic(account, authority, seconds, wifiOnly) + val wifiOnly = accountSettings.getSyncWifiOnly() + workerManager.enablePeriodic(account, dataType, syncInterval, wifiOnly) } else - workerManager.disablePeriodic(account, authority) + workerManager.disablePeriodic(account, dataType) - // Also enable/disable content change triggered syncs - if (seconds != null) + // also enable/disable content-triggered syncs + val possibleAuthorities = dataType.possibleAuthorities(context) + val authority: String? = when (dataType) { + SyncDataType.CONTACTS -> context.getString(R.string.address_books_authority) + SyncDataType.EVENTS -> CalendarContract.AUTHORITY + SyncDataType.TASKS -> tasksAppManager.get().currentProvider()?.authority + } + if (authority != null && syncInterval != null) { + // enable authority, but completely disable all other possible authorities (for instance, tasks apps which are not the current task app) syncFramework.enableSyncOnContentChange(account, authority) + for (disableAuthority in possibleAuthorities - authority) + syncFramework.disableSyncAbility(account, disableAuthority) + } else + for (authority in possibleAuthorities) + syncFramework.disableSyncOnContentChange(account, authority) + } + + /** + * Updates automatic synchronization of the given account and all data types according to the account settings. + * + * If there's a [Service] for the given account and data type, automatic sync is enabled (with details from [AccountSettings]). + * Otherwise, automatic synchronization is disabled. + * + * @param account account for which automatic synchronization shall be updated + */ + fun updateAutomaticSync(account: Account) { + for (dataType in SyncDataType.entries) + updateAutomaticSync(account, dataType) + } + + /** + * Updates automatic synchronization of the given account and data type according to the account services and settings. + * + * If there's a [Service] for the given account and data type, automatic sync is enabled (with details from [AccountSettings]). + * Otherwise, automatic synchronization is disabled. + * + * @param account account for which automatic synchronization shall be updated + * @param dataType sync data type for which automatic synchronization shall be updated + */ + fun updateAutomaticSync(account: Account, dataType: SyncDataType) { + val serviceType = when (dataType) { + SyncDataType.CONTACTS -> Service.TYPE_CARDDAV + SyncDataType.EVENTS, + SyncDataType.TASKS -> Service.TYPE_CALDAV + } + val hasService = serviceRepository.getByAccountAndType(account.name, serviceType) != null + + val hasProvider = if (dataType == SyncDataType.TASKS) + tasksAppManager.get().currentProvider() != null + else + true + + if (hasService && hasProvider) + enableAutomaticSync(account, dataType) else - syncFramework.disableSyncOnContentChange(account, authority) + disableAutomaticSync(account, dataType) } } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncAdapterServices.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncAdapterServices.kt index 5c9a781af..586e43bff 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncAdapterServices.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncAdapterServices.kt @@ -129,7 +129,7 @@ abstract class SyncAdapterService: Service() { authority logger.fine("Starting OneTimeSyncWorker for $account $workerAuthority and waiting for it") - val workerName = syncWorkerManager.enqueueOneTime(account, authority = workerAuthority, upload = upload) + val workerName = syncWorkerManager.enqueueOneTime(account, dataType = SyncDataType.fromAuthority(context, workerAuthority), upload = upload) /* 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 diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt new file mode 100644 index 000000000..3551d275b --- /dev/null +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncDataType.kt @@ -0,0 +1,81 @@ +/* + * Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details. + */ + +package at.bitfire.davdroid.sync + +import android.content.Context +import android.provider.CalendarContract +import android.provider.ContactsContract +import at.bitfire.davdroid.R +import at.bitfire.ical4android.TaskProvider +import dagger.hilt.EntryPoint +import dagger.hilt.InstallIn +import dagger.hilt.android.EntryPointAccessors +import dagger.hilt.components.SingletonComponent + +enum class SyncDataType { + + CONTACTS, + EVENTS, + TASKS; + + @EntryPoint + @InstallIn(SingletonComponent::class) + interface SyncDataTypeEntryPoint { + fun tasksAppManager(): TasksAppManager + } + + + fun possibleAuthorities(context: Context): List = + when (this) { + CONTACTS -> listOf( + ContactsContract.AUTHORITY, + context.getString(R.string.address_books_authority) + ) + EVENTS -> listOf( + CalendarContract.AUTHORITY + ) + TASKS -> + TaskProvider.ProviderName.entries.map { it.authority } + } + + + /** + * Gets the authority that should be used for managing sync adapters for this account and data type. + * + * In case of contacts, it will return the address books authority (and not the contacts authority). + */ + fun toSyncAuthority(context: Context): String? = when(this) { + CONTACTS -> + context.getString(R.string.address_books_authority) + EVENTS -> + CalendarContract.AUTHORITY + TASKS -> { + val entryPoint = EntryPointAccessors.fromApplication(context) + val tasksAppManager = entryPoint.tasksAppManager() + tasksAppManager.currentProvider()?.authority + } + } + + + companion object { + + fun fromAuthority(context: Context, authority: String): SyncDataType { + return when (authority) { + context.getString(R.string.address_books_authority), + ContactsContract.AUTHORITY -> + CONTACTS + CalendarContract.AUTHORITY -> + EVENTS + TaskProvider.ProviderName.JtxBoard.authority, + TaskProvider.ProviderName.TasksOrg.authority, + TaskProvider.ProviderName.OpenTasks.authority -> + TASKS + else -> throw IllegalArgumentException("Unknown authority: $authority") + } + } + + } + +} \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncFrameworkIntegration.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncFrameworkIntegration.kt index 365de346e..401d8edd5 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncFrameworkIntegration.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/SyncFrameworkIntegration.kt @@ -35,6 +35,7 @@ class SyncFrameworkIntegration @Inject constructor( * Enable this account/provider to be syncable. */ fun enableSyncAbility(account: Account, authority: String) { + logger.fine("Enabling sync framework for account=$account, authority=$authority") if (ContentResolver.getIsSyncable(account, authority) != 1) ContentResolver.setIsSyncable(account, authority, 1) } @@ -43,6 +44,7 @@ class SyncFrameworkIntegration @Inject constructor( * Disable this account/provider to be syncable. */ fun disableSyncAbility(account: Account, authority: String) { + logger.fine("Disabling sync framework for account=$account, authority=$authority") if (ContentResolver.getIsSyncable(account, authority) != 0) ContentResolver.setIsSyncable(account, authority, 0) } @@ -90,6 +92,7 @@ class SyncFrameworkIntegration @Inject constructor( */ @WorkerThread private fun setSyncOnContentChange(account: Account, authority: String, enable: Boolean): Boolean { + logger.fine("Setting content-triggered syncs (sync framework) for account=$account, authority=$authority to enable=$enable") // Try up to 10 times with 100 ms pause repeat(10) { if (setContentTrigger(account, authority, enable)) { @@ -120,11 +123,9 @@ class SyncFrameworkIntegration @Inject constructor( */ private fun setContentTrigger(account: Account, authority: String, enable: Boolean): Boolean = if (enable) { - logger.fine("Enabling content-triggered sync of $account/$authority") ContentResolver.setSyncAutomatically(account, authority, true) /* return */ ContentResolver.getSyncAutomatically(account, authority) } else { - logger.fine("Disabling content-triggered sync of $account/$authority") ContentResolver.setSyncAutomatically(account, authority, false) /* return */ !ContentResolver.getSyncAutomatically(account, authority) } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt index 7b5645799..827702fc3 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/Syncer.kt @@ -17,6 +17,7 @@ import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.resource.LocalCollection import at.bitfire.davdroid.resource.LocalDataStore import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.ui.NotificationRegistry import dagger.hilt.android.qualifiers.ApplicationContext import okhttp3.HttpUrl import okhttp3.HttpUrl.Companion.toHttpUrl @@ -74,6 +75,9 @@ abstract class Syncer, CollectionType: @Inject lateinit var logger: Logger + @Inject + lateinit var notificationRegistry: NotificationRegistry + @Inject lateinit var serviceRepository: DavServiceRepository @@ -252,6 +256,7 @@ abstract class Syncer, CollectionType: context.contentResolver.acquireContentProviderClient(authority) } catch (e: SecurityException) { logger.log(Level.WARNING, "Missing permissions for authority $authority", e) + notificationRegistry.notifyPermissions() null }.use { provider -> if (provider == null) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksAppManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksAppManager.kt index 7b5b0592f..4543c774f 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksAppManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/TasksAppManager.kt @@ -4,7 +4,6 @@ package at.bitfire.davdroid.sync -import android.accounts.Account import android.app.PendingIntent import android.content.Context import android.content.Intent @@ -12,12 +11,8 @@ import android.content.pm.PackageManager import android.graphics.drawable.BitmapDrawable import android.net.Uri import androidx.core.app.NotificationCompat -import at.bitfire.davdroid.InvalidAccountException import at.bitfire.davdroid.R -import at.bitfire.davdroid.db.AppDatabase -import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.repository.AccountRepository -import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.Settings import at.bitfire.davdroid.settings.SettingsManager import at.bitfire.davdroid.ui.NotificationRegistry @@ -35,11 +30,9 @@ import javax.inject.Inject * Responsible for setting/getting the currently used tasks app, and for communicating with it. */ class TasksAppManager @Inject constructor( - private val automaticSyncManager: AutomaticSyncManager, @ApplicationContext private val context: Context, private val accountRepository: Lazy, - private val accountSettingsFactory: AccountSettings.Factory, - private val db: AppDatabase, + private val automaticSyncManager: AutomaticSyncManager, private val logger: Logger, private val notificationRegistry: Lazy, private val settingsManager: SettingsManager @@ -48,7 +41,7 @@ class TasksAppManager @Inject constructor( /** * Gets the currently selected tasks app, if installed. * - * @return currently selected tasks app, or `null` if no tasks app is selected or the selected app is not installed + * @return currently selected tasks app (when installed), or `null` if no tasks app is selected or the selected app is not installed */ fun currentProvider(): ProviderName? { val authority = settingsManager.getString(Settings.SELECTED_TASKS_PROVIDER) ?: return null @@ -76,62 +69,21 @@ class TasksAppManager @Inject constructor( /** - * Sets up sync for the current TaskProvider (and disables sync for unavailable task providers): - * - * 1. Makes selected tasks authority _syncable_ in the sync framework, all other authorities _not syncable_. - * 2. Creates periodic sync worker for selected authority, disables periodic sync workers for all other authorities. - * 3. If the permissions don't allow synchronizing with the selected tasks app, a notification is shown. - * - * Called - * - * - when a user explicitly selects another task app, or - * - when there previously was no (usable) tasks app and [at.bitfire.davdroid.TasksAppWatcher] detected a new one. + * Sets up sync for the selected TaskProvider. */ fun selectProvider(selectedProvider: ProviderName?) { logger.info("Selecting tasks app: $selectedProvider") - settingsManager.putString(Settings.SELECTED_TASKS_PROVIDER, selectedProvider?.authority) - - var permissionsRequired = false // whether additional permissions are required - - // check all accounts and (de)activate task provider(s) if a CalDAV service is defined - for (account in accountRepository.get().getAll()) { - val hasCalDAV = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV) != null - for (providerName in ProviderName.entries) { - val syncable = hasCalDAV && providerName == selectedProvider - - // enable/disable sync for the given account and authority - setSyncable(account, providerName.authority, syncable) + val selectedAuthority = selectedProvider?.authority + settingsManager.putString(Settings.SELECTED_TASKS_PROVIDER, selectedAuthority) - // if sync has just been enabled: check whether additional permissions are required - if (syncable && !PermissionUtils.havePermissions(context, providerName.permissions)) - permissionsRequired = true - } - } - - if (permissionsRequired) { - logger.warning("Tasks synchronization is now enabled for at least one account, but permissions are not granted") + // check permission + if (selectedProvider != null && !PermissionUtils.havePermissions(context, selectedProvider.permissions)) notificationRegistry.get().notifyPermissions() - } - } - private fun setSyncable(account: Account, authority: String, syncable: Boolean) { - try { - val settings = accountSettingsFactory.create(account) - if (syncable) { - logger.info("Enabling $authority sync for $account") - - // set sync interval according to settings; also updates periodic sync workers and sync framework on-content-change - val interval = settings.getTasksSyncInterval() ?: settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL) - settings.setSyncInterval(authority, interval) - } else { - logger.info("Disabling $authority sync for $account") - automaticSyncManager.disableAutomaticSync(account, authority) - } - } catch (_: InvalidAccountException) { - // account has already been removed, make sure periodic sync is disabled, too - automaticSyncManager.disableAutomaticSync(account, authority) - } + // check all accounts and update task sync + for (account in accountRepository.get().getAll()) + automaticSyncManager.updateAutomaticSync(account, SyncDataType.TASKS) } @@ -160,7 +112,7 @@ class TasksAppManager @Inject constructor( val icon = pm.getApplicationIcon(e.provider.packageName) if (icon is BitmapDrawable) notify.setLargeIcon(icon.bitmap) - } catch (ignored: PackageManager.NameNotFoundException) { + } catch (_: PackageManager.NameNotFoundException) { // couldn't get provider app icon } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt index 692deb9f5..45bce8d4c 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/BaseSyncWorker.kt @@ -8,7 +8,6 @@ import android.accounts.Account import android.content.ContentResolver import android.content.Context import android.os.Build -import android.provider.CalendarContract import androidx.annotation.IntDef import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat @@ -25,11 +24,14 @@ import at.bitfire.davdroid.sync.AddressBookSyncer import at.bitfire.davdroid.sync.CalendarSyncer import at.bitfire.davdroid.sync.JtxSyncer import at.bitfire.davdroid.sync.SyncConditions +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncResult import at.bitfire.davdroid.sync.Syncer import at.bitfire.davdroid.sync.TaskSyncer +import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.ui.NotificationRegistry import at.bitfire.ical4android.TaskProvider +import dagger.Lazy import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.delay import kotlinx.coroutines.runInterruptible @@ -40,7 +42,7 @@ import java.util.logging.Logger import javax.inject.Inject abstract class BaseSyncWorker( - context: Context, + private val context: Context, private val workerParams: WorkerParameters, private val syncDispatcher: CoroutineDispatcher ) : CoroutineWorker(context, workerParams) { @@ -69,6 +71,9 @@ abstract class BaseSyncWorker( @Inject lateinit var syncConditionsFactory: SyncConditions.Factory + @Inject + lateinit var tasksAppManager: Lazy + @Inject lateinit var taskSyncer: TaskSyncer.Factory @@ -79,9 +84,9 @@ abstract class BaseSyncWorker( inputData.getString(INPUT_ACCOUNT_NAME) ?: throw IllegalArgumentException("INPUT_ACCOUNT_NAME required"), inputData.getString(INPUT_ACCOUNT_TYPE) ?: throw IllegalArgumentException("INPUT_ACCOUNT_TYPE required") ) - val authority = inputData.getString(INPUT_AUTHORITY) ?: throw IllegalArgumentException("INPUT_AUTHORITY required") + val dataType = SyncDataType.valueOf(inputData.getString(INPUT_DATA_TYPE) ?: throw IllegalArgumentException("INPUT_SYNC_DATA_TYPE required")) - val syncTag = commonTag(account, authority) + val syncTag = commonTag(account, dataType) logger.info("${javaClass.simpleName} called for $syncTag") if (!runningSyncs.add(syncTag)) { @@ -90,14 +95,14 @@ abstract class BaseSyncWorker( } // Dismiss any pending push notification - pushNotificationManager.dismiss(account, authority) + pushNotificationManager.dismiss(account, dataType) try { val accountSettings = try { accountSettingsFactory.create(account) } catch (_: InvalidAccountException) { val workId = workerParams.id - logger.warning("Account $account doesn't exist anymore, cancelling worker $workId") + logger.warning("No valid account settings for account $account, cancelling worker $workId") val workManager = WorkManager.getInstance(applicationContext) workManager.cancelWorkById(workId) @@ -123,7 +128,7 @@ abstract class BaseSyncWorker( } } - return doSyncWork(account, authority, accountSettings) + return doSyncWork(account, dataType) } finally { logger.info("${javaClass.simpleName} finished for $syncTag") runningSyncs -= syncTag @@ -133,12 +138,8 @@ abstract class BaseSyncWorker( } } - open suspend fun doSyncWork( - account: Account, - authority: String, - accountSettings: AccountSettings - ): Result = withContext(syncDispatcher) { - logger.info("Running ${javaClass.name}: account=$account, authority=$authority") + suspend fun doSyncWork(account: Account, dataType: SyncDataType): Result = withContext(syncDispatcher) { + logger.info("Running ${javaClass.name}: account=$account, dataType=$dataType") // pass possibly supplied flags to the selected syncer val extrasList = mutableListOf() @@ -156,18 +157,25 @@ abstract class BaseSyncWorker( val syncResult = SyncResult() // What are we going to sync? Select syncer based on authority - val syncer = when (authority) { - applicationContext.getString(R.string.address_books_authority) -> + val syncer = when (dataType) { + SyncDataType.CONTACTS -> addressBookSyncer.create(account, extras, syncResult) - CalendarContract.AUTHORITY -> + SyncDataType.EVENTS -> calendarSyncer.create(account, extras, syncResult) - TaskProvider.ProviderName.JtxBoard.authority -> - jtxSyncer.create(account, extras, syncResult) - TaskProvider.ProviderName.OpenTasks.authority, - TaskProvider.ProviderName.TasksOrg.authority -> - taskSyncer.create(account, authority, extras, syncResult) - else -> - throw IllegalArgumentException("Invalid authority $authority") + SyncDataType.TASKS -> { + val currentProvider = tasksAppManager.get().currentProvider() + when (currentProvider) { + TaskProvider.ProviderName.JtxBoard -> + jtxSyncer.create(account, extras, syncResult) + TaskProvider.ProviderName.OpenTasks, + TaskProvider.ProviderName.TasksOrg -> + taskSyncer.create(account, currentProvider.authority, extras, syncResult) + else -> { + logger.warning("No valid tasks provider found, aborting sync") + return@withContext Result.failure() + } + } + } } // Start syncing @@ -181,7 +189,7 @@ abstract class BaseSyncWorker( // Check for errors if (syncResult.hasError()) { - val softErrorNotificationTag = account.type + "-" + account.name + "-" + authority + val softErrorNotificationTag = "${account.type}-${account.name}-$dataType" // On soft errors the sync is retried a few times before considered failed if (syncResult.hasSoftError()) { @@ -241,7 +249,7 @@ abstract class BaseSyncWorker( // common worker input parameters const val INPUT_ACCOUNT_NAME = "accountName" const val INPUT_ACCOUNT_TYPE = "accountType" - const val INPUT_AUTHORITY = "authority" + const val INPUT_DATA_TYPE = "dataType" /** set to `true` for user-initiated sync that skips network checks */ const val INPUT_MANUAL = "manual" @@ -263,8 +271,6 @@ abstract class BaseSyncWorker( /** * How often this work will be retried to run after soft (network) errors. - * - * Retry strategy is defined in work request ([enqueue]). */ internal const val MAX_RUN_ATTEMPTS = 5 @@ -276,8 +282,8 @@ abstract class BaseSyncWorker( /** * This tag shall be added to every worker that is enqueued by a subclass. */ - fun commonTag(account: Account, authority: String): String = - "sync-$authority ${account.type}/${account.name}" + fun commonTag(account: Account, dataType: SyncDataType): String = + "sync-$dataType ${account.type}/${account.name}" } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt index b00a465ec..af4bb7f60 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/OneTimeSyncWorker.kt @@ -12,6 +12,7 @@ import androidx.work.ForegroundInfo import androidx.work.WorkManager import androidx.work.WorkerParameters import at.bitfire.davdroid.R +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncDispatcher import at.bitfire.davdroid.ui.NotificationRegistry import dagger.assisted.Assisted @@ -56,12 +57,13 @@ class OneTimeSyncWorker @AssistedInject constructor( * * Mainly used to query [WorkManager] for work state (by unique work name or tag). * - * @param account the account this worker is running for - * @param authority the authority this worker is running for - * @return Name of this worker composed as "onetime-sync $authority ${account.type}/${account.name}" + * @param account the account this worker is running for + * @param dataType data type to be synchronized + * + * @return Name of this worker composed as "onetime-sync $authority ${account.type}/${account.name}" */ - fun workerName(account: Account, authority: String): String = - "onetime-sync $authority ${account.type}/${account.name}" + fun workerName(account: Account, dataType: SyncDataType): String = + "onetime-sync $dataType ${account.type}/${account.name}" } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt index 09de3e045..4a0238dce 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/PeriodicSyncWorker.kt @@ -10,6 +10,7 @@ import androidx.annotation.VisibleForTesting import androidx.hilt.work.HiltWorker import androidx.work.WorkManager import androidx.work.WorkerParameters +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncDispatcher import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -51,12 +52,13 @@ class PeriodicSyncWorker @AssistedInject constructor( * * Mainly used to query [WorkManager] for work state (by unique work name or tag). * - * @param account the account this worker is running for - * @param authority the authority this worker is running for + * @param account the account this worker is running for + * @param dataType data type to be synchronized + * * @return Name of this worker composed as "periodic-sync $authority ${account.type}/${account.name}" */ - fun workerName(account: Account, authority: String): String = - "periodic-sync $authority ${account.type}/${account.name}" + fun workerName(account: Account, dataType: SyncDataType): String = + "periodic-sync $dataType ${account.type}/${account.name}" } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt index 694cd42e4..6866eb144 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/sync/worker/SyncWorkerManager.kt @@ -24,12 +24,12 @@ import androidx.work.WorkInfo import androidx.work.WorkManager import androidx.work.WorkQuery import androidx.work.WorkRequest -import at.bitfire.davdroid.R import at.bitfire.davdroid.push.PushNotificationManager +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_ACCOUNT_NAME import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_ACCOUNT_TYPE -import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_AUTHORITY +import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_DATA_TYPE import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_MANUAL import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_RESYNC import at.bitfire.davdroid.sync.worker.BaseSyncWorker.Companion.INPUT_UPLOAD @@ -67,14 +67,14 @@ class SyncWorkerManager @Inject constructor( */ fun buildOneTime( account: Account, - authority: String, + dataType: SyncDataType, manual: Boolean = false, @InputResync resync: Int = NO_RESYNC, upload: Boolean = false ): OneTimeWorkRequest { // worker arguments val argumentsBuilder = Data.Builder() - .putString(INPUT_AUTHORITY, authority) + .putString(INPUT_DATA_TYPE, dataType.toString()) .putString(INPUT_ACCOUNT_NAME, account.name) .putString(INPUT_ACCOUNT_TYPE, account.type) if (manual) @@ -88,8 +88,8 @@ class SyncWorkerManager @Inject constructor( .setRequiredNetworkType(NetworkType.CONNECTED) // require a network connection .build() return OneTimeWorkRequestBuilder() - .addTag(OneTimeSyncWorker.workerName(account, authority)) - .addTag(commonTag(account, authority)) + .addTag(OneTimeSyncWorker.workerName(account, dataType)) + .addTag(commonTag(account, dataType)) .setInputData(argumentsBuilder.build()) .setBackoffCriteria( BackoffPolicy.EXPONENTIAL, @@ -110,7 +110,7 @@ class SyncWorkerManager @Inject constructor( * Requests immediate synchronization of an account with a specific authority. * * @param account account to sync - * @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]) + * @param dataType type of data to synchronize * @param manual user-initiated sync (ignores network checks) * @param resync whether to request (full) re-synchronization or not * @param upload see [ContentResolver.SYNC_EXTRAS_UPLOAD] – only used for contacts sync and Android 7 workaround @@ -120,26 +120,27 @@ class SyncWorkerManager @Inject constructor( */ fun enqueueOneTime( account: Account, - authority: String, + dataType: SyncDataType, manual: Boolean = false, @InputResync resync: Int = NO_RESYNC, upload: Boolean = false, fromPush: Boolean = false ): String { + logger.info("Enqueueing unique worker for account=$account, dataType=$dataType, manual=$manual, resync=$resync, upload=$upload, fromPush=$fromPush") + // enqueue and start syncing - val name = OneTimeSyncWorker.workerName(account, authority) + val name = OneTimeSyncWorker.workerName(account, dataType) val request = buildOneTime( account = account, - authority = authority, + dataType = dataType, manual = manual, resync = resync, upload = upload ) if (fromPush) { logger.fine("Showing push sync pending notification for $name") - pushNotificationManager.notify(account, authority) + pushNotificationManager.notify(account, dataType) } - logger.info("Enqueueing unique worker: $name, tags = ${request.tags}") WorkManager.getInstance(context).enqueueUniqueWork( name, /* If sync is already running, just continue. @@ -164,10 +165,10 @@ class SyncWorkerManager @Inject constructor( upload: Boolean = false, fromPush: Boolean = false ) { - for (authority in syncAuthorities()) + for (dataType in SyncDataType.entries) enqueueOneTime( account = account, - authority = authority, + dataType = dataType, manual = manual, resync = resync, upload = upload, @@ -185,9 +186,9 @@ class SyncWorkerManager @Inject constructor( * * @return periodic sync work request for the given arguments */ - fun buildPeriodic(account: Account, authority: String, interval: Long, syncWifiOnly: Boolean): PeriodicWorkRequest { + fun buildPeriodic(account: Account, dataType: SyncDataType, interval: Long, syncWifiOnly: Boolean): PeriodicWorkRequest { val arguments = Data.Builder() - .putString(INPUT_AUTHORITY, authority) + .putString(INPUT_DATA_TYPE, dataType.toString()) .putString(INPUT_ACCOUNT_NAME, account.name) .putString(INPUT_ACCOUNT_TYPE, account.type) .build() @@ -199,8 +200,8 @@ class SyncWorkerManager @Inject constructor( NetworkType.CONNECTED ).build() return PeriodicWorkRequestBuilder(interval, TimeUnit.SECONDS) - .addTag(PeriodicSyncWorker.workerName(account, authority)) - .addTag(commonTag(account, authority)) + .addTag(PeriodicSyncWorker.workerName(account, dataType)) + .addTag(commonTag(account, dataType)) .setInputData(arguments) .setConstraints(constraints) .build() @@ -210,14 +211,15 @@ class SyncWorkerManager @Inject constructor( * Activate periodic synchronization of an account with a specific authority. * * @param account account to sync - * @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]]) + * @param dataType type of data to synchronize * @param interval interval between recurring syncs in seconds * @return operation object to check when and whether activation was successful */ - fun enablePeriodic(account: Account, authority: String, interval: Long, syncWifiOnly: Boolean): Operation { - val workRequest = buildPeriodic(account, authority, interval, syncWifiOnly) + fun enablePeriodic(account: Account, dataType: SyncDataType, interval: Long, syncWifiOnly: Boolean): Operation { + logger.fine("Updating periodic worker for account=$account, dataType=$dataType, interval=$interval, syncWifiOnly=$syncWifiOnly") + val workRequest = buildPeriodic(account, dataType, interval, syncWifiOnly) return WorkManager.getInstance(context).enqueueUniquePeriodicWork( - PeriodicSyncWorker.workerName(account, authority), + PeriodicSyncWorker.workerName(account, dataType), // if a periodic sync exists already, we want to update it with the new interval // and/or new required network type (applies on next iteration of periodic worker) ExistingPeriodicWorkPolicy.UPDATE, @@ -229,12 +231,14 @@ class SyncWorkerManager @Inject constructor( * Disables periodic synchronization of an account for a specific authority. * * @param account account to sync - * @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]]) + * @param dataType type of data to synchronize * @return operation object to check process state of work cancellation */ - fun disablePeriodic(account: Account, authority: String): Operation = - WorkManager.getInstance(context) - .cancelUniqueWork(PeriodicSyncWorker.workerName(account, authority)) + fun disablePeriodic(account: Account, dataType: SyncDataType): Operation { + logger.fine("Disabling periodic worker for account=$account, dataType=$dataType") + return WorkManager.getInstance(context) + .cancelUniqueWork(PeriodicSyncWorker.workerName(account, dataType)) + } // common / helpers @@ -244,9 +248,9 @@ class SyncWorkerManager @Inject constructor( */ fun cancelAllWork(account: Account) { val workManager = WorkManager.getInstance(context) - for (authority in syncAuthorities()) { - workManager.cancelUniqueWork(OneTimeSyncWorker.workerName(account, authority)) - workManager.cancelUniqueWork(PeriodicSyncWorker.workerName(account, authority)) + for (dataType in SyncDataType.entries) { + workManager.cancelUniqueWork(OneTimeSyncWorker.workerName(account, dataType)) + workManager.cancelUniqueWork(PeriodicSyncWorker.workerName(account, dataType)) } } @@ -264,15 +268,15 @@ class SyncWorkerManager @Inject constructor( fun hasAnyFlow( workStates: List, account: Account? = null, - authorities: List? = null, - whichTag: (account: Account, authority: String) -> String = { account, authority -> - commonTag(account, authority) + dataTypes: Iterable? = null, + whichTag: (account: Account, dataType: SyncDataType) -> String = { account, dataType -> + commonTag(account, dataType) } ): Flow { val workQuery = WorkQuery.Builder.fromStates(workStates) - if (account != null && authorities != null) + if (account != null && dataTypes != null) workQuery.addTags( - authorities.map { authority -> whichTag(account, authority) } + dataTypes.map { dataType -> whichTag(account, dataType) } ) return WorkManager.getInstance(context) .getWorkInfosFlow(workQuery.build()) @@ -281,29 +285,4 @@ class SyncWorkerManager @Inject constructor( } } - /** - * Returns a list of all available sync authorities: - * - * 1. calendar authority - * 2. address books authority - * 3. current tasks authority (if available) - * - * Checking the availability of authorities may be relatively expensive, so the - * result should be cached for the current operation. - * - * @return list of available sync authorities for DAVx5 accounts - */ - fun syncAuthorities(): List { - val result = mutableListOf( - CalendarContract.AUTHORITY, - context.getString(R.string.address_books_authority) - ) - - tasksAppManager.get().currentProvider()?.let { taskProvider -> - result += taskProvider.authority - } - - return result - } - } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt index 650d3661e..309a7dc78 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/AccountsModel.kt @@ -18,6 +18,7 @@ import androidx.work.WorkQuery import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.worker.BaseSyncWorker import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker import at.bitfire.davdroid.sync.worker.SyncWorkerManager @@ -84,7 +85,6 @@ class AccountsModel @AssistedInject constructor( private val runningWorkers = workManager.getWorkInfosFlow(WorkQuery.fromStates(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING)) val accountInfos: Flow> = combine(accounts, runningWorkers) { accounts, workInfos -> - val authorities = syncWorkerManager.syncAuthorities() val collator = Collator.getInstance() accounts @@ -96,15 +96,15 @@ class AccountsModel @AssistedInject constructor( info.state == WorkInfo.State.RUNNING && ( services.any { serviceId -> info.tags.contains(RefreshCollectionsWorker.workerName(serviceId)) - } || authorities.any { authority -> - info.tags.contains(BaseSyncWorker.commonTag(account, authority)) + } || SyncDataType.entries.any { dataType -> + info.tags.contains(BaseSyncWorker.commonTag(account, dataType)) } ) } -> AccountProgress.Active workInfos.any { info -> - info.state == WorkInfo.State.ENQUEUED && authorities.any { authority -> - info.tags.contains(OneTimeSyncWorker.workerName(account, authority)) + info.state == WorkInfo.State.ENQUEUED && SyncDataType.entries.any { dataType -> + info.tags.contains(OneTimeSyncWorker.workerName(account, dataType)) } } -> AccountProgress.Pending diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt index 05384f5a3..fdb39197e 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/DebugInfoModel.kt @@ -46,6 +46,7 @@ import at.bitfire.davdroid.repository.AccountRepository import at.bitfire.davdroid.resource.LocalAddressBook import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.SyncFrameworkIntegration import at.bitfire.davdroid.sync.worker.BaseSyncWorker import at.bitfire.ical4android.TaskProvider @@ -528,7 +529,7 @@ class DebugInfoModel @AssistedInject constructor( * @return the requested information */ private fun dumpAccount(account: Account, accountSettings: AccountSettings?, infos: Iterable): String { - val table = TextTable("Authority", "isSyncable", "syncsOnContentChange", "Interval", "Entries") + val table = TextTable("Authority", "isSyncable", "syncsOnContentChange", "Entries") for (info in infos) { var nrEntries = "—" if (info.countUri != null) @@ -545,7 +546,6 @@ class DebugInfoModel @AssistedInject constructor( info.authority, syncFramework.isSyncable(account, info.authority), syncFramework.syncsOnContentChange(account, info.authority), - accountSettings?.getSyncInterval(info.authority)?.takeIf { it >= 0 }?.let {"${it/60} min"}, nrEntries ) } @@ -559,20 +559,14 @@ class DebugInfoModel @AssistedInject constructor( */ private fun dumpSyncWorkersInfo(account: Account): String { val table = TextTable("Tags", "Authority", "State", "Next run", "Retries", "Generation", "Periodicity") - listOf( - context.getString(R.string.address_books_authority), - CalendarContract.AUTHORITY, - TaskProvider.ProviderName.JtxBoard.authority, - TaskProvider.ProviderName.OpenTasks.authority, - TaskProvider.ProviderName.TasksOrg.authority - ).forEach { authority -> - val tag = BaseSyncWorker.commonTag(account, authority) + for (dataType in SyncDataType.entries) { + val tag = BaseSyncWorker.commonTag(account, dataType) WorkManager.getInstance(context).getWorkInfos( WorkQuery.Builder.fromTags(listOf(tag)).build() ).get().forEach { workInfo -> table.addLine( workInfo.tags.map { it.replace("\\bat\\.bitfire\\.davdroid\\.".toRegex(), ".") }, - authority, + dataType, "${workInfo.state} (${workInfo.stopReason})", workInfo.nextScheduleTimeMillis.let { nextRun -> when (nextRun) { diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt index 1e209fa37..0e075450b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/TasksModel.kt @@ -67,7 +67,7 @@ class TasksModel @Inject constructor( try { context.packageManager.getPackageInfo(packageName, 0) true - } catch (e: PackageManager.NameNotFoundException) { + } catch (_: PackageManager.NameNotFoundException) { false } diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt index ddc81a4a8..786164e3b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountProgressUseCase.kt @@ -9,6 +9,7 @@ import android.content.Context import androidx.work.WorkInfo import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.worker.OneTimeSyncWorker import at.bitfire.davdroid.sync.worker.SyncWorkerManager import dagger.hilt.android.qualifiers.ApplicationContext @@ -27,11 +28,11 @@ class AccountProgressUseCase @Inject constructor( operator fun invoke( account: Account, serviceFlow: Flow, - authoritiesFlow: Flow> + dataTypes: Iterable ): Flow { val serviceRefreshing = isServiceRefreshing(serviceFlow) - val syncPending = isSyncPending(account, authoritiesFlow) - val syncRunning = isSyncRunning(account, authoritiesFlow) + val syncPending = isSyncPending(account, dataTypes) + val syncRunning = isSyncRunning(account, dataTypes) return combine(serviceRefreshing, syncPending, syncRunning) { refreshing, pending, syncing -> when { @@ -52,27 +53,23 @@ class AccountProgressUseCase @Inject constructor( } @OptIn(ExperimentalCoroutinesApi::class) - fun isSyncPending(account: Account, authoritiesFlow: Flow>): Flow = - authoritiesFlow.flatMapLatest { authorities -> - syncWorkerManager.hasAnyFlow( - workStates = listOf(WorkInfo.State.ENQUEUED), - account = account, - authorities = authorities, - whichTag = { _, authority -> - // we are only interested in pending OneTimeSyncWorkers because there's always a pending PeriodicSyncWorker - OneTimeSyncWorker.workerName(account, authority) - } - ) - } + fun isSyncPending(account: Account, dataTypes: Iterable): Flow = + syncWorkerManager.hasAnyFlow( + workStates = listOf(WorkInfo.State.ENQUEUED), + account = account, + dataTypes = dataTypes, + whichTag = { _, authority -> + // we are only interested in pending OneTimeSyncWorkers because there's always a pending PeriodicSyncWorker + OneTimeSyncWorker.workerName(account, authority) + } + ) @OptIn(ExperimentalCoroutinesApi::class) - fun isSyncRunning(account: Account, authoritiesFlow: Flow>): Flow = - authoritiesFlow.flatMapLatest { authorities -> - syncWorkerManager.hasAnyFlow( - workStates = listOf(WorkInfo.State.RUNNING), - account = account, - authorities = authorities - ) - } + fun isSyncRunning(account: Account, dataTypes: Iterable): Flow = + syncWorkerManager.hasAnyFlow( + workStates = listOf(WorkInfo.State.RUNNING), + account = account, + dataTypes = dataTypes + ) } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt index ec0e248ce..e1aa83fe1 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountScreenModel.kt @@ -6,7 +6,6 @@ package at.bitfire.davdroid.ui.account import android.accounts.Account import android.content.Context -import android.provider.CalendarContract import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue @@ -23,6 +22,7 @@ import at.bitfire.davdroid.repository.DavCollectionRepository import at.bitfire.davdroid.repository.DavServiceRepository import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker import at.bitfire.davdroid.settings.AccountSettings +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.worker.SyncWorkerManager import dagger.assisted.Assisted @@ -35,7 +35,6 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted -import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @@ -95,7 +94,7 @@ class AccountScreenModel @AssistedInject constructor( val cardDavProgress: Flow = accountProgressUseCase( account = account, serviceFlow = cardDavSvc, - authoritiesFlow = flowOf(listOf(context.getString(R.string.address_books_authority))) + dataTypes = listOf(SyncDataType.CONTACTS) ) val addressBooks = getServiceCollectionPager(cardDavSvc, Collection.TYPE_ADDRESSBOOK, showOnlyPersonal) @@ -107,13 +106,10 @@ class AccountScreenModel @AssistedInject constructor( homeSets.isNotEmpty() } val tasksProvider = tasksAppManager.currentProviderFlow() - private val calDavAuthorities = tasksProvider.map { tasks -> - listOfNotNull(CalendarContract.AUTHORITY, tasks?.authority) - } val calDavProgress = accountProgressUseCase( account = account, serviceFlow = calDavSvc, - authoritiesFlow = calDavAuthorities + dataTypes = listOf(SyncDataType.EVENTS, SyncDataType.TASKS) ) val calendars = getServiceCollectionPager(calDavSvc, Collection.TYPE_CALENDAR, showOnlyPersonal) val subscriptions = getServiceCollectionPager(calDavSvc, Collection.TYPE_WEBCAL, showOnlyPersonal) 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 d9ea160cc..7a8e12585 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 @@ -2,19 +2,17 @@ package at.bitfire.davdroid.ui.account import android.accounts.Account import android.content.Context -import android.provider.CalendarContract import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import at.bitfire.davdroid.R import at.bitfire.davdroid.db.AppDatabase import at.bitfire.davdroid.db.Credentials import at.bitfire.davdroid.db.Service import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.settings.SettingsManager +import at.bitfire.davdroid.sync.SyncDataType import at.bitfire.davdroid.sync.TasksAppManager import at.bitfire.davdroid.sync.worker.BaseSyncWorker import at.bitfire.davdroid.sync.worker.SyncWorkerManager -import at.bitfire.ical4android.TaskProvider import at.bitfire.vcard4android.GroupMethod import dagger.assisted.Assisted import dagger.assisted.AssistedFactory @@ -106,11 +104,11 @@ class AccountSettingsModel @AssistedInject constructor( _uiState.value = UiState( hasContactsSync = hasContactsSync, - syncIntervalContacts = accountSettings.getSyncInterval(context.getString(R.string.address_books_authority)), + syncIntervalContacts = accountSettings.getSyncInterval(SyncDataType.CONTACTS), hasCalendarsSync = hasCalendarSync, - syncIntervalCalendars = accountSettings.getSyncInterval(CalendarContract.AUTHORITY), + syncIntervalCalendars = accountSettings.getSyncInterval(SyncDataType.EVENTS), hasTasksSync = hasTasksSync, - syncIntervalTasks = tasksProvider?.let { accountSettings.getSyncInterval(it.authority) }, + syncIntervalTasks = accountSettings.getSyncInterval(SyncDataType.TASKS), syncWifiOnly = accountSettings.getSyncWifiOnly(), syncWifiOnlySSIDs = accountSettings.getSyncWifiOnlySSIDs(), @@ -129,24 +127,22 @@ class AccountSettingsModel @AssistedInject constructor( fun updateContactsSyncInterval(syncInterval: Long) { CoroutineScope(Dispatchers.Default).launch { - accountSettings.setSyncInterval(context.getString(R.string.address_books_authority), syncInterval) + accountSettings.setSyncInterval(SyncDataType.CONTACTS, syncInterval.takeUnless { it == -1L }) reload() } } fun updateCalendarSyncInterval(syncInterval: Long) { CoroutineScope(Dispatchers.Default).launch { - accountSettings.setSyncInterval(CalendarContract.AUTHORITY, syncInterval) + accountSettings.setSyncInterval(SyncDataType.EVENTS, syncInterval.takeUnless { it == -1L }) reload() } } fun updateTasksSyncInterval(syncInterval: Long) { CoroutineScope(Dispatchers.Default).launch { - tasksProvider?.authority?.let { tasksAuthority -> - accountSettings.setSyncInterval(tasksAuthority, syncInterval) - reload() - } + accountSettings.setSyncInterval(SyncDataType.TASKS, syncInterval.takeUnless { it == -1L }) + reload() } } @@ -207,41 +203,38 @@ class AccountSettingsModel @AssistedInject constructor( accountSettings.setGroupMethod(groupMethod) reload() - resync( - authority = context.getString(R.string.address_books_authority), - fullResync = true - ) + resync(SyncDataType.CONTACTS, fullResync = true) } /** * Initiates calendar re-synchronization. * * @param fullResync whether sync shall download all events again - * (_true_: sets [at.bitfire.davdroid.sync.Syncer.SYNC_EXTRAS_FULL_RESYNC], - * _false_: sets [at.bitfire.davdroid.sync.Syncer.SYNC_EXTRAS_RESYNC]) + * (_true_: sets [BaseSyncWorker.FULL_RESYNC], + * _false_: sets [BaseSyncWorker.RESYNC]) * @param tasks whether tasks shall be synchronized, too (false: only events, true: events and tasks) */ private fun resyncCalendars(fullResync: Boolean, tasks: Boolean) { - resync(CalendarContract.AUTHORITY, fullResync) + resync(SyncDataType.EVENTS, fullResync) if (tasks) - resync(TaskProvider.ProviderName.OpenTasks.authority, fullResync) + resync(SyncDataType.TASKS, fullResync) } /** * Initiates re-synchronization for given authority. * - * @param authority authority to re-sync - * @param fullResync whether sync shall download all events again - * (_true_: sets [at.bitfire.davdroid.sync.worker.BaseSyncWorker.FULL_RESYNC], + * @param dataType type of data to synchronize + * @param fullResync whether sync shall download all events again + * (_true_: sets [BaseSyncWorker.FULL_RESYNC], * _false_: sets [BaseSyncWorker.RESYNC]) */ - private fun resync(authority: String, fullResync: Boolean) { - val resync = + private fun resync(dataType: SyncDataType, fullResync: Boolean) { + val resync: Int = if (fullResync) BaseSyncWorker.FULL_RESYNC else BaseSyncWorker.RESYNC - syncWorkerManager.enqueueOneTime(account, authority = authority, resync = resync) + syncWorkerManager.enqueueOneTime(account, dataType, resync = resync) } } \ No newline at end of file diff --git a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsScreen.kt b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsScreen.kt index a9c040358..dc9e54b4b 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsScreen.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/ui/account/AccountSettingsScreen.kt @@ -52,7 +52,6 @@ import androidx.hilt.navigation.compose.hiltViewModel import at.bitfire.davdroid.Constants import at.bitfire.davdroid.R import at.bitfire.davdroid.db.Credentials -import at.bitfire.davdroid.settings.AccountSettings import at.bitfire.davdroid.ui.AppTheme import at.bitfire.davdroid.ui.composable.ActionCard import at.bitfire.davdroid.ui.composable.EditTextInputDialog @@ -457,7 +456,7 @@ fun SyncIntervalSetting( icon = icon, name = stringResource(name), summary = - if (syncInterval == null || syncInterval == AccountSettings.SYNC_INTERVAL_MANUALLY) + if (syncInterval == null) stringResource(R.string.settings_sync_summary_manually) else stringResource(R.string.settings_sync_summary_periodically, syncInterval / 60), diff --git a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt index d2a4a884e..437a70306 100644 --- a/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt +++ b/app/src/main/kotlin/at/bitfire/davdroid/util/PermissionUtils.kt @@ -133,7 +133,7 @@ object PermissionUtils { * @return whether at least one of [permissions] is granted */ fun haveAnyPermission(context: Context, permissions: Array) = - permissions.any { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } + permissions.any { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } /** * Checks whether all given permissions are granted. @@ -144,7 +144,7 @@ object PermissionUtils { * @return whether all [permissions] are granted */ fun havePermissions(context: Context, permissions: Array) = - permissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } + permissions.all { ContextCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED } fun showAppSettings(context: Context) { val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS,