mirror of
https://github.com/bitfireAT/davx5-ose
synced 2024-07-23 19:50:18 +00:00
work manager manual sync (bitfireAT/davx5#109)
* rabase with dev branch * added test for checking whether manual work-manager sync queues worker * overwrite getForegroundInfo to show a "sync running" notification, to run expedited work on Android <12 * basic error state with sensible feedback from syncframework * remove integer state flags and pass SyncResult as string * Manual sync cancellation * rabase with dev branch * Minor changes - add Jtx Board sync adapter to sync worker - use new notification ID for sync worker * status bar reflects sync of SyncWorker and sync framework correctly * [WIP] custom hilt SyncComponent * fix autoclose cast not available below api24 * SyncScope implementation using WeakReference * Remove unnecessary logging call * AddressBooksSyncAdapter.sync uses SyncWorker instead of ContentResolve to call requestSync * add some code documentation * move all utility objects into one package * Also check SyncWorker state for accounts list sync status bar * clean up imports * Remove duplicate copyright notices * Minor changes Co-authored-by: Ricki Hirner <hirner@bitfire.at>
This commit is contained in:
parent
262592a3d9
commit
cfee0f3461
|
@ -49,7 +49,7 @@ class SyncAdapterTest {
|
|||
fun setUp() {
|
||||
hiltRule.inject()
|
||||
|
||||
syncAdapter = TestSyncAdapter(context, db)
|
||||
syncAdapter = TestSyncAdapter(targetContext)
|
||||
}
|
||||
|
||||
|
||||
|
@ -117,11 +117,11 @@ class SyncAdapterTest {
|
|||
assertEquals(1, syncAdapter.syncCalled.get())
|
||||
|
||||
// check whether contextClassLoader is set
|
||||
assertEquals(context.classLoader, Thread.currentThread().contextClassLoader)
|
||||
assertEquals(targetContext.classLoader, Thread.currentThread().contextClassLoader)
|
||||
}
|
||||
|
||||
|
||||
class TestSyncAdapter(context: Context, db: AppDatabase): SyncAdapterService.SyncAdapter(context, db) {
|
||||
class TestSyncAdapter(context: Context): SyncAdapterService.SyncAdapter(context) {
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
@ -148,4 +148,4 @@ class SyncAdapterTest {
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,107 @@
|
|||
/***************************************************************************************************
|
||||
* 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.content.Context
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import android.util.Log
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.work.Configuration
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.impl.utils.SynchronousExecutor
|
||||
import androidx.work.testing.WorkManagerTestInitHelper
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import com.google.common.util.concurrent.ListenableFuture
|
||||
import dagger.hilt.android.testing.HiltAndroidRule
|
||||
import dagger.hilt.android.testing.HiltAndroidTest
|
||||
import org.junit.After
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Before
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
@HiltAndroidTest
|
||||
class SyncWorkerTest {
|
||||
|
||||
@get:Rule
|
||||
var hiltRule = HiltAndroidRule(this)
|
||||
|
||||
val context: 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")
|
||||
|
||||
@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)
|
||||
|
||||
// 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
|
||||
fun removeAccount() {
|
||||
accountManager.removeAccountExplicitly(account)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testRequestSync_enqueuesWorker() {
|
||||
SyncWorker.requestSync(context, account, CalendarContract.AUTHORITY)
|
||||
val workerName = SyncWorker.workerName(account, CalendarContract.AUTHORITY)
|
||||
assertTrue(workScheduledOrRunning(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(workerName))
|
||||
|
||||
// here we could test whether stopping the work really interrupts the sync thread
|
||||
}
|
||||
|
||||
private fun workScheduledOrRunning(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
|
||||
}
|
||||
for (workInfo in workInfoList) {
|
||||
val state = workInfo.state
|
||||
if (state == WorkInfo.State.RUNNING || state == WorkInfo.State.ENQUEUED)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
|
@ -12,7 +12,7 @@ import at.bitfire.dav4jvm.DavCollection
|
|||
import at.bitfire.dav4jvm.MultiResponseCallback
|
||||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.property.GetCTag
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.resource.LocalResource
|
||||
|
|
|
@ -59,6 +59,14 @@
|
|||
|
||||
<service android:name=".ForegroundService"/>
|
||||
|
||||
<provider
|
||||
android:name="androidx.startup.InitializationProvider"
|
||||
android:authorities="${applicationId}.androidx-startup"
|
||||
tools:node="remove">
|
||||
</provider>
|
||||
|
||||
<service android:name=".ForegroundService"/>
|
||||
|
||||
<activity android:name=".ui.intro.IntroActivity" android:theme="@style/AppTheme.NoActionBar" />
|
||||
<activity
|
||||
android:name=".ui.AccountsActivity"
|
||||
|
|
|
@ -44,6 +44,7 @@ class App: Application(), Thread.UncaughtExceptionHandler, Configuration.Provide
|
|||
|
||||
@Inject lateinit var accountsUpdatedListener: AccountsUpdatedListener
|
||||
@Inject lateinit var storageLowReceiver: StorageLowReceiver
|
||||
|
||||
@Inject lateinit var workerFactory: HiltWorkerFactory
|
||||
|
||||
override fun onCreate() {
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.room.*
|
|||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.UrlUtils
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
|
|
@ -12,7 +12,7 @@ import androidx.room.Entity
|
|||
import androidx.room.ForeignKey
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import at.bitfire.davdroid.DavUtils.MEDIA_TYPE_OCTET_STREAM
|
||||
import at.bitfire.davdroid.util.DavUtils.MEDIA_TYPE_OCTET_STREAM
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||
|
|
72
app/src/main/java/at/bitfire/davdroid/di/SyncComponent.kt
Normal file
72
app/src/main/java/at/bitfire/davdroid/di/SyncComponent.kt
Normal file
|
@ -0,0 +1,72 @@
|
|||
/***************************************************************************************************
|
||||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid.di
|
||||
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import dagger.hilt.DefineComponent
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import java.lang.ref.WeakReference
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Provider
|
||||
import javax.inject.Scope
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Scope
|
||||
@Retention(AnnotationRetention.BINARY)
|
||||
annotation class SyncScoped
|
||||
|
||||
/**
|
||||
* Custom Hilt component for running syncs, lifetime managed by [SyncComponentManager].
|
||||
* Dependencies installed in this component and scoped with [SyncScoped] (like SyncValidators)
|
||||
* will have a lifetime of all active syncs.
|
||||
*/
|
||||
@SyncScoped
|
||||
@DefineComponent(parent = SingletonComponent::class)
|
||||
interface SyncComponent
|
||||
|
||||
@DefineComponent.Builder
|
||||
interface SyncComponentBuilder {
|
||||
fun build(): SyncComponent
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages the lifecycle of [SyncComponent] by using [WeakReference].
|
||||
*
|
||||
* @sample at.bitfire.davdroid.syncadapter.LicenseValidator
|
||||
* @sample at.bitfire.davdroid.syncadapter.PaymentValidator
|
||||
*/
|
||||
@Singleton
|
||||
class SyncComponentManager @Inject constructor(
|
||||
val provider: Provider<SyncComponentBuilder>
|
||||
) {
|
||||
|
||||
private var componentRef: WeakReference<SyncComponent>? = null
|
||||
|
||||
/**
|
||||
* Returns a [SyncComponent]. When there is already a known [SyncComponent],
|
||||
* it will be used. Otherwise, a new one will be created and returned.
|
||||
*
|
||||
* It is then stored using a [WeakReference] – so as long as the component
|
||||
* stays in memory, it will always be returned. When it's not used anymore
|
||||
* by anyone, it can be removed by garbage collection. After this, it will be
|
||||
* created again when [get] is called.
|
||||
*
|
||||
* @return singleton [SyncComponent] (will be garbage collected when not referenced anymore)
|
||||
*/
|
||||
@Synchronized
|
||||
fun get(): SyncComponent {
|
||||
val component = componentRef?.get()
|
||||
|
||||
// check for cached component
|
||||
if (component != null)
|
||||
return component
|
||||
|
||||
// cached component not available, build new one
|
||||
val newComponent = provider.get().build()
|
||||
componentRef = WeakReference(newComponent)
|
||||
return newComponent
|
||||
}
|
||||
|
||||
}
|
|
@ -14,7 +14,7 @@ import android.provider.ContactsContract.CommonDataKinds.GroupMembership
|
|||
import android.provider.ContactsContract.Groups
|
||||
import android.provider.ContactsContract.RawContacts
|
||||
import android.util.Base64
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
|
|
|
@ -12,7 +12,7 @@ import android.net.Uri
|
|||
import android.provider.CalendarContract.Calendars
|
||||
import android.provider.CalendarContract.Events
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -7,7 +7,7 @@ package at.bitfire.davdroid.resource
|
|||
import android.accounts.Account
|
||||
import android.content.ContentProviderClient
|
||||
import android.content.ContentValues
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.ical4android.JtxCollection
|
||||
|
|
|
@ -10,7 +10,7 @@ import android.content.ContentValues
|
|||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -11,7 +11,7 @@ import at.bitfire.dav4jvm.exception.DavException
|
|||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.dav4jvm.exception.UnauthorizedException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.log.StringHandler
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.preference.PreferenceManager
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.closeCompat
|
||||
import at.bitfire.davdroid.util.closeCompat
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
|
|
|
@ -14,8 +14,7 @@ import android.os.Bundle
|
|||
import android.provider.ContactsContract
|
||||
import androidx.core.content.ContextCompat
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.closeCompat
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.util.closeCompat
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
import at.bitfire.davdroid.db.Service
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
@ -33,13 +32,9 @@ import java.util.logging.Level
|
|||
|
||||
class AddressBooksSyncAdapterService : SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = AddressBooksSyncAdapter(this, appDatabase)
|
||||
override fun syncAdapter() = AddressBooksSyncAdapter(this)
|
||||
|
||||
|
||||
class AddressBooksSyncAdapter(
|
||||
context: Context,
|
||||
appDatabase: AppDatabase
|
||||
) : SyncAdapter(context, appDatabase) {
|
||||
class AddressBooksSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
|
@ -64,10 +59,7 @@ class AddressBooksSyncAdapterService : SyncAdapterService() {
|
|||
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)
|
||||
val syncExtras = Bundle(extras)
|
||||
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_SETTINGS, true)
|
||||
syncExtras.putBoolean(ContentResolver.SYNC_EXTRAS_IGNORE_BACKOFF, true)
|
||||
ContentResolver.requestSync(addressBookAccount, ContactsContract.AUTHORITY, syncExtras)
|
||||
SyncWorker.requestSync(context, addressBookAccount)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Logger.log.log(Level.SEVERE, "Couldn't sync address books", e)
|
||||
|
|
|
@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.MultiResponseCallback
|
|||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
|
|
|
@ -27,13 +27,9 @@ import kotlin.collections.set
|
|||
|
||||
class CalendarsSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = CalendarsSyncAdapter(this, appDatabase)
|
||||
override fun syncAdapter() = CalendarsSyncAdapter(this)
|
||||
|
||||
|
||||
class CalendarsSyncAdapter(
|
||||
context: Context,
|
||||
appDatabase: AppDatabase
|
||||
) : SyncAdapter(context, appDatabase) {
|
||||
class CalendarsSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
|
|
|
@ -24,13 +24,9 @@ class ContactsSyncAdapterService: SyncAdapterService() {
|
|||
const val PREVIOUS_GROUP_METHOD = "previous_group_method"
|
||||
}
|
||||
|
||||
override fun syncAdapter() = ContactsSyncAdapter(this, appDatabase)
|
||||
override fun syncAdapter() = ContactsSyncAdapter(this)
|
||||
|
||||
|
||||
class ContactsSyncAdapter(
|
||||
context: Context,
|
||||
appDatabase: AppDatabase
|
||||
) : SyncAdapter(context, appDatabase) {
|
||||
class ContactsSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
|
|
|
@ -16,8 +16,8 @@ import at.bitfire.dav4jvm.MultiResponseCallback
|
|||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.DavUtils.sameTypeAs
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils.sameTypeAs
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
|
|
|
@ -30,13 +30,9 @@ import kotlin.collections.set
|
|||
|
||||
class JtxSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = JtxSyncAdapter(this, appDatabase)
|
||||
override fun syncAdapter() = JtxSyncAdapter(this)
|
||||
|
||||
|
||||
class JtxSyncAdapter(
|
||||
context: Context,
|
||||
appDatabase: AppDatabase
|
||||
) : SyncAdapter(context, appDatabase) {
|
||||
class JtxSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
|
||||
|
@ -113,4 +109,4 @@ class JtxSyncAdapterService: SyncAdapterService() {
|
|||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.MultiResponseCallback
|
|||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
|
|
|
@ -12,19 +12,21 @@ import android.net.NetworkCapabilities
|
|||
import android.net.wifi.WifiManager
|
||||
import android.os.Bundle
|
||||
import androidx.core.content.getSystemService
|
||||
import at.bitfire.davdroid.ConcurrentUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
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 dagger.hilt.android.AndroidEntryPoint
|
||||
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
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
abstract class SyncAdapterService: Service() {
|
||||
|
||||
companion object {
|
||||
|
@ -61,9 +63,6 @@ abstract class SyncAdapterService: Service() {
|
|||
const val SYNC_EXTRAS_FULL_RESYNC = "full_resync"
|
||||
}
|
||||
|
||||
@Inject lateinit var appDatabase: AppDatabase
|
||||
|
||||
|
||||
protected abstract fun syncAdapter(): AbstractThreadedSyncAdapter
|
||||
|
||||
override fun onBind(intent: Intent?) = syncAdapter().syncAdapterBinder!!
|
||||
|
@ -78,8 +77,7 @@ abstract class SyncAdapterService: Service() {
|
|||
* Also provides some useful methods that can be used by derived sync adapters.
|
||||
*/
|
||||
abstract class SyncAdapter(
|
||||
context: Context,
|
||||
val db: AppDatabase
|
||||
context: Context
|
||||
): AbstractThreadedSyncAdapter(
|
||||
context,
|
||||
true // isSyncable shouldn't be -1 because DAVx5 sets it to 0 or 1.
|
||||
|
@ -104,6 +102,17 @@ abstract class SyncAdapterService: Service() {
|
|||
}
|
||||
|
||||
|
||||
@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) {
|
||||
|
@ -136,8 +145,9 @@ abstract class SyncAdapterService: Service() {
|
|||
}
|
||||
})
|
||||
Logger.log.log(Level.INFO, "Sync for $currentSyncKey finished", syncResult)
|
||||
else
|
||||
else {
|
||||
Logger.log.warning("There's already another running sync for $currentSyncKey, aborting")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSecurityException(account: Account, extras: Bundle, authority: String, syncResult: SyncResult) {
|
||||
|
|
|
@ -18,7 +18,7 @@ import androidx.annotation.WorkerThread
|
|||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Service
|
||||
|
|
197
app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt
Normal file
197
app/src/main/java/at/bitfire/davdroid/syncadapter/SyncWorker.kt
Normal file
|
@ -0,0 +1,197 @@
|
|||
/***************************************************************************************************
|
||||
* 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 android.provider.ContactsContract
|
||||
import androidx.concurrent.futures.CallbackToFutureAdapter
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.lifecycle.Transformations
|
||||
import androidx.work.*
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.util.LiveDataUtils
|
||||
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.logging.Level
|
||||
|
||||
/**
|
||||
* Handles sync requests
|
||||
*/
|
||||
@HiltWorker
|
||||
class SyncWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted workerParams: WorkerParameters
|
||||
) : Worker(appContext, workerParams) {
|
||||
|
||||
companion object {
|
||||
|
||||
const val ARG_ACCOUNT_NAME = "accountName"
|
||||
const val ARG_ACCOUNT_TYPE = "accountType"
|
||||
const val ARG_AUTHORITY = "authority"
|
||||
|
||||
fun workerName(account: Account, authority: String): String {
|
||||
return "explicit-sync $authority ${account.type}/${account.name}"
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests immediate synchronization of an account with all applicable
|
||||
* authorities (contacts, calendars, …).
|
||||
*
|
||||
* @param account account to sync
|
||||
*/
|
||||
fun requestSync(context: Context, account: Account) {
|
||||
for (authority in DavUtils.syncAuthorities(context))
|
||||
requestSync(context, account, authority)
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests immediate synchronization of an account with a specific authority.
|
||||
*
|
||||
* @param account account to sync
|
||||
* @param authority authority to sync (for instance: [CalendarContract.AUTHORITY]])
|
||||
*/
|
||||
fun requestSync(context: Context, account: Account, authority: String) {
|
||||
val arguments = Data.Builder()
|
||||
.putString(ARG_AUTHORITY, authority)
|
||||
.putString(ARG_ACCOUNT_NAME, account.name)
|
||||
.putString(ARG_ACCOUNT_TYPE, account.type)
|
||||
.build()
|
||||
val workRequest = OneTimeWorkRequestBuilder<SyncWorker>()
|
||||
.setInputData(arguments)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.build()
|
||||
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(
|
||||
workerName(account, authority),
|
||||
ExistingWorkPolicy.KEEP, // if sync is already running, just continue
|
||||
workRequest
|
||||
)
|
||||
}
|
||||
|
||||
fun stopSync(context: Context, account: Account, authority: String) {
|
||||
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.
|
||||
*
|
||||
* @param workState state of worker 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
|
||||
*/
|
||||
fun isSomeWorkerInState(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) =
|
||||
Transformations.map(WorkManager.getInstance(context).getWorkInfosForUniqueWorkLiveData(workerName(account, authority))) { workInfoList ->
|
||||
workInfoList.any { workInfo -> workInfo.state == workState }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
/** thread which runs the actual sync code (can be interrupted to stop synchronization) */
|
||||
var syncThread: Thread? = null
|
||||
|
||||
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 sync worker: account=$account, authority=$authority")
|
||||
|
||||
val syncAdapter = when (authority) {
|
||||
applicationContext.getString(R.string.address_books_authority) ->
|
||||
AddressBooksSyncAdapterService.AddressBooksSyncAdapter(applicationContext)
|
||||
CalendarContract.AUTHORITY ->
|
||||
CalendarsSyncAdapterService.CalendarsSyncAdapter(applicationContext)
|
||||
ContactsContract.AUTHORITY ->
|
||||
ContactsSyncAdapterService.ContactsSyncAdapter(applicationContext)
|
||||
TaskProvider.ProviderName.JtxBoard.authority ->
|
||||
JtxSyncAdapterService.JtxSyncAdapter(applicationContext)
|
||||
TaskProvider.ProviderName.OpenTasks.authority,
|
||||
TaskProvider.ProviderName.TasksOrg.authority ->
|
||||
TasksSyncAdapterService.TasksSyncAdapter(applicationContext)
|
||||
else ->
|
||||
throw IllegalArgumentException("Invalid authority $authority")
|
||||
}
|
||||
|
||||
// Pass flags to the sync adapter. Note that these are 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 the WorkManager and from the sync framework).
|
||||
val extras = Bundle(2)
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
|
||||
val result = SyncResult()
|
||||
|
||||
val provider: ContentProviderClient? =
|
||||
try {
|
||||
applicationContext.contentResolver.acquireContentProviderClient(authority)
|
||||
} catch (e: SecurityException) {
|
||||
Logger.log.log(Level.WARNING, "Missing permissions to acquire ContentProviderClient for $authority", e)
|
||||
null
|
||||
}
|
||||
if (provider == null) {
|
||||
Logger.log.warning("Couldn't acquire ContentProviderClient for $authority")
|
||||
return Result.failure()
|
||||
}
|
||||
|
||||
try {
|
||||
syncThread = Thread.currentThread()
|
||||
syncAdapter.onPerformSync(account, extras, authority, provider, result)
|
||||
} catch (e: SecurityException) {
|
||||
syncAdapter.onSecurityException(account, extras, authority, result)
|
||||
} finally {
|
||||
provider.closeCompat()
|
||||
}
|
||||
|
||||
if (result.hasError())
|
||||
return Result.failure(Data.Builder()
|
||||
.putString("syncresult", result.toString())
|
||||
.putString("syncResultStats", result.stats.toString())
|
||||
.build())
|
||||
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
override fun onStopped() {
|
||||
Logger.log.info("Stopping sync thread")
|
||||
syncThread?.interrupt()
|
||||
}
|
||||
|
||||
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> =
|
||||
CallbackToFutureAdapter.getFuture { completer ->
|
||||
val notification = NotificationUtils.newBuilder(applicationContext, NotificationUtils.CHANNEL_STATUS)
|
||||
.setSmallIcon(R.drawable.ic_foreground_notify)
|
||||
.setContentTitle(applicationContext.getString(R.string.foreground_service_notify_title))
|
||||
.setContentText(applicationContext.getString(R.string.foreground_service_notify_text))
|
||||
.setStyle(NotificationCompat.BigTextStyle())
|
||||
.setCategory(NotificationCompat.CATEGORY_STATUS)
|
||||
.setOngoing(true)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.build()
|
||||
completer.set(ForegroundInfo(NotificationUtils.NOTIFY_SYNC_EXPEDITED, notification))
|
||||
}
|
||||
|
||||
}
|
|
@ -30,13 +30,9 @@ import java.util.logging.Level
|
|||
*/
|
||||
open class TasksSyncAdapterService: SyncAdapterService() {
|
||||
|
||||
override fun syncAdapter() = TasksSyncAdapter(this, appDatabase)
|
||||
override fun syncAdapter() = TasksSyncAdapter(this)
|
||||
|
||||
|
||||
class TasksSyncAdapter(
|
||||
context: Context,
|
||||
appDatabase: AppDatabase,
|
||||
) : SyncAdapter(context, appDatabase) {
|
||||
class TasksSyncAdapter(context: Context) : SyncAdapter(context) {
|
||||
|
||||
override fun sync(account: Account, extras: Bundle, authority: String, httpClient: Lazy<HttpClient>, provider: ContentProviderClient, syncResult: SyncResult) {
|
||||
try {
|
||||
|
@ -121,4 +117,4 @@ open class TasksSyncAdapterService: SyncAdapterService() {
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import at.bitfire.dav4jvm.MultiResponseCallback
|
|||
import at.bitfire.dav4jvm.Response
|
||||
import at.bitfire.dav4jvm.exception.DavException
|
||||
import at.bitfire.dav4jvm.property.*
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.SyncState
|
||||
|
|
|
@ -26,8 +26,8 @@ import androidx.recyclerview.widget.DiffUtil
|
|||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.ListAdapter
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.DavUtils.SyncStatus
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils.SyncStatus
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.AccountListBinding
|
||||
import at.bitfire.davdroid.databinding.AccountListItemBinding
|
||||
|
|
|
@ -15,9 +15,9 @@ import androidx.appcompat.app.ActionBarDrawerToggle
|
|||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.view.GravityCompat
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.ActivityAccountsBinding
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.ui.intro.IntroActivity
|
||||
import at.bitfire.davdroid.ui.setup.LoginActivity
|
||||
import com.google.android.material.navigation.NavigationView
|
||||
|
@ -111,7 +111,7 @@ class AccountsActivity: AppCompatActivity(), NavigationView.OnNavigationItemSele
|
|||
|
||||
val accounts = allAccounts()
|
||||
for (account in accounts)
|
||||
DavUtils.requestSync(this, account)
|
||||
SyncWorker.requestSync(this, account)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@ import android.view.ViewGroup
|
|||
import android.widget.ArrayAdapter
|
||||
import android.widget.Filter
|
||||
import android.widget.TextView
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.HomeSet
|
||||
|
||||
|
|
|
@ -25,10 +25,10 @@ import androidx.lifecycle.AndroidViewModel
|
|||
import androidx.lifecycle.MutableLiveData
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.PackageChangedReceiver
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
import at.bitfire.davdroid.PermissionUtils.CALENDAR_PERMISSIONS
|
||||
import at.bitfire.davdroid.PermissionUtils.CONTACT_PERMISSIONS
|
||||
import at.bitfire.davdroid.PermissionUtils.havePermissions
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS
|
||||
import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS
|
||||
import at.bitfire.davdroid.util.PermissionUtils.havePermissions
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.ActivityPermissionsBinding
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
|
|
|
@ -20,8 +20,8 @@ import androidx.appcompat.app.AppCompatActivity
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
import androidx.lifecycle.*
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
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
|
||||
|
@ -88,7 +88,7 @@ class AccountActivity: AppCompatActivity() {
|
|||
})
|
||||
|
||||
binding.sync.setOnClickListener {
|
||||
DavUtils.requestSync(this, model.account)
|
||||
SyncWorker.requestSync(this, model.account)
|
||||
Snackbar.make(binding.viewPager, R.string.account_synchronizing_now, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ package at.bitfire.davdroid.ui.account
|
|||
import android.content.Intent
|
||||
import android.view.*
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.AccountCarddavItemBinding
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
|
|
|
@ -8,7 +8,7 @@ import android.content.Intent
|
|||
import android.view.*
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.AccountCaldavItemBinding
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
|
|
|
@ -31,7 +31,9 @@ import at.bitfire.davdroid.db.Collection
|
|||
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
|
||||
|
@ -305,36 +307,55 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
|
|||
// observe RefreshCollectionsWorker status
|
||||
val isRefreshing = RefreshCollectionsWorker.isWorkerInState(context, serviceId, WorkInfo.State.RUNNING)
|
||||
|
||||
// observe whether sync is active
|
||||
private var syncStatusHandle: Any? = null
|
||||
val isSyncActive = MutableLiveData<Boolean>()
|
||||
val isSyncPending = MutableLiveData<Boolean>()
|
||||
// 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.isSomeWorkerInState(context,
|
||||
WorkInfo.State.RUNNING,
|
||||
accountModel.account,
|
||||
authorities)
|
||||
private val isSyncWorkerEnqueued = SyncWorker.isSomeWorkerInState(context,
|
||||
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) {
|
||||
syncStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_PENDING + ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this@Model)
|
||||
checkSyncStatus()
|
||||
syncFrameworkStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_PENDING +
|
||||
ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this@Model)
|
||||
checkSyncFrameworkStatus()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCleared() {
|
||||
syncStatusHandle?.let { ContentResolver.removeStatusChangeListener(it) }
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
RefreshCollectionsWorker.refreshCollections(context, serviceId)
|
||||
syncFrameworkStatusHandle?.let {
|
||||
ContentResolver.removeStatusChangeListener(it)
|
||||
}
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
override fun onStatusChanged(which: Int) {
|
||||
checkSyncStatus()
|
||||
checkSyncFrameworkStatus()
|
||||
}
|
||||
|
||||
@AnyThread
|
||||
@Synchronized
|
||||
private fun checkSyncStatus() {
|
||||
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)
|
||||
|
@ -343,22 +364,31 @@ abstract class CollectionsFragment: Fragment(), SwipeRefreshLayout.OnRefreshList
|
|||
val syncActive = addrBookAccounts.any { ContentResolver.isSyncActive(it, ContactsContract.AUTHORITY) }
|
||||
val syncPending = addrBookAccounts.any { ContentResolver.isSyncPending(it, ContactsContract.AUTHORITY) }
|
||||
|
||||
isSyncActive.postValue(mainSyncActive || syncActive)
|
||||
isSyncPending.postValue(mainSyncPending || syncPending)
|
||||
isSyncFrameworkActive.postValue(mainSyncActive || syncActive)
|
||||
isSyncFrameworkPending.postValue(mainSyncPending || syncPending)
|
||||
|
||||
} else {
|
||||
// CalDAV tab
|
||||
val authorities = mutableListOf(CalendarContract.AUTHORITY)
|
||||
taskProvider?.let {
|
||||
authorities += it.authority
|
||||
}
|
||||
isSyncActive.postValue(authorities.any {
|
||||
isSyncFrameworkActive.postValue(authorities.any {
|
||||
ContentResolver.isSyncActive(accountModel.account, it)
|
||||
})
|
||||
isSyncPending.postValue(authorities.any {
|
||||
isSyncFrameworkPending.postValue(authorities.any {
|
||||
ContentResolver.isSyncPending(accountModel.account, it)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// actions
|
||||
|
||||
fun refresh() {
|
||||
RefreshCollectionsWorker.refreshCollections(context, serviceId)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -15,7 +15,7 @@ import androidx.fragment.app.viewModels
|
|||
import androidx.lifecycle.*
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.XmlUtils
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
|
|
|
@ -27,16 +27,15 @@ import androidx.lifecycle.MutableLiveData
|
|||
import androidx.lifecycle.Observer
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.closeCompat
|
||||
import at.bitfire.davdroid.*
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.resource.LocalTaskList
|
||||
import at.bitfire.davdroid.settings.AccountSettings
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import at.bitfire.davdroid.syncadapter.AccountsUpdatedListener
|
||||
import at.bitfire.davdroid.util.closeCompat
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
|
@ -220,7 +219,7 @@ class RenameAccountFragment: DialogFragment() {
|
|||
}
|
||||
|
||||
// synchronize again
|
||||
DavUtils.requestSync(context, newAccount)
|
||||
SyncWorker.requestSync(context, newAccount)
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import androidx.lifecycle.ViewModel
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.preference.*
|
||||
import at.bitfire.davdroid.InvalidAccountException
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.db.Credentials
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -22,9 +22,9 @@ import androidx.lifecycle.*
|
|||
import androidx.room.Transaction
|
||||
import at.bitfire.dav4jvm.UrlUtils
|
||||
import at.bitfire.davdroid.Constants
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.closeCompat
|
||||
import at.bitfire.davdroid.util.closeCompat
|
||||
import at.bitfire.davdroid.databinding.AccountCaldavItemBinding
|
||||
import at.bitfire.davdroid.db.AppDatabase
|
||||
import at.bitfire.davdroid.db.Collection
|
||||
|
|
|
@ -26,7 +26,7 @@ import androidx.core.location.LocationManagerCompat
|
|||
import androidx.core.text.HtmlCompat
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.databinding.ActivityWifiPermissionsBinding
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -10,9 +10,9 @@ import android.view.LayoutInflater
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.fragment.app.Fragment
|
||||
import at.bitfire.davdroid.PermissionUtils
|
||||
import at.bitfire.davdroid.PermissionUtils.CALENDAR_PERMISSIONS
|
||||
import at.bitfire.davdroid.PermissionUtils.CONTACT_PERMISSIONS
|
||||
import at.bitfire.davdroid.util.PermissionUtils
|
||||
import at.bitfire.davdroid.util.PermissionUtils.CALENDAR_PERMISSIONS
|
||||
import at.bitfire.davdroid.util.PermissionUtils.CONTACT_PERMISSIONS
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.ical4android.TaskProvider
|
||||
import javax.inject.Inject
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.util
|
||||
|
||||
import android.content.ContentProviderClient
|
||||
import android.os.Build
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.util
|
||||
|
||||
import java.util.*
|
||||
|
|
@ -2,20 +2,23 @@
|
|||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.util
|
||||
|
||||
import android.accounts.Account
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.CalendarContract
|
||||
import android.provider.ContactsContract
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.work.WorkInfo
|
||||
import at.bitfire.davdroid.Android10Resolver
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.resource.LocalAddressBook
|
||||
import at.bitfire.davdroid.resource.TaskUtils
|
||||
import at.bitfire.davdroid.syncadapter.SyncWorker
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.MediaType
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
|
@ -178,27 +181,22 @@ object DavUtils {
|
|||
if (addrBookAccounts.any { ContentResolver.isSyncActive(it, ContactsContract.AUTHORITY) })
|
||||
return SyncStatus.ACTIVE
|
||||
|
||||
// check get pending syncs
|
||||
// check pending syncs
|
||||
if (authorities.any { ContentResolver.isSyncPending(account, it) } ||
|
||||
addrBookAccounts.any { ContentResolver.isSyncPending(it, ContactsContract.AUTHORITY) })
|
||||
return SyncStatus.PENDING
|
||||
|
||||
// Also check SyncWorkers
|
||||
val pending = SyncWorker.isSomeWorkerInState(context, WorkInfo.State.ENQUEUED, account, authorities.toList()).value
|
||||
if (pending != null && pending == true)
|
||||
return SyncStatus.PENDING
|
||||
val running = SyncWorker.isSomeWorkerInState(context, WorkInfo.State.RUNNING, account, authorities.toList()).value
|
||||
if (running != null && running == true)
|
||||
return SyncStatus.ACTIVE
|
||||
|
||||
return SyncStatus.IDLE
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an immediate, manual sync of all available authorities for the given account.
|
||||
*
|
||||
* @param account account to sync
|
||||
*/
|
||||
fun requestSync(context: Context, account: Account) {
|
||||
for (authority in syncAuthorities(context)) {
|
||||
val extras = Bundle(2)
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true) // manual sync
|
||||
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true) // run immediately (don't queue)
|
||||
ContentResolver.requestSync(account, authority, extras)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all available sync authorities for main accounts (!= address book accounts):
|
36
app/src/main/java/at/bitfire/davdroid/util/LiveDataUtils.kt
Normal file
36
app/src/main/java/at/bitfire/davdroid/util/LiveDataUtils.kt
Normal file
|
@ -0,0 +1,36 @@
|
|||
/***************************************************************************************************
|
||||
* 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 }
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
* Copyright © All Contributors. See LICENSE and AUTHORS in the root directory for details.
|
||||
**************************************************************************************************/
|
||||
|
||||
package at.bitfire.davdroid
|
||||
package at.bitfire.davdroid.util
|
||||
|
||||
import android.Manifest
|
||||
import android.app.PendingIntent
|
||||
|
@ -16,6 +16,8 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.location.LocationManagerCompat
|
||||
import at.bitfire.davdroid.BuildConfig
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.ui.PermissionsActivity
|
|
@ -23,6 +23,7 @@ import at.bitfire.dav4jvm.exception.HttpException
|
|||
import at.bitfire.davdroid.*
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
import at.bitfire.davdroid.ui.NotificationUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.webdav.cache.MemoryCache
|
||||
import at.bitfire.davdroid.webdav.cache.SegmentedCache
|
||||
import okhttp3.Headers
|
||||
|
|
|
@ -12,7 +12,7 @@ import androidx.core.app.NotificationCompat
|
|||
import androidx.core.app.NotificationManagerCompat
|
||||
import at.bitfire.dav4jvm.DavResource
|
||||
import at.bitfire.dav4jvm.exception.HttpException
|
||||
import at.bitfire.davdroid.DavUtils
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import at.bitfire.davdroid.HttpClient
|
||||
import at.bitfire.davdroid.R
|
||||
import at.bitfire.davdroid.log.Logger
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import at.bitfire.davdroid.util.ConcurrentUtils
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
package at.bitfire.davdroid
|
||||
|
||||
import at.bitfire.davdroid.util.DavUtils
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrl
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
|
|
Loading…
Reference in a new issue