Periodic workers directly run sync (#648)

* [WIP] Don't create a separate SyncWorker for every sync (run directly within onetime/periodic sync instead)

* [WIP] address books

* Account(s)Activity: don't show pending workers

* Migration to set new periodic sync worker tags

* Fix tests

* ContactsSyncAdapter issues address book sync on main account (not contacts sync)

* SyncAdapterService: optimize blocking with Flow instead of LiveData
This commit is contained in:
Ricki Hirner 2024-03-14 20:08:38 +01:00 committed by GitHub
parent a16fd468fd
commit 30122a79f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
23 changed files with 543 additions and 882 deletions

View file

@ -13,15 +13,20 @@ import android.net.NetworkCapabilities.NET_CAPABILITY_VALIDATED
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.junit4.MockKRule
import io.mockk.mockk
import junit.framework.TestCase.assertFalse
import junit.framework.TestCase.assertTrue
import org.junit.Before
import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class ConnectionUtilsTest {
@get:Rule
val mockkRule = MockKRule(this)
private val connectivityManager = mockk<ConnectivityManager>()
private val network1 = mockk<Network>()
private val network2 = mockk<Network>()

View file

@ -10,11 +10,7 @@ import android.util.Log
import androidx.hilt.work.HiltWorkerFactory
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.WorkManager
import androidx.work.await
import androidx.work.testing.WorkManagerTestInitHelper
import at.bitfire.davdroid.TestUtils.workScheduledOrRunning
import at.bitfire.davdroid.TestUtils.workScheduledOrRunningOrSuccessful
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.db.Credentials
@ -30,8 +26,8 @@ import at.bitfire.davdroid.ui.setup.LoginModel
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.junit4.MockKRule
import io.mockk.mockk
import kotlinx.coroutines.runBlocking
import okhttp3.mockwebserver.Dispatcher
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
@ -53,6 +49,8 @@ class RefreshCollectionsWorkerTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
val mockkRule = MockKRule(this)
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext

View file

@ -1,222 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.ui.setup
import android.accounts.AccountManager
import android.content.ContentResolver
import android.content.Context
import android.provider.CalendarContract
import android.util.Log
import androidx.annotation.RequiresApi
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.WorkManagerTestInitHelper
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TestUtils.getOrAwaitValue
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.servicedetection.DavResourceFinder
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.ical4android.TaskProvider
import at.bitfire.vcard4android.GroupMethod
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.*
import org.junit.*
import org.junit.Assert.*
import javax.inject.Inject
// COMMENTED OUT because it doesn't run reliably [see https://github.com/bitfireAT/davx5/pull/320]
/*@HiltAndroidTest
class AccountDetailsFragmentTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@get:Rule
var instantTaskExecutorRule = InstantTaskExecutorRule() // required for TestUtils: LiveData.getOrAwaitValue()
@Inject
lateinit var db: AppDatabase
@Inject
lateinit var settingsManager: SettingsManager
private val targetContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
private val fakeCredentials = Credentials("test", "test")
@Before
fun setUp() {
hiltRule.inject()
// The test application is an instance of HiltTestApplication, which doesn't initialize notification channels.
// However, we need notification channels for the ongoing work notifications.
NotificationUtils.createChannels(targetContext)
// Initialize WorkManager for instrumentation tests.
val config = Configuration.Builder()
.setMinimumLoggingLevel(Log.DEBUG)
.build()
WorkManagerTestInitHelper.initializeTestWorkManager(targetContext, config)
}
@After
fun tearDown() {
// Remove accounts created by tests
val am = AccountManager.get(targetContext)
val accounts = am.getAccountsByType(targetContext.getString(R.string.account_type))
for (account in accounts) {
am.removeAccountExplicitly(account)
}
}
@Test
fun testModel_CreateAccount_configuresContactsAndCalendars() {
val accountName = "MyAccountName"
val emptyServiceInfo = DavResourceFinder.Configuration.ServiceInfo()
val config = DavResourceFinder.Configuration(emptyServiceInfo, emptyServiceInfo, false, "")
// Create account -> should also set sync interval in settings
val accountCreated = AccountDetailsFragment.Model(targetContext, db, settingsManager)
.createAccount(accountName, fakeCredentials, config, GroupMethod.GROUP_VCARDS)
assertTrue(accountCreated.getOrAwaitValue(5))
// Get the created account
val account = AccountManager.get(targetContext)
.getAccountsByType(targetContext.getString(R.string.account_type))
.first { account -> account.name == accountName }
for (authority in listOf(
targetContext.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
)) {
// Check isSyncable was set
assertEquals(1, ContentResolver.getIsSyncable(account, authority))
// Check default sync interval was set for
// [AccountSettings.KEY_SYNC_INTERVAL_ADDRESSBOOKS],
// [AccountSettings.KEY_SYNC_INTERVAL_CALENDARS]
assertEquals(
settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL),
AccountSettings(targetContext, account).getSyncInterval(authority)
)
}
}
@Test
@RequiresApi(28) // for mockkObject
fun testModel_CreateAccount_configuresCalendarsWithTasks() {
for (provider in listOf(
TaskProvider.ProviderName.JtxBoard,
TaskProvider.ProviderName.OpenTasks,
TaskProvider.ProviderName.TasksOrg
)) {
val accountName = "testAccount-$provider"
val emptyServiceInfo = DavResourceFinder.Configuration.ServiceInfo()
val config = DavResourceFinder.Configuration(emptyServiceInfo, emptyServiceInfo, false, "")
// Mock TaskUtils currentProvider method, pretending that one of the task apps is installed :)
mockkObject(TaskUtils)
every { TaskUtils.currentProvider(targetContext) } returns provider
assertEquals(provider, TaskUtils.currentProvider(targetContext))
// Create account -> should also set tasks sync interval in settings
val accountCreated =
AccountDetailsFragment.Model(targetContext, db, settingsManager)
.createAccount(accountName, fakeCredentials, config, GroupMethod.GROUP_VCARDS)
assertTrue(accountCreated.getOrAwaitValue(5))
// Get the created account
val account = AccountManager.get(targetContext)
.getAccountsByType(targetContext.getString(R.string.account_type))
.first { account -> account.name == accountName }
// Calendar: Check isSyncable and default interval are set correctly
assertEquals(1, ContentResolver.getIsSyncable(account, CalendarContract.AUTHORITY))
assertEquals(
settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL),
AccountSettings(targetContext, account).getSyncInterval(CalendarContract.AUTHORITY)
)
// Tasks: Check isSyncable and default sync interval were set
assertEquals(1, ContentResolver.getIsSyncable(account, provider.authority))
assertEquals(
settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL),
AccountSettings(targetContext, account).getSyncInterval(provider.authority)
)
}
}
@Test
@RequiresApi(28)
fun testModel_CreateAccount_configuresCalendarsWithoutTasks() {
try {
val accountName = "testAccount"
val emptyServiceInfo = DavResourceFinder.Configuration.ServiceInfo()
val config = DavResourceFinder.Configuration(emptyServiceInfo, emptyServiceInfo, false, "")
// Mock TaskUtils currentProvider method, pretending that no task app is installed
mockkObject(TaskUtils)
every { TaskUtils.currentProvider(targetContext) } returns null
assertEquals(null, TaskUtils.currentProvider(targetContext))
// Mock static ContentResolver calls
// TODO: Should not be needed, see below
mockkStatic(ContentResolver::class)
every { ContentResolver.setIsSyncable(any(), any(), any()) } returns Unit
every { ContentResolver.getIsSyncable(any(), any()) } returns 1
// Create account will try to start an initial collection refresh, which we don't need, so we mockk it
mockkObject(RefreshCollectionsWorker.Companion)
every { RefreshCollectionsWorker.refreshCollections(any(), any()) } returns ""
// Create account -> should also set tasks sync interval in settings
val accountCreated = AccountDetailsFragment.Model(targetContext, db, settingsManager)
.createAccount(accountName, fakeCredentials, config, GroupMethod.GROUP_VCARDS)
assertTrue(accountCreated.getOrAwaitValue(5))
// Get the created account
val account = AccountManager.get(targetContext)
.getAccountsByType(targetContext.getString(R.string.account_type))
.first { account -> account.name == accountName }
val accountSettings = AccountSettings(targetContext, account)
// Calendar: Check automatic sync is enabled and default interval are set correctly
assertEquals(1, ContentResolver.getIsSyncable(account, CalendarContract.AUTHORITY))
assertEquals(
settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL),
accountSettings.getSyncInterval(CalendarContract.AUTHORITY)
)
// Tasks: Check isSyncable state is unknown (=-1) and sync interval is "unset" (=null)
for (authority in listOf(
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.OpenTasks.authority,
TaskProvider.ProviderName.TasksOrg.authority
)) {
// Until below is fixed, just verify the method for enabling sync did not get called
verify(exactly = 0) { ContentResolver.setIsSyncable(account, authority, 1) }
// TODO: Flaky, returns 1, although it should not. It only returns -1 if the test is run
// alone, on a blank emulator and if it's the first test run.
// This seems to have to do with the previous calls to ContentResolver by other tests.
// Finding a a way of resetting the ContentResolver before each test is run should
// solve the issue.
//assertEquals(-1, ContentResolver.getIsSyncable(account, authority))
//assertNull(accountSettings.getSyncInterval(authority)) // Depends on above
}
} catch (e: InterruptedException) {
// The sync adapter framework will start a sync, which can get interrupted. We don't care
// about being interrupted. If it happens the test is not too important.
}
}
}*/

View file

@ -10,6 +10,7 @@ import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.WebDavMount
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.junit4.MockKRule
import io.mockk.spyk
import okhttp3.mockwebserver.MockResponse
import okhttp3.mockwebserver.MockWebServer
@ -26,6 +27,8 @@ class AddWebdavMountActivityTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val mockkRule = MockKRule(this)
@Inject
lateinit var db: AppDatabase

View file

@ -14,7 +14,6 @@ import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.WorkManagerTestInitHelper
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TestUtils.workScheduledOrRunningOrSuccessful
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.network.ConnectionUtils
import at.bitfire.davdroid.settings.AccountSettings
@ -22,6 +21,7 @@ import at.bitfire.davdroid.ui.NotificationUtils
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.every
import io.mockk.junit4.MockKRule
import io.mockk.mockk
import io.mockk.mockkObject
import org.junit.After
@ -32,7 +32,7 @@ import org.junit.Rule
import org.junit.Test
@HiltAndroidTest
class SyncWorkerTest {
class BaseSyncWorkerTest {
val context = InstrumentationRegistry.getInstrumentation().targetContext
@ -42,6 +42,8 @@ class SyncWorkerTest {
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val mockkRule = MockKRule(this)
@Before
fun inject() = hiltRule.inject()
@ -70,18 +72,11 @@ class SyncWorkerTest {
}
@Test
fun testEnqueue_enqueuesWorker() {
SyncWorker.enqueue(context, account, CalendarContract.AUTHORITY, true)
val workerName = SyncWorker.workerName(account, CalendarContract.AUTHORITY)
assertTrue(workScheduledOrRunningOrSuccessful(context, workerName))
}
@Test
fun testWifiConditionsMet_withoutWifi() {
val accountSettings = mockk<AccountSettings>()
every { accountSettings.getSyncWifiOnly() } returns false
assertTrue(SyncWorker.wifiConditionsMet(context, accountSettings))
assertTrue(BaseSyncWorker.wifiConditionsMet(context, accountSettings))
}
@Test
@ -91,10 +86,10 @@ class SyncWorkerTest {
mockkObject(ConnectionUtils)
every { ConnectionUtils.wifiAvailable(any()) } returns true
mockkObject(SyncWorker.Companion)
every { SyncWorker.Companion.correctWifiSsid(any(), any()) } returns true
mockkObject(BaseSyncWorker.Companion)
every { BaseSyncWorker.correctWifiSsid(any(), any()) } returns true
assertTrue(SyncWorker.wifiConditionsMet(context, accountSettings))
assertTrue(BaseSyncWorker.wifiConditionsMet(context, accountSettings))
}
@Test
@ -104,10 +99,10 @@ class SyncWorkerTest {
mockkObject(ConnectionUtils)
every { ConnectionUtils.wifiAvailable(any()) } returns false
mockkObject(SyncWorker.Companion)
every { SyncWorker.Companion.correctWifiSsid(any(), any()) } returns true
mockkObject(BaseSyncWorker.Companion)
every { BaseSyncWorker.correctWifiSsid(any(), any()) } returns true
assertFalse(SyncWorker.wifiConditionsMet(context, accountSettings))
assertFalse(BaseSyncWorker.wifiConditionsMet(context, accountSettings))
}

View file

@ -0,0 +1,24 @@
package at.bitfire.davdroid.syncadapter
import android.content.Context
import android.provider.CalendarContract
import androidx.test.platform.app.InstrumentationRegistry
import at.bitfire.davdroid.TestUtils
import at.bitfire.davdroid.syncadapter.SyncManagerTest.Companion.account
import dagger.hilt.android.testing.HiltAndroidTest
import org.junit.Assert
import org.junit.Test
@HiltAndroidTest
class OneTimeSyncWorkerTest {
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
@Test
fun testEnqueue_enqueuesWorker() {
OneTimeSyncWorker.enqueue(context, account, CalendarContract.AUTHORITY)
val workerName = OneTimeSyncWorker.workerName(account, CalendarContract.AUTHORITY)
Assert.assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context, workerName))
}
}

View file

@ -7,23 +7,28 @@ package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
import android.content.Context
import android.provider.CalendarContract
import android.provider.ContactsContract
import android.util.Log
import androidx.test.platform.app.InstrumentationRegistry
import androidx.work.Configuration
import androidx.work.testing.TestWorkerBuilder
import androidx.work.ListenableWorker
import androidx.work.WorkManager
import androidx.work.testing.TestListenableWorkerBuilder
import androidx.work.testing.WorkManagerTestInitHelper
import androidx.work.workDataOf
import at.bitfire.davdroid.R
import at.bitfire.davdroid.TestUtils
import at.bitfire.davdroid.TestUtils.workScheduledOrRunning
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.ical4android.TaskProvider
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import io.mockk.junit4.MockKRule
import io.mockk.mockkObject
import io.mockk.verify
import kotlinx.coroutines.runBlocking
import org.junit.AfterClass
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
@ -31,14 +36,13 @@ import org.junit.Before
import org.junit.BeforeClass
import org.junit.Rule
import org.junit.Test
import java.util.concurrent.Executors
@HiltAndroidTest
class PeriodicSyncWorkerTest {
companion object {
val context = InstrumentationRegistry.getInstrumentation().targetContext
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
private val accountManager = AccountManager.get(context)
private val account = Account("Test Account", context.getString(R.string.account_type))
@ -70,10 +74,10 @@ class PeriodicSyncWorkerTest {
}
private val executor = Executors.newSingleThreadExecutor()
@get:Rule
val hiltRule = HiltAndroidRule(this)
@get:Rule
val mockkRule = MockKRule(this)
@Before
fun inject() {
@ -99,48 +103,28 @@ class PeriodicSyncWorkerTest {
@Test
fun doWork_cancelsItselfOnInvalidAccount() {
val invalidAccount = Account("invalid", context.getString(R.string.account_type))
val authority = CalendarContract.AUTHORITY
// Enable the PeriodicSyncWorker
PeriodicSyncWorker.enable(context, invalidAccount, authority, 15*60, false)
assertTrue(workScheduledOrRunning(context, PeriodicSyncWorker.workerName(account, authority)))
// Run PeriodicSyncWorker as TestWorker
val inputData = workDataOf(
PeriodicSyncWorker.ARG_AUTHORITY to authority,
PeriodicSyncWorker.ARG_ACCOUNT_NAME to invalidAccount.name,
PeriodicSyncWorker.ARG_ACCOUNT_TYPE to invalidAccount.type
BaseSyncWorker.ARG_AUTHORITY to CalendarContract.AUTHORITY,
BaseSyncWorker.ARG_ACCOUNT_NAME to invalidAccount.name,
BaseSyncWorker.ARG_ACCOUNT_TYPE to invalidAccount.type
)
val result = TestWorkerBuilder<PeriodicSyncWorker>(context, executor, inputData).build().doWork()
// Verify that the PeriodicSyncWorker cancelled itself
assertTrue(result is androidx.work.ListenableWorker.Result.Failure)
assertFalse(workScheduledOrRunning(context, PeriodicSyncWorker.workerName(invalidAccount, authority)))
}
// mock WorkManager to observe cancellation call
val workManager = WorkManager.getInstance(context)
mockkObject(workManager)
@Test
fun doWork_immediatelyEnqueuesSyncWorkerForGivenAuthority() {
val authorities = listOf(
context.getString(R.string.address_books_authority),
CalendarContract.AUTHORITY,
ContactsContract.AUTHORITY,
TaskProvider.ProviderName.JtxBoard.authority,
TaskProvider.ProviderName.OpenTasks.authority,
TaskProvider.ProviderName.TasksOrg.authority
)
for (authority in authorities) {
val inputData = workDataOf(
PeriodicSyncWorker.ARG_AUTHORITY to authority,
PeriodicSyncWorker.ARG_ACCOUNT_NAME to account.name,
PeriodicSyncWorker.ARG_ACCOUNT_TYPE to account.type
)
// Run PeriodicSyncWorker as TestWorker
TestWorkerBuilder<PeriodicSyncWorker>(context, executor, inputData).build().doWork()
// run test worker, expect failure
val testWorker = TestListenableWorkerBuilder<PeriodicSyncWorker>(context, inputData).build()
val result = runBlocking {
testWorker.doWork()
}
assertTrue(result is ListenableWorker.Result.Failure)
// Check the PeriodicSyncWorker enqueued the right SyncWorker
assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context,
SyncWorker.workerName(account, authority)
))
// verify that worker called WorkManager.cancelWorkById(<its ID>)
verify {
workManager.cancelWorkById(testWorker.id)
}
}

View file

@ -12,7 +12,6 @@ import android.provider.CalendarContract
import androidx.annotation.WorkerThread
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.db.Credentials
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
@ -47,13 +46,12 @@ class AccountSettings(
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AccountSettingsEntryPoint {
fun appDatabase(): AppDatabase
fun settingsManager(): SettingsManager
}
companion object {
const val CURRENT_VERSION = 14
const val CURRENT_VERSION = 15
const val KEY_SETTINGS_VERSION = "version"
const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks"
@ -137,7 +135,6 @@ class AccountSettings(
}
val db = EntryPointAccessors.fromApplication(context, AccountSettingsEntryPoint::class.java).appDatabase()
val settings = EntryPointAccessors.fromApplication(context, AccountSettingsEntryPoint::class.java).settingsManager()
val accountManager: AccountManager = AccountManager.get(context)
@ -501,10 +498,7 @@ class AccountSettings(
try {
val migrations = AccountSettingsMigrations(
context = context,
db = db,
settings = settings,
account = account,
accountManager = accountManager,
accountSettings = this
)
val updateProc = AccountSettingsMigrations::class.java.getDeclaredMethod("update_${fromVersion}_$toVersion")

View file

@ -23,6 +23,7 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.resource.LocalTask
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.util.setAndVerifyUserData
import at.bitfire.ical4android.AndroidCalendar
@ -32,6 +33,10 @@ import at.bitfire.ical4android.UnknownProperty
import at.bitfire.vcard4android.ContactsStorageException
import at.bitfire.vcard4android.GroupMethod
import at.techbee.jtx.JtxContract.asSyncAdapter
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
import dagger.hilt.components.SingletonComponent
import net.fortuna.ical4j.model.Property
import net.fortuna.ical4j.model.property.Url
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
@ -42,20 +47,43 @@ import java.util.logging.Level
class AccountSettingsMigrations(
val context: Context,
val db: AppDatabase,
val settings: SettingsManager,
val account: Account,
val accountManager: AccountManager,
val accountSettings: AccountSettings
) {
@EntryPoint
@InstallIn(SingletonComponent::class)
interface AccountSettingsMigrationsEntryPoint {
fun appDatabase(): AppDatabase
fun settingsManager(): SettingsManager
}
val db = EntryPointAccessors.fromApplication<AccountSettingsMigrationsEntryPoint>(context).appDatabase()
val settings = EntryPointAccessors.fromApplication<AccountSettingsMigrationsEntryPoint>(context).settingsManager()
val accountManager: AccountManager = AccountManager.get(context)
/**
* 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].
*/
@Suppress("unused","FunctionName")
fun update_14_15() {
for (authority in SyncUtils.syncAuthorities(context)) {
val interval = accountSettings.getSyncInterval(authority)
accountSettings.setSyncInterval(authority, interval ?: AccountSettings.SYNC_INTERVAL_MANUALLY)
}
}
/**
* Disables all sync adapter periodic syncs for every authority. Then enables
* corresponding PeriodicSyncWorkers
*/
@Suppress("unused","FunctionName")
fun update_13_14() {
// Cancel any potentially running syncs for this account (sync framework)
ContentResolver.cancelSync(account, null)

View file

@ -17,8 +17,10 @@ import at.bitfire.davdroid.db.Service
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.Settings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.util.setAndVerifyUserData
import dagger.hilt.EntryPoint
import dagger.hilt.InstallIn
import dagger.hilt.android.EntryPointAccessors
@ -31,8 +33,7 @@ import java.util.logging.Level
* Sync logic for address books
*/
class AddressBookSyncer(
context: Context,
private val expedited: Boolean
context: Context
) : Syncer(context) {
@EntryPoint
@ -41,23 +42,38 @@ class AddressBookSyncer(
fun settingsManager(): SettingsManager
}
companion object {
const val PREVIOUS_GROUP_METHOD = "previous_group_method"
}
val entryPoint = EntryPointAccessors.fromApplication(context, AddressBooksSyncAdapterEntryPoint::class.java)
val settingsManager = entryPoint.settingsManager()
override fun sync(
account: Account,
extras: Array<String>,
authority: String,
authority: String, // address book authority (not contacts authority)
httpClient: Lazy<HttpClient>,
provider: ContentProviderClient,
provider: ContentProviderClient, // for noop address book provider (not for contacts provider)
syncResult: SyncResult
) {
try {
if (updateLocalAddressBooks(account, syncResult))
for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) {
Logger.log.log(Level.INFO, "Running sync for address book", addressBookAccount)
SyncWorker.enqueue(context, addressBookAccount, ContactsContract.AUTHORITY, expedited = expedited)
if (updateLocalAddressBooks(account, syncResult)) {
context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)?.use { contactsProvider ->
for (addressBookAccount in LocalAddressBook.findAll(context, null, account).map { it.account }) {
Logger.log.info("Synchronizing address book $addressBookAccount")
syncAddresBook(
addressBookAccount,
extras,
ContactsContract.AUTHORITY,
httpClient,
contactsProvider,
syncResult
)
}
}
}
} catch (e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
}
@ -123,4 +139,43 @@ class AddressBookSyncer(
return true
}
fun syncAddresBook(
account: Account,
extras: Array<String>,
authority: String,
httpClient: Lazy<HttpClient>,
provider: ContentProviderClient,
syncResult: SyncResult
) {
try {
val accountSettings = AccountSettings(context, account)
val addressBook = LocalAddressBook(context, account, provider)
// handle group method change
val groupMethod = accountSettings.getGroupMethod().name
accountSettings.accountManager.getUserData(account, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod ->
if (previousGroupMethod != groupMethod) {
Logger.log.info("Group method changed, deleting all local contacts/groups")
// delete all local contacts and groups so that they will be downloaded again
provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null)
provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null)
// reset sync state
addressBook.syncState = null
}
}
accountSettings.accountManager.setAndVerifyUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
Logger.log.info("Synchronizing address book: ${addressBook.url}")
Logger.log.info("Taking settings from: ${addressBook.mainAccount}")
ContactsSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, provider, addressBook).performSync()
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e)
}
Logger.log.info("Contacts sync complete")
}
}

View file

@ -1,7 +1,3 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
@ -13,92 +9,44 @@ import android.content.SyncResult
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.annotation.IntDef
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.getSystemService
import androidx.hilt.work.HiltWorker
import androidx.lifecycle.LiveData
import androidx.lifecycle.map
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.CoroutineWorker
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkInfo
import androidx.work.WorkManager
import androidx.work.WorkQuery
import androidx.work.WorkRequest
import androidx.work.WorkerParameters
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.ConnectionUtils.internetAvailable
import at.bitfire.davdroid.network.ConnectionUtils.wifiAvailable
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.network.ConnectionUtils
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.ui.NotificationUtils
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
import at.bitfire.davdroid.ui.account.WifiPermissionsActivity
import at.bitfire.davdroid.util.PermissionUtils
import at.bitfire.ical4android.TaskProvider
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import kotlinx.coroutines.delay
import kotlinx.coroutines.runInterruptible
import kotlinx.coroutines.withContext
import java.util.concurrent.TimeUnit
import java.util.Collections
import java.util.logging.Level
/**
* Handles immediate sync requests and cancellations of accounts and respective content authorities,
* by creating appropriate workers.
*
* The different sync workers each carry a unique work name composed of the account and authority they
* are syncing. See [SyncWorker.workerName] for more information.
*
* By enqueuing this worker ([SyncWorker.enqueue]) a sync will be started immediately (as soon as
* possible). Currently, there are three scenarios starting a sync:
*
* 1) *manual sync*: User presses an in-app sync button and enqueues this worker directly.
* 2) *periodic sync*: User defines time interval to sync in app settings. The [PeriodicSyncWorker] runs
* in the background and enqueues this worker when due.
* 3) *content-triggered sync*: User changes a calendar event, task or contact, or presses a sync
* button in one of the responsible apps. The [SyncAdapterService] is notified of this and enqueues
* this worker.
*
* Expedited: when run manually
*
* Long-running: no
*/
@HiltWorker
class SyncWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters
abstract class BaseSyncWorker(
appContext: Context,
val workerParams: WorkerParameters
) : CoroutineWorker(appContext, workerParams) {
companion object {
// Worker input parameters
internal const val ARG_ACCOUNT_NAME = "accountName"
internal const val ARG_ACCOUNT_TYPE = "accountType"
internal const val ARG_AUTHORITY = "authority"
/** Boolean. Set to `true` when the job was requested as expedited job. */
private const val ARG_EXPEDITED = "expedited"
private const val ARG_UPLOAD = "upload"
private const val ARG_RESYNC = "resync"
@IntDef(NO_RESYNC, RESYNC, FULL_RESYNC)
annotation class ArgResync
const val NO_RESYNC = 0
const val RESYNC = 1
const val FULL_RESYNC = 2
// common worker input parameters
const val ARG_ACCOUNT_NAME = "accountName"
const val ARG_ACCOUNT_TYPE = "accountType"
const val ARG_AUTHORITY = "authority"
/**
* How often this work will be retried to run after soft (network) errors.
@ -108,115 +56,30 @@ class SyncWorker @AssistedInject constructor(
internal const val MAX_RUN_ATTEMPTS = 5
/**
* Unique work name of this worker. Can also be used as tag.
*
* Mainly used to query [WorkManager] for work state (by unique work name or tag).
*
* *NOTE:* SyncWorkers for address book accounts bear the unique worker name of their parent
* account (main account) as tag. This makes it easier to query the overall sync status of a
* main account.
*
* @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 "sync $authority ${account.type}/${account.name}"
* Set of currently running syncs, identified by their [commonTag].
*/
fun workerName(account: Account, authority: String) =
"sync $authority ${account.type}/${account.name}"
private val runningSyncs = Collections.synchronizedSet(HashSet<String>())
/**
* Requests immediate synchronization of an account with all applicable
* authorities (contacts, calendars, ).
*
* @see enqueue
* Stops running sync workers and removes pending sync workers from queue, for all authorities.
*/
fun enqueueAllAuthorities(
context: Context,
account: Account,
@ArgResync resync: Int = NO_RESYNC,
upload: Boolean = false
) {
for (authority in SyncUtils.syncAuthorities(context))
enqueue(context, account, authority, expedited = true, resync = resync, upload = upload)
fun cancelAllWork(context: Context, account: Account) {
val workManager = WorkManager.getInstance(context)
for (authority in SyncUtils.syncAuthorities(context)) {
workManager.cancelUniqueWork(OneTimeSyncWorker.workerName(account, authority))
workManager.cancelUniqueWork(PeriodicSyncWorker.workerName(account, authority))
}
}
/**
* 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 resync whether to request (full) re-synchronization or not
* @param upload see [ContentResolver.SYNC_EXTRAS_UPLOAD] used only for contacts sync
* and android 7 workaround
* @return existing or newly created worker name
* This tag shall be added to every worker that is enqueued by a subclass.
*/
fun enqueue(
context: Context,
account: Account,
authority: String,
expedited: Boolean,
@ArgResync resync: Int = NO_RESYNC,
upload: Boolean = false
): String {
// Worker arguments
val argumentsBuilder = Data.Builder()
.putString(ARG_AUTHORITY, authority)
.putString(ARG_ACCOUNT_NAME, account.name)
.putString(ARG_ACCOUNT_TYPE, account.type)
if (expedited)
argumentsBuilder.putBoolean(ARG_EXPEDITED, true)
if (resync != NO_RESYNC)
argumentsBuilder.putInt(ARG_RESYNC, resync)
argumentsBuilder.putBoolean(ARG_UPLOAD, upload)
// build work request
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // require a network connection
.build()
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
.addTag(workerName(account, authority))
.setInputData(argumentsBuilder.build())
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS, // 30 sec
TimeUnit.MILLISECONDS
)
.setConstraints(constraints)
.apply {
// If this is a sub sync worker (address book sync), add the main account tag as well
if (account.type == context.getString(R.string.account_type_address_book)) {
val mainAccount = LocalAddressBook.mainAccount(context, account)
addTag(workerName(mainAccount, authority))
}
}
if (expedited)
workRequest.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
// enqueue and start syncing
val name = workerName(account, authority)
val request = workRequest.build()
Logger.log.log(Level.INFO, "Enqueueing unique worker: $name, expedited = $expedited, tags = ${request.tags}")
WorkManager.getInstance(context).enqueueUniqueWork(
name,
ExistingWorkPolicy.KEEP, // If sync is already running, just continue.
// Existing retried work will not be replaced (for instance when
// PeriodicSyncWorker enqueues another scheduled sync).
request
)
return name
}
fun commonTag(account: Account, authority: String): String =
"sync-$authority ${account.type}/${account.name}"
/**
* Stops running sync worker or removes pending sync from queue, for all authorities.
*/
fun cancelSync(context: Context, account: Account) {
for (authority in SyncUtils.syncAuthorities(context))
WorkManager.getInstance(context).cancelUniqueWork(workerName(account, authority))
}
/**
* Will tell whether >0 [SyncWorker] exists, belonging to given account and authorities,
* and which are/is in the given worker state.
* Will tell whether >0 sync workers (both [PeriodicSyncWorker] and [OneTimeSyncWorker])
* exist, belonging to given account and authorities, and which are/is in the given worker state.
*
* @param workStates list of states of workers to match
* @param account the account which the workers belong to
@ -233,7 +96,7 @@ class SyncWorker @AssistedInject constructor(
.fromStates(workStates)
if (account != null && authorities != null)
workQuery.addTags(
authorities.map { authority -> workerName(account, authority) }
authorities.map { authority -> commonTag(account, authority) }
)
return WorkManager.getInstance(context)
.getWorkInfosLiveData(workQuery.build()).map { workInfoList ->
@ -241,8 +104,30 @@ class SyncWorker @AssistedInject constructor(
}
}
/**
* Checks whether user imposed sync conditions from settings are met:
* - Sync only on WiFi?
* - Sync only on specific WiFi (SSID)?
*
* @param accountSettings Account settings of the account to check (and is to be synced)
* @return *true* if conditions are met; *false* if not
*/
fun wifiConditionsMet(context: Context, accountSettings: AccountSettings): Boolean {
// May we sync without WiFi?
if (!accountSettings.getSyncWifiOnly())
return true // yes, continue
// connection checks
// WiFi required, is it available?
val connectivityManager = context.getSystemService<ConnectivityManager>()!!
if (!ConnectionUtils.wifiAvailable(connectivityManager)) {
Logger.log.info("Not on connected WiFi, stopping")
return false
}
// If execution reaches this point, we're on a connected WiFi
// Check whether we are connected to the correct WiFi (in case SSID was provided)
return correctWifiSsid(context, accountSettings)
}
/**
* Checks whether we are connected to the correct wifi (SSID) defined by user in the
@ -279,64 +164,70 @@ class SyncWorker @AssistedInject constructor(
return true
}
/**
* Checks whether user imposed sync conditions from settings are met:
* - Sync only on WiFi?
* - Sync only on specific WiFi (SSID)?
*
* @param accountSettings Account settings of the account to check (and is to be synced)
* @return *true* if conditions are met; *false* if not
*/
internal fun wifiConditionsMet(context: Context, accountSettings: AccountSettings): Boolean {
// May we sync without WiFi?
if (!accountSettings.getSyncWifiOnly())
return true // yes, continue
// WiFi required, is it available?
val connectivityManager = context.getSystemService<ConnectivityManager>()!!
if (!wifiAvailable(connectivityManager)) {
Logger.log.info("Not on connected WiFi, stopping")
return false
}
// If execution reaches this point, we're on a connected WiFi
// Check whether we are connected to the correct WiFi (in case SSID was provided)
return correctWifiSsid(context, accountSettings)
}
}
private val dispatcher = SyncWorkDispatcher.getInstance(applicationContext)
private val notificationManager = NotificationManagerCompat.from(applicationContext)
override suspend fun doWork(): Result = withContext(dispatcher) {
override suspend fun doWork(): Result {
// ensure we got the required arguments
val account = Account(
inputData.getString(ARG_ACCOUNT_NAME) ?: throw IllegalArgumentException("$ARG_ACCOUNT_NAME required"),
inputData.getString(ARG_ACCOUNT_TYPE) ?: throw IllegalArgumentException("$ARG_ACCOUNT_TYPE required")
)
val authority = inputData.getString(ARG_AUTHORITY) ?: throw IllegalArgumentException("$ARG_AUTHORITY required")
val expedited = inputData.getBoolean(ARG_EXPEDITED, false)
// check internet connection
val ignoreVpns = AccountSettings(applicationContext, account).getIgnoreVpns()
val connectivityManager = applicationContext.getSystemService<ConnectivityManager>()!!
if (!internetAvailable(connectivityManager, ignoreVpns)) {
Logger.log.info("WorkManager started SyncWorker without Internet connection. Aborting.")
return@withContext Result.failure()
val syncTag = commonTag(account, authority)
Logger.log.info("${javaClass.simpleName} called for $syncTag")
if (!runningSyncs.add(syncTag)) {
Logger.log.info("There's already another worker running for $syncTag, skipping")
return Result.success()
}
try {
val accountSettings = try {
AccountSettings(applicationContext, account)
} catch (e: InvalidAccountException) {
val workId = workerParams.id
Logger.log.warning("Account $account doesn't exist anymore, cancelling worker $workId")
val workManager = WorkManager.getInstance(applicationContext)
workManager.cancelWorkById(workId)
return Result.failure()
}
// check internet connection
val ignoreVpns = accountSettings.getIgnoreVpns()
val connectivityManager = applicationContext.getSystemService<ConnectivityManager>()!!
if (!ConnectionUtils.internetAvailable(connectivityManager, ignoreVpns)) {
Logger.log.info("WorkManager started SyncWorker without Internet connection. Aborting.")
return Result.failure()
}
return doSyncWork(account, authority, accountSettings)
} finally {
Logger.log.info("${javaClass.simpleName} finished for $syncTag")
runningSyncs -= syncTag
}
}
open suspend fun doSyncWork(
account: Account,
authority: String,
accountSettings: AccountSettings
): Result = withContext(dispatcher) {
Logger.log.info("Running sync worker: account=$account, authority=$authority")
// What are we going to sync? Select syncer based on authority
val syncer: Syncer = when (authority) {
applicationContext.getString(R.string.address_books_authority) ->
AddressBookSyncer(applicationContext, expedited)
AddressBookSyncer(applicationContext)
CalendarContract.AUTHORITY ->
CalendarSyncer(applicationContext)
ContactsContract.AUTHORITY ->
ContactSyncer(applicationContext)
TaskProvider.ProviderName.JtxBoard.authority ->
JtxSyncer(applicationContext)
TaskProvider.ProviderName.OpenTasks.authority,
@ -348,11 +239,11 @@ class SyncWorker @AssistedInject constructor(
// pass possibly supplied flags to the selected syncer
val extras = mutableListOf<String>()
when (inputData.getInt(ARG_RESYNC, NO_RESYNC)) {
RESYNC -> extras.add(Syncer.SYNC_EXTRAS_RESYNC)
FULL_RESYNC -> extras.add(Syncer.SYNC_EXTRAS_FULL_RESYNC)
when (inputData.getInt(OneTimeSyncWorker.ARG_RESYNC, OneTimeSyncWorker.NO_RESYNC)) {
OneTimeSyncWorker.RESYNC -> extras.add(Syncer.SYNC_EXTRAS_RESYNC)
OneTimeSyncWorker.FULL_RESYNC -> extras.add(Syncer.SYNC_EXTRAS_FULL_RESYNC)
}
if (inputData.getBoolean(ARG_UPLOAD, false))
if (inputData.getBoolean(OneTimeSyncWorker.ARG_UPLOAD, false))
// Comes in through SyncAdapterService and is used only by ContactsSyncManager for an Android 7 workaround.
extras.add(ContentResolver.SYNC_EXTRAS_UPLOAD)
@ -439,21 +330,4 @@ class SyncWorker @AssistedInject constructor(
return@withContext Result.success()
}
/**
* Used by WorkManager to show a foreground service notification for expedited jobs on Android <12.
*/
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_STATUS)
.setSmallIcon(R.drawable.ic_foreground_notify)
.setContentTitle(applicationContext.getString(R.string.foreground_service_notify_title))
.setContentText(applicationContext.getString(R.string.foreground_service_notify_text))
.setStyle(NotificationCompat.BigTextStyle())
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
.build()
return ForegroundInfo(NotificationUtils.NOTIFY_SYNC_EXPEDITED, notification)
}
}

View file

@ -1,65 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.content.ContentProviderClient
import android.content.Context
import android.content.SyncResult
import android.provider.ContactsContract
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.network.HttpClient
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.util.setAndVerifyUserData
import java.util.logging.Level
/**
* Sync logic for contacts
*/
class ContactSyncer(context: Context): Syncer(context) {
companion object {
const val PREVIOUS_GROUP_METHOD = "previous_group_method"
}
override fun sync(
account: Account,
extras: Array<String>,
authority: String,
httpClient: Lazy<HttpClient>,
provider: ContentProviderClient,
syncResult: SyncResult
) {
try {
val accountSettings = AccountSettings(context, account)
val addressBook = LocalAddressBook(context, account, provider)
// handle group method change
val groupMethod = accountSettings.getGroupMethod().name
accountSettings.accountManager.getUserData(account, PREVIOUS_GROUP_METHOD)?.let { previousGroupMethod ->
if (previousGroupMethod != groupMethod) {
Logger.log.info("Group method changed, deleting all local contacts/groups")
// delete all local contacts and groups so that they will be downloaded again
provider.delete(addressBook.syncAdapterURI(ContactsContract.RawContacts.CONTENT_URI), null, null)
provider.delete(addressBook.syncAdapterURI(ContactsContract.Groups.CONTENT_URI), null, null)
// reset sync state
addressBook.syncState = null
}
}
accountSettings.accountManager.setAndVerifyUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
Logger.log.info("Synchronizing address book: ${addressBook.url}")
Logger.log.info("Taking settings from: ${addressBook.mainAccount}")
ContactsSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, provider, addressBook).performSync()
} catch(e: Exception) {
Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e)
}
Logger.log.info("Contacts sync complete")
}
}

View file

@ -0,0 +1,166 @@
package at.bitfire.davdroid.syncadapter
import android.accounts.Account
import android.content.ContentResolver
import android.content.Context
import android.provider.CalendarContract
import androidx.annotation.IntDef
import androidx.core.app.NotificationCompat
import androidx.hilt.work.HiltWorker
import androidx.work.BackoffPolicy
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.ForegroundInfo
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.OutOfQuotaPolicy
import androidx.work.WorkManager
import androidx.work.WorkRequest
import androidx.work.WorkerParameters
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.ui.NotificationUtils
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import java.util.concurrent.TimeUnit
import java.util.logging.Level
/**
* One-time sync worker.
*
* Expedited: yes
*
* Long-running: no
*/
@HiltWorker
class OneTimeSyncWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters
) : BaseSyncWorker(appContext, workerParams) {
companion object {
const val ARG_UPLOAD = "upload"
const val ARG_RESYNC = "resync"
@IntDef(NO_RESYNC, RESYNC, FULL_RESYNC)
annotation class ArgResync
const val NO_RESYNC = 0
const val RESYNC = 1
const val FULL_RESYNC = 2
/**
* Unique work name of this worker. Can also be used as tag.
*
* 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}"
*/
fun workerName(account: Account, authority: String): String =
"onetime-sync $authority ${account.type}/${account.name}"
/**
* Requests immediate synchronization of an account with all applicable
* authorities (contacts, calendars, ).
*
* @see enqueue
*/
fun enqueueAllAuthorities(
context: Context,
account: Account,
@ArgResync resync: Int = NO_RESYNC,
upload: Boolean = false
) {
for (authority in SyncUtils.syncAuthorities(context))
enqueue(context, account, authority, resync = resync, upload = upload)
}
/**
* 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 resync whether to request (full) re-synchronization or not
* @param upload see [ContentResolver.SYNC_EXTRAS_UPLOAD] used only for contacts sync
* and android 7 workaround
* @return existing or newly created worker name
*/
fun enqueue(
context: Context,
account: Account,
authority: String,
@ArgResync resync: Int = NO_RESYNC,
upload: Boolean = false
): String {
// Worker arguments
val argumentsBuilder = Data.Builder()
.putString(ARG_AUTHORITY, authority)
.putString(ARG_ACCOUNT_NAME, account.name)
.putString(ARG_ACCOUNT_TYPE, account.type)
if (resync != NO_RESYNC)
argumentsBuilder.putInt(ARG_RESYNC, resync)
argumentsBuilder.putBoolean(ARG_UPLOAD, upload)
// build work request
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED) // require a network connection
.build()
val workRequest = OneTimeWorkRequestBuilder<OneTimeSyncWorker>()
.addTag(workerName(account, authority))
.addTag(commonTag(account, authority))
.setInputData(argumentsBuilder.build())
.setBackoffCriteria(
BackoffPolicy.EXPONENTIAL,
WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS, // 30 sec
TimeUnit.MILLISECONDS
)
.setConstraints(constraints)
/* OneTimeSyncWorker is started by user or sync framework when there are local changes.
In both cases, synchronization should be done as soon as possible, so we set expedited. */
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
// enqueue and start syncing
val name = workerName(account, authority)
val request = workRequest.build()
Logger.log.log(Level.INFO, "Enqueueing unique worker: $name, tags = ${request.tags}")
WorkManager.getInstance(context).enqueueUniqueWork(
name,
/* If sync is already running, just continue.
Existing retried work will not be replaced (for instance when
PeriodicSyncWorker enqueues another scheduled sync). */
ExistingWorkPolicy.KEEP,
request
)
return name
}
}
/**
* Used by WorkManager to show a foreground service notification for expedited jobs on Android <12.
*/
override suspend fun getForegroundInfo(): ForegroundInfo {
val notification = NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_STATUS)
.setSmallIcon(R.drawable.ic_foreground_notify)
.setContentTitle(applicationContext.getString(R.string.foreground_service_notify_title))
.setContentText(applicationContext.getString(R.string.foreground_service_notify_text))
.setStyle(NotificationCompat.BigTextStyle())
.setCategory(NotificationCompat.CATEGORY_STATUS)
.setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_LOW)
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
.build()
return ForegroundInfo(NotificationUtils.NOTIFY_SYNC_EXPEDITED, notification)
}
override suspend fun doWork(): Result {
return super.doWork()
}
}

View file

@ -15,9 +15,7 @@ import androidx.work.NetworkType
import androidx.work.Operation
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.AccountSettings
import dagger.assisted.Assisted
@ -28,13 +26,13 @@ import java.util.concurrent.TimeUnit
/**
* Handles scheduled sync requests.
*
* Enqueues immediate [SyncWorker] syncs at the appropriate moment.
*
* The different periodic sync workers each carry a unique work name composed of the account and
* authority which they are responsible for. For each account there will be multiple dedicated periodic
* sync workers for each authority. See [PeriodicSyncWorker.workerName] for more information.
*
* Deferrable: yes (PeriodicWorkRequest)
* Deferrable: yes (periodic)
*
* Expedited: no ( no [getForegroundInfo])
*
* Long-running: no
*/
@ -42,13 +40,9 @@ import java.util.concurrent.TimeUnit
class PeriodicSyncWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted workerParams: WorkerParameters
) : Worker(appContext, workerParams) {
) : BaseSyncWorker(appContext, workerParams) {
companion object {
// Worker input parameters
internal const val ARG_ACCOUNT_NAME = "accountName"
internal const val ARG_ACCOUNT_TYPE = "accountType"
internal const val ARG_AUTHORITY = "authority"
/**
* Unique work name of this worker. Can also be used as tag.
@ -57,7 +51,7 @@ class PeriodicSyncWorker @AssistedInject constructor(
*
* @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 "sync $authority ${account.type}/${account.name}"
* @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}"
@ -85,6 +79,7 @@ class PeriodicSyncWorker @AssistedInject constructor(
).build()
val workRequest = PeriodicWorkRequestBuilder<PeriodicSyncWorker>(interval, TimeUnit.SECONDS)
.addTag(workerName(account, authority))
.addTag(commonTag(account, authority))
.setInputData(arguments)
.setConstraints(constraints)
.build()
@ -110,29 +105,19 @@ class PeriodicSyncWorker @AssistedInject constructor(
}
override fun doWork(): Result {
val account = Account(
inputData.getString(ARG_ACCOUNT_NAME) ?: throw IllegalArgumentException("$ARG_ACCOUNT_NAME required"),
inputData.getString(ARG_ACCOUNT_TYPE) ?: throw IllegalArgumentException("$ARG_ACCOUNT_TYPE required")
)
val authority = inputData.getString(ARG_AUTHORITY) ?: throw IllegalArgumentException("$ARG_AUTHORITY required")
Logger.log.info("Running periodic sync worker: account=$account, authority=$authority")
override suspend fun doSyncWork(
account: Account,
authority: String,
accountSettings: AccountSettings
): Result {
Logger.log.info("Running periodic sync for account=$account, authority=$authority")
val accountSettings = try {
AccountSettings(applicationContext, account)
} catch (e: InvalidAccountException) {
Logger.log.warning("Account $account doesn't exist anymore, cancelling periodic sync")
disable(applicationContext, account, authority)
return Result.failure()
}
if (!SyncWorker.wifiConditionsMet(applicationContext, accountSettings)) {
Logger.log.info("Sync conditions not met. Won't run sync.")
if (!wifiConditionsMet(applicationContext, accountSettings)) {
Logger.log.info("WiFi conditions not met. Won't run periodic sync.")
return Result.failure()
}
// Just request immediate sync
Logger.log.info("Requesting immediate sync")
SyncWorker.enqueue(applicationContext, account, authority, expedited = false)
return Result.success()
return super.doSyncWork(account, authority, accountSettings)
}
}

View file

@ -13,15 +13,15 @@ import android.content.Context
import android.content.Intent
import android.content.SyncResult
import android.os.Bundle
import androidx.lifecycle.Observer
import androidx.work.WorkInfo
import android.provider.ContactsContract
import androidx.work.WorkManager
import at.bitfire.davdroid.InvalidAccountException
import at.bitfire.davdroid.R
import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.settings.AccountSettings
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.TimeoutCancellationException
import kotlinx.coroutines.cancel
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withTimeout
import java.util.logging.Level
@ -63,7 +63,7 @@ abstract class SyncAdapterService: Service() {
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
// We seem to have to pass this old SyncFramework extra for an Android 7 workaround
val upload = extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD)
Logger.log.info("Sync request via sync framework (upload=$upload)")
Logger.log.info("Sync request via sync framework for $account $authority (upload=$upload)")
val accountSettings = try {
AccountSettings(context, account)
@ -73,52 +73,45 @@ abstract class SyncAdapterService: Service() {
}
// Should we run the sync at all?
if (!SyncWorker.wifiConditionsMet(context, accountSettings)) {
if (!BaseSyncWorker.wifiConditionsMet(context, accountSettings)) {
Logger.log.info("Sync conditions not met. Aborting sync framework initiated sync")
return
}
Logger.log.fine("Sync framework now starting SyncWorker")
val workerName = SyncWorker.enqueue(context, account, authority, expedited = true, upload = upload)
/* Special case for contacts: because address books are separate accounts, changed contacts cause
this method to be called with authority = ContactsContract.AUTHORITY. However the sync worker shall be run for the
address book authority instead. */
val workerAccount = accountSettings.account // main account in case of an address book account
val workerAuthority =
if (authority == ContactsContract.AUTHORITY)
context.getString(R.string.address_books_authority)
else
authority
// Block the onPerformSync method to simulate an ongoing sync
Logger.log.fine("Blocking sync framework until SyncWorker finishes")
Logger.log.fine("Starting OneTimeSyncWorker for $workerAccount $workerAuthority and waiting for it")
val workerName = OneTimeSyncWorker.enqueue(context, workerAccount, 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 check periodically whether the sync has
// finished, putting the thread to sleep in between checks.
val workManager = WorkManager.getInstance(context)
val status = workManager.getWorkInfosForUniqueWorkLiveData(workerName)
val observer = Observer<List<WorkInfo>> { workInfoList ->
for (workInfo in workInfoList) {
if (workInfo.state.isFinished)
finished.complete(true)
}
}
try {
runBlocking(Dispatchers.Main) { // observeForever not allowed in background thread
status.observeForever(observer)
}
runBlocking {
try {
withTimeout(10 * 60 * 1000) { // block max. 10 minutes
finished.await()
withTimeout(10 * 60 * 1000) { // block max. 10 minutes
// wait for finished worker state
workManager.getWorkInfosForUniqueWorkFlow(workerName).collect { info ->
if (info.any { it.state.isFinished })
cancel(CancellationException("$workerName has finished"))
}
} catch (e: TimeoutCancellationException) {
Logger.log.info("Sync job timed out, won't block sync framework anymore")
}
}
} finally {
// remove observer in any case
runBlocking(Dispatchers.Main) {
status.removeObserver(observer)
}
} catch (e: CancellationException) {
// waiting for work was cancelled, either by timeout or because the worker has finished
Logger.log.log(Level.FINE, "Not waiting for OneTimeSyncWorker anymore (this is not an error)", e)
}
Logger.log.info("Returning to sync framework")
Logger.log.fine("Returning to sync framework")
}
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
@ -144,4 +137,4 @@ class CalendarsSyncAdapterService: SyncAdapterService()
class ContactsSyncAdapterService: SyncAdapterService()
class JtxSyncAdapterService: SyncAdapterService()
class OpenTasksSyncAdapterService: SyncAdapterService()
class TasksOrgSyncAdapterService: SyncAdapterService()
class TasksOrgSyncAdapterService: SyncAdapterService()

View file

@ -14,7 +14,6 @@ import android.content.pm.PackageManager
import android.graphics.drawable.BitmapDrawable
import android.net.Uri
import android.provider.CalendarContract
import android.provider.ContactsContract
import androidx.annotation.WorkerThread
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
@ -96,16 +95,13 @@ object SyncUtils {
* Checking the availability of authorities may be relatively expensive, so the
* result should be cached for the current operation.
*
* @param withContacts whether to add contacts authority
* @return list of available sync authorities for main accounts
*/
fun syncAuthorities(context: Context, withContacts: Boolean = false): List<String> {
fun syncAuthorities(context: Context): List<String> {
val result = mutableListOf(
CalendarContract.AUTHORITY,
context.getString(R.string.address_books_authority)
)
if (withContacts)
result.add(ContactsContract.AUTHORITY)
TaskUtils.currentProvider(context)?.let { taskProvider ->
result += taskProvider.authority
}

View file

@ -83,8 +83,9 @@ import androidx.work.WorkQuery
import at.bitfire.davdroid.R
import at.bitfire.davdroid.db.AppDatabase
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
import at.bitfire.davdroid.syncadapter.SyncUtils
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.ui.account.AccountActivity
import at.bitfire.davdroid.ui.intro.IntroActivity
import at.bitfire.davdroid.ui.setup.LoginActivity
@ -167,7 +168,6 @@ class AccountsActivity: AppCompatActivity() {
)
.verticalScroll(rememberScrollState())
) {
// background image
Image(
painterResource(R.drawable.accounts_background),
@ -365,7 +365,7 @@ class AccountsActivity: AppCompatActivity() {
}
}
fun update() = viewModelScope.launch(Dispatchers.Default) {
val authorities = SyncUtils.syncAuthorities(application, withContacts = true)
val authorities = SyncUtils.syncAuthorities(application)
val collator = Collator.getInstance()
postValue(myAccounts
.toList()
@ -381,7 +381,7 @@ class AccountsActivity: AppCompatActivity() {
},
isSyncing = workInfos.any { info ->
authorities.any { authority ->
info.tags.contains(SyncWorker.workerName(account, authority))
info.tags.contains(BaseSyncWorker.commonTag(account, authority))
}
}
)
@ -413,7 +413,7 @@ class AccountsActivity: AppCompatActivity() {
// Enqueue sync worker for all accounts and authorities. Will sync once internet is available
for (account in allAccounts())
SyncWorker.enqueueAllAuthorities(context, account)
OneTimeSyncWorker.enqueueAllAuthorities(context, account)
}

View file

@ -82,8 +82,7 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.LocalAddressBook
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
import at.bitfire.davdroid.ui.widget.CardWithImage
import at.bitfire.ical4android.TaskProvider
import at.bitfire.ical4android.TaskProvider.ProviderName
@ -870,30 +869,26 @@ class DebugInfoActivity : AppCompatActivity() {
TaskProvider.ProviderName.OpenTasks.authority,
TaskProvider.ProviderName.TasksOrg.authority
).forEach { authority ->
for (workerName in listOf(
SyncWorker.workerName(account, authority),
PeriodicSyncWorker.workerName(account, authority)
)) {
WorkManager.getInstance(context).getWorkInfos(
WorkQuery.Builder.fromUniqueWorkNames(listOf(workerName)).build()
).get().forEach { workInfo ->
table.addLine(
workInfo.tags.map { it.replace("\\bat\\.bitfire\\.davdroid\\.".toRegex(), ".") },
authority,
"${workInfo.state} (${workInfo.stopReason})",
workInfo.nextScheduleTimeMillis.let { nextRun ->
when (nextRun) {
Long.MAX_VALUE -> ""
else -> DateUtils.getRelativeTimeSpanString(nextRun)
}
},
workInfo.runAttemptCount,
workInfo.generation,
workInfo.periodicityInfo?.let { periodicity ->
"every ${periodicity.repeatIntervalMillis/60000} min"
} ?: "not periodic"
)
}
val tag = BaseSyncWorker.commonTag(account, authority)
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,
"${workInfo.state} (${workInfo.stopReason})",
workInfo.nextScheduleTimeMillis.let { nextRun ->
when (nextRun) {
Long.MAX_VALUE -> ""
else -> DateUtils.getRelativeTimeSpanString(nextRun)
}
},
workInfo.runAttemptCount,
workInfo.generation,
workInfo.periodicityInfo?.let { periodicity ->
"every ${periodicity.repeatIntervalMillis/60000} min"
} ?: "not periodic"
)
}
}
return table.toString()

View file

@ -63,7 +63,6 @@ import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
@ -79,7 +78,7 @@ import at.bitfire.davdroid.db.Collection
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.PermissionsActivity
import at.bitfire.davdroid.ui.widget.ActionCard
@ -128,26 +127,14 @@ class AccountActivity : AppCompatActivity() {
AppTheme {
val cardDavSvc by model.cardDavSvc.observeAsState()
val canCreateAddressBook by model.canCreateAddressBook.observeAsState(false)
val cardDavRefreshing by model.cardDavRefreshingActive.observeAsState(false)
val cardDavSyncActive by model.cardDavSyncActive.observeAsState(false)
val cardDavSyncPending by model.cardDavSyncPending.observeAsState(false)
val cardDavProgress = when {
cardDavRefreshing || cardDavSyncActive -> ServiceProgressValue.ACTIVE
cardDavSyncPending -> ServiceProgressValue.PENDING
else -> ServiceProgressValue.IDLE
}
val cardDavRefreshing by model.cardDavRefreshing.observeAsState(false)
val cardDavSyncing by model.cardDavSyncing.observeAsState(false)
val addressBooks by model.addressBooksPager.observeAsState()
val calDavSvc by model.calDavSvc.observeAsState()
val canCreateCalendar by model.canCreateCalendar.observeAsState(false)
val calDavRefreshing by model.calDavRefreshingActive.observeAsState(false)
val calDavSyncActive by model.calDavSyncActive.observeAsState(false)
val calDavSyncPending by model.calDavSyncPending.observeAsState(false)
val calDavProgress = when {
calDavRefreshing || calDavSyncActive -> ServiceProgressValue.ACTIVE
calDavSyncPending -> ServiceProgressValue.PENDING
else -> ServiceProgressValue.IDLE
}
val calDavRefreshing by model.calDavRefreshing.observeAsState(false)
val calDavSyncing by model.calDavSyncing.observeAsState(false)
val calendars by model.calendarsPager.observeAsState()
val subscriptions by model.webcalPager.observeAsState()
@ -164,12 +151,12 @@ class AccountActivity : AppCompatActivity() {
},
hasCardDav = cardDavSvc != null,
canCreateAddressBook = canCreateAddressBook,
cardDavProgress = cardDavProgress,
cardDavSyncing = cardDavRefreshing || cardDavSyncing,
cardDavRefreshing = cardDavRefreshing,
addressBooks = addressBooks?.flow?.collectAsLazyPagingItems(),
hasCalDav = calDavSvc != null,
canCreateCalendar = canCreateCalendar,
calDavProgress = calDavProgress,
calDavSyncing = calDavRefreshing || calDavSyncing,
calDavRefreshing = calDavRefreshing,
calendars = calendars?.flow?.collectAsLazyPagingItems(),
subscriptions = subscriptions?.flow?.collectAsLazyPagingItems(),
@ -192,7 +179,7 @@ class AccountActivity : AppCompatActivity() {
}
},
onSync = {
SyncWorker.enqueueAllAuthorities(this, model.account)
OneTimeSyncWorker.enqueueAllAuthorities(this, model.account)
},
onAccountSettings = {
val intent = Intent(this, AccountSettingsActivity::class.java)
@ -239,12 +226,6 @@ class AccountActivity : AppCompatActivity() {
}
enum class ServiceProgressValue {
IDLE,
PENDING,
ACTIVE
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterialApi::class)
@Composable
fun AccountOverview(
@ -253,12 +234,12 @@ fun AccountOverview(
onSetShowOnlyPersonal: (showOnlyPersonal: Boolean) -> Unit,
hasCardDav: Boolean,
canCreateAddressBook: Boolean,
cardDavProgress: ServiceProgressValue,
cardDavSyncing: Boolean,
cardDavRefreshing: Boolean,
addressBooks: LazyPagingItems<Collection>?,
hasCalDav: Boolean,
canCreateCalendar: Boolean,
calDavProgress: ServiceProgressValue,
calDavSyncing: Boolean,
calDavRefreshing: Boolean,
calendars: LazyPagingItems<Collection>?,
subscriptions: LazyPagingItems<Collection>?,
@ -425,7 +406,7 @@ fun AccountOverview(
idxCardDav ->
ServiceTab(
requiredPermissions = listOf(Manifest.permission.WRITE_CONTACTS),
refreshing = cardDavProgress,
refreshing = cardDavSyncing,
collections = addressBooks,
onUpdateCollectionSync = onUpdateCollectionSync,
onChangeForceReadOnly = onChangeForceReadOnly
@ -438,7 +419,7 @@ fun AccountOverview(
}
ServiceTab(
requiredPermissions = permissions,
refreshing = calDavProgress,
refreshing = calDavSyncing,
collections = calendars,
onUpdateCollectionSync = onUpdateCollectionSync,
onChangeForceReadOnly = onChangeForceReadOnly
@ -469,7 +450,7 @@ fun AccountOverview(
ServiceTab(
requiredPermissions = listOf(Manifest.permission.WRITE_CALENDAR),
refreshing = calDavProgress,
refreshing = calDavSyncing,
collections = subscriptions,
onSubscribe = onSubscribe
)
@ -498,12 +479,12 @@ fun AccountOverview_CardDAV_CalDAV() {
onSetShowOnlyPersonal = {},
hasCardDav = true,
canCreateAddressBook = false,
cardDavProgress = ServiceProgressValue.ACTIVE,
cardDavSyncing = true,
cardDavRefreshing = false,
addressBooks = null,
hasCalDav = true,
canCreateCalendar = true,
calDavProgress = ServiceProgressValue.PENDING,
calDavSyncing = false,
calDavRefreshing = false,
calendars = null,
subscriptions = null
@ -693,7 +674,7 @@ fun DeleteAccountDialog(
@Composable
fun ServiceTab(
requiredPermissions: List<String>,
refreshing: ServiceProgressValue,
refreshing: Boolean,
collections: LazyPagingItems<Collection>?,
onUpdateCollectionSync: (collectionId: Long, sync: Boolean) -> Unit = { _, _ -> },
onChangeForceReadOnly: (collectionId: Long, forceReadOnly: Boolean) -> Unit = { _, _ -> },
@ -704,34 +685,19 @@ fun ServiceTab(
Column {
// progress indicator
val progressAlpha by animateFloatAsState(
when (refreshing) {
ServiceProgressValue.ACTIVE -> 1f
ServiceProgressValue.PENDING -> .5f
else -> 0f
},
label = "cardDavProgress"
if (refreshing) 1f else 0f,
label = "progressAlpha"
)
when (refreshing) {
ServiceProgressValue.ACTIVE ->
// indeterminate
LinearProgressIndicator(
color = MaterialTheme.colors.secondary,
modifier = Modifier
.graphicsLayer(alpha = progressAlpha)
.fillMaxWidth()
)
ServiceProgressValue.PENDING ->
// determinate 100%, but semi-transparent (see progressAlpha)
LinearProgressIndicator(
color = MaterialTheme.colors.secondary,
progress = 1f,
modifier = Modifier
.alpha(progressAlpha)
.fillMaxWidth()
)
else ->
Spacer(Modifier.height(ProgressIndicatorDefaults.StrokeWidth))
}
if (refreshing)
// indeterminate
LinearProgressIndicator(
color = MaterialTheme.colors.secondary,
modifier = Modifier
.graphicsLayer(alpha = progressAlpha)
.fillMaxWidth()
)
else
Spacer(Modifier.height(ProgressIndicatorDefaults.StrokeWidth))
// permissions warning
val permissionsState = rememberMultiplePermissionsState(requiredPermissions)

View file

@ -39,8 +39,9 @@ import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.syncadapter.AccountsCleanupWorker
import at.bitfire.davdroid.syncadapter.BaseSyncWorker
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
import at.bitfire.davdroid.syncadapter.SyncWorker
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
@ -94,18 +95,12 @@ class AccountModel @AssistedInject constructor(
else
MutableLiveData(false)
}
val cardDavRefreshingActive = cardDavSvc.switchMap { svc ->
val cardDavRefreshing = cardDavSvc.switchMap { svc ->
if (svc == null)
return@switchMap null
RefreshCollectionsWorker.exists(application, RefreshCollectionsWorker.workerName(svc.id))
}
val cardDavSyncPending = SyncWorker.exists(
getApplication(),
listOf(WorkInfo.State.ENQUEUED),
account,
listOf(context.getString(R.string.address_books_authority), ContactsContract.AUTHORITY)
)
val cardDavSyncActive = SyncWorker.exists(
val cardDavSyncing = BaseSyncWorker.exists(
getApplication(),
listOf(WorkInfo.State.RUNNING),
account,
@ -120,18 +115,12 @@ class AccountModel @AssistedInject constructor(
else
MutableLiveData(false)
}
val calDavRefreshingActive = calDavSvc.switchMap { svc ->
val calDavRefreshing = calDavSvc.switchMap { svc ->
if (svc == null)
return@switchMap null
RefreshCollectionsWorker.exists(application, RefreshCollectionsWorker.workerName(svc.id))
}
val calDavSyncPending = SyncWorker.exists(
getApplication(),
listOf(WorkInfo.State.ENQUEUED),
account,
listOf(CalendarContract.AUTHORITY)
)
val calDavSyncActive = SyncWorker.exists(
val calDavSyncing = BaseSyncWorker.exists(
getApplication(),
listOf(WorkInfo.State.RUNNING),
account,
@ -246,9 +235,9 @@ class AccountModel @AssistedInject constructor(
}
// cancel maybe running synchronization
SyncWorker.cancelSync(context, oldAccount)
for (addrBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)))
SyncWorker.cancelSync(context, addrBookAccount)
BaseSyncWorker.cancelAllWork(context, oldAccount)
/*for (addrBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)))
SyncWorker.cancelSync(context, addrBookAccount)*/
// update account name references in database
try {
@ -296,7 +285,7 @@ class AccountModel @AssistedInject constructor(
}
// synchronize again
SyncWorker.enqueueAllAuthorities(context, newAccount)
OneTimeSyncWorker.enqueueAllAuthorities(context, newAccount)
}

View file

@ -66,7 +66,7 @@ import at.bitfire.davdroid.log.Logger
import at.bitfire.davdroid.resource.TaskUtils
import at.bitfire.davdroid.settings.AccountSettings
import at.bitfire.davdroid.settings.SettingsManager
import at.bitfire.davdroid.syncadapter.SyncWorker
import at.bitfire.davdroid.syncadapter.OneTimeSyncWorker
import at.bitfire.davdroid.syncadapter.Syncer
import at.bitfire.davdroid.ui.AppTheme
import at.bitfire.davdroid.ui.widget.ActionCard
@ -825,8 +825,12 @@ class AccountSettingsActivity: AppCompatActivity() {
* _false_: sets [Syncer.SYNC_EXTRAS_RESYNC])
*/
private fun resync(authority: String, fullResync: Boolean) {
val resync = if (fullResync) SyncWorker.FULL_RESYNC else SyncWorker.RESYNC
SyncWorker.enqueue(context, account, authority, expedited = true, resync = resync)
val resync =
if (fullResync)
OneTimeSyncWorker.FULL_RESYNC
else
OneTimeSyncWorker.RESYNC
OneTimeSyncWorker.enqueue(context, account, authority, resync = resync)
}
}

View file

@ -1,39 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid.util
import java.util.Collections
object ConcurrentUtils {
private val running = Collections.synchronizedSet(HashSet<Any>())
/**
* Guards a code block by a key the block will only run when there is currently no
* other running code block with the same key (compared by [Object.equals]).
*
* @param key guarding key to determine whether the code block will be run
* @param block this code block will be run, but not more than one time at once per key
*
* @return *true* if the code block was executed (i.e. there was no running code block with this key);
* *false* if there was already another running block with that key, so that the code block wasn't executed
*/
fun runSingle(key: Any, block: () -> Unit): Boolean {
if (!running.add(key)) // already running?
return false // this key is already in use, refuse execution
// key is now in running
try {
block()
return true
} finally {
running.remove(key)
// key is now not in running anymore; further calls will succeed
}
}
}

View file

@ -1,67 +0,0 @@
/***************************************************************************************************
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
**************************************************************************************************/
package at.bitfire.davdroid
import at.bitfire.davdroid.util.ConcurrentUtils
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Assert.fail
import org.junit.Test
import java.util.concurrent.atomic.AtomicInteger
import kotlin.concurrent.thread
class ConcurrentUtilsTest {
@Test
fun testRunSingle_DifferentKeys_Sequentially() {
val nrCalled = AtomicInteger()
for (i in 0 until 10)
ConcurrentUtils.runSingle(i) { nrCalled.incrementAndGet() }
assertEquals(10, nrCalled.get())
}
@Test
fun testRunSingle_DifferentKeys_Parallel() {
val nrCalled = AtomicInteger()
val threads = mutableListOf<Thread>()
for (i in 0 until 10)
threads += thread {
ConcurrentUtils.runSingle(i) {
nrCalled.incrementAndGet()
Thread.sleep(100)
}
}
threads.forEach { it.join() }
assertEquals(10, nrCalled.get())
}
@Test
fun testRunSingle_SameKey_Sequentially() {
val key = "a"
val nrCalled = AtomicInteger()
for (i in 0 until 10)
ConcurrentUtils.runSingle(key) { nrCalled.incrementAndGet() }
assertEquals(10, nrCalled.get())
}
@Test
fun testRunSingle_SameKey_Nested() {
val key = "a"
var outerBlockExecuted = false
ConcurrentUtils.runSingle(key) {
outerBlockExecuted = true
// Now a code block with the key is already running, further ones should be ignored
assertFalse(ConcurrentUtils.runSingle(key) {
fail("Shouldn't have been called")
})
}
assertTrue(outerBlockExecuted)
}
}