mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-06 19:34:23 +00:00
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:
parent
a16fd468fd
commit
30122a79f3
|
@ -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>()
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
||||
}*/
|
|
@ -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
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
|
@ -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()
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue