Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Do not require internet connection for worker if only local subscriptions are used #389

Open
wants to merge 10 commits into
base: dev
Choose a base branch
from
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ import at.bitfire.ical4android.util.MiscUtils.closeCompat
import at.bitfire.icsdroid.AppAccount
import at.bitfire.icsdroid.BaseSyncWorker.Companion.ONLY_MIGRATE
import at.bitfire.icsdroid.Constants.TAG
import at.bitfire.icsdroid.SyncWorker
import at.bitfire.icsdroid.NetworkSyncWorker
import at.bitfire.icsdroid.calendar.LocalCalendar
import at.bitfire.icsdroid.db.AppDatabase
import at.bitfire.icsdroid.db.CalendarCredentials
Expand Down Expand Up @@ -139,7 +139,7 @@ class CalendarToRoomMigrationTest {
try {
runBlocking {
// run worker
val result = TestListenableWorkerBuilder<SyncWorker>(appContext)
val result = TestListenableWorkerBuilder<NetworkSyncWorker>(appContext)
.setInputData(Data.Builder()
.putBoolean(ONLY_MIGRATE, true)
.build())
Expand Down Expand Up @@ -177,7 +177,7 @@ class CalendarToRoomMigrationTest {
try {
runBlocking {
// run worker
val result = TestListenableWorkerBuilder<SyncWorker>(appContext)
val result = TestListenableWorkerBuilder<NetworkSyncWorker>(appContext)
.setInputData(Data.Builder()
.putBoolean(ONLY_MIGRATE, true)
.build())
Expand Down
69 changes: 67 additions & 2 deletions app/src/main/java/at/bitfire/icsdroid/BaseSyncWorker.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,36 @@ import android.content.ContentUris
import android.content.Context
import android.util.Log
import androidx.work.CoroutineWorker
import androidx.work.NetworkType
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import at.bitfire.ical4android.AndroidCalendar
import at.bitfire.ical4android.util.MiscUtils.closeCompat
import at.bitfire.icsdroid.BaseSyncWorker.Companion.FORCE_RESYNC
import at.bitfire.icsdroid.BaseSyncWorker.Companion.ONLY_MIGRATE
import at.bitfire.icsdroid.calendar.LocalCalendar
import at.bitfire.icsdroid.db.AppDatabase
import at.bitfire.icsdroid.db.CalendarCredentials
import at.bitfire.icsdroid.db.entity.Credential
import at.bitfire.icsdroid.db.entity.Subscription
import at.bitfire.icsdroid.ui.NotificationUtils
import kotlinx.coroutines.flow.combine

/**
* Base class for synchronization workers. It provides the basic functionality for synchronizing
* subscriptions with their respective servers and local calendars.
*
* @param context required for managing work
* @param workerParams any additional parameters for the worker. See their respective kdocs for
* more information. Options:
* - [FORCE_RESYNC]
* - [ONLY_MIGRATE]
* @param filter a filter function that determines which subscriptions should be synchronized
*/
open class BaseSyncWorker(
context: Context,
workerParams: WorkerParameters
workerParams: WorkerParameters,
private val filter: (Subscription) -> Boolean
) : CoroutineWorker(context, workerParams) {
companion object {
/**
Expand All @@ -31,8 +48,55 @@ open class BaseSyncWorker(
* fetching data.
*/
const val ONLY_MIGRATE = "onlyMigration"

/**
* Enqueues a sync job for immediate execution, both for local and network subscriptions.
* If the sync is forced, the "requires network connection" constraint won't be set.
*
* @param context required for managing work
* @param force *true* enqueues the sync regardless of the network state; *false* adds a [NetworkType.CONNECTED] constraint
* @param forceResync *true* ignores all locally stored data and fetched everything from the server again
* @param onlyMigrate *true* only runs synchronization, without fetching data.
*/
fun run(
context: Context,
force: Boolean = false,
forceResync: Boolean = false,
onlyMigrate: Boolean = false
) {
NetworkSyncWorker.run(context, force, forceResync, onlyMigrate)
if (!onlyMigrate) {
// Migration is performed by SyncWorker. Do not schedule LocalSyncWorker if onlyMigrate is true.
LocalSyncWorker.run(context, forceResync)
}
}

/**
* Obtains the combined status flows of [NetworkSyncWorker] and [LocalSyncWorker].
*/
fun statusFlow(context: Context) = combine(
NetworkSyncWorker.statusFlow(context),
LocalSyncWorker.statusFlow(context)
) { sync, local -> sync + local }

/**
* Cancels all the synchronization jobs.
*/
fun cancel(context: Context) {
val wm = WorkManager.getInstance(context)
wm.cancelUniqueWork(NetworkSyncWorker.NAME)
wm.cancelUniqueWork(LocalSyncWorker.NAME)
}
}

/**
* Constructs a new BaseSyncWorker without any filter.
*/
constructor(
context: Context,
workerParams: WorkerParameters
): this(context, workerParams, { true })

private val database = AppDatabase.getInstance(applicationContext)
private val subscriptionsDao = database.subscriptionsDao()
private val credentialsDao = database.credentialsDao()
Expand Down Expand Up @@ -72,7 +136,8 @@ open class BaseSyncWorker(
AndroidCalendar.insertColors(provider, account)

// sync local calendars
for (subscription in subscriptionsDao.getAll()) {
val subscriptions = subscriptionsDao.getAll().filter(filter)
for (subscription in subscriptions) {
// Make sure the subscription has a matching calendar
subscription.calendarId ?: continue
val calendar = LocalCalendar.findById(account, provider, subscription.calendarId)
Expand Down
52 changes: 52 additions & 0 deletions app/src/main/java/at/bitfire/icsdroid/LocalSyncWorker.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package at.bitfire.icsdroid

import android.content.Context
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import androidx.work.workDataOf

/**
* Synchronizes all subscriptions from local fs.
* Always runs, regardless of current network condition, and filters local URLs (not http(s)).
*/
class LocalSyncWorker(
context: Context,
workerParams: WorkerParameters
) : BaseSyncWorker(context, workerParams, { it.url.scheme?.startsWith("http") == false }) {

companion object {

/** The name of the worker. Tags the unique work. */
const val NAME = "LocalSyncWorker"

/**
* Enqueues a sync job for immediate execution. If the sync is forced,
* the "requires network connection" constraint won't be set.
*
* @param context required for managing work
* @param forceResync *true* ignores all locally stored data and fetched everything from the server again
*/
fun run(
context: Context,
forceResync: Boolean = false
) {
val request = OneTimeWorkRequestBuilder<LocalSyncWorker>()
.setInputData(
workDataOf(
FORCE_RESYNC to forceResync,
)
)

WorkManager.getInstance(context)
.beginUniqueWork(NAME, ExistingWorkPolicy.KEEP, request.build())
.enqueue()
}

fun statusFlow(context: Context) =
WorkManager.getInstance(context).getWorkInfosForUniqueWorkFlow(NAME)

}

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,14 @@ import androidx.work.WorkerParameters
import androidx.work.workDataOf
import at.bitfire.icsdroid.Constants.TAG

class SyncWorker(
/**
* Synchronizes all subscriptions with their respective servers.
* Only runs if the network is available, and filters remote URLs (http(s)).
*/
class NetworkSyncWorker(
context: Context,
workerParams: WorkerParameters
) : BaseSyncWorker(context, workerParams) {
) : BaseSyncWorker(context, workerParams, { it.url.scheme?.startsWith("http") == true }) {

companion object {

Expand All @@ -40,7 +44,7 @@ class SyncWorker(
forceResync: Boolean = false,
onlyMigrate: Boolean = false
) {
val request = OneTimeWorkRequestBuilder<SyncWorker>()
val request = OneTimeWorkRequestBuilder<NetworkSyncWorker>()
.setInputData(
workDataOf(
FORCE_RESYNC to forceResync,
Expand Down
12 changes: 7 additions & 5 deletions app/src/main/java/at/bitfire/icsdroid/SyncAdapter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,12 @@
package at.bitfire.icsdroid

import android.accounts.Account
import android.content.*
import android.content.AbstractThreadedSyncAdapter
import android.content.ContentProviderClient
import android.content.ContentResolver
import android.content.Context
import android.content.SyncResult
import android.os.Bundle
import androidx.work.WorkManager
import at.bitfire.icsdroid.ui.NotificationUtils

class SyncAdapter(
Expand All @@ -16,13 +19,12 @@ class SyncAdapter(

override fun onPerformSync(account: Account, extras: Bundle, authority: String, provider: ContentProviderClient, syncResult: SyncResult) {
val manual = extras.containsKey(ContentResolver.SYNC_EXTRAS_MANUAL)
SyncWorker.run(context, manual)
BaseSyncWorker.run(context, manual)
}

override fun onSyncCanceled(thread: Thread?) = onSyncCanceled()
override fun onSyncCanceled() {
val wm = WorkManager.getInstance(context)
wm.cancelUniqueWork(SyncWorker.NAME)
BaseSyncWorker.cancel(context)
}

/**
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/java/at/bitfire/icsdroid/db/AppDatabase.kt
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import androidx.sqlite.db.SupportSQLiteDatabase
import at.bitfire.icsdroid.SyncWorker
import at.bitfire.icsdroid.BaseSyncWorker
import at.bitfire.icsdroid.db.AppDatabase.Companion.getInstance
import at.bitfire.icsdroid.db.dao.CredentialsDao
import at.bitfire.icsdroid.db.dao.SubscriptionsDao
Expand Down Expand Up @@ -78,7 +78,7 @@ abstract class AppDatabase : RoomDatabase() {
.fallbackToDestructiveMigration()
.addCallback(object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
SyncWorker.run(context, onlyMigrate = true)
BaseSyncWorker.run(context, onlyMigrate = true)
}
})
.build()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import at.bitfire.icsdroid.BaseSyncWorker
import at.bitfire.icsdroid.Constants
import at.bitfire.icsdroid.R
import at.bitfire.icsdroid.SyncWorker
import at.bitfire.icsdroid.db.AppDatabase
import at.bitfire.icsdroid.db.entity.Credential
import at.bitfire.icsdroid.db.entity.Subscription
Expand Down Expand Up @@ -79,7 +79,7 @@ class CreateSubscriptionModel(application: Application) : AndroidViewModel(appli
}

// sync the subscription to reflect the changes in the calendar provider
SyncWorker.run(getApplication())
BaseSyncWorker.run(getApplication())

uiState = uiState.copy(success = true)
} catch (e: Exception) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
package at.bitfire.icsdroid.model

import android.app.Application
import android.content.Context
import android.util.Log
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.core.app.ShareCompat
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import at.bitfire.icsdroid.BaseSyncWorker
import at.bitfire.icsdroid.Constants
import at.bitfire.icsdroid.R
import at.bitfire.icsdroid.SyncWorker
import at.bitfire.icsdroid.db.AppDatabase
import at.bitfire.icsdroid.db.entity.Credential
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.firstOrNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

Expand Down Expand Up @@ -72,7 +69,7 @@ class EditSubscriptionModel(
uiState = uiState.copy(successMessage = getApplication<Application>().getString(R.string.edit_calendar_saved))

// sync the subscription to reflect the changes in the calendar provider
SyncWorker.run(getApplication(), forceResync = true)
BaseSyncWorker.run(getApplication(), forceResync = true)
} ?: Log.w(Constants.TAG, "There's no subscription to update")
}
}
Expand All @@ -86,7 +83,7 @@ class EditSubscriptionModel(
subscriptionsDao.delete(subscriptionWithCredentials.subscription)

// sync the subscription to reflect the changes in the calendar provider
SyncWorker.run(getApplication())
BaseSyncWorker.run(getApplication())

// notify UI about success
uiState = uiState.copy(successMessage = getApplication<Application>().getString(R.string.edit_calendar_deleted))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,11 +18,11 @@ import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import androidx.work.WorkInfo
import at.bitfire.icsdroid.AppAccount
import at.bitfire.icsdroid.BaseSyncWorker
import at.bitfire.icsdroid.BuildConfig
import at.bitfire.icsdroid.PermissionUtils
import at.bitfire.icsdroid.R
import at.bitfire.icsdroid.Settings
import at.bitfire.icsdroid.SyncWorker
import at.bitfire.icsdroid.dataStore
import at.bitfire.icsdroid.db.AppDatabase
import kotlinx.coroutines.Dispatchers
Expand All @@ -46,7 +46,7 @@ class SubscriptionsModel(application: Application): AndroidViewModel(application
private set

/** whether there are running sync workers */
val isRefreshing = SyncWorker.statusFlow(application).map { workInfos ->
val isRefreshing = BaseSyncWorker.statusFlow(application).map { workInfos ->
workInfos.any { it.state == WorkInfo.State.RUNNING }
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)

Expand Down Expand Up @@ -115,7 +115,7 @@ class SubscriptionsModel(application: Application): AndroidViewModel(application
}

fun onRefreshRequested() {
SyncWorker.run(getApplication(), true)
BaseSyncWorker.run(getApplication(), true)
}

fun onToggleDarkMode(forceDarkMode: Boolean) = viewModelScope.launch(Dispatchers.IO) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import at.bitfire.icsdroid.BaseSyncWorker
import at.bitfire.icsdroid.PermissionUtils
import at.bitfire.icsdroid.SyncWorker
import at.bitfire.icsdroid.model.SubscriptionsModel
import at.bitfire.icsdroid.service.ComposableStartupService
import at.bitfire.icsdroid.ui.InfoActivity
Expand Down Expand Up @@ -61,7 +61,7 @@ class CalendarListActivity: AppCompatActivity() {
requestCalendarPermissions = PermissionUtils.registerCalendarPermissionRequest(this) {
model.checkSyncSettings()

SyncWorker.run(this)
BaseSyncWorker.run(this)
}

// Register the notifications permission request
Expand Down
Loading