mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-10-07 03:42:59 +00:00
Workmanager scheduled sync (bitfireAT/davx5#216)
Closes bitfireAT/davx5#14 * extract WorkManager util functions and kdoc * add PeriodicSyncWorker * refactor and some kdoc * use PeriodicSyncWorker and add todos * allow SyncAdapter arguments to be passed to SyncWorker and have re-synchronization use SyncWorker * handle sync cancellation in AccountSetting migration * handle sync cancellation when account is renamed * remove sync frameworks global sync setting and sync state awareness for the GUI * Observe sync worker status in AccountListFragment * Create setup for stepwise refactor of the sync adapters. This should keep the app from failing to build. * Create new abstract BaseSyncer class for generic sync code. Adapt consumers and tests to use the new class. * Move calender sync code to new CalenderSyncer class which is independent from the sync framework * In CalendarSyncAdapterService pass sync requests from Sync Adapter Framework to SyncWorker * Use CalenderSyncer in SyncWorker * Move contacts sync code to new ContactsSyncer class. * Move address book sync code to new AddressBookSyncer class * Move jtx sync code to new JtxSyncer class * Move tasks sync code to new TaskSyncer class * Remove duplicate code in inherited sync adapters. * Remove refactoring helper interface, duplicate generic sync code and some linting. * Adapt tests for new Syncer class and move to the new package * Remove remaining duplicate code in SyncAdapterService, add todos and edit kdoc. * Move all the single line sync adapter services into one file. * Remove concurrent sync runner code and its test, as we now use WorkManagers one time work requests. * Remove SAF manual sync flag usage where unnecessary. * Drop ability to prioritise collections for sync, as not used and hindering removal of sync adapter arguments. * Pass simple string array to SyncWorker instead of bundle to simplify code. * Restructure work query code. * Get debug info from account settings and WorkManager. * Write tests for PeriodicSyncWorker * Test account creation will set a default sync interval for CardDAV and CalDAV * Throw an exception if accountManager returns null for sync interval value * Do proper interval check and add tests for AccountSettings * Use work manager query to determine whether work is in a specific state * [WIP] Add test to check that task provider is configured correctly on account creation * Edit test checking that task sync is configured correctly on account creation with/without installed task app(s), by mocking TaskUtil * Edit test such that it does not require a flaky flag * Remove periodic sync when tasks app is uninstalled * Bring back content triggered syncs * Rename enqueueSyncWorker method to enqueue only for clarity * Enable SyncAdapterFramework to cancel running SyncWorker * Add test ensuring that SyncWorker.onStopped() interrupts the running sync thread * Add retry policy sync on soft errors * Check users sync conditions before enqueueing SyncWorker * Add test for whether user sync conditions are treated correctly and kdoc * Rename ambiguous shorthand "SAF" to "SyncFramework", as SAF usually means StorageAccessFramework * Migration: Disable sync framework periodic syncs when interval is changed for specified authority * Add Workers info to debug info * Use WorkInfo.runAttemptCount to fail work after 20 soft errors * Notify user if retry limit for soft errors has been reached * Remove left over concurrency sync run tests prevention * Migration: Continue to remove periodic sync framework syncs until user migration to PeriodicSyncWorker syncs is complete * Kdoc and small changes * Migration: Change to hard migration strategy * Drop repairSyncIntervals method in favor of hard migration strategy * Improve debug info of workers * Remove sync framework periodic syncs, created by enabling content triggered syncs * Change minimum sync interval to 15 min; minor other changes * Fix tests * Implement requested changes and update kdoc * Add network connectivity restrictions to PeriodicSyncWorker * Minor changes * Move back sync classes to syncadapter package for now (can be separated later) * Add KDoc * Changes from review * Rename test methods * Add back global sync status warning Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
299fb27af4
commit
f9d6bb153c
|
@ -162,6 +162,7 @@ dependencies {
|
|||
androidTestImplementation "com.google.dagger:hilt-android-testing:${versions.hilt}"
|
||||
kaptAndroidTest "com.google.dagger:hilt-android-compiler:${versions.hilt}"
|
||||
|
||||
androidTestImplementation "androidx.arch.core:core-testing:2.2.0"
|
||||
androidTestImplementation 'androidx.test:core-ktx:1.5.0'
|
||||
androidTestImplementation 'androidx.test:runner:1.5.2'
|
||||
androidTestImplementation 'androidx.test:rules:1.5.0'
|
||||
|
|
|
@ -4,33 +4,69 @@
|
|||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import androidx.work.WorkQuery
|
||||
import org.jetbrains.annotations.TestOnly
|
||||
import java.util.concurrent.CountDownLatch
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.concurrent.TimeoutException
|
||||
|
||||
object TestUtils {
|
||||
|
||||
val targetApplication by lazy { InstrumentationRegistry.getInstrumentation().targetContext.applicationContext as Application }
|
||||
@TestOnly
|
||||
fun workScheduledOrRunning(context: Context, workerName: String): Boolean =
|
||||
workInStates(context, workerName, listOf(
|
||||
WorkInfo.State.ENQUEUED,
|
||||
WorkInfo.State.RUNNING
|
||||
))
|
||||
|
||||
fun workScheduledOrRunning(context: Context, workerName: String): Boolean {
|
||||
val future: ListenableFuture<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosForUniqueWork(workerName)
|
||||
val workInfoList: List<WorkInfo>
|
||||
try {
|
||||
workInfoList = future.get()
|
||||
} catch (e: Exception) {
|
||||
Logger.log.severe("Failed to retrieve work info list for worker $workerName", )
|
||||
return false
|
||||
@TestOnly
|
||||
fun workScheduledOrRunningOrSuccessful(context: Context, workerName: String): Boolean =
|
||||
workInStates(context, workerName, listOf(
|
||||
WorkInfo.State.ENQUEUED,
|
||||
WorkInfo.State.RUNNING,
|
||||
WorkInfo.State.SUCCEEDED
|
||||
))
|
||||
|
||||
@TestOnly
|
||||
fun workInStates(context: Context, workerName: String, states: List<WorkInfo.State>): Boolean =
|
||||
WorkManager.getInstance(context).getWorkInfos(WorkQuery.Builder
|
||||
.fromUniqueWorkNames(listOf(workerName))
|
||||
.addStates(states)
|
||||
.build()
|
||||
).get().isNotEmpty()
|
||||
|
||||
|
||||
/* Copyright 2019 Google LLC.
|
||||
SPDX-License-Identifier: Apache-2.0 */
|
||||
@TestOnly
|
||||
fun <T> LiveData<T>.getOrAwaitValue(
|
||||
time: Long = 2,
|
||||
timeUnit: TimeUnit = TimeUnit.SECONDS
|
||||
): T {
|
||||
var data: T? = null
|
||||
val latch = CountDownLatch(1)
|
||||
val observer = object : Observer<T> {
|
||||
override fun onChanged(value: T) {
|
||||
data = value
|
||||
latch.countDown()
|
||||
this@getOrAwaitValue.removeObserver(this)
|
||||
}
|
||||
}
|
||||
for (workInfo in workInfoList) {
|
||||
val state = workInfo.state
|
||||
if (state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED)
|
||||
return true
|
||||
|
||||
this.observeForever(observer)
|
||||
|
||||
// Don't wait indefinitely if the LiveData is not set.
|
||||
if (!latch.await(time, timeUnit)) {
|
||||
throw TimeoutException("LiveData value was never set.")
|
||||
}
|
||||
return false
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return data as T
|
||||
}
|
||||
|
||||
}
|
|
@ -6,6 +6,7 @@ package at.bitfire.davdroid.resource
|
|||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import androidx.arch.core.executor.testing.InstantTaskExecutorRule
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.R
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
|
@ -18,7 +19,7 @@ import org.junit.Test
|
|||
@HiltAndroidTest
|
||||
class LocalAddressBookTest {
|
||||
|
||||
@get:Rule()
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
|
|
@ -7,17 +7,27 @@ package at.bitfire.davdroid.settings
|
|||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ContentResolver
|
||||
import android.os.Build
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.util.Log
|
||||
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
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.syncadapter.AccountUtils
|
||||
import at.bitfire.davdroid.syncadapter.PeriodicSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.SyncManagerTest
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
|
@ -30,22 +40,47 @@ class AccountSettingsTest {
|
|||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@get:Rule
|
||||
var instantTaskExecutorRule = InstantTaskExecutorRule()
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
private val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
val account = Account("Test Account", context.getString(R.string.account_type))
|
||||
val account = Account(javaClass.canonicalName, SyncManagerTest.context.getString(R.string.account_type))
|
||||
val fakeCredentials = Credentials("test", "test")
|
||||
|
||||
val authorities = listOf(
|
||||
context.getString(R.string.address_books_authority),
|
||||
CalendarContract.AUTHORITY,
|
||||
TaskProvider.ProviderName.JtxBoard.authority,
|
||||
TaskProvider.ProviderName.OpenTasks.authority,
|
||||
TaskProvider.ProviderName.TasksOrg.authority
|
||||
)
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
assertTrue(AccountUtils.createAccount(context, account, AccountSettings.initialUserData(fakeCredentials)))
|
||||
assertTrue(AccountUtils.createAccount(
|
||||
context,
|
||||
account,
|
||||
AccountSettings.initialUserData(fakeCredentials)
|
||||
))
|
||||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
|
||||
|
||||
// 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(context)
|
||||
|
||||
// Initialize WorkManager for instrumentation tests.
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
}
|
||||
|
||||
@After
|
||||
|
@ -58,7 +93,8 @@ class AccountSettingsTest {
|
|||
@Test
|
||||
fun testSyncIntervals() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val presetIntervals = context.resources.getStringArray(R.array.settings_sync_interval_seconds)
|
||||
val presetIntervals =
|
||||
context.resources.getStringArray(R.array.settings_sync_interval_seconds)
|
||||
.map { it.toLong() }
|
||||
.filter { it != AccountSettings.SYNC_INTERVAL_MANUALLY }
|
||||
for (interval in presetIntervals) {
|
||||
|
@ -68,21 +104,41 @@ class AccountSettingsTest {
|
|||
}
|
||||
|
||||
@Test
|
||||
fun testSyncIntervals_IsNotSyncable() {
|
||||
fun testSyncIntervals_Syncable() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val interval = 15*60L // 15 min
|
||||
val result = settings.setSyncInterval(ContactsContract.AUTHORITY, interval)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) // below Android 7, Android returns true for whatever reason
|
||||
assertFalse(result)
|
||||
val result = settings.setSyncInterval(CalendarContract.AUTHORITY, interval)
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Test(expected = IllegalArgumentException::class)
|
||||
fun testSyncIntervals_TooShort() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val interval = 60L // 1 min is not supported by Android
|
||||
val result = settings.setSyncInterval(CalendarContract.AUTHORITY, interval)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) // below Android 7, Android returns true for whatever reason
|
||||
assertFalse(result)
|
||||
settings.setSyncInterval(CalendarContract.AUTHORITY, interval)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSyncIntervals_activatesPeriodicSyncWorker() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val interval = 15*60L
|
||||
for (authority in authorities) {
|
||||
ContentResolver.setIsSyncable(account, authority, 1)
|
||||
assertTrue(settings.setSyncInterval(authority, interval))
|
||||
assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context, PeriodicSyncWorker.workerName(account, authority)))
|
||||
assertEquals(interval, settings.getSyncInterval(authority))
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testSyncIntervals_disablesPeriodicSyncWorker() {
|
||||
val settings = AccountSettings(context, account)
|
||||
val interval = AccountSettings.SYNC_INTERVAL_MANUALLY // -1
|
||||
for (authority in authorities) {
|
||||
ContentResolver.setIsSyncable(account, authority, 1)
|
||||
assertTrue(settings.setSyncInterval(authority, interval))
|
||||
assertFalse(TestUtils.workScheduledOrRunningOrSuccessful(context, PeriodicSyncWorker.workerName(account, authority)))
|
||||
assertEquals(AccountSettings.SYNC_INTERVAL_MANUALLY, settings.getSyncInterval(authority))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
/***************************************************************************************************
|
||||
* 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
|
||||
|
||||
@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() {
|
||||
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 -> 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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,126 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ContentResolver
|
||||
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.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 org.junit.After
|
||||
import org.junit.AfterClass
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertTrue
|
||||
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
|
||||
|
||||
private val accountManager = AccountManager.get(context)
|
||||
private val account = Account("Test Account", context.getString(R.string.account_type))
|
||||
private val fakeCredentials = Credentials("test", "test")
|
||||
|
||||
@BeforeClass
|
||||
@JvmStatic
|
||||
fun setUp() {
|
||||
// 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(context)
|
||||
|
||||
// Initialize WorkManager for instrumentation tests.
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
|
||||
assertTrue(AccountUtils.createAccount(context, account, AccountSettings.initialUserData(fakeCredentials)))
|
||||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
|
||||
}
|
||||
|
||||
@AfterClass
|
||||
@JvmStatic
|
||||
fun removeAccount() {
|
||||
accountManager.removeAccountExplicitly(account)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun enable_enqueuesPeriodicWorker() {
|
||||
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60)
|
||||
val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY)
|
||||
assertTrue(workScheduledOrRunning(context, workerName))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun disable_removesPeriodicWorker() {
|
||||
PeriodicSyncWorker.enable(context, account, CalendarContract.AUTHORITY, 60)
|
||||
PeriodicSyncWorker.disable(context, account, CalendarContract.AUTHORITY)
|
||||
val workerName = PeriodicSyncWorker.workerName(account, CalendarContract.AUTHORITY)
|
||||
assertFalse(workScheduledOrRunning(context, workerName))
|
||||
}
|
||||
|
||||
@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()
|
||||
|
||||
// Check the PeriodicSyncWorker enqueued the right SyncWorker
|
||||
assertTrue(TestUtils.workScheduledOrRunningOrSuccessful(context,
|
||||
SyncWorker.workerName(account, authority)
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.os.Bundle
|
||||
import androidx.test.filters.SmallTest
|
||||
import at.bitfire.davdroid.syncadapter.SyncAdapterService.SyncAdapter
|
||||
import org.junit.Assert.assertArrayEquals
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class SyncAdapterServiceTest {
|
||||
|
||||
@Test
|
||||
@SmallTest
|
||||
fun testPriorityCollections() {
|
||||
val extras = Bundle()
|
||||
assertTrue(SyncAdapter.priorityCollections(extras).isEmpty())
|
||||
|
||||
extras.putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, "")
|
||||
assertTrue(SyncAdapter.priorityCollections(extras).isEmpty())
|
||||
|
||||
extras.putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, "123")
|
||||
assertArrayEquals(longArrayOf(123), SyncAdapter.priorityCollections(extras).toLongArray())
|
||||
|
||||
extras.putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, ",x,")
|
||||
assertTrue(SyncAdapter.priorityCollections(extras).isEmpty())
|
||||
|
||||
extras.putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, "1,2,3")
|
||||
assertArrayEquals(longArrayOf(1,2,3), SyncAdapter.priorityCollections(extras).toLongArray())
|
||||
}
|
||||
|
||||
}
|
|
@ -1,151 +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.os.Bundle
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidTest
|
||||
class SyncAdapterTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
val context by lazy { InstrumentationRegistry.getInstrumentation().context }
|
||||
val targetContext by lazy { InstrumentationRegistry.getInstrumentation().targetContext }
|
||||
|
||||
/** use our WebDAV provider as a mock provider because it's our own and we don't need any permissions for it */
|
||||
val mockAuthority = targetContext.getString(R.string.webdav_authority)
|
||||
val mockProvider = context.contentResolver.acquireContentProviderClient(mockAuthority)!!
|
||||
|
||||
val account = Account("test", "com.example.test")
|
||||
|
||||
@Inject lateinit var db: AppDatabase
|
||||
lateinit var syncAdapter: TestSyncAdapter
|
||||
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
syncAdapter = TestSyncAdapter(targetContext)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testPriorityCollections() {
|
||||
assertTrue(SyncAdapterService.SyncAdapter.priorityCollections(Bundle()).isEmpty())
|
||||
assertArrayEquals(arrayOf(1L,2L), SyncAdapterService.SyncAdapter.priorityCollections(Bundle(1).apply {
|
||||
putString(SyncAdapterService.SYNC_EXTRAS_PRIORITY_COLLECTIONS, "1,error,2")
|
||||
}).toTypedArray())
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testOnPerformSync_allowsSequentialSyncs() {
|
||||
for (i in 0 until 5)
|
||||
syncAdapter.onPerformSync(account, Bundle(), mockAuthority, mockProvider, SyncResult())
|
||||
assertEquals(5, syncAdapter.syncCalled.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPerformSync_allowsSimultaneousSyncs() {
|
||||
val extras = Bundle(1)
|
||||
extras.putLong(TestSyncAdapter.EXTRA_WAIT, 100) // sync takes 100 ms
|
||||
|
||||
val syncThreads = mutableListOf<Thread>()
|
||||
for (i in 0 until 100) { // within 100 ms, at least 2 threads should be created and run simultaneously
|
||||
syncThreads += Thread({
|
||||
syncAdapter.onPerformSync(account, extras, "$mockAuthority-$i", mockProvider, SyncResult())
|
||||
}).apply {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
// wait for all threads
|
||||
syncThreads.forEach { it.join() }
|
||||
|
||||
assertEquals(100, syncAdapter.syncCalled.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPerformSync_preventsDuplicateSyncs() {
|
||||
val extras = Bundle(1)
|
||||
extras.putLong(TestSyncAdapter.EXTRA_WAIT, 500) // sync takes 500 ms
|
||||
|
||||
val syncThreads = mutableListOf<Thread>()
|
||||
for (i in 0 until 100) { // creating 100 threads should be possible within in 500 ms
|
||||
syncThreads += Thread({
|
||||
syncAdapter.onPerformSync(account, extras, mockAuthority, mockProvider, SyncResult())
|
||||
}).apply {
|
||||
start()
|
||||
}
|
||||
}
|
||||
|
||||
// wait for all threads
|
||||
syncThreads.forEach { it.join() }
|
||||
|
||||
assertEquals(1, syncAdapter.syncCalled.get())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPerformSync_runsSyncAndSetsClassLoader() {
|
||||
syncAdapter.onPerformSync(account, Bundle(), mockAuthority, mockProvider, SyncResult())
|
||||
|
||||
// check whether onPerformSync() actually calls sync()
|
||||
assertEquals(1, syncAdapter.syncCalled.get())
|
||||
|
||||
// check whether contextClassLoader is set
|
||||
assertEquals(targetContext.classLoader, Thread.currentThread().contextClassLoader)
|
||||
}
|
||||
|
||||
|
||||
class TestSyncAdapter(context: Context): SyncAdapterService.SyncAdapter(context) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* How long the sync() method shall wait
|
||||
*/
|
||||
val EXTRA_WAIT = "waitMillis"
|
||||
}
|
||||
|
||||
val syncCalled = AtomicInteger()
|
||||
|
||||
override fun sync(
|
||||
account: Account,
|
||||
extras: Bundle,
|
||||
authority: String,
|
||||
httpClient: Lazy<HttpClient>,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult
|
||||
) {
|
||||
val wait = extras.getLong(EXTRA_WAIT)
|
||||
Thread.sleep(wait)
|
||||
|
||||
syncCalled.incrementAndGet()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -6,10 +6,14 @@ package at.bitfire.davdroid.syncadapter
|
|||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import at.bitfire.dav4jvm.PropStat
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.Response.HrefRelation
|
||||
|
@ -20,6 +24,7 @@ import at.bitfire.davdroid.db.Credentials
|
|||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import okhttp3.Protocol
|
||||
|
@ -34,17 +39,6 @@ import javax.inject.Inject
|
|||
@HiltAndroidTest
|
||||
class SyncManagerTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
@ -67,13 +61,42 @@ class SyncManagerTest {
|
|||
|
||||
}
|
||||
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
@Inject
|
||||
lateinit var settingsManager: SettingsManager
|
||||
|
||||
@Inject
|
||||
lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
val server = MockWebServer()
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
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(context)
|
||||
|
||||
// Initialize WorkManager for instrumentation tests.
|
||||
val config = Configuration.Builder()
|
||||
.setMinimumLoggingLevel(Log.DEBUG)
|
||||
.setWorkerFactory(workerFactory)
|
||||
.build()
|
||||
WorkManagerTestInitHelper.initializeTestWorkManager(context, config)
|
||||
}
|
||||
|
||||
|
||||
private fun syncManager(collection: LocalTestCollection) =
|
||||
TestSyncManager(
|
||||
context,
|
||||
account,
|
||||
Bundle(),
|
||||
arrayOf(),
|
||||
"TestAuthority",
|
||||
HttpClient.Builder().build(),
|
||||
SyncResult(),
|
||||
|
|
|
@ -7,42 +7,55 @@ 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.testing.WorkManagerTestInitHelper
|
||||
import androidx.work.workDataOf
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.TestUtils.workScheduledOrRunning
|
||||
import at.bitfire.davdroid.TestUtils.workScheduledOrRunningOrSuccessful
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
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.mockk
|
||||
import io.mockk.mockkObject
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertNotNull
|
||||
import org.junit.Assert.assertNull
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
@HiltAndroidTest
|
||||
class SyncWorkerTest {
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context: Context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
private val accountManager = AccountManager.get(context)
|
||||
private val account = Account("Test Account", context.getString(R.string.account_type))
|
||||
private val fakeCredentials = Credentials("test", "test")
|
||||
|
||||
private val executor = Executors.newSingleThreadExecutor()
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
@Before
|
||||
fun inject() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
assertTrue(AccountUtils.createAccount(context, account, AccountSettings.initialUserData(fakeCredentials)))
|
||||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 0)
|
||||
|
@ -63,21 +76,81 @@ class SyncWorkerTest {
|
|||
accountManager.removeAccountExplicitly(account)
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testRequestSync_enqueuesWorker() {
|
||||
SyncWorker.requestSync(context, account, CalendarContract.AUTHORITY)
|
||||
fun testEnqueue_enqueuesWorker() {
|
||||
SyncWorker.enqueue(context, account, CalendarContract.AUTHORITY)
|
||||
val workerName = SyncWorker.workerName(account, CalendarContract.AUTHORITY)
|
||||
assertTrue(workScheduledOrRunning(context, workerName))
|
||||
assertTrue(workScheduledOrRunningOrSuccessful(context, workerName))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testStopSync_stopsWorker() {
|
||||
SyncWorker.requestSync(context, account, CalendarContract.AUTHORITY)
|
||||
SyncWorker.stopSync(context, account, CalendarContract.AUTHORITY)
|
||||
val workerName = SyncWorker.workerName(account, CalendarContract.AUTHORITY)
|
||||
assertFalse(workScheduledOrRunning(context, workerName))
|
||||
fun testWifiConditionsMet_withoutWifi() {
|
||||
val accountSettings = mockk<AccountSettings>()
|
||||
every { accountSettings.getSyncWifiOnly() } returns false
|
||||
assertTrue(SyncWorker.wifiConditionsMet(context, accountSettings))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiConditionsMet_anyWifi_wifiEnabled() {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
accountSettings.setSyncWiFiOnly(true)
|
||||
|
||||
// here we could test whether stopping the work really interrupts the sync thread
|
||||
mockkObject(SyncWorker.Companion)
|
||||
every { SyncWorker.Companion.wifiAvailable(any()) } returns true
|
||||
every { SyncWorker.Companion.correctWifiSsid(any(), any()) } returns true
|
||||
|
||||
assertTrue(SyncWorker.wifiConditionsMet(context, accountSettings))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiConditionsMet_anyWifi_wifiDisabled() {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
accountSettings.setSyncWiFiOnly(true)
|
||||
|
||||
mockkObject(SyncWorker.Companion)
|
||||
every { SyncWorker.Companion.wifiAvailable(any()) } returns false
|
||||
every { SyncWorker.Companion.correctWifiSsid(any(), any()) } returns true
|
||||
|
||||
assertFalse(SyncWorker.wifiConditionsMet(context, accountSettings))
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testWifiConditionsMet_correctWifiSsid() {
|
||||
// TODO: Write test
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testWifiConditionsMet_wrongWifiSsid() {
|
||||
// TODO: Write test
|
||||
}
|
||||
|
||||
|
||||
@Test
|
||||
fun testOnStopped_interruptsSyncThread() {
|
||||
val authority = CalendarContract.AUTHORITY
|
||||
val inputData = workDataOf(
|
||||
SyncWorker.ARG_AUTHORITY to authority,
|
||||
SyncWorker.ARG_ACCOUNT_NAME to account.name,
|
||||
SyncWorker.ARG_ACCOUNT_TYPE to account.type
|
||||
)
|
||||
|
||||
// Create SyncWorker as TestWorker
|
||||
val testSyncWorker = TestWorkerBuilder<SyncWorker>(context, executor, inputData).build()
|
||||
assertNull(testSyncWorker.syncThread)
|
||||
|
||||
// Run SyncWorker and assert sync thread is alive
|
||||
testSyncWorker.doWork()
|
||||
assertNotNull(testSyncWorker.syncThread)
|
||||
assertTrue(testSyncWorker.syncThread!!.isAlive)
|
||||
assertFalse(testSyncWorker.syncThread!!.isInterrupted) // Sync running
|
||||
|
||||
// Stop SyncWorker and assert sync thread was interrupted
|
||||
testSyncWorker.onStopped()
|
||||
assertNotNull(testSyncWorker.syncThread)
|
||||
assertTrue(testSyncWorker.syncThread!!.isAlive)
|
||||
assertTrue(testSyncWorker.syncThread!!.isInterrupted) // Sync thread interrupted
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/***************************************************************************************************
|
||||
* 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 androidx.test.platform.app.InstrumentationRegistry
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
@HiltAndroidTest
|
||||
class SyncerTest {
|
||||
|
||||
@get:Rule
|
||||
val hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context = InstrumentationRegistry.getInstrumentation().targetContext
|
||||
|
||||
/** use our WebDAV provider as a mock provider because it's our own and we don't need any permissions for it */
|
||||
val mockAuthority = context.getString(R.string.webdav_authority)
|
||||
val mockProvider = context.contentResolver!!.acquireContentProviderClient(mockAuthority)!!
|
||||
|
||||
val account = Account(javaClass.canonicalName, context.getString(R.string.account_type))
|
||||
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testOnPerformSync_runsSyncAndSetsClassLoader() {
|
||||
val syncer = TestSyncer(context)
|
||||
syncer.onPerformSync(account, arrayOf(), mockAuthority, mockProvider, SyncResult())
|
||||
|
||||
// check whether onPerformSync() actually calls sync()
|
||||
assertEquals(1, syncer.syncCalled.get())
|
||||
|
||||
// check whether contextClassLoader is set
|
||||
assertEquals(context.classLoader, Thread.currentThread().contextClassLoader)
|
||||
}
|
||||
|
||||
|
||||
class TestSyncer(context: Context) : Syncer(context) {
|
||||
|
||||
val syncCalled = AtomicInteger()
|
||||
|
||||
override fun sync(
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: Lazy<HttpClient>,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult
|
||||
) {
|
||||
Thread.sleep(1000)
|
||||
syncCalled.incrementAndGet()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,6 @@ package at.bitfire.davdroid.syncadapter
|
|||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.dav4jvm.DavCollection
|
||||
import at.bitfire.dav4jvm.MultiResponseCallback
|
||||
import at.bitfire.dav4jvm.Response
|
||||
|
@ -26,7 +25,7 @@ import org.junit.Assert.assertEquals
|
|||
class TestSyncManager(
|
||||
context: Context,
|
||||
account: Account,
|
||||
extras: Bundle,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: HttpClient,
|
||||
syncResult: SyncResult,
|
||||
|
|
|
@ -13,7 +13,6 @@ import androidx.core.graphics.drawable.toBitmap
|
|||
import androidx.hilt.work.HiltWorkerFactory
|
||||
import androidx.work.Configuration
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils
|
||||
import at.bitfire.davdroid.ui.DebugInfoActivity
|
||||
|
@ -89,9 +88,6 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide
|
|||
|
||||
// create/update app shortcuts
|
||||
UiUtils.updateShortcuts(this)
|
||||
|
||||
// check/repair sync intervals
|
||||
AccountSettings.repairSyncIntervals(this)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,6 @@ import at.bitfire.davdroid.db.SyncState
|
|||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.syncadapter.AccountUtils
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils.removePeriodicSyncs
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.vcard4android.*
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
@ -63,7 +62,7 @@ open class LocalAddressBook(
|
|||
throw IllegalStateException("Couldn't create address book account")
|
||||
|
||||
val addressBook = LocalAddressBook(context, account, provider)
|
||||
addressBook.updateSyncSettings()
|
||||
addressBook.updateSyncFrameworkSettings()
|
||||
|
||||
// initialize Contacts Provider Settings
|
||||
val values = ContentValues(2)
|
||||
|
@ -230,7 +229,7 @@ open class LocalAddressBook(
|
|||
fun update(info: Collection, forceReadOnly: Boolean) {
|
||||
val newAccountName = accountName(mainAccount, info)
|
||||
|
||||
if (account.name != newAccountName && Build.VERSION.SDK_INT >= 21) {
|
||||
if (account.name != newAccountName) {
|
||||
// no need to re-assign contacts to new account, because they will be deleted by contacts provider in any case
|
||||
val accountManager = AccountManager.get(context)
|
||||
val future = accountManager.renameAccount(account, newAccountName, null, null)
|
||||
|
@ -261,7 +260,7 @@ open class LocalAddressBook(
|
|||
}
|
||||
|
||||
// make sure it will still be synchronized when contacts are updated
|
||||
updateSyncSettings()
|
||||
updateSyncFrameworkSettings()
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
|
@ -277,17 +276,23 @@ open class LocalAddressBook(
|
|||
/**
|
||||
* Updates the sync framework settings for this address book:
|
||||
*
|
||||
* - Contacts sync of this address book account shall be possible → isSyncable = 1
|
||||
* - When a contact is changed, a sync shall be initiated (ContactsSyncAdapter) -> syncAutomatically = true
|
||||
* - However, we don't want a periodic (ContactsSyncAdapter) sync for this address book
|
||||
* because contact synchronization is handled by AddressBooksSyncAdapter
|
||||
* (which has its own periodic sync according to the account's contacts sync interval). */
|
||||
fun updateSyncSettings() {
|
||||
* - Contacts sync of this address book account shall be possible -> isSyncable = 1
|
||||
* - When a contact is changed, a sync shall be initiated -> syncAutomatically = true
|
||||
* - Remove unwanted sync framework periodic syncs created by setSyncAutomatically, as
|
||||
* we use PeriodicSyncWorker for scheduled syncs
|
||||
*/
|
||||
fun updateSyncFrameworkSettings() {
|
||||
// Enable sync-ability
|
||||
if (ContentResolver.getIsSyncable(account, ContactsContract.AUTHORITY) != 1)
|
||||
ContentResolver.setIsSyncable(account, ContactsContract.AUTHORITY, 1)
|
||||
|
||||
// Enable content trigger
|
||||
if (!ContentResolver.getSyncAutomatically(account, ContactsContract.AUTHORITY))
|
||||
ContentResolver.setSyncAutomatically(account, ContactsContract.AUTHORITY, true)
|
||||
removePeriodicSyncs(account, ContactsContract.AUTHORITY)
|
||||
|
||||
// Remove periodic syncs (setSyncAutomatically also creates periodic syncs, which we don't want)
|
||||
for (periodicSync in ContentResolver.getPeriodicSyncs(account, ContactsContract.AUTHORITY))
|
||||
ContentResolver.removePeriodicSync(periodicSync.account, periodicSync.authority, periodicSync.extras)
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -329,7 +329,7 @@ class RefreshCollectionsWorker @AssistedInject constructor(
|
|||
|
||||
try {
|
||||
DavResource(httpClient, homeSetUrl).propfind(1, *DAV_COLLECTION_PROPERTIES) { response, relation ->
|
||||
// NB: This callback may be called multiple times ([MultiResponseCallback])
|
||||
// Note: This callback may be called multiple times ([MultiResponseCallback])
|
||||
if (!response.isSuccess())
|
||||
return@propfind
|
||||
|
||||
|
|
|
@ -6,10 +6,7 @@ package at.bitfire.davdroid.settings
|
|||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.ContentUris
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.content.*
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
|
@ -31,6 +28,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.PeriodicSyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils
|
||||
import at.bitfire.davdroid.util.closeCompat
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
|
@ -79,7 +77,7 @@ class AccountSettings(
|
|||
|
||||
companion object {
|
||||
|
||||
const val CURRENT_VERSION = 13
|
||||
const val CURRENT_VERSION = 14
|
||||
const val KEY_SETTINGS_VERSION = "version"
|
||||
|
||||
const val KEY_SYNC_INTERVAL_ADDRESSBOOKS = "sync_interval_addressbooks"
|
||||
|
@ -148,53 +146,6 @@ class AccountSettings(
|
|||
return bundle
|
||||
}
|
||||
|
||||
fun repairSyncIntervals(context: Context) {
|
||||
val addressBooksAuthority = context.getString(R.string.address_books_authority)
|
||||
val taskAuthority = TaskUtils.currentProvider(context)?.authority
|
||||
|
||||
val am = AccountManager.get(context)
|
||||
for (account in am.getAccountsByType(context.getString(R.string.account_type)))
|
||||
try {
|
||||
val settings = AccountSettings(context, account)
|
||||
|
||||
// repair address book sync
|
||||
settings.getSavedAddressbooksSyncInterval()?.let { shouldBe ->
|
||||
val authority = addressBooksAuthority
|
||||
val current = settings.getSyncInterval(authority)
|
||||
if (current != shouldBe) {
|
||||
Logger.log.warning("${account.name}: $authority sync interval should be $shouldBe but is $current -> setting to $shouldBe")
|
||||
if (!settings.setSyncInterval(authority, shouldBe))
|
||||
Logger.log.warning("${account.name}: repairing/setting the sync interval for $authority failed")
|
||||
}
|
||||
}
|
||||
|
||||
// repair calendar sync
|
||||
settings.getSavedCalendarsSyncInterval()?.let { shouldBe ->
|
||||
val authority = CalendarContract.AUTHORITY
|
||||
val current = settings.getSyncInterval(authority)
|
||||
if (current != shouldBe) {
|
||||
Logger.log.warning("${account.name}: $authority sync interval should be $shouldBe but is $current -> setting to $shouldBe")
|
||||
if (!settings.setSyncInterval(authority, shouldBe))
|
||||
Logger.log.warning("${account.name}: repairing/setting the sync interval for $authority failed")
|
||||
}
|
||||
}
|
||||
|
||||
if (taskAuthority != null)
|
||||
// repair calendar sync
|
||||
settings.getSavedTasksSyncInterval()?.let { shouldBe ->
|
||||
val authority = taskAuthority
|
||||
val current = settings.getSyncInterval(authority)
|
||||
if (current != shouldBe) {
|
||||
Logger.log.warning("${account.name}: $authority sync interval should be $shouldBe but is $current -> setting to $shouldBe")
|
||||
if (!settings.setSyncInterval(authority, shouldBe))
|
||||
Logger.log.warning("${account.name}: repairing/setting the sync interval for $authority failed")
|
||||
}
|
||||
}
|
||||
} catch (ignored: InvalidAccountException) {
|
||||
// account doesn't exist (anymore)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -250,81 +201,125 @@ class AccountSettings(
|
|||
|
||||
// sync. settings
|
||||
|
||||
/**
|
||||
* Gets the currently set sync interval for this account in seconds.
|
||||
*
|
||||
* @param authority authority to check (for instance: [CalendarContract.AUTHORITY]])
|
||||
* @return sync interval in seconds; *[SYNC_INTERVAL_MANUALLY]* if manual sync; *null* if not set
|
||||
*/
|
||||
fun getSyncInterval(authority: String): Long? {
|
||||
if (ContentResolver.getIsSyncable(account, authority) <= 0)
|
||||
return null
|
||||
|
||||
return if (ContentResolver.getSyncAutomatically(account, authority))
|
||||
ContentResolver.getPeriodicSyncs(account, authority).firstOrNull()?.period ?: SYNC_INTERVAL_MANUALLY
|
||||
else
|
||||
SYNC_INTERVAL_MANUALLY
|
||||
val key = when {
|
||||
authority == context.getString(R.string.address_books_authority) ->
|
||||
KEY_SYNC_INTERVAL_ADDRESSBOOKS
|
||||
authority == CalendarContract.AUTHORITY ->
|
||||
KEY_SYNC_INTERVAL_CALENDARS
|
||||
TaskProvider.ProviderName.values().any { it.authority == authority } ->
|
||||
KEY_SYNC_INTERVAL_TASKS
|
||||
else -> throw IllegalArgumentException("Authority does not exist: $authority")
|
||||
}
|
||||
return accountManager.getUserData(account, key)?.toLong()
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Sets the sync interval and enables/disables automatic sync for the given account and authority.
|
||||
* Sets the sync interval and en- or disables periodic sync for the given account and authority.
|
||||
* Does *not* call [ContentResolver.setIsSyncable].
|
||||
*
|
||||
* This method blocks until the settings have arrived in the sync framework, so it should not
|
||||
* be called from the UI thread.
|
||||
* This method blocks until a worker as been created and enqueued (sync active) or removed
|
||||
* (sync disabled), so it should not be called from the UI thread.
|
||||
*
|
||||
* @param authority sync authority (like [CalendarContract.AUTHORITY])
|
||||
* @param seconds if [SYNC_INTERVAL_MANUALLY]: automatic sync will be disabled;
|
||||
* otherwise: automatic sync will be enabled and set to the given number of seconds
|
||||
* otherwise (≥ 15 min): automatic sync will be enabled and set to the given number of seconds
|
||||
*
|
||||
* @return whether the sync interval was successfully set
|
||||
* @throws IllegalArgumentException when [seconds] is not [SYNC_INTERVAL_MANUALLY] but less than 15 min
|
||||
*/
|
||||
@WorkerThread
|
||||
fun setSyncInterval(authority: String, seconds: Long): Boolean {
|
||||
/* Ugly hack: because there is no callback for when the sync status/interval has been
|
||||
updated, we need to make this call blocking. */
|
||||
val setInterval: () -> Boolean =
|
||||
if (seconds == SYNC_INTERVAL_MANUALLY) {
|
||||
{
|
||||
Logger.log.fine("Disabling automatic sync of $account/$authority")
|
||||
ContentResolver.setSyncAutomatically(account, authority, false)
|
||||
if (seconds != SYNC_INTERVAL_MANUALLY && seconds < 60*15)
|
||||
throw IllegalArgumentException("<15 min is not supported by Android")
|
||||
|
||||
/* return */ !ContentResolver.getSyncAutomatically(account, authority)
|
||||
}
|
||||
} else {
|
||||
{
|
||||
Logger.log.fine("Setting automatic sync of $account/$authority to $seconds seconds")
|
||||
ContentResolver.setSyncAutomatically(account, authority, true)
|
||||
ContentResolver.addPeriodicSync(account, authority, Bundle(), seconds)
|
||||
|
||||
/* return */ ContentResolver.getSyncAutomatically(account, authority) &&
|
||||
ContentResolver.getPeriodicSyncs(account, authority).firstOrNull()?.period == seconds
|
||||
}
|
||||
}
|
||||
|
||||
// try up to 10 times with 100 ms pause
|
||||
var success = false
|
||||
for (idxTry in 0 until 10) {
|
||||
success = setInterval()
|
||||
if (success)
|
||||
break
|
||||
Thread.sleep(100)
|
||||
}
|
||||
|
||||
if (!success)
|
||||
val onlyManually = seconds == SYNC_INTERVAL_MANUALLY
|
||||
try {
|
||||
if (onlyManually) {
|
||||
Logger.log.fine("Disabling periodic sync of $account/$authority")
|
||||
PeriodicSyncWorker.disable(context, account, authority)
|
||||
} else {
|
||||
Logger.log.fine("Setting periodic sync of $account/$authority to $seconds seconds")
|
||||
PeriodicSyncWorker.enable(context, account, authority, seconds)
|
||||
}.result.get() // On operation (enable/disable) failure exception is thrown
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Failed to set sync interval of $account/$authority to $seconds seconds", e)
|
||||
return false
|
||||
|
||||
// store sync interval in account settings (used when the provider is switched)
|
||||
when {
|
||||
authority == context.getString(R.string.address_books_authority) ->
|
||||
accountManager.setUserData(account, KEY_SYNC_INTERVAL_ADDRESSBOOKS, seconds.toString())
|
||||
|
||||
authority == CalendarContract.AUTHORITY ->
|
||||
accountManager.setUserData(account, KEY_SYNC_INTERVAL_CALENDARS, seconds.toString())
|
||||
|
||||
TaskProvider.ProviderName.values().any { it.authority == authority } ->
|
||||
accountManager.setUserData(account, KEY_SYNC_INTERVAL_TASKS, seconds.toString())
|
||||
}
|
||||
|
||||
// Also enable/disable content change triggered syncs (SyncFramework automatic sync).
|
||||
// We could make this a separate user adjustable setting later on.
|
||||
setSyncOnContentChange(authority, !onlyManually)
|
||||
|
||||
// Store (user defined) sync interval in account settings. Used when the provider is
|
||||
// switched or re-installed
|
||||
val key = when {
|
||||
authority == context.getString(R.string.address_books_authority) ->
|
||||
KEY_SYNC_INTERVAL_ADDRESSBOOKS
|
||||
authority == CalendarContract.AUTHORITY ->
|
||||
KEY_SYNC_INTERVAL_CALENDARS
|
||||
TaskProvider.ProviderName.values().any { it.authority == authority } ->
|
||||
KEY_SYNC_INTERVAL_TASKS
|
||||
else ->
|
||||
throw IllegalArgumentException("Sync interval not applicable to authority $authority")
|
||||
}
|
||||
accountManager.setUserData(account, key, seconds.toString())
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
fun getSavedAddressbooksSyncInterval() = accountManager.getUserData(account, KEY_SYNC_INTERVAL_ADDRESSBOOKS)?.toLong()
|
||||
fun getSavedCalendarsSyncInterval() = accountManager.getUserData(account, KEY_SYNC_INTERVAL_CALENDARS)?.toLong()
|
||||
/**
|
||||
* Enables/disables sync adapter automatic sync (content triggered sync) for the given
|
||||
* account and authority. Does *not* call [ContentResolver.setIsSyncable].
|
||||
*
|
||||
* We use the sync adapter framework only for the trigger, actual syncing is implemented
|
||||
* with WorkManager. The trigger comes in through SyncAdapterService.
|
||||
*
|
||||
* This method blocks until the sync-on-content-change has been enabled or disabled, so it
|
||||
* should not be called from the UI thread.
|
||||
*
|
||||
* @param enable *true* enables automatic sync; *false* disables it
|
||||
* @param authority sync authority (like [CalendarContract.AUTHORITY])
|
||||
* @return whether the content triggered sync was enabled successfully
|
||||
*/
|
||||
@WorkerThread
|
||||
fun setSyncOnContentChange(authority: String, enable: Boolean): Boolean {
|
||||
// Enable content change triggers (sync adapter framework)
|
||||
val setContentTrigger: () -> Boolean =
|
||||
/* Ugly hack: because there is no callback for when the sync status/interval has been
|
||||
updated, we need to make this call blocking. */
|
||||
if (enable) {{
|
||||
Logger.log.fine("Enabling content-triggered sync of $account/$authority")
|
||||
ContentResolver.setSyncAutomatically(account, authority, true) // enables content triggers
|
||||
// Remove unwanted sync framework periodic syncs created by setSyncAutomatically
|
||||
for (periodicSync in ContentResolver.getPeriodicSyncs(account, authority))
|
||||
ContentResolver.removePeriodicSync(periodicSync.account, periodicSync.authority, periodicSync.extras)
|
||||
/* return */ ContentResolver.getSyncAutomatically(account, authority)
|
||||
}} else {{
|
||||
Logger.log.fine("Disabling content-triggered sync of $account/$authority")
|
||||
ContentResolver.setSyncAutomatically(account, authority, false) // disables content triggers
|
||||
/* return */ !ContentResolver.getSyncAutomatically(account, authority)
|
||||
}}
|
||||
|
||||
// try up to 10 times with 100 ms pause
|
||||
for (idxTry in 0 until 10) {
|
||||
if (setContentTrigger())
|
||||
// successfully set
|
||||
return true
|
||||
Thread.sleep(100)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun getSavedTasksSyncInterval() = accountManager.getUserData(account, KEY_SYNC_INTERVAL_TASKS)?.toLong()
|
||||
|
||||
fun getSyncWifiOnly() =
|
||||
|
@ -467,6 +462,62 @@ class AccountSettings(
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables all sync adapter periodic syncs for every authority. Then enables
|
||||
* corresponding PeriodicSyncWorkers
|
||||
*/
|
||||
@Suppress("unused","FunctionName")
|
||||
fun update_13_14() {
|
||||
val authorities = listOf(
|
||||
context.getString(R.string.address_books_authority),
|
||||
CalendarContract.AUTHORITY,
|
||||
TaskProvider.ProviderName.JtxBoard.authority,
|
||||
TaskProvider.ProviderName.OpenTasks.authority,
|
||||
TaskProvider.ProviderName.TasksOrg.authority
|
||||
)
|
||||
|
||||
for (authority in authorities) {
|
||||
// Enable PeriodicSyncWorker (WorkManager), with known intervals
|
||||
v14_enableWorkManager(authority)
|
||||
// Disable periodic syncs (sync adapter framework)
|
||||
v14_disableSyncFramework(authority)
|
||||
}
|
||||
}
|
||||
private fun v14_enableWorkManager(authority: String) {
|
||||
getSyncInterval(authority)?.let { syncInterval ->
|
||||
if (!setSyncInterval(authority, syncInterval))
|
||||
Logger.log.severe("Failed to enable PeriodicSyncWorker for $authority")
|
||||
}
|
||||
}
|
||||
private fun v14_disableSyncFramework(authority: String) {
|
||||
// Cancel potentially running sync
|
||||
ContentResolver.cancelSync(account, authority)
|
||||
|
||||
// Disable periodic syncs (sync adapter framework)
|
||||
val disable: () -> Boolean = {
|
||||
/* Ugly hack: because there is no callback for when the sync status/interval has been
|
||||
updated, we need to make this call blocking. */
|
||||
val syncs = ContentResolver.getPeriodicSyncs(account, authority)
|
||||
for (sync in syncs) {
|
||||
Logger.log.fine("Disabling sync framework periodic syncs of $account/$authority")
|
||||
ContentResolver.removePeriodicSync(sync.account, sync.authority, sync.extras)
|
||||
}
|
||||
/* return */
|
||||
!syncs.all { sync ->
|
||||
ContentResolver.getPeriodicSyncs(sync.account, sync.authority).isEmpty()
|
||||
}
|
||||
}
|
||||
// try up to 10 times with 100 ms pause
|
||||
var success = false
|
||||
for (idxTry in 0 until 10) {
|
||||
success = disable()
|
||||
if (success)
|
||||
break
|
||||
Thread.sleep(100)
|
||||
}
|
||||
if (!success)
|
||||
Logger.log.severe("Failed to disable sync framework periodic syncs for $authority")
|
||||
}
|
||||
|
||||
@Suppress("unused","FunctionName")
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.settings.Settings
|
||||
import at.bitfire.davdroid.settings.SettingsManager
|
||||
import at.bitfire.davdroid.util.closeCompat
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Sync logic for address books
|
||||
*/
|
||||
class AddressBookSyncer(context: Context): Syncer(context) {
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface AddressBooksSyncAdapterEntryPoint {
|
||||
fun settingsManager(): SettingsManager
|
||||
}
|
||||
|
||||
val entryPoint = EntryPointAccessors.fromApplication(context, AddressBooksSyncAdapterEntryPoint::class.java)
|
||||
val settingsManager = entryPoint.settingsManager()
|
||||
|
||||
override fun sync(
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: Lazy<HttpClient>,
|
||||
provider: ContentProviderClient,
|
||||
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)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
|
||||
}
|
||||
|
||||
Logger.log.info("Address book sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean {
|
||||
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV)
|
||||
|
||||
val remoteAddressBooks = mutableMapOf<HttpUrl, Collection>()
|
||||
if (service != null)
|
||||
for (collection in db.collectionDao().getByServiceAndSync(service.id))
|
||||
remoteAddressBooks[collection.url] = collection
|
||||
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (remoteAddressBooks.isEmpty())
|
||||
Logger.log.info("No contacts permission, but no address book selected for synchronization")
|
||||
else
|
||||
Logger.log.warning("No contacts permission, but address books are selected for synchronization")
|
||||
return false
|
||||
}
|
||||
|
||||
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
|
||||
try {
|
||||
if (contactsProvider == null) {
|
||||
Logger.log.severe("Couldn't access contacts provider")
|
||||
syncResult.databaseError = true
|
||||
return false
|
||||
}
|
||||
|
||||
val forceAllReadOnly = settingsManager.getBoolean(Settings.FORCE_READ_ONLY_ADDRESSBOOKS)
|
||||
|
||||
// delete/update local address books
|
||||
for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
|
||||
val url = addressBook.url.toHttpUrl()
|
||||
val info = remoteAddressBooks[url]
|
||||
if (info == null) {
|
||||
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
|
||||
addressBook.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
try {
|
||||
Logger.log.log(Level.FINE, "Updating local address book $url", info)
|
||||
addressBook.update(info, forceAllReadOnly)
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
|
||||
}
|
||||
// we already have a local address book for this remote collection, don't take into consideration anymore
|
||||
remoteAddressBooks -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local address books
|
||||
for ((_, info) in remoteAddressBooks) {
|
||||
Logger.log.log(Level.INFO, "Adding local address book", info)
|
||||
LocalAddressBook.create(context, contactsProvider, account, info, forceAllReadOnly)
|
||||
}
|
||||
} finally {
|
||||
contactsProvider?.closeCompat()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
|
@ -1,131 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.Manifest
|
||||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.util.closeCompat
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
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 dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.util.logging.Level
|
||||
|
||||
class AddressBooksSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = AddressBooksSyncAdapter(this)
|
||||
|
||||
class AddressBooksSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface AddressBooksSyncAdapterEntryPoint {
|
||||
fun settingsManager(): SettingsManager
|
||||
}
|
||||
|
||||
val entryPoint = EntryPointAccessors.fromApplication(context, AddressBooksSyncAdapterEntryPoint::class.java)
|
||||
val settingsManager = entryPoint.settingsManager()
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
|
||||
/* don't run sync if
|
||||
- sync conditions (e.g. "sync only in WiFi") are not met AND
|
||||
- this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
|
||||
*/
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
|
||||
return
|
||||
|
||||
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.requestSync(context, addressBookAccount, ContactsContract.AUTHORITY)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
|
||||
}
|
||||
|
||||
Logger.log.info("Address book sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalAddressBooks(account: Account, syncResult: SyncResult): Boolean {
|
||||
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CARDDAV)
|
||||
|
||||
val remoteAddressBooks = mutableMapOf<HttpUrl, Collection>()
|
||||
if (service != null)
|
||||
for (collection in db.collectionDao().getByServiceAndSync(service.id))
|
||||
remoteAddressBooks[collection.url] = collection
|
||||
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.WRITE_CONTACTS) != PackageManager.PERMISSION_GRANTED) {
|
||||
if (remoteAddressBooks.isEmpty())
|
||||
Logger.log.info("No contacts permission, but no address book selected for synchronization")
|
||||
else
|
||||
Logger.log.warning("No contacts permission, but address books are selected for synchronization")
|
||||
return false
|
||||
}
|
||||
|
||||
val contactsProvider = context.contentResolver.acquireContentProviderClient(ContactsContract.AUTHORITY)
|
||||
try {
|
||||
if (contactsProvider == null) {
|
||||
Logger.log.severe("Couldn't access contacts provider")
|
||||
syncResult.databaseError = true
|
||||
return false
|
||||
}
|
||||
|
||||
val forceAllReadOnly = settingsManager.getBoolean(Settings.FORCE_READ_ONLY_ADDRESSBOOKS)
|
||||
|
||||
// delete/update local address books
|
||||
for (addressBook in LocalAddressBook.findAll(context, contactsProvider, account)) {
|
||||
val url = addressBook.url.toHttpUrl()
|
||||
val info = remoteAddressBooks[url]
|
||||
if (info == null) {
|
||||
Logger.log.log(Level.INFO, "Deleting obsolete local address book", url)
|
||||
addressBook.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
try {
|
||||
Logger.log.log(Level.FINE, "Updating local address book $url", info)
|
||||
addressBook.update(info, forceAllReadOnly)
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't rename address book account", e)
|
||||
}
|
||||
// we already have a local address book for this remote collection, don't take into consideration anymore
|
||||
remoteAddressBooks -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local address books
|
||||
for ((_, info) in remoteAddressBooks) {
|
||||
Logger.log.log(Level.INFO, "Adding local address book", info)
|
||||
LocalAddressBook.create(context, contactsProvider, account, info, forceAllReadOnly)
|
||||
}
|
||||
} finally {
|
||||
contactsProvider?.closeCompat()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,6 @@ package at.bitfire.davdroid.syncadapter
|
|||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.dav4jvm.DavCalendar
|
||||
import at.bitfire.dav4jvm.MultiResponseCallback
|
||||
import at.bitfire.dav4jvm.Response
|
||||
|
@ -46,7 +45,7 @@ class CalendarSyncManager(
|
|||
context: Context,
|
||||
account: Account,
|
||||
accountSettings: AccountSettings,
|
||||
extras: Bundle,
|
||||
extras: Array<String>,
|
||||
httpClient: HttpClient,
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
|
|
|
@ -0,0 +1,91 @@
|
|||
/***************************************************************************************************
|
||||
* 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.CalendarContract
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Sync logic for calendars
|
||||
*/
|
||||
class CalendarSyncer(context: Context): Syncer(context) {
|
||||
|
||||
override fun sync(
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: Lazy<HttpClient>,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult
|
||||
) {
|
||||
try {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
|
||||
if (accountSettings.getEventColors())
|
||||
AndroidCalendar.insertColors(provider, account)
|
||||
else
|
||||
AndroidCalendar.removeColors(provider, account)
|
||||
|
||||
updateLocalCalendars(provider, account, accountSettings)
|
||||
|
||||
val calendars = AndroidCalendar
|
||||
.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
|
||||
for (calendar in calendars) {
|
||||
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
|
||||
CalendarSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, calendar).performSync()
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
|
||||
}
|
||||
Logger.log.info("Calendar sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
|
||||
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||
|
||||
val remoteCalendars = mutableMapOf<HttpUrl, Collection>()
|
||||
if (service != null)
|
||||
for (collection in db.collectionDao().getSyncCalendars(service.id)) {
|
||||
remoteCalendars[collection.url] = collection
|
||||
}
|
||||
|
||||
// delete/update local calendars
|
||||
val updateColors = settings.getManageCalendarColors()
|
||||
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
|
||||
calendar.name?.let {
|
||||
val url = it.toHttpUrl()
|
||||
val info = remoteCalendars[url]
|
||||
if (info == null) {
|
||||
Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
|
||||
calendar.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.log(Level.FINE, "Updating local calendar $url", info)
|
||||
calendar.update(info, updateColors)
|
||||
// we already have a local calendar for this remote collection, don't take into consideration anymore
|
||||
remoteCalendars -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local calendars
|
||||
for ((_, info) in remoteCalendars) {
|
||||
Logger.log.log(Level.INFO, "Adding local calendar", info)
|
||||
LocalCalendar.create(account, provider, info)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,104 +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.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalCalendar
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.ical4android.AndroidCalendar
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.util.logging.Level
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.set
|
||||
|
||||
class CalendarsSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = CalendarsSyncAdapter(this)
|
||||
|
||||
class CalendarsSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
|
||||
/* don't run sync if
|
||||
- sync conditions (e.g. "sync only in WiFi") are not met AND
|
||||
- this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
|
||||
*/
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
|
||||
return
|
||||
|
||||
if (accountSettings.getEventColors())
|
||||
AndroidCalendar.insertColors(provider, account)
|
||||
else
|
||||
AndroidCalendar.removeColors(provider, account)
|
||||
|
||||
updateLocalCalendars(provider, account, accountSettings)
|
||||
|
||||
val priorityCalendars = priorityCollections(extras)
|
||||
val calendars = AndroidCalendar
|
||||
.find(account, provider, LocalCalendar.Factory, "${CalendarContract.Calendars.SYNC_EVENTS}!=0", null)
|
||||
.sortedByDescending { priorityCalendars.contains(it.id) }
|
||||
for (calendar in calendars) {
|
||||
Logger.log.info("Synchronizing calendar #${calendar.id}, URL: ${calendar.name}")
|
||||
CalendarSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, calendar).let {
|
||||
it.performSync()
|
||||
}
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync calendars", e)
|
||||
}
|
||||
Logger.log.info("Calendar sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalCalendars(provider: ContentProviderClient, account: Account, settings: AccountSettings) {
|
||||
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||
|
||||
val remoteCalendars = mutableMapOf<HttpUrl, Collection>()
|
||||
if (service != null)
|
||||
for (collection in db.collectionDao().getSyncCalendars(service.id)) {
|
||||
remoteCalendars[collection.url] = collection
|
||||
}
|
||||
|
||||
// delete/update local calendars
|
||||
val updateColors = settings.getManageCalendarColors()
|
||||
for (calendar in AndroidCalendar.find(account, provider, LocalCalendar.Factory, null, null))
|
||||
calendar.name?.let {
|
||||
val url = it.toHttpUrl()
|
||||
val info = remoteCalendars[url]
|
||||
if (info == null) {
|
||||
Logger.log.log(Level.INFO, "Deleting obsolete local calendar", url)
|
||||
calendar.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.log(Level.FINE, "Updating local calendar $url", info)
|
||||
calendar.update(info, updateColors)
|
||||
// we already have a local calendar for this remote collection, don't take into consideration anymore
|
||||
remoteCalendars -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local calendars
|
||||
for ((_, info) in remoteCalendars) {
|
||||
Logger.log.log(Level.INFO, "Adding local calendar", info)
|
||||
LocalCalendar.create(account, provider, info)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/***************************************************************************************************
|
||||
* 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.HttpClient
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
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.setUserData(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,73 +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.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import android.provider.ContactsContract
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import java.util.logging.Level
|
||||
|
||||
class ContactsSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
companion object {
|
||||
const val PREVIOUS_GROUP_METHOD = "previous_group_method"
|
||||
}
|
||||
|
||||
override fun syncAdapter() = ContactsSyncAdapter(this)
|
||||
|
||||
class ContactsSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, 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.setUserData(account, PREVIOUS_GROUP_METHOD, groupMethod)
|
||||
|
||||
/* don't run sync if
|
||||
- sync conditions (e.g. "sync only in WiFi") are not met AND
|
||||
- this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
|
||||
*/
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
|
||||
return
|
||||
|
||||
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).let {
|
||||
it.performSync()
|
||||
}
|
||||
} catch(e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync contacts", e)
|
||||
}
|
||||
Logger.log.info("Contacts sync complete")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -10,7 +10,6 @@ import android.content.ContentResolver
|
|||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import at.bitfire.dav4jvm.DavAddressBook
|
||||
import at.bitfire.dav4jvm.MultiResponseCallback
|
||||
import at.bitfire.dav4jvm.Response
|
||||
|
@ -83,7 +82,7 @@ class ContactsSyncManager(
|
|||
account: Account,
|
||||
accountSettings: AccountSettings,
|
||||
httpClient: HttpClient,
|
||||
extras: Bundle,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
val provider: ContentProviderClient,
|
||||
|
@ -114,7 +113,7 @@ class ContactsSyncManager(
|
|||
// workaround for Android 7 which sets DIRTY flag when only meta-data is changed
|
||||
val reallyDirty = localCollection.verifyDirty()
|
||||
val deleted = localCollection.findDeleted().size
|
||||
if (extras.containsKey(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
|
||||
if (extras.contains(ContentResolver.SYNC_EXTRAS_UPLOAD) && reallyDirty == 0 && deleted == 0) {
|
||||
Logger.log.info("This sync was called to up-sync dirty/deleted contacts, but no contacts have been changed")
|
||||
return false
|
||||
}
|
||||
|
@ -216,7 +215,7 @@ class ContactsSyncManager(
|
|||
groupStrategy.beforeUploadDirty()
|
||||
|
||||
// generate UID/file name for newly created contacts
|
||||
var superModified = super.uploadDirty()
|
||||
val superModified = super.uploadDirty()
|
||||
|
||||
// return true when any operation returned true
|
||||
return modified or superModified
|
||||
|
|
|
@ -1,113 +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.accounts.AccountManager
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalJtxCollection
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.ical4android.JtxCollection
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.util.logging.Level
|
||||
import kotlin.collections.component1
|
||||
import kotlin.collections.component2
|
||||
import kotlin.collections.set
|
||||
|
||||
class JtxSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = JtxSyncAdapter(this)
|
||||
|
||||
class JtxSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
|
||||
try {
|
||||
// check whether jtx Board is new enough
|
||||
TaskProvider.checkVersion(context, TaskProvider.ProviderName.JtxBoard)
|
||||
|
||||
// make sure account can be seen by task provider
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
/* Warning: If setAccountVisibility is called, Android 12 broadcasts the
|
||||
AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION Intent. This cancels running syncs
|
||||
and starts them again! So make sure setAccountVisibility is only called when necessary. */
|
||||
val am = AccountManager.get(context)
|
||||
if (am.getAccountVisibility(account, TaskProvider.ProviderName.JtxBoard.packageName) != AccountManager.VISIBILITY_VISIBLE)
|
||||
am.setAccountVisibility(account, TaskProvider.ProviderName.JtxBoard.packageName, AccountManager.VISIBILITY_VISIBLE)
|
||||
}
|
||||
/* don't run sync if
|
||||
- sync conditions (e.g. "sync only in WiFi") are not met AND
|
||||
- this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
|
||||
*/
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
|
||||
return
|
||||
|
||||
// sync list of collections
|
||||
updateLocalCollections(account, provider)
|
||||
|
||||
// sync contents of collections
|
||||
val collections = JtxCollection.find(account, provider, context, LocalJtxCollection.Factory, null, null)
|
||||
for (collection in collections) {
|
||||
Logger.log.info("Synchronizing $collection")
|
||||
JtxSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, collection).let {
|
||||
it.performSync()
|
||||
}
|
||||
}
|
||||
|
||||
} catch (e: TaskProvider.ProviderTooOldException) {
|
||||
SyncUtils.notifyProviderTooOld(context, e)
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync jtx collections", e)
|
||||
}
|
||||
Logger.log.info("jtx sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalCollections(account: Account, client: ContentProviderClient) {
|
||||
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||
|
||||
val remoteCollections = mutableMapOf<HttpUrl, Collection>()
|
||||
if (service != null)
|
||||
for (collection in db.collectionDao().getSyncJtxCollections(service.id))
|
||||
remoteCollections[collection.url] = collection
|
||||
|
||||
for (jtxCollection in JtxCollection.find(account, client, context, LocalJtxCollection.Factory, null, null))
|
||||
jtxCollection.url?.let { strUrl ->
|
||||
val url = strUrl.toHttpUrl()
|
||||
val info = remoteCollections[url]
|
||||
if (info == null) {
|
||||
Logger.log.fine("Deleting obsolete local collection $url")
|
||||
jtxCollection.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.log(Level.FINE, "Updating local collection $url", info)
|
||||
val owner = info.ownerId?.let { db.principalDao().get(it) }
|
||||
jtxCollection.updateCollection(info, owner)
|
||||
// we already have a local task list for this remote collection, don't take into consideration anymore
|
||||
remoteCollections -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local collections
|
||||
for ((_,info) in remoteCollections) {
|
||||
Logger.log.log(Level.INFO, "Adding local collections", info)
|
||||
val owner = info.ownerId?.let { db.principalDao().get(it) }
|
||||
LocalJtxCollection.create(account, client, info, owner)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,6 @@ package at.bitfire.davdroid.syncadapter
|
|||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.dav4jvm.DavCalendar
|
||||
import at.bitfire.dav4jvm.MultiResponseCallback
|
||||
import at.bitfire.dav4jvm.Response
|
||||
|
@ -38,7 +37,7 @@ class JtxSyncManager(
|
|||
context: Context,
|
||||
account: Account,
|
||||
accountSettings: AccountSettings,
|
||||
extras: Bundle,
|
||||
extras: Array<String>,
|
||||
httpClient: HttpClient,
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
|
|
104
app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncer.kt
Normal file
104
app/src/main/java/at/bitfire/davdroid/syncadapter/JtxSyncer.kt
Normal file
|
@ -0,0 +1,104 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Build
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalJtxCollection
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.ical4android.JtxCollection
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Sync logic for jtx board
|
||||
*/
|
||||
class JtxSyncer(context: Context): Syncer(context) {
|
||||
|
||||
override fun sync(
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: Lazy<HttpClient>,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult
|
||||
) {
|
||||
try {
|
||||
// check whether jtx Board is new enough
|
||||
TaskProvider.checkVersion(context, TaskProvider.ProviderName.JtxBoard)
|
||||
|
||||
// make sure account can be seen by task provider
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
/* Warning: If setAccountVisibility is called, Android 12 broadcasts the
|
||||
AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION Intent. This cancels running syncs
|
||||
and starts them again! So make sure setAccountVisibility is only called when necessary. */
|
||||
val am = AccountManager.get(context)
|
||||
if (am.getAccountVisibility(account, TaskProvider.ProviderName.JtxBoard.packageName) != AccountManager.VISIBILITY_VISIBLE)
|
||||
am.setAccountVisibility(account, TaskProvider.ProviderName.JtxBoard.packageName, AccountManager.VISIBILITY_VISIBLE)
|
||||
}
|
||||
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
|
||||
// sync list of collections
|
||||
updateLocalCollections(account, provider)
|
||||
|
||||
// sync contents of collections
|
||||
val collections = JtxCollection.find(account, provider, context, LocalJtxCollection.Factory, null, null)
|
||||
for (collection in collections) {
|
||||
Logger.log.info("Synchronizing $collection")
|
||||
JtxSyncManager(context, account, accountSettings, extras, httpClient.value, authority, syncResult, collection).performSync()
|
||||
}
|
||||
|
||||
} catch (e: TaskProvider.ProviderTooOldException) {
|
||||
SyncUtils.notifyProviderTooOld(context, e)
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync jtx collections", e)
|
||||
}
|
||||
Logger.log.info("jtx sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalCollections(account: Account, client: ContentProviderClient) {
|
||||
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||
|
||||
val remoteCollections = mutableMapOf<HttpUrl, Collection>()
|
||||
if (service != null)
|
||||
for (collection in db.collectionDao().getSyncJtxCollections(service.id))
|
||||
remoteCollections[collection.url] = collection
|
||||
|
||||
for (jtxCollection in JtxCollection.find(account, client, context, LocalJtxCollection.Factory, null, null))
|
||||
jtxCollection.url?.let { strUrl ->
|
||||
val url = strUrl.toHttpUrl()
|
||||
val info = remoteCollections[url]
|
||||
if (info == null) {
|
||||
Logger.log.fine("Deleting obsolete local collection $url")
|
||||
jtxCollection.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.log(Level.FINE, "Updating local collection $url", info)
|
||||
val owner = info.ownerId?.let { db.principalDao().get(it) }
|
||||
jtxCollection.updateCollection(info, owner)
|
||||
// we already have a local task list for this remote collection, don't take into consideration anymore
|
||||
remoteCollections -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local collections
|
||||
for ((_,info) in remoteCollections) {
|
||||
Logger.log.log(Level.INFO, "Adding local collections", info)
|
||||
val owner = info.ownerId?.let { db.principalDao().get(it) }
|
||||
LocalJtxCollection.create(account, client, info, owner)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
class OpenTasksSyncAdapterService: TasksSyncAdapterService()
|
|
@ -0,0 +1,134 @@
|
|||
/***************************************************************************************************
|
||||
* 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.Context
|
||||
import android.provider.CalendarContract
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.*
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
|
||||
/**
|
||||
* Handles scheduled sync requests.
|
||||
*
|
||||
* Enqueues immediate [SyncWorker] syncs at the appropriate moment. This will prevent the actual
|
||||
* sync code from running twice simultaneously (for manual and scheduled sync).
|
||||
*
|
||||
* For each account there will be multiple dedicated workers running for each authority.
|
||||
*/
|
||||
@HiltWorker
|
||||
class PeriodicSyncWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted workerParams: WorkerParameters
|
||||
) : Worker(appContext, workerParams) {
|
||||
|
||||
companion object {
|
||||
|
||||
private const val WORKER_TAG = "periodic-sync"
|
||||
|
||||
// Worker input parameters
|
||||
internal const val ARG_ACCOUNT_NAME = "accountName"
|
||||
internal const val ARG_ACCOUNT_TYPE = "accountType"
|
||||
internal const val ARG_AUTHORITY = "authority"
|
||||
|
||||
/**
|
||||
* Name of this worker.
|
||||
* Used to distinguish between other work processes. A worker names are unique. There can
|
||||
* never be two running workers with the same name.
|
||||
*/
|
||||
fun workerName(account: Account, authority: String): String =
|
||||
"$WORKER_TAG $authority ${account.type}/${account.name}"
|
||||
|
||||
/**
|
||||
* Activate scheduled synchronization of an account with a specific authority.
|
||||
*
|
||||
* @param account account to sync
|
||||
* @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]])
|
||||
* @param interval interval between recurring syncs in seconds
|
||||
* @return operation object to check when and whether activation was successful
|
||||
*/
|
||||
fun enable(context: Context, account: Account, authority: String, interval: Long): Operation {
|
||||
val arguments = Data.Builder()
|
||||
.putString(ARG_AUTHORITY, authority)
|
||||
.putString(ARG_ACCOUNT_NAME, account.name)
|
||||
.putString(ARG_ACCOUNT_TYPE, account.type)
|
||||
.build()
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiredNetworkType(
|
||||
if (AccountSettings(context, account).getSyncWifiOnly())
|
||||
NetworkType.UNMETERED
|
||||
else
|
||||
NetworkType.CONNECTED
|
||||
).build()
|
||||
val workRequest = PeriodicWorkRequestBuilder<PeriodicSyncWorker>(interval, TimeUnit.SECONDS)
|
||||
.addTag(WORKER_TAG)
|
||||
.setInputData(arguments)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
return WorkManager.getInstance(context).enqueueUniquePeriodicWork(
|
||||
workerName(account, authority),
|
||||
// if a periodic sync exists already, we want to update it with the new interval
|
||||
// and/or new required network type (applies on next iteration of periodic worker)
|
||||
ExistingPeriodicWorkPolicy.UPDATE,
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables scheduled synchronization of an account for a specific authority.
|
||||
*
|
||||
* @param account account to sync
|
||||
* @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]])
|
||||
* @return operation object to check process state of work cancellation
|
||||
*/
|
||||
fun disable(context: Context, account: Account, authority: String): Operation =
|
||||
WorkManager.getInstance(context)
|
||||
.cancelUniqueWork(workerName(account, authority))
|
||||
|
||||
/**
|
||||
* Finds out whether the [PeriodicSyncWorker] is currently enqueued or running
|
||||
*
|
||||
* @param account account to check
|
||||
* @param authority authority to check (for instance: [CalendarContract.AUTHORITY]])
|
||||
* @return boolean whether the [PeriodicSyncWorker] is running or enqueued
|
||||
*/
|
||||
fun isEnabled(context: Context, account: Account, authority: String): Boolean =
|
||||
WorkManager.getInstance(context)
|
||||
.getWorkInfos(
|
||||
WorkQuery.Builder
|
||||
.fromUniqueWorkNames(listOf(workerName(account, authority)))
|
||||
.addStates(listOf(WorkInfo.State.ENQUEUED, WorkInfo.State.RUNNING))
|
||||
.build()
|
||||
).get()
|
||||
.isNotEmpty()
|
||||
|
||||
}
|
||||
|
||||
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")
|
||||
|
||||
val accountSettings = AccountSettings(applicationContext, account)
|
||||
if (!SyncWorker.wifiConditionsMet(applicationContext, accountSettings)) {
|
||||
Logger.log.info("Sync conditions not met. Won't run sync.")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
// Just request immediate sync
|
||||
Logger.log.info("Requesting immediate sync")
|
||||
SyncWorker.enqueue(applicationContext, account, authority)
|
||||
return Result.success()
|
||||
}
|
||||
}
|
|
@ -1,215 +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.app.Service
|
||||
import android.content.*
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.getSystemService
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.account.WifiPermissionsActivity
|
||||
import at.bitfire.davdroid.util.ConcurrentUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
abstract class SyncAdapterService: Service() {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Specifies an list of IDs which are requested to be synchronized before
|
||||
* the other collections. For instance, if some calendars of a CalDAV
|
||||
* account are visible in the calendar app and others are hidden, the visible calendars can
|
||||
* be synchronized first, so that the "Refresh" action in the calendar app is more responsive.
|
||||
*
|
||||
* Extra type: String (comma-separated list of IDs)
|
||||
*
|
||||
* In case of calendar sync, the extra value is a list of Android calendar IDs.
|
||||
* In case of task sync, the extra value is an a list of OpenTask task list IDs.
|
||||
*/
|
||||
const val SYNC_EXTRAS_PRIORITY_COLLECTIONS = "priority_collections"
|
||||
|
||||
/**
|
||||
* Requests a re-synchronization of all entries. For instance, if this extra is
|
||||
* set for a calendar sync, all remote events will be listed and checked for remote
|
||||
* changes again.
|
||||
*
|
||||
* Useful if settings which modify the remote resource list (like the CalDAV setting
|
||||
* "sync events n days in the past") have been changed.
|
||||
*/
|
||||
const val SYNC_EXTRAS_RESYNC = "resync"
|
||||
|
||||
/**
|
||||
* Requests a full re-synchronization of all entries. For instance, if this extra is
|
||||
* set for an address book sync, all contacts will be downloaded again and updated in the
|
||||
* local storage.
|
||||
*
|
||||
* Useful if settings which modify parsing/local behavior have been changed.
|
||||
*/
|
||||
const val SYNC_EXTRAS_FULL_RESYNC = "full_resync"
|
||||
}
|
||||
|
||||
protected abstract fun syncAdapter(): AbstractThreadedSyncAdapter
|
||||
|
||||
override fun onBind(intent: Intent?) = syncAdapter().syncAdapterBinder!!
|
||||
|
||||
|
||||
/**
|
||||
* Base class for our sync adapters. Guarantees that
|
||||
*
|
||||
* 1. not more than one sync adapter per account and authority is running at a time,
|
||||
* 2. `Thread.currentThread().contextClassLoader` is set to the current context's class loader.
|
||||
*
|
||||
* Also provides some useful methods that can be used by derived sync adapters.
|
||||
*/
|
||||
abstract class SyncAdapter(
|
||||
context: Context
|
||||
): AbstractThreadedSyncAdapter(
|
||||
context,
|
||||
true // isSyncable shouldn't be -1 because DAVx5 sets it to 0 or 1.
|
||||
// However, if it is -1 by accident, set it to 1 to avoid endless sync loops.
|
||||
) {
|
||||
|
||||
companion object {
|
||||
|
||||
fun priorityCollections(extras: Bundle): Set<Long> {
|
||||
val ids = mutableSetOf<Long>()
|
||||
extras.getString(SYNC_EXTRAS_PRIORITY_COLLECTIONS)?.let { rawIds ->
|
||||
for (rawId in rawIds.split(','))
|
||||
try {
|
||||
ids += rawId.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
Logger.log.log(Level.WARNING, "Couldn't parse SYNC_EXTRAS_PRIORITY_COLLECTIONS", e)
|
||||
}
|
||||
}
|
||||
return ids
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface SyncAdapterEntryPoint {
|
||||
fun appDatabase(): AppDatabase
|
||||
}
|
||||
|
||||
private val syncAdapterEntryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java)
|
||||
internal val db = syncAdapterEntryPoint.appDatabase()
|
||||
|
||||
|
||||
|
||||
abstract fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult)
|
||||
|
||||
override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
Logger.log.log(Level.INFO, "$authority sync of $account has been initiated", extras.keySet().joinToString(", "))
|
||||
|
||||
// prevent multiple syncs of the same authority and account to run simultaneously
|
||||
val currentSyncKey = Pair(authority, account)
|
||||
if (ConcurrentUtils.runSingle(currentSyncKey) {
|
||||
// required for ServiceLoader -> ical4j -> ical4android
|
||||
Thread.currentThread().contextClassLoader = context.classLoader
|
||||
|
||||
val accountSettings by lazy { AccountSettings(context, account) }
|
||||
val httpClient = lazy { HttpClient.Builder(context, accountSettings).build() }
|
||||
|
||||
try {
|
||||
val runSync = /* always true in open-source edition */ true
|
||||
|
||||
if (runSync)
|
||||
sync(account, extras, authority, httpClient, provider, syncResult)
|
||||
|
||||
} catch (e: InvalidAccountException) {
|
||||
Logger.log.log(
|
||||
Level.WARNING,
|
||||
"Account was removed during synchronization",
|
||||
e
|
||||
)
|
||||
} finally {
|
||||
if (httpClient.isInitialized())
|
||||
httpClient.value.close()
|
||||
}
|
||||
})
|
||||
Logger.log.log(Level.INFO, "Sync for $currentSyncKey finished", syncResult)
|
||||
else {
|
||||
Logger.log.warning("There's already another running sync for $currentSyncKey, aborting")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
|
||||
Logger.log.log(Level.WARNING, "Security exception when opening content provider for $authority")
|
||||
}
|
||||
|
||||
override fun onSyncCanceled() {
|
||||
Logger.log.info("Sync thread cancelled! Interrupting sync")
|
||||
super.onSyncCanceled()
|
||||
}
|
||||
|
||||
override fun onSyncCanceled(thread: Thread) {
|
||||
Logger.log.info("Sync thread ${thread.id} cancelled! Interrupting sync")
|
||||
super.onSyncCanceled(thread)
|
||||
}
|
||||
|
||||
|
||||
protected fun checkSyncConditions(settings: AccountSettings): Boolean {
|
||||
if (settings.getSyncWifiOnly()) {
|
||||
// WiFi required
|
||||
val connectivityManager = context.getSystemService<ConnectivityManager>()!!
|
||||
|
||||
// check for connected WiFi network
|
||||
var wifiAvailable = false
|
||||
connectivityManager.allNetworks.forEach { network ->
|
||||
connectivityManager.getNetworkCapabilities(network)?.let { capabilities ->
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
|
||||
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
|
||||
wifiAvailable = true
|
||||
}
|
||||
}
|
||||
if (!wifiAvailable) {
|
||||
Logger.log.info("Not on connected WiFi, stopping")
|
||||
return false
|
||||
}
|
||||
// if execution reaches this point, we're on a connected WiFi
|
||||
|
||||
settings.getSyncWifiOnlySSIDs()?.let { onlySSIDs ->
|
||||
// check required permissions and location status
|
||||
if (!PermissionUtils.canAccessWifiSsid(context)) {
|
||||
// not all permissions granted; show notification
|
||||
val intent = Intent(context, WifiPermissionsActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra(WifiPermissionsActivity.EXTRA_ACCOUNT, settings.account)
|
||||
PermissionUtils.notifyPermissions(context, intent)
|
||||
|
||||
Logger.log.warning("Can't access WiFi SSID, aborting sync")
|
||||
return false
|
||||
}
|
||||
|
||||
val wifi = context.getSystemService<WifiManager>()!!
|
||||
val info = wifi.connectionInfo
|
||||
if (info == null || !onlySSIDs.contains(info.ssid.trim('"'))) {
|
||||
Logger.log.info("Connected to wrong WiFi network (${info.ssid}), aborting sync")
|
||||
return false
|
||||
} else
|
||||
Logger.log.fine("Connected to WiFi network ${info.ssid}")
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.app.Service
|
||||
import android.content.AbstractThreadedSyncAdapter
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import java.util.logging.Level
|
||||
|
||||
abstract class SyncAdapterService: Service() {
|
||||
|
||||
fun syncAdapter() = SyncAdapter(this)
|
||||
|
||||
override fun onBind(intent: Intent?) = syncAdapter().syncAdapterBinder!!
|
||||
|
||||
/**
|
||||
* Entry point for the sync adapter framework.
|
||||
*
|
||||
* Handles incoming sync requests from the sync adapter framework.
|
||||
*
|
||||
* Although we do not use the sync adapter for syncing anymore, we keep this sole
|
||||
* adapter to provide exported services, which allow android system components and calendar,
|
||||
* contacts or task apps to sync via DAVx5.
|
||||
*/
|
||||
class SyncAdapter(
|
||||
context: Context
|
||||
): AbstractThreadedSyncAdapter(
|
||||
context,
|
||||
true // isSyncable shouldn't be -1 because DAVx5 sets it to 0 or 1.
|
||||
// However, if it is -1 by accident, set it to 1 to avoid endless sync loops.
|
||||
) {
|
||||
|
||||
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 adapter (upload=$upload)")
|
||||
|
||||
// Should we run the sync at all?
|
||||
if (!SyncWorker.wifiConditionsMet(context, AccountSettings(context, account))) {
|
||||
Logger.log.info("Sync conditions not met. Aborting sync adapter")
|
||||
return
|
||||
}
|
||||
|
||||
Logger.log.fine("Sync adapter now handing over to SyncWorker")
|
||||
SyncWorker.enqueue(context, account, authority, upload = upload)
|
||||
}
|
||||
|
||||
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
|
||||
Logger.log.log(Level.WARNING, "Security exception for $account/$authority")
|
||||
}
|
||||
|
||||
override fun onSyncCanceled() {
|
||||
Logger.log.info("Ignoring sync adapter cancellation")
|
||||
super.onSyncCanceled()
|
||||
}
|
||||
|
||||
override fun onSyncCanceled(thread: Thread) {
|
||||
Logger.log.info("Ignoring sync adapter cancellation")
|
||||
super.onSyncCanceled(thread)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// exported sync adapter services; we need a separate class for each authority
|
||||
class AddressBooksSyncAdapterService: SyncAdapterService()
|
||||
class CalendarsSyncAdapterService: SyncAdapterService()
|
||||
class ContactsSyncAdapterService: SyncAdapterService()
|
||||
class JtxSyncAdapterService: SyncAdapterService()
|
||||
class OpenTasksSyncAdapterService: SyncAdapterService()
|
||||
class TasksOrgSyncAdapterService: SyncAdapterService()
|
|
@ -11,7 +11,6 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import android.content.SyncResult
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.RemoteException
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
|
@ -68,7 +67,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
|
|||
val account: Account,
|
||||
val accountSettings: AccountSettings,
|
||||
val httpClient: HttpClient,
|
||||
val extras: Bundle,
|
||||
val extras: Array<String>,
|
||||
val authority: String,
|
||||
val syncResult: SyncResult,
|
||||
val localCollection: CollectionType
|
||||
|
@ -159,7 +158,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
|
|||
Logger.log.info("Processing local deletes/updates")
|
||||
val modificationsPresent = processLocallyDeleted() || uploadDirty()
|
||||
|
||||
if (extras.containsKey(SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC)) {
|
||||
if (extras.contains(Syncer.SYNC_EXTRAS_FULL_RESYNC)) {
|
||||
Logger.log.info("Forcing re-synchronization of all entries")
|
||||
|
||||
// forget sync state of collection (→ initial sync in case of SyncAlgorithm.COLLECTION_SYNC)
|
||||
|
@ -256,7 +255,7 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
|
|||
|
||||
}, { e, local, remote ->
|
||||
when (e) {
|
||||
// sync was cancelled or account has been removed: re-throw to SyncAdapterService
|
||||
// sync was cancelled or account has been removed: re-throw to SyncAdapterService (now BaseSyncer)
|
||||
is InterruptedException,
|
||||
is InterruptedIOException,
|
||||
is InvalidAccountException ->
|
||||
|
@ -466,8 +465,8 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
|
|||
* [uploadDirty] were true), a sync is always required and this method
|
||||
* should *not* be evaluated.
|
||||
*
|
||||
* Will return _true_ if [SyncAdapterService.SYNC_EXTRAS_RESYNC] and/or
|
||||
* [SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC] is set in [extras].
|
||||
* Will return _true_ if [Syncer.SYNC_EXTRAS_RESYNC] and/or
|
||||
* [Syncer.SYNC_EXTRAS_FULL_RESYNC] is set in [extras].
|
||||
*
|
||||
* @param state remote sync state to compare local sync state with
|
||||
*
|
||||
|
@ -475,8 +474,8 @@ abstract class SyncManager<ResourceType: LocalResource<*>, out CollectionType: L
|
|||
* sync algorithm is required
|
||||
*/
|
||||
protected open fun syncRequired(state: SyncState?): Boolean {
|
||||
if (extras.containsKey(SyncAdapterService.SYNC_EXTRAS_RESYNC) ||
|
||||
extras.containsKey(SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC))
|
||||
if (extras.contains(Syncer.SYNC_EXTRAS_RESYNC) ||
|
||||
extras.contains(Syncer.SYNC_EXTRAS_FULL_RESYNC))
|
||||
return true
|
||||
|
||||
val localState = localCollection.lastSyncState
|
||||
|
|
|
@ -1,63 +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.ContentResolver
|
||||
import android.content.Context
|
||||
import android.provider.ContactsContract
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
|
||||
enum class SyncStatus {
|
||||
ACTIVE, PENDING, IDLE;
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns the sync status of a given account. Checks the account itself and possible
|
||||
* sub-accounts (address book accounts).
|
||||
*
|
||||
* @param authorities sync authorities to check (usually taken from [syncAuthorities])
|
||||
*
|
||||
* @return sync status of the given account
|
||||
*/
|
||||
fun fromAccount(context: Context, authorities: Iterable<String>, account: Account): SyncStatus {
|
||||
// check sync framework syncs are active or pending
|
||||
if (authorities.any { ContentResolver.isSyncActive(account, it) })
|
||||
return SyncStatus.ACTIVE
|
||||
val addrBookAccounts = LocalAddressBook.findAll(context, null, account).map { it.account }
|
||||
if (addrBookAccounts.any { ContentResolver.isSyncActive(it, ContactsContract.AUTHORITY) })
|
||||
return SyncStatus.ACTIVE
|
||||
if (authorities.any { ContentResolver.isSyncPending(account, it) } ||
|
||||
addrBookAccounts.any { ContentResolver.isSyncPending(it, ContactsContract.AUTHORITY) })
|
||||
return SyncStatus.PENDING
|
||||
|
||||
// Also check SyncWorkers
|
||||
val workerNames = authorities.map { authority ->
|
||||
SyncWorker.workerName(account, authority)
|
||||
}
|
||||
val workQuery = WorkQuery.Builder
|
||||
.fromUniqueWorkNames(workerNames)
|
||||
.addStates(listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED))
|
||||
.build()
|
||||
val workInfos = WorkManager.getInstance(context).getWorkInfos(workQuery).get()
|
||||
when {
|
||||
workInfos.any { workInfo ->
|
||||
workInfo.state == WorkInfo.State.RUNNING
|
||||
} -> return SyncStatus.ACTIVE
|
||||
|
||||
workInfos.any { workInfo ->
|
||||
workInfo.state == WorkInfo.State.ENQUEUED
|
||||
} -> return SyncStatus.PENDING
|
||||
}
|
||||
|
||||
// None active or pending? Then we're idle ..
|
||||
return SyncStatus.IDLE
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -88,11 +88,6 @@ object SyncUtils {
|
|||
nm.notifyIfPossible(NotificationUtils.NOTIFY_TASKS_PROVIDER_TOO_OLD, notify.build())
|
||||
}
|
||||
|
||||
fun removePeriodicSyncs(account: Account, authority: String) {
|
||||
for (sync in ContentResolver.getPeriodicSyncs(account, authority))
|
||||
ContentResolver.removePeriodicSync(sync.account, sync.authority, sync.extras)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all available sync authorities for main accounts (!= address book accounts):
|
||||
*
|
||||
|
@ -116,7 +111,6 @@ object SyncUtils {
|
|||
return result
|
||||
}
|
||||
|
||||
|
||||
// task sync utils
|
||||
|
||||
@WorkerThread
|
||||
|
|
|
@ -8,30 +8,52 @@ import android.accounts.Account
|
|||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.NetworkCapabilities
|
||||
import android.net.wifi.WifiManager
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import androidx.annotation.IntDef
|
||||
import androidx.concurrent.futures.CallbackToFutureAdapter
|
||||
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.*
|
||||
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.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import androidx.work.WorkRequest
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.util.LiveDataUtils
|
||||
import at.bitfire.davdroid.ui.NotificationUtils.notifyIfPossible
|
||||
import at.bitfire.davdroid.ui.account.WifiPermissionsActivity
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.closeCompat
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import java.util.concurrent.TimeUnit
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Handles sync requests
|
||||
* Handles immediate sync requests, status queries and cancellation for one or multiple authorities
|
||||
*/
|
||||
@HiltWorker
|
||||
class SyncWorker @AssistedInject constructor(
|
||||
|
@ -41,104 +63,224 @@ class SyncWorker @AssistedInject constructor(
|
|||
|
||||
companion object {
|
||||
|
||||
const val ARG_ACCOUNT_NAME = "accountName"
|
||||
const val ARG_ACCOUNT_TYPE = "accountType"
|
||||
const val ARG_AUTHORITY = "authority"
|
||||
// Worker input parameters
|
||||
internal const val ARG_ACCOUNT_NAME = "accountName"
|
||||
internal const val ARG_ACCOUNT_TYPE = "accountType"
|
||||
internal const val ARG_AUTHORITY = "authority"
|
||||
|
||||
const val ARG_RESYNC = "resync"
|
||||
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
|
||||
|
||||
fun workerName(account: Account, authority: String): String {
|
||||
return "explicit-sync $authority ${account.type}/${account.name}"
|
||||
}
|
||||
// This SyncWorker's tag
|
||||
const val TAG_SYNC = "sync"
|
||||
|
||||
/**
|
||||
* How often this work will be retried to run after soft (network) errors.
|
||||
*
|
||||
* Retry strategy is defined in work request ([enqueue]).
|
||||
*/
|
||||
internal const val MAX_RUN_ATTEMPTS = 5
|
||||
|
||||
/**
|
||||
* Name of this worker.
|
||||
* Used to distinguish between other work processes. There must only ever be one worker with the exact same name.
|
||||
*/
|
||||
fun workerName(account: Account, authority: String) =
|
||||
"$TAG_SYNC $authority ${account.type}/${account.name}"
|
||||
|
||||
/**
|
||||
* Requests immediate synchronization of an account with all applicable
|
||||
* authorities (contacts, calendars, …).
|
||||
*
|
||||
* @param account account to sync
|
||||
* @see enqueue
|
||||
*/
|
||||
fun requestSync(context: Context, account: Account, @ArgResync resync: Int = NO_RESYNC) {
|
||||
fun enqueueAllAuthorities(
|
||||
context: Context,
|
||||
account: Account,
|
||||
@ArgResync resync: Int = NO_RESYNC,
|
||||
upload: Boolean = false
|
||||
) {
|
||||
for (authority in SyncUtils.syncAuthorities(context))
|
||||
requestSync(context, account, authority, resync)
|
||||
enqueue(context, account, authority, resync, 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 re-synchronization
|
||||
* @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
|
||||
*/
|
||||
fun requestSync(context: Context, account: Account, authority: String, @ArgResync resync: Int = NO_RESYNC) {
|
||||
fun enqueue(
|
||||
context: Context,
|
||||
account: Account,
|
||||
authority: String,
|
||||
@ArgResync resync: Int = NO_RESYNC,
|
||||
upload: Boolean = false
|
||||
) {
|
||||
// Worker arguments
|
||||
val argumentsBuilder = Data.Builder()
|
||||
.putString(ARG_AUTHORITY, authority)
|
||||
.putString(ARG_ACCOUNT_NAME, account.name)
|
||||
.putString(ARG_ACCOUNT_TYPE, account.type)
|
||||
if (resync != 0)
|
||||
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(TAG_SYNC)
|
||||
.setInputData(argumentsBuilder.build())
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.setBackoffCriteria(
|
||||
BackoffPolicy.EXPONENTIAL,
|
||||
WorkRequest.MIN_BACKOFF_MILLIS,
|
||||
TimeUnit.MILLISECONDS
|
||||
)
|
||||
.setConstraints(constraints)
|
||||
.build()
|
||||
|
||||
// enqueue and start syncing
|
||||
Logger.log.log(Level.INFO, "Enqueueing unique worker: ${workerName(account, authority)}")
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||
workerName(account, authority),
|
||||
ExistingWorkPolicy.KEEP, // if sync is already running, just continue
|
||||
ExistingWorkPolicy.KEEP, // If sync is already running, just continue.
|
||||
// Existing retried work will not be replaced (for instance when
|
||||
// PeriodicSyncWorker enqueues another scheduled sync).
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
|
||||
fun stopSync(context: Context, account: Account, authority: String) {
|
||||
WorkManager.getInstance(context).cancelUniqueWork(workerName(account, authority))
|
||||
/**
|
||||
* 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 a worker exists, which belongs to given account and authorities,
|
||||
* and that is in the given worker state.
|
||||
* Will tell whether >0 [SyncWorker] exists, belonging to given account and authorities,
|
||||
* and which are/is in the given worker state.
|
||||
*
|
||||
* @param workState state of worker to match
|
||||
* @param workStates list of states of workers to match
|
||||
* @param account the account which the workers belong to
|
||||
* @param authorities type of sync work
|
||||
* @return boolean *true* if at least one worker with matching state was found; *false* otherwise
|
||||
* @param authorities type of sync work, ie [CalendarContract.AUTHORITY]
|
||||
* @return *true* if at least one worker with matching query was found; *false* otherwise
|
||||
*/
|
||||
fun existsForAccount(context: Context, workState: WorkInfo.State, account: Account, authorities: List<String>) =
|
||||
LiveDataUtils.liveDataLogicOr(
|
||||
authorities.map { authority -> isWorkerInState(context, workState, account, authority) }
|
||||
)
|
||||
|
||||
fun isWorkerInState(context: Context, workState: WorkInfo.State, account: Account, authority: String) =
|
||||
WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(workerName(account, authority)).map { workInfoList ->
|
||||
workInfoList.any { workInfo -> workInfo.state == workState }
|
||||
}
|
||||
fun exists(
|
||||
context: Context,
|
||||
workStates: List<WorkInfo.State>,
|
||||
account: Account? = null,
|
||||
authorities: List<String>? = null
|
||||
): LiveData<Boolean> {
|
||||
val workQuery = WorkQuery.Builder
|
||||
.fromTags(listOf(TAG_SYNC))
|
||||
.addStates(workStates)
|
||||
if (account != null && authorities != null)
|
||||
workQuery.addUniqueWorkNames(
|
||||
authorities.map { authority -> workerName(account, authority) }
|
||||
)
|
||||
return WorkManager.getInstance(context)
|
||||
.getWorkInfosLiveData(workQuery.build()).map { workInfoList ->
|
||||
workInfoList.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Finds out whether SyncWorkers with given statuses exist
|
||||
* Checks whether user imposed sync conditions from settings are met:
|
||||
* - Sync only on WiFi?
|
||||
* - Sync only on specific WiFi (SSID)?
|
||||
*
|
||||
* @param statuses statuses to check
|
||||
* @return whether SyncWorkers matching the statuses were found
|
||||
* @param accountSettings Account settings of the account to check (and is to be synced)
|
||||
* @return *true* if conditions are met; *false* if not
|
||||
*/
|
||||
fun existsWithStatuses(context: Context, statuses: List<WorkInfo.State>): LiveData<Boolean> {
|
||||
val workQuery = WorkQuery.Builder
|
||||
.fromStates(statuses)
|
||||
.build()
|
||||
return WorkManager.getInstance(context).getWorkInfosLiveData(workQuery).map {
|
||||
it.isNotEmpty()
|
||||
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?
|
||||
if (!wifiAvailable(context)) {
|
||||
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 working WiFi
|
||||
*/
|
||||
internal fun wifiAvailable(context: Context): Boolean {
|
||||
val connectivityManager = context.getSystemService<ConnectivityManager>()!!
|
||||
connectivityManager.allNetworks.forEach { network ->
|
||||
connectivityManager.getNetworkCapabilities(network)?.let { capabilities ->
|
||||
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
|
||||
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED))
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether we are connected to the correct wifi (SSID) defined by user in the
|
||||
* account settings.
|
||||
*
|
||||
* Note: Should be connected to some wifi before calling.
|
||||
*
|
||||
* @param accountSettings Settings of account to check
|
||||
* @return *true* if connected to the correct wifi OR no wifi names were specified in
|
||||
* account settings; *false* otherwise
|
||||
*/
|
||||
internal fun correctWifiSsid(context: Context, accountSettings: AccountSettings): Boolean {
|
||||
accountSettings.getSyncWifiOnlySSIDs()?.let { onlySSIDs ->
|
||||
// check required permissions and location status
|
||||
if (!PermissionUtils.canAccessWifiSsid(context)) {
|
||||
// not all permissions granted; show notification
|
||||
val intent = Intent(context, WifiPermissionsActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
intent.putExtra(WifiPermissionsActivity.EXTRA_ACCOUNT, accountSettings.account)
|
||||
PermissionUtils.notifyPermissions(context, intent)
|
||||
|
||||
Logger.log.warning("Can't access WiFi SSID, aborting sync")
|
||||
return false
|
||||
}
|
||||
|
||||
val wifi = context.getSystemService<WifiManager>()!!
|
||||
val info = wifi.connectionInfo
|
||||
if (info == null || !onlySSIDs.contains(info.ssid.trim('"'))) {
|
||||
Logger.log.info("Connected to wrong WiFi network (${info.ssid}), aborting sync")
|
||||
return false
|
||||
}
|
||||
Logger.log.fine("Connected to WiFi network ${info.ssid}")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
|
||||
/** thread which runs the actual sync code (can be interrupted to stop synchronization) */
|
||||
var syncThread: Thread? = null
|
||||
|
||||
override 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")
|
||||
|
@ -146,36 +288,34 @@ class SyncWorker @AssistedInject constructor(
|
|||
val authority = inputData.getString(ARG_AUTHORITY) ?: throw IllegalArgumentException("$ARG_AUTHORITY required")
|
||||
Logger.log.info("Running sync worker: account=$account, authority=$authority")
|
||||
|
||||
val syncAdapter = when (authority) {
|
||||
// What are we going to sync? Select syncer based on authority
|
||||
val syncer: Syncer = when (authority) {
|
||||
applicationContext.getString(R.string.address_books_authority) ->
|
||||
AddressBooksSyncAdapterService.AddressBooksSyncAdapter(applicationContext)
|
||||
AddressBookSyncer(applicationContext)
|
||||
CalendarContract.AUTHORITY ->
|
||||
CalendarsSyncAdapterService.CalendarsSyncAdapter(applicationContext)
|
||||
CalendarSyncer(applicationContext)
|
||||
ContactsContract.AUTHORITY ->
|
||||
ContactsSyncAdapterService.ContactsSyncAdapter(applicationContext)
|
||||
ContactSyncer(applicationContext)
|
||||
TaskProvider.ProviderName.JtxBoard.authority ->
|
||||
JtxSyncAdapterService.JtxSyncAdapter(applicationContext)
|
||||
JtxSyncer(applicationContext)
|
||||
TaskProvider.ProviderName.OpenTasks.authority,
|
||||
TaskProvider.ProviderName.TasksOrg.authority ->
|
||||
TasksSyncAdapterService.TasksSyncAdapter(applicationContext)
|
||||
TaskSyncer(applicationContext)
|
||||
else ->
|
||||
throw IllegalArgumentException("Invalid authority $authority")
|
||||
}
|
||||
|
||||
// Pass flags to the sync adapter. Note that these may be sync framework flags, but they
|
||||
// don't have anything to do with the sync framework anymore. They only exist because we
|
||||
// still use the same sync code called from two locations (from WorkManager and from the
|
||||
// sync framework).
|
||||
val extras = Bundle()
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
|
||||
|
||||
// pass flags which are used by the sync code
|
||||
when (extras.getInt(ARG_RESYNC)) {
|
||||
FULL_RESYNC -> extras.putBoolean(SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC, true)
|
||||
RESYNC -> extras.putBoolean(SyncAdapterService.SYNC_EXTRAS_RESYNC, true)
|
||||
// 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)
|
||||
}
|
||||
if (inputData.getBoolean(ARG_UPLOAD, false))
|
||||
// Comes in through SyncAdapterService and is used only by ContactsSyncManager for an Android 7 workaround.
|
||||
extras.add(ContentResolver.SYNC_EXTRAS_UPLOAD)
|
||||
|
||||
// acquire ContentProviderClient of authority to be synced
|
||||
val provider: ContentProviderClient? =
|
||||
try {
|
||||
applicationContext.contentResolver.acquireContentProviderClient(authority)
|
||||
|
@ -188,21 +328,58 @@ class SyncWorker @AssistedInject constructor(
|
|||
return Result.failure()
|
||||
}
|
||||
|
||||
// Start syncing. We still use the sync adapter framework's SyncResult to pass the sync results, but this
|
||||
// is only for legacy reasons and can be replaced by an own result class in the future.
|
||||
val result = SyncResult()
|
||||
try {
|
||||
syncThread = Thread.currentThread()
|
||||
syncAdapter.onPerformSync(account, extras, authority, provider, result)
|
||||
syncer.onPerformSync(account, extras.toTypedArray(), authority, provider, result)
|
||||
} catch (e: SecurityException) {
|
||||
syncAdapter.onSecurityException(account, extras, authority, result)
|
||||
Logger.log.log(Level.WARNING, "Security exception when opening content provider for $authority")
|
||||
} finally {
|
||||
provider.closeCompat()
|
||||
}
|
||||
|
||||
if (result.hasError())
|
||||
return Result.failure(Data.Builder()
|
||||
// Check for errors
|
||||
if (result.hasError()) {
|
||||
val syncResult = Data.Builder()
|
||||
.putString("syncresult", result.toString())
|
||||
.putString("syncResultStats", result.stats.toString())
|
||||
.build())
|
||||
.build()
|
||||
|
||||
// On soft errors the sync is retried a few times before considered failed
|
||||
if (result.hasSoftError()) {
|
||||
Logger.log.warning("Soft error while syncing: result=$result, stats=${result.stats}")
|
||||
if (runAttemptCount < MAX_RUN_ATTEMPTS) {
|
||||
Logger.log.warning("Retrying on soft error (attempt $runAttemptCount of $MAX_RUN_ATTEMPTS)")
|
||||
return Result.retry()
|
||||
}
|
||||
|
||||
Logger.log.warning("Max retries on soft errors reached ($runAttemptCount of $MAX_RUN_ATTEMPTS). Treating as failed")
|
||||
|
||||
notificationManager.notifyIfPossible(
|
||||
NotificationUtils.NOTIFY_SYNC_ERROR,
|
||||
NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_SYNC_IO_ERRORS)
|
||||
.setSmallIcon(R.drawable.ic_sync_problem_notify)
|
||||
.setContentTitle(account.name)
|
||||
.setContentText(applicationContext.getString(R.string.sync_error_retry_limit_reached))
|
||||
.setSubText(account.name)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setCategory(NotificationCompat.CATEGORY_ERROR)
|
||||
.build()
|
||||
)
|
||||
|
||||
return Result.failure(syncResult)
|
||||
}
|
||||
|
||||
// On a hard error - fail with an error message
|
||||
// Note: SyncManager should have notified the user
|
||||
if (result.hasHardError()) {
|
||||
Logger.log.warning("Hard error while syncing: result=$result, stats=${result.stats}")
|
||||
return Result.failure(syncResult)
|
||||
}
|
||||
}
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
|
96
app/src/main/java/at/bitfire/davdroid/syncadapter/Syncer.kt
Normal file
96
app/src/main/java/at/bitfire/davdroid/syncadapter/Syncer.kt
Normal file
|
@ -0,0 +1,96 @@
|
|||
/***************************************************************************************************
|
||||
* 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 at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import dagger.hilt.EntryPoint
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.EntryPointAccessors
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.util.*
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Base class for sync code.
|
||||
*
|
||||
* Contains generic sync code, equal for all sync authorities, checks sync conditions and does
|
||||
* validation.
|
||||
*
|
||||
* Also provides useful methods that can be used by derived syncers ie [CalendarSyncer], etc.
|
||||
*/
|
||||
abstract class Syncer(val context: Context) {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Requests a re-synchronization of all entries. For instance, if this extra is
|
||||
* set for a calendar sync, all remote events will be listed and checked for remote
|
||||
* changes again.
|
||||
*
|
||||
* Useful if settings which modify the remote resource list (like the CalDAV setting
|
||||
* "sync events n days in the past") have been changed.
|
||||
*/
|
||||
const val SYNC_EXTRAS_RESYNC = "resync"
|
||||
|
||||
/**
|
||||
* Requests a full re-synchronization of all entries. For instance, if this extra is
|
||||
* set for an address book sync, all contacts will be downloaded again and updated in the
|
||||
* local storage.
|
||||
*
|
||||
* Useful if settings which modify parsing/local behavior have been changed.
|
||||
*/
|
||||
const val SYNC_EXTRAS_FULL_RESYNC = "full_resync"
|
||||
|
||||
}
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface SyncAdapterEntryPoint {
|
||||
fun appDatabase(): AppDatabase
|
||||
}
|
||||
|
||||
private val syncAdapterEntryPoint = EntryPointAccessors.fromApplication(context, SyncAdapterEntryPoint::class.java)
|
||||
internal val db = syncAdapterEntryPoint.appDatabase()
|
||||
|
||||
|
||||
abstract fun sync(account: Account, extras: Array<String>, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult)
|
||||
|
||||
fun onPerformSync(
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult
|
||||
) {
|
||||
Logger.log.log(Level.INFO, "$authority sync of $account initiated", extras.joinToString(", "))
|
||||
|
||||
val accountSettings by lazy { AccountSettings(context, account) }
|
||||
val httpClient = lazy { HttpClient.Builder(context, accountSettings).build() }
|
||||
|
||||
try {
|
||||
val runSync = true /* ose */
|
||||
if (runSync)
|
||||
sync(account, extras, authority, httpClient, provider, syncResult)
|
||||
} catch (e: InvalidAccountException) {
|
||||
Logger.log.log(Level.WARNING, "Account was removed during synchronization", e)
|
||||
} finally {
|
||||
if (httpClient.isInitialized())
|
||||
httpClient.value.close()
|
||||
Logger.log.log(
|
||||
Level.INFO,
|
||||
"$authority sync of $account finished",
|
||||
extras.joinToString(", "))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
108
app/src/main/java/at/bitfire/davdroid/syncadapter/TaskSyncer.kt
Normal file
108
app/src/main/java/at/bitfire/davdroid/syncadapter/TaskSyncer.kt
Normal file
|
@ -0,0 +1,108 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
import android.accounts.Account
|
||||
import android.accounts.AccountManager
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Build
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.dmfs.tasks.contract.TaskContract
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Sync logic for tasks in CalDAV collections ({@code VTODO}).
|
||||
*/
|
||||
class TaskSyncer(context: Context): Syncer(context) {
|
||||
|
||||
override fun sync(
|
||||
account: Account,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
httpClient: Lazy<HttpClient>,
|
||||
provider: ContentProviderClient,
|
||||
syncResult: SyncResult
|
||||
) {
|
||||
try {
|
||||
val providerName = TaskProvider.ProviderName.fromAuthority(authority)
|
||||
val taskProvider = TaskProvider.fromProviderClient(context, providerName, provider)
|
||||
|
||||
// make sure account can be seen by task provider
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
/* Warning: If setAccountVisibility is called, Android 12 broadcasts the
|
||||
AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION Intent. This cancels running syncs
|
||||
and starts them again! So make sure setAccountVisibility is only called when necessary. */
|
||||
val am = AccountManager.get(context)
|
||||
if (am.getAccountVisibility(account, providerName.packageName) != AccountManager.VISIBILITY_VISIBLE)
|
||||
am.setAccountVisibility(account, providerName.packageName, AccountManager.VISIBILITY_VISIBLE)
|
||||
}
|
||||
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
|
||||
updateLocalTaskLists(taskProvider, account, accountSettings)
|
||||
|
||||
val taskLists = AndroidTaskList
|
||||
.find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)
|
||||
for (taskList in taskLists) {
|
||||
Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
|
||||
TasksSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, taskList).performSync()
|
||||
}
|
||||
} catch (e: TaskProvider.ProviderTooOldException) {
|
||||
SyncUtils.notifyProviderTooOld(context, e)
|
||||
syncResult.databaseError = true
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
|
||||
syncResult.databaseError = true
|
||||
}
|
||||
|
||||
Logger.log.info("Task sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
|
||||
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||
|
||||
val remoteTaskLists = mutableMapOf<HttpUrl, Collection>()
|
||||
if (service != null)
|
||||
for (collection in db.collectionDao().getSyncTaskLists(service.id)) {
|
||||
remoteTaskLists[collection.url] = collection
|
||||
}
|
||||
|
||||
// delete/update local task lists
|
||||
val updateColors = settings.getManageCalendarColors()
|
||||
|
||||
for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
|
||||
list.syncId?.let {
|
||||
val url = it.toHttpUrl()
|
||||
val info = remoteTaskLists[url]
|
||||
if (info == null) {
|
||||
Logger.log.fine("Deleting obsolete local task list $url")
|
||||
list.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.log(Level.FINE, "Updating local task list $url", info)
|
||||
list.update(info, updateColors)
|
||||
// we already have a local task list for this remote collection, don't take into consideration anymore
|
||||
remoteTaskLists -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local task lists
|
||||
for ((_,info) in remoteTaskLists) {
|
||||
Logger.log.log(Level.INFO, "Adding local task list", info)
|
||||
LocalTaskList.create(account, provider, info)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.syncadapter
|
||||
|
||||
class TasksOrgSyncAdapterService: TasksSyncAdapterService()
|
|
@ -1,119 +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.accounts.AccountManager
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.ical4android.AndroidTaskList
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.dmfs.tasks.contract.TaskContract
|
||||
import java.util.logging.Level
|
||||
|
||||
/**
|
||||
* Synchronization manager for CalDAV collections; handles tasks ({@code VTODO}).
|
||||
*/
|
||||
open class TasksSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = TasksSyncAdapter(this)
|
||||
|
||||
class TasksSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
val providerName = TaskProvider.ProviderName.fromAuthority(authority)
|
||||
val taskProvider = TaskProvider.fromProviderClient(context, providerName, provider)
|
||||
|
||||
// make sure account can be seen by task provider
|
||||
if (Build.VERSION.SDK_INT >= 26) {
|
||||
/* Warning: If setAccountVisibility is called, Android 12 broadcasts the
|
||||
AccountManager.LOGIN_ACCOUNTS_CHANGED_ACTION Intent. This cancels running syncs
|
||||
and starts them again! So make sure setAccountVisibility is only called when necessary. */
|
||||
val am = AccountManager.get(context)
|
||||
if (am.getAccountVisibility(account, providerName.packageName) != AccountManager.VISIBILITY_VISIBLE)
|
||||
am.setAccountVisibility(account, providerName.packageName, AccountManager.VISIBILITY_VISIBLE)
|
||||
}
|
||||
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
/* don't run sync if
|
||||
- sync conditions (e.g. "sync only in WiFi") are not met AND
|
||||
- this is is an automatic sync (i.e. manual syncs are run regardless of sync conditions)
|
||||
*/
|
||||
if (!extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL) && !checkSyncConditions(accountSettings))
|
||||
return
|
||||
|
||||
updateLocalTaskLists(taskProvider, account, accountSettings)
|
||||
|
||||
val priorityTaskLists = priorityCollections(extras)
|
||||
val taskLists = AndroidTaskList
|
||||
.find(account, taskProvider, LocalTaskList.Factory, "${TaskContract.TaskLists.SYNC_ENABLED}!=0", null)
|
||||
.sortedByDescending { priorityTaskLists.contains(it.id) }
|
||||
for (taskList in taskLists) {
|
||||
Logger.log.info("Synchronizing task list #${taskList.id} [${taskList.syncId}]")
|
||||
TasksSyncManager(context, account, accountSettings, httpClient.value, extras, authority, syncResult, taskList).let {
|
||||
it.performSync()
|
||||
}
|
||||
}
|
||||
} catch (e: TaskProvider.ProviderTooOldException) {
|
||||
SyncUtils.notifyProviderTooOld(context, e)
|
||||
syncResult.databaseError = true
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync task lists", e)
|
||||
syncResult.databaseError = true
|
||||
}
|
||||
|
||||
Logger.log.info("Task sync complete")
|
||||
}
|
||||
|
||||
private fun updateLocalTaskLists(provider: TaskProvider, account: Account, settings: AccountSettings) {
|
||||
val service = db.serviceDao().getByAccountAndType(account.name, Service.TYPE_CALDAV)
|
||||
|
||||
val remoteTaskLists = mutableMapOf<HttpUrl, Collection>()
|
||||
if (service != null)
|
||||
for (collection in db.collectionDao().getSyncTaskLists(service.id)) {
|
||||
remoteTaskLists[collection.url] = collection
|
||||
}
|
||||
|
||||
// delete/update local task lists
|
||||
val updateColors = settings.getManageCalendarColors()
|
||||
|
||||
for (list in AndroidTaskList.find(account, provider, LocalTaskList.Factory, null, null))
|
||||
list.syncId?.let {
|
||||
val url = it.toHttpUrl()
|
||||
val info = remoteTaskLists[url]
|
||||
if (info == null) {
|
||||
Logger.log.fine("Deleting obsolete local task list $url")
|
||||
list.delete()
|
||||
} else {
|
||||
// remote CollectionInfo found for this local collection, update data
|
||||
Logger.log.log(Level.FINE, "Updating local task list $url", info)
|
||||
list.update(info, updateColors)
|
||||
// we already have a local task list for this remote collection, don't take into consideration anymore
|
||||
remoteTaskLists -= url
|
||||
}
|
||||
}
|
||||
|
||||
// create new local task lists
|
||||
for ((_,info) in remoteTaskLists) {
|
||||
Logger.log.log(Level.INFO, "Adding local task list", info)
|
||||
LocalTaskList.create(account, provider, info)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,6 @@ package at.bitfire.davdroid.syncadapter
|
|||
import android.accounts.Account
|
||||
import android.content.Context
|
||||
import android.content.SyncResult
|
||||
import android.os.Bundle
|
||||
import at.bitfire.dav4jvm.DavCalendar
|
||||
import at.bitfire.dav4jvm.MultiResponseCallback
|
||||
import at.bitfire.dav4jvm.Response
|
||||
|
@ -42,7 +41,7 @@ class TasksSyncManager(
|
|||
account: Account,
|
||||
accountSettings: AccountSettings,
|
||||
httpClient: HttpClient,
|
||||
extras: Bundle,
|
||||
extras: Array<String>,
|
||||
authority: String,
|
||||
syncResult: SyncResult,
|
||||
localCollection: LocalTaskList
|
||||
|
|
|
@ -11,14 +11,18 @@ import android.accounts.OnAccountsUpdateListener
|
|||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SyncStatusObserver
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.view.*
|
||||
import android.view.LayoutInflater
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
|
@ -31,11 +35,13 @@ import androidx.recyclerview.widget.LinearLayoutManager
|
|||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.AccountListBinding
|
||||
import at.bitfire.davdroid.databinding.AccountListItemBinding
|
||||
import at.bitfire.davdroid.syncadapter.SyncStatus
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils
|
||||
import at.bitfire.davdroid.syncadapter.SyncUtils.syncAuthorities
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.ui.account.AccountActivity
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -53,7 +59,6 @@ class AccountListFragment: Fragment() {
|
|||
|
||||
private var syncStatusSnackbar: Snackbar? = null
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
setHasOptionsMenu(true)
|
||||
|
||||
|
@ -215,7 +220,7 @@ class AccountListFragment: Fragment() {
|
|||
class Model @Inject constructor(
|
||||
application: Application,
|
||||
private val warnings: AppWarningsManager
|
||||
): AndroidViewModel(application), OnAccountsUpdateListener, SyncStatusObserver {
|
||||
): AndroidViewModel(application), OnAccountsUpdateListener {
|
||||
|
||||
data class AccountInfo(
|
||||
val account: Account,
|
||||
|
@ -230,14 +235,11 @@ class AccountListFragment: Fragment() {
|
|||
|
||||
// Accounts
|
||||
private val accountsUpdated = MutableLiveData<Boolean>()
|
||||
private val syncFrameworkStatusChanged = MutableLiveData<Boolean>()
|
||||
private val syncWorkersActive = SyncWorker.existsWithStatuses(
|
||||
application.applicationContext, listOf(WorkInfo.State.RUNNING))
|
||||
private val syncWorkersActive = SyncWorker.exists(application, listOf(WorkInfo.State.RUNNING))
|
||||
|
||||
val accounts = object : MediatorLiveData<List<AccountInfo>>() {
|
||||
init {
|
||||
addSource(accountsUpdated) { recalculate() }
|
||||
addSource(syncFrameworkStatusChanged) { recalculate() }
|
||||
addSource(syncWorkersActive) { recalculate() }
|
||||
}
|
||||
|
||||
|
@ -253,7 +255,7 @@ class AccountListFragment: Fragment() {
|
|||
val accountsWithInfo = sortedAccounts.map { account ->
|
||||
AccountInfo(
|
||||
account,
|
||||
SyncStatus.fromAccount(context, syncAuthorities, account)
|
||||
SyncStatus.fromAccount(context, account, syncAuthorities)
|
||||
)
|
||||
}
|
||||
value = accountsWithInfo
|
||||
|
@ -266,12 +268,6 @@ class AccountListFragment: Fragment() {
|
|||
init {
|
||||
// watch accounts
|
||||
accountManager.addOnAccountsUpdatedListener(this, null, true)
|
||||
|
||||
// watch account status
|
||||
ContentResolver.addStatusChangeListener(
|
||||
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE or ContentResolver.SYNC_OBSERVER_TYPE_PENDING,
|
||||
this
|
||||
)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
|
@ -279,11 +275,6 @@ class AccountListFragment: Fragment() {
|
|||
accountsUpdated.postValue(true)
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
override fun onStatusChanged(which: Int) {
|
||||
syncFrameworkStatusChanged.postValue(true)
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
accountManager.removeOnAccountsUpdatedListener(this)
|
||||
warnings.close()
|
||||
|
@ -291,4 +282,45 @@ class AccountListFragment: Fragment() {
|
|||
|
||||
}
|
||||
|
||||
enum class SyncStatus {
|
||||
ACTIVE, PENDING, IDLE;
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Returns the sync status of a given account. Checks the account itself and possible
|
||||
* sub-accounts (address book accounts).
|
||||
*
|
||||
* @param account account to check
|
||||
* @param authorities sync authorities to check (usually taken from [syncAuthorities])
|
||||
*
|
||||
* @return sync status of the given account
|
||||
*/
|
||||
fun fromAccount(context: Context, account: Account, authorities: List<String>): SyncStatus {
|
||||
val workerNames = authorities.map { authority ->
|
||||
SyncWorker.workerName(account, authority)
|
||||
}
|
||||
val workQuery = WorkQuery.Builder
|
||||
.fromTags(listOf(SyncWorker.TAG_SYNC))
|
||||
.addUniqueWorkNames(workerNames)
|
||||
.addStates(listOf(WorkInfo.State.RUNNING, WorkInfo.State.ENQUEUED))
|
||||
.build()
|
||||
|
||||
val workInfos = WorkManager.getInstance(context).getWorkInfos(workQuery).get()
|
||||
|
||||
return when {
|
||||
workInfos.any { workInfo ->
|
||||
workInfo.state == WorkInfo.State.RUNNING
|
||||
} -> ACTIVE
|
||||
|
||||
workInfos.any {workInfo ->
|
||||
workInfo.state == WorkInfo.State.ENQUEUED
|
||||
} -> PENDING
|
||||
|
||||
else -> IDLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -11,6 +11,7 @@ import android.content.pm.ShortcutManager
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.MenuItem
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
|
@ -109,9 +110,16 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
|
|||
if (Build.VERSION.SDK_INT >= 25)
|
||||
getSystemService<ShortcutManager>()?.reportShortcutUsed(UiUtils.SHORTCUT_SYNC_ALL)
|
||||
|
||||
// Check we are connected
|
||||
if (!SyncWorker.wifiAvailable(applicationContext)) {
|
||||
Toast.makeText(this, R.string.no_internet_connection, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
|
||||
// Enqueue sync worker for all accounts and authorities
|
||||
val accounts = allAccounts()
|
||||
for (account in accounts)
|
||||
SyncWorker.requestSync(this, account)
|
||||
SyncWorker.enqueueAllAuthorities(this, account)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ class AppWarningsManager @Inject constructor(
|
|||
init {
|
||||
Logger.log.fine("Watching for warning conditions")
|
||||
|
||||
// Automatic sync
|
||||
// Automatic Sync
|
||||
syncStatusObserver = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
|
||||
onStatusChanged(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS)
|
||||
|
||||
|
|
|
@ -31,6 +31,8 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkQuery
|
||||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
|
@ -43,13 +45,12 @@ 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.ical4android.TaskProvider
|
||||
import at.bitfire.ical4android.TaskProvider.ProviderName
|
||||
import at.bitfire.ical4android.util.MiscUtils
|
||||
import at.bitfire.ical4android.util.MiscUtils.ContentProviderClientHelper.closeCompat
|
||||
import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter as asCalendarSyncAdapter
|
||||
import at.bitfire.vcard4android.Utils.asSyncAdapter as asContactsSyncAdapter
|
||||
import at.techbee.jtx.JtxContract
|
||||
import at.techbee.jtx.JtxContract.asSyncAdapter as asJtxSyncAdapter
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
|
@ -62,6 +63,7 @@ import org.apache.commons.io.FileUtils
|
|||
import org.apache.commons.io.IOUtils
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.apache.commons.lang3.exception.ExceptionUtils
|
||||
import org.apache.commons.text.WordUtils
|
||||
import org.dmfs.tasks.contract.TaskContract
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
|
@ -69,6 +71,9 @@ import java.util.logging.Level
|
|||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipOutputStream
|
||||
import javax.inject.Inject
|
||||
import at.bitfire.ical4android.util.MiscUtils.UriHelper.asSyncAdapter as asCalendarSyncAdapter
|
||||
import at.bitfire.vcard4android.Utils.asSyncAdapter as asContactsSyncAdapter
|
||||
import at.techbee.jtx.JtxContract.asSyncAdapter as asJtxSyncAdapter
|
||||
|
||||
@AndroidEntryPoint
|
||||
class DebugInfoActivity : AppCompatActivity() {
|
||||
|
@ -458,8 +463,8 @@ class DebugInfoActivity : AppCompatActivity() {
|
|||
}
|
||||
writer.append('\n')
|
||||
|
||||
writer.append("\nACCOUNTS\n\n")
|
||||
// main accounts
|
||||
writer.append("\nACCOUNTS\n\n")
|
||||
val accountManager = AccountManager.get(context)
|
||||
val mainAccounts = accountManager.getAccountsByType(context.getString(R.string.account_type))
|
||||
val addressBookAccounts = accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)).toMutableList()
|
||||
|
@ -553,6 +558,7 @@ class DebugInfoActivity : AppCompatActivity() {
|
|||
private fun dumpMainAccount(account: Account, writer: Writer) {
|
||||
writer.append(" - Account: ${account.name}\n")
|
||||
writer.append(dumpAccount(account, AccountDumpInfo.mainAccount(context, account)))
|
||||
writer.append(dumpSyncWorkersInfo(account))
|
||||
try {
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
writer.append(" WiFi only: ${accountSettings.getSyncWifiOnly()}")
|
||||
|
@ -581,7 +587,7 @@ class DebugInfoActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun dumpAccount(account: Account, infos: Iterable<AccountDumpInfo>): String {
|
||||
val table = TextTable("Authority", "Syncable", "Auto-sync", "Interval", "Entries")
|
||||
val table = TextTable("Authority", "getIsSyncable", "getSyncAutomatically", "PeriodicSyncWorker", "Interval", "Entries")
|
||||
for (info in infos) {
|
||||
var nrEntries = "—"
|
||||
var client: ContentProviderClient? = null
|
||||
|
@ -599,19 +605,54 @@ class DebugInfoActivity : AppCompatActivity() {
|
|||
} finally {
|
||||
client?.closeCompat()
|
||||
}
|
||||
val accountSettings = AccountSettings(context, account)
|
||||
table.addLine(
|
||||
info.authority,
|
||||
ContentResolver.getIsSyncable(account, info.authority),
|
||||
ContentResolver.getSyncAutomatically(account, info.authority),
|
||||
ContentResolver.getPeriodicSyncs(account, info.authority).firstOrNull()?.let { periodicSync ->
|
||||
"${periodicSync.period / 60} min"
|
||||
},
|
||||
ContentResolver.getSyncAutomatically(account, info.authority), // content-triggered sync
|
||||
PeriodicSyncWorker.isEnabled(context, account, info.authority), // should always be false for address book accounts
|
||||
accountSettings.getSyncInterval(info.authority)?.let {"${it/60} min"},
|
||||
nrEntries
|
||||
)
|
||||
}
|
||||
return table.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets sync workers info
|
||||
* Note: WorkManager does not return worker names when queried, so we create them and ask
|
||||
* whether they exist one by one
|
||||
*/
|
||||
private fun dumpSyncWorkersInfo(account: Account): String {
|
||||
val table = TextTable("Tags", "Authority", "State", "Retries", "Generation", "ID")
|
||||
listOf(
|
||||
context.getString(R.string.address_books_authority),
|
||||
CalendarContract.AUTHORITY,
|
||||
TaskProvider.ProviderName.JtxBoard.authority,
|
||||
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 { StringUtils.removeStartIgnoreCase(it, SyncWorker::class.java.getPackage()!!.name + ".") },
|
||||
authority,
|
||||
workInfo.state,
|
||||
workInfo.runAttemptCount,
|
||||
workInfo.generation,
|
||||
workInfo.id
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
return table.toString()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -21,15 +21,14 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
import androidx.lifecycle.*
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.databinding.ActivityAccountBinding
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
|
@ -88,8 +87,8 @@ class AccountActivity: AppCompatActivity() {
|
|||
})
|
||||
|
||||
binding.sync.setOnClickListener {
|
||||
SyncWorker.requestSync(this, model.account)
|
||||
Snackbar.make(binding.viewPager, R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show()
|
||||
// enqueue sync worker for all authorities of this account
|
||||
SyncWorker.enqueueAllAuthorities(this, model.account)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -229,12 +228,12 @@ class AccountActivity: AppCompatActivity() {
|
|||
override fun getItemPosition(obj: Any) = POSITION_NONE
|
||||
|
||||
override fun getPageTitle(position: Int): String =
|
||||
when (position) {
|
||||
idxCardDav -> activity.getString(R.string.account_carddav)
|
||||
idxCalDav -> activity.getString(R.string.account_caldav)
|
||||
idxWebcal -> activity.getString(R.string.account_webcal)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
when (position) {
|
||||
idxCardDav -> activity.getString(R.string.account_carddav)
|
||||
idxCalDav -> activity.getString(R.string.account_caldav)
|
||||
idxWebcal -> activity.getString(R.string.account_webcal)
|
||||
else -> throw IllegalArgumentException()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,6 @@ import android.provider.CalendarContract
|
|||
import android.provider.ContactsContract
|
||||
import android.view.*
|
||||
import android.widget.PopupMenu
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.activityViewModels
|
||||
|
@ -28,18 +27,15 @@ import at.bitfire.davdroid.databinding.AccountCollectionsBinding
|
|||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.resource.TaskUtils
|
||||
import at.bitfire.davdroid.servicedetection.RefreshCollectionsWorker
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.ui.PermissionsActivity
|
||||
import at.bitfire.davdroid.util.LiveDataUtils
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedFactory
|
||||
import dagger.assisted.AssistedInject
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -271,7 +267,7 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
|
|||
@Assisted val accountModel: AccountActivity.Model,
|
||||
@Assisted val serviceId: Long,
|
||||
@Assisted val collectionType: String
|
||||
): ViewModel(), SyncStatusObserver {
|
||||
): ViewModel() {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
@ -305,82 +301,21 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
|
|||
// observe RefreshCollectionsWorker status
|
||||
val isRefreshing = RefreshCollectionsWorker.isWorkerInState(context, RefreshCollectionsWorker.workerName(serviceId), WorkInfo.State.RUNNING)
|
||||
|
||||
// observe whether sync framework is active
|
||||
private var syncFrameworkStatusHandle: Any? = null
|
||||
private val isSyncFrameworkActive = MutableLiveData<Boolean>()
|
||||
private val isSyncFrameworkPending = MutableLiveData<Boolean>()
|
||||
|
||||
// observe SyncWorker state
|
||||
private val authorities =
|
||||
if (collectionType == Collection.TYPE_ADDRESSBOOK)
|
||||
listOf(context.getString(R.string.address_books_authority), ContactsContract.AUTHORITY)
|
||||
else
|
||||
listOf(CalendarContract.AUTHORITY, taskProvider?.authority).filterNotNull()
|
||||
private val isSyncWorkerRunning = SyncWorker.existsForAccount(context,
|
||||
WorkInfo.State.RUNNING,
|
||||
val isSyncActive = SyncWorker.exists(context,
|
||||
listOf(WorkInfo.State.RUNNING),
|
||||
accountModel.account,
|
||||
authorities)
|
||||
private val isSyncWorkerEnqueued = SyncWorker.existsForAccount(context,
|
||||
WorkInfo.State.ENQUEUED,
|
||||
val isSyncPending = SyncWorker.exists(context,
|
||||
listOf(WorkInfo.State.ENQUEUED),
|
||||
accountModel.account,
|
||||
authorities)
|
||||
|
||||
// observe and combine states of sync framework and SyncWorker
|
||||
val isSyncActive = LiveDataUtils.liveDataLogicOr(listOf(isSyncFrameworkActive, isSyncWorkerRunning))
|
||||
val isSyncPending = LiveDataUtils.liveDataLogicOr(listOf(isSyncFrameworkPending, isSyncWorkerEnqueued))
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.Default) {
|
||||
syncFrameworkStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_PENDING +
|
||||
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this@Model)
|
||||
checkSyncFrameworkStatus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
syncFrameworkStatusHandle?.let {
|
||||
ContentResolver.removeStatusChangeListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
override fun onStatusChanged(which: Int) {
|
||||
checkSyncFrameworkStatus()
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Synchronized
|
||||
private fun checkSyncFrameworkStatus() {
|
||||
// SyncFramework only, isSyncFrameworkActive/Pending gets combined in logic OR with SyncWorker state
|
||||
if (collectionType == Collection.TYPE_ADDRESSBOOK) {
|
||||
// CardDAV tab
|
||||
val mainAuthority = context.getString(R.string.address_books_authority)
|
||||
val mainSyncActive = ContentResolver.isSyncActive(accountModel.account, mainAuthority)
|
||||
val mainSyncPending = ContentResolver.isSyncPending(accountModel.account, mainAuthority)
|
||||
|
||||
val addrBookAccounts = LocalAddressBook.findAll(context, null, accountModel.account).map { it.account }
|
||||
val syncActive = addrBookAccounts.any { ContentResolver.isSyncActive(it, ContactsContract.AUTHORITY) }
|
||||
val syncPending = addrBookAccounts.any { ContentResolver.isSyncPending(it, ContactsContract.AUTHORITY) }
|
||||
|
||||
isSyncFrameworkActive.postValue(mainSyncActive || syncActive)
|
||||
isSyncFrameworkPending.postValue(mainSyncPending || syncPending)
|
||||
|
||||
} else {
|
||||
// CalDAV tab
|
||||
val authorities = mutableListOf(CalendarContract.AUTHORITY)
|
||||
taskProvider?.let {
|
||||
authorities += it.authority
|
||||
}
|
||||
isSyncFrameworkActive.postValue(authorities.any {
|
||||
ContentResolver.isSyncActive(accountModel.account, it)
|
||||
})
|
||||
isSyncFrameworkPending.postValue(authorities.any {
|
||||
ContentResolver.isSyncPending(accountModel.account, it)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// actions
|
||||
|
||||
fun refresh() {
|
||||
|
|
|
@ -165,9 +165,9 @@ class RenameAccountFragment: DialogFragment() {
|
|||
Logger.log.info("Updating account name references")
|
||||
|
||||
// cancel maybe running synchronization
|
||||
ContentResolver.cancelSync(oldAccount, null)
|
||||
SyncWorker.cancelSync(context, oldAccount)
|
||||
for (addrBookAccount in accountManager.getAccountsByType(context.getString(R.string.account_type_address_book)))
|
||||
ContentResolver.cancelSync(addrBookAccount, null)
|
||||
SyncWorker.cancelSync(context, addrBookAccount)
|
||||
|
||||
// update account name references in database
|
||||
try {
|
||||
|
@ -219,7 +219,7 @@ class RenameAccountFragment: DialogFragment() {
|
|||
}
|
||||
|
||||
// synchronize again
|
||||
SyncWorker.requestSync(context, newAccount)
|
||||
SyncWorker.enqueueAllAuthorities(context, newAccount)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,10 +6,8 @@ package at.bitfire.davdroid.ui.account
|
|||
|
||||
import android.accounts.Account
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.SyncStatusObserver
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
|
@ -27,16 +25,16 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.*
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
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.SyncAdapterService
|
||||
import at.bitfire.davdroid.syncadapter.Syncer
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.ui.UiUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import at.bitfire.vcard4android.GroupMethod
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
|
@ -390,7 +388,7 @@ class SettingsActivity: AppCompatActivity() {
|
|||
@ApplicationContext val context: Context,
|
||||
val settings: SettingsManager,
|
||||
@Assisted val account: Account
|
||||
): ViewModel(), SyncStatusObserver, SettingsManager.OnChangeListener {
|
||||
): ViewModel(), SettingsManager.OnChangeListener {
|
||||
|
||||
@AssistedFactory
|
||||
interface Factory {
|
||||
|
@ -399,8 +397,6 @@ class SettingsActivity: AppCompatActivity() {
|
|||
|
||||
private var accountSettings: AccountSettings? = null
|
||||
|
||||
private var statusChangeListener: Any? = null
|
||||
|
||||
// settings
|
||||
val syncIntervalContacts = MutableLiveData<Long>()
|
||||
val syncIntervalCalendars = MutableLiveData<Long>()
|
||||
|
@ -433,26 +429,15 @@ class SettingsActivity: AppCompatActivity() {
|
|||
accountSettings = AccountSettings(context, account)
|
||||
|
||||
settings.addOnChangeListener(this)
|
||||
statusChangeListener = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_SETTINGS, this)
|
||||
|
||||
reload()
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
super.onCleared()
|
||||
|
||||
statusChangeListener?.let {
|
||||
ContentResolver.removeStatusChangeListener(it)
|
||||
statusChangeListener = null
|
||||
}
|
||||
settings.removeOnChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onStatusChanged(which: Int) {
|
||||
Logger.log.info("Sync settings changed")
|
||||
reload()
|
||||
}
|
||||
|
||||
override fun onSettingsChanged() {
|
||||
Logger.log.info("Settings changed")
|
||||
reload()
|
||||
|
@ -545,8 +530,8 @@ class SettingsActivity: AppCompatActivity() {
|
|||
* Initiates calendar re-synchronization.
|
||||
*
|
||||
* @param fullResync whether sync shall download all events again
|
||||
* (_true_: sets [SyncAdapterService.SYNC_EXTRAS_FULL_RESYNC],
|
||||
* _false_: sets [ContentResolver.SYNC_EXTRAS_MANUAL])
|
||||
* (_true_: sets [Syncer.SYNC_EXTRAS_FULL_RESYNC],
|
||||
* _false_: sets [Syncer.SYNC_EXTRAS_RESYNC])
|
||||
* @param tasks whether tasks shall be synchronized, too (false: only events, true: events and tasks)
|
||||
*/
|
||||
private fun resyncCalendars(fullResync: Boolean, tasks: Boolean) {
|
||||
|
@ -555,13 +540,17 @@ class SettingsActivity: AppCompatActivity() {
|
|||
resync(TaskProvider.ProviderName.OpenTasks.authority, fullResync)
|
||||
}
|
||||
|
||||
/**
|
||||
* Initiates re-synchronization for given authority.
|
||||
*
|
||||
* @param authority authority to re-sync
|
||||
* @param fullResync whether sync shall download all events again
|
||||
* (_true_: sets [Syncer.SYNC_EXTRAS_FULL_RESYNC],
|
||||
* _false_: sets [Syncer.SYNC_EXTRAS_RESYNC])
|
||||
*/
|
||||
private fun resync(authority: String, fullResync: Boolean) {
|
||||
val resync =
|
||||
if (fullResync)
|
||||
SyncWorker.FULL_RESYNC
|
||||
else
|
||||
SyncWorker.RESYNC
|
||||
SyncWorker.requestSync(context, account, authority, resync)
|
||||
val resync = if (fullResync) SyncWorker.FULL_RESYNC else SyncWorker.RESYNC
|
||||
SyncWorker.enqueue(context, account, authority, resync)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -53,7 +53,7 @@ class AccountDetailsFragment : Fragment() {
|
|||
@Inject lateinit var settings: SettingsManager
|
||||
|
||||
val loginModel by activityViewModels<LoginModel>()
|
||||
val model by viewModels<AccountDetailsModel>()
|
||||
val model by viewModels<Model>()
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
@ -140,7 +140,7 @@ class AccountDetailsFragment : Fragment() {
|
|||
|
||||
|
||||
@HiltViewModel
|
||||
class AccountDetailsModel @Inject constructor(
|
||||
class Model @Inject constructor(
|
||||
@ApplicationContext val context: Context,
|
||||
val db: AppDatabase,
|
||||
val settingsManager: SettingsManager
|
||||
|
@ -155,6 +155,16 @@ class AccountDetailsFragment : Fragment() {
|
|||
nameError.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new main account with discovered services and enables periodic syncs with
|
||||
* default sync interval times.
|
||||
*
|
||||
* @param name Name of the account
|
||||
* @param credentials Server credentials
|
||||
* @param config Discovered server capabilities for syncable authorities
|
||||
* @param groupMethod Whether CardDAV contact groups are separate VCards or as contact categories
|
||||
* @return *true* if account creation was succesful; *false* otherwise (for instance because an account with this name already exists)
|
||||
*/
|
||||
fun createAccount(name: String, credentials: Credentials?, config: DavResourceFinder.Configuration, groupMethod: GroupMethod): LiveData<Boolean> {
|
||||
val result = MutableLiveData<Boolean>()
|
||||
viewModelScope.launch(Dispatchers.Default + NonCancellable) {
|
||||
|
@ -175,6 +185,7 @@ class AccountDetailsFragment : Fragment() {
|
|||
val accountSettings = AccountSettings(context, account)
|
||||
val defaultSyncInterval = settingsManager.getLong(Settings.DEFAULT_SYNC_INTERVAL)
|
||||
|
||||
// Configure CardDAV service
|
||||
val addrBookAuthority = context.getString(R.string.address_books_authority)
|
||||
if (config.cardDAV != null) {
|
||||
// insert CardDAV service
|
||||
|
@ -192,6 +203,7 @@ class AccountDetailsFragment : Fragment() {
|
|||
} else
|
||||
ContentResolver.setIsSyncable(account, addrBookAuthority, 0)
|
||||
|
||||
// Configure CalDAV service
|
||||
if (config.calDAV != null) {
|
||||
// insert CalDAV service
|
||||
val id = insertService(name, Service.TYPE_CALDAV, config.calDAV)
|
||||
|
@ -203,12 +215,15 @@ class AccountDetailsFragment : Fragment() {
|
|||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 1)
|
||||
accountSettings.setSyncInterval(CalendarContract.AUTHORITY, defaultSyncInterval)
|
||||
|
||||
// if task provider present, set task sync interval and enable sync
|
||||
val taskProvider = TaskUtils.currentProvider(context)
|
||||
if (taskProvider != null) {
|
||||
ContentResolver.setIsSyncable(account, taskProvider.authority, 1)
|
||||
accountSettings.setSyncInterval(taskProvider.authority, defaultSyncInterval)
|
||||
// further changes will be handled by TasksWatcher on app start or when tasks app is (un)installed
|
||||
}
|
||||
Logger.log.info("Tasks provider ${taskProvider.authority} found. Tasks sync enabled.")
|
||||
} else
|
||||
Logger.log.info("No tasks provider found. Did not enable tasks sync.")
|
||||
} else
|
||||
ContentResolver.setIsSyncable(account, CalendarContract.AUTHORITY, 0)
|
||||
|
||||
|
|
|
@ -1,36 +0,0 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.util
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.lifecycle.MediatorLiveData
|
||||
|
||||
object LiveDataUtils {
|
||||
|
||||
/**
|
||||
* Combines multiple [LiveData] inputs with logical OR to another [LiveData].
|
||||
*
|
||||
* It's value is *null* as soon as no input is added or as long as no input
|
||||
* has emitted a value. As soon as at least one input has emitted a value,
|
||||
* the value of the combined object becomes *true* or *false*.
|
||||
*
|
||||
* @param inputs inputs to be combined with logical OR
|
||||
* @return [LiveData] that is *true* when at least one input becomes *true*; *false* otherwise
|
||||
*/
|
||||
fun liveDataLogicOr(inputs: Iterable<LiveData<Boolean>>) = object : MediatorLiveData<Boolean>() {
|
||||
init {
|
||||
inputs.forEach { liveData ->
|
||||
addSource(liveData) {
|
||||
recalculate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun recalculate() {
|
||||
value = inputs.any { it.value == true }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -7,7 +7,7 @@
|
|||
<import type="android.view.View"/>
|
||||
<variable
|
||||
name="details"
|
||||
type="at.bitfire.davdroid.ui.setup.AccountDetailsFragment.AccountDetailsModel"/>
|
||||
type="at.bitfire.davdroid.ui.setup.AccountDetailsFragment.Model"/>
|
||||
</data>
|
||||
|
||||
<LinearLayout
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
<string name="help">Help</string>
|
||||
<string name="manage_accounts">Manage accounts</string>
|
||||
<string name="share">Share</string>
|
||||
<string name="no_internet_connection">No internet connection</string>
|
||||
|
||||
<string name="database_destructive_migration_title">Database corrupted</string>
|
||||
<string name="database_destructive_migration_text">All accounts have been removed locally.</string>
|
||||
|
@ -241,7 +242,6 @@
|
|||
<string name="account_no_webcals">There are no calendar subscriptions (yet).</string>
|
||||
<string name="account_swipe_down">Swipe down to refresh the list from the server.</string>
|
||||
<string name="account_synchronize_now">Synchronize now</string>
|
||||
<string name="account_synchronizing_now">Synchronizing now</string>
|
||||
<string name="account_settings">Account settings</string>
|
||||
<string name="account_rename">Rename account</string>
|
||||
<string name="account_rename_new_name">Unsaved local data may be dismissed. Re-synchronization is required after renaming. New account name:</string>
|
||||
|
@ -481,7 +481,7 @@
|
|||
<string name="webdav_notification_upload">Uploading WebDAV file</string>
|
||||
<string name="webdav_provider_root_title">WebDAV mount</string>
|
||||
|
||||
<!-- sync adapters -->
|
||||
<!-- sync -->
|
||||
<string name="sync_error_permissions">DAVx⁵ permissions</string>
|
||||
<string name="sync_error_permissions_text">Additional permissions required</string>
|
||||
<string name="sync_error_tasks_too_old">%s too old</string>
|
||||
|
@ -491,6 +491,7 @@
|
|||
<string name="sync_error_http_dav">HTTP server error – %s</string>
|
||||
<string name="sync_error_local_storage">Local storage error – %s</string>
|
||||
<string name="sync_error_retry">Retry</string>
|
||||
<string name="sync_error_retry_limit_reached">Soft error (max retries reached)</string>
|
||||
<string name="sync_error_view_item">View item</string>
|
||||
<string name="sync_invalid_contact">Received invalid contact from server</string>
|
||||
<string name="sync_invalid_event">Received invalid event from server</string>
|
||||
|
|
Loading…
Reference in a new issue