Commit e074e0ae authored by Ricki Hirner's avatar Ricki Hirner

Use WorkManager

* Use WorkManager for sync (to avoid interruption of sync adapter by Android after 1 min without network traffic)
* retain only one main event per UID (to remove duplicates which may have been created by previous sync failures)
parent 4a0d326c
......@@ -49,6 +49,7 @@ dependencies {
implementation 'com.android.support:appcompat-v7:27.1.1'
implementation 'com.android.support:cardview-v7:27.1.1'
implementation 'com.android.support:design:27.1.1'
implementation 'android.arch.work:work-runtime-ktx:1.0.0-alpha08'
implementation 'com.github.yukuku:ambilwarna:2.0.1'
implementation 'commons-io:commons-io:2.6'
......
......@@ -33,10 +33,7 @@ object AppAccount {
}
}
fun isSyncActive() =
ContentResolver.isSyncActive(AppAccount.account, CalendarContract.AUTHORITY)
fun getSyncInterval(context: Context): Long {
fun syncInterval(): Long {
var syncInterval = SYNC_INTERVAL_MANUALLY
if (ContentResolver.getSyncAutomatically(account, CalendarContract.AUTHORITY))
for (sync in ContentResolver.getPeriodicSyncs(account, CalendarContract.AUTHORITY))
......@@ -44,7 +41,7 @@ object AppAccount {
return syncInterval
}
fun setSyncInterval(syncInterval: Long) {
fun syncInterval(syncInterval: Long) {
if (syncInterval == SYNC_INTERVAL_MANUALLY) {
Log.i(Constants.TAG, "Disabling automatic synchronization")
ContentResolver.setSyncAutomatically(account, CalendarContract.AUTHORITY, false)
......
This diff is collapsed.
/*
* Copyright (c) Ricki Hirner (bitfire web engineering).
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the GNU Public License v3.0
* which accompanies this distribution, and is available at
* http://www.gnu.org/licenses/gpl.html
*/
package at.bitfire.icsdroid
import android.accounts.Account
import android.content.ContentProviderClient
import android.os.Build
import android.provider.CalendarContract
import android.util.Log
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.icsdroid.db.LocalCalendar
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
class SyncWorker: Worker() {
companion object {
private const val NAME = "SyncWorker"
val syncRunning = AtomicBoolean()
/**
* Enqueues a sync job for soon execution.
*/
fun run() {
val request = OneTimeWorkRequestBuilder<SyncWorker>().build()
WorkManager.getInstance()
.beginUniqueWork(NAME, ExistingWorkPolicy.KEEP, request)
.enqueue()
}
fun liveStatus() = WorkManager.getInstance().getStatusesForUniqueWork(NAME)
}
private val syncQueue = LinkedBlockingQueue<Runnable>()
private val syncExecutor = ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors(),
5, TimeUnit.SECONDS,
syncQueue
)
override fun doWork(): Result {
if (syncRunning.get()) {
Log.w(Constants.TAG, "There's already another sync running, aborting")
return Result.SUCCESS
}
applicationContext.contentResolver.acquireContentProviderClient(CalendarContract.AUTHORITY)?.let { providerClient ->
try {
syncRunning.set(true)
return performSync(AppAccount.account, providerClient)
} finally {
syncRunning.set(false)
if (Build.VERSION.SDK_INT >= 24)
providerClient.close()
else
providerClient.release()
}
}
return Result.FAILURE
}
private fun performSync(account: Account, provider: ContentProviderClient): Result {
Log.i(Constants.TAG, "Synchronizing ${account.name}")
try {
LocalCalendar.findAll(account, provider)
.filter { it.isSynced }
.forEach { syncExecutor.execute(ProcessEventsTask(applicationContext, it)) }
syncExecutor.shutdown()
while (!syncExecutor.awaitTermination(1, TimeUnit.MINUTES))
Log.i(Constants.TAG, "Sync still running for another minute")
} catch (e: CalendarStorageException) {
Log.e(Constants.TAG, "Calendar storage exception", e)
} catch (e: InterruptedException) {
Log.e(Constants.TAG, "Thread interrupted", e)
}
return Result.SUCCESS
}
}
......@@ -120,19 +120,20 @@ class LocalCalendar private constructor(
fun queryByUID(uid: String) =
queryEvents("${Events._SYNC_ID}=?", arrayOf(uid))
fun retainByUID(uids: Set<String>): Int {
fun retainByUID(uids: MutableSet<String>): Int {
var deleted = 0
try {
provider.query(syncAdapterURI(Events.CONTENT_URI, account),
arrayOf(Events._ID, Events._SYNC_ID, Events.ORIGINAL_SYNC_ID),
"${Events.CALENDAR_ID}=?", arrayOf(id.toString()), null).use { row ->
"${Events.CALENDAR_ID}=? AND ${Events.ORIGINAL_SYNC_ID} IS NULL", arrayOf(id.toString()), null).use { row ->
while (row.moveToNext()) {
val eventId = row.getLong(0)
val syncId = row.getString(1)
val originalSyncId = row.getString(2)
if (!uids.contains(syncId) && !uids.contains(originalSyncId)) {
if (!uids.contains(syncId)) {
provider.delete(syncAdapterURI(ContentUris.withAppendedId(Events.CONTENT_URI, eventId), account), null, null)
deleted++
uids -= syncId
}
}
}
......
......@@ -10,12 +10,12 @@ package at.bitfire.icsdroid.ui
import android.Manifest
import android.annotation.SuppressLint
import android.arch.lifecycle.Observer
import android.content.*
import android.content.pm.PackageManager
import android.database.ContentObserver
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.os.PowerManager
import android.provider.CalendarContract
import android.provider.Settings
......@@ -31,21 +31,16 @@ import android.util.Log
import android.view.*
import android.widget.AdapterView
import android.widget.ArrayAdapter
import androidx.work.State
import at.bitfire.ical4android.CalendarStorageException
import at.bitfire.icsdroid.AppAccount
import at.bitfire.icsdroid.BuildConfig
import at.bitfire.icsdroid.Constants
import at.bitfire.icsdroid.R
import at.bitfire.icsdroid.*
import at.bitfire.icsdroid.db.LocalCalendar
import kotlinx.android.synthetic.main.calendar_list_activity.*
import kotlinx.android.synthetic.main.calendar_list_item.view.*
import java.text.DateFormat
import java.util.*
class CalendarListActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<List<LocalCalendar>>, AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener, SyncStatusObserver {
private var syncStatusHandle: Any? = null
private var syncStatusHandler: Handler? = null
class CalendarListActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<List<LocalCalendar>>, AdapterView.OnItemClickListener, SwipeRefreshLayout.OnRefreshListener {
private var listAdapter: CalendarListAdapter? = null
......@@ -85,6 +80,17 @@ class CalendarListActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<L
checkSyncSettings()
}
}, false)
SyncWorker.liveStatus().observe(this, Observer { statuses ->
val running = statuses?.any { it.state == State.RUNNING } ?: false
Log.d(Constants.TAG, "Sync running: $running")
refresh.isRefreshing = running
})
}
override fun onDestroy() {
super.onDestroy()
SyncWorker.liveStatus().removeObservers(this)
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
......@@ -109,43 +115,21 @@ class CalendarListActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<L
override fun onResume() {
super.onResume()
val handler = Handler(Handler.Callback {
val syncActive = AppAccount.isSyncActive()
Log.d(Constants.TAG, "Is sync. active? ${if (syncActive) "yes" else "no"}")
// workaround: see https://code.google.com/p/android/issues/detail?id=77712
refresh.post {
refresh.isRefreshing = syncActive
}
true
})
syncStatusHandle = ContentResolver.addStatusChangeListener(ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE, this)
handler.sendEmptyMessage(0)
syncStatusHandler = handler
checkSyncSettings()
}
override fun onPause() {
super.onPause()
if (syncStatusHandle != null)
ContentResolver.removeStatusChangeListener(syncStatusHandle)
}
override fun onStatusChanged(which: Int) {
if (which == ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE)
syncStatusHandler?.sendEmptyMessage(0)
}
private fun checkSyncSettings() {
snackBar?.dismiss()
snackBar = null
when {
AppAccount.getSyncInterval(this) == AppAccount.SYNC_INTERVAL_MANUALLY -> {
// periodic sync not enabled
AppAccount.syncInterval() == AppAccount.SYNC_INTERVAL_MANUALLY -> {
snackBar = Snackbar.make(coordinator, R.string.calendar_list_sync_interval_manually, Snackbar.LENGTH_INDEFINITE)
snackBar?.show()
}
// automatic sync not enabled
!ContentResolver.getMasterSyncAutomatically() -> {
snackBar = Snackbar.make(coordinator, R.string.calendar_list_master_sync_disabled, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.calendar_list_master_sync_enable) {
......@@ -154,10 +138,10 @@ class CalendarListActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<L
snackBar?.show()
}
// Android >= 6 AND not whitelisted from battery saving AND sync interval < 1 day
// periodic sync enabled AND Android >= 6 AND not whitelisted from battery saving AND sync interval < 1 day
Build.VERSION.SDK_INT >= 23 &&
!(getSystemService(Context.POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(BuildConfig.APPLICATION_ID) &&
AppAccount.getSyncInterval(this) < 86400 -> {
AppAccount.syncInterval() < 86400 -> {
snackBar = Snackbar.make(coordinator, R.string.calendar_list_battery_whitelist, Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.calendar_list_battery_whitelist_settings) { _ ->
val intent = Intent(Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS)
......@@ -207,10 +191,7 @@ class CalendarListActivity: AppCompatActivity(), LoaderManager.LoaderCallbacks<L
}
override fun onRefresh() {
val extras = Bundle(2)
extras.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true)
extras.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true)
ContentResolver.requestSync(AppAccount.account, CalendarContract.AUTHORITY, extras)
SyncWorker.run()
}
fun onShowInfo(item: MenuItem) {
......
......@@ -29,13 +29,13 @@ class SyncIntervalDialogFragment: DialogFragment() {
val v = requireActivity().layoutInflater.inflate(R.layout.set_sync_interval, null)
val currentSyncInterval = AppAccount.getSyncInterval(requireActivity())
val currentSyncInterval = AppAccount.syncInterval()
if (syncIntervalSeconds.contains(currentSyncInterval))
v.sync_interval.setSelection(syncIntervalSeconds.indexOf(currentSyncInterval))
builder .setView(v)
.setPositiveButton(R.string.set_sync_interval_save) { _, _ ->
AppAccount.setSyncInterval(syncIntervalSeconds[v.sync_interval.selectedItemPosition])
AppAccount.syncInterval(syncIntervalSeconds[v.sync_interval.selectedItemPosition])
}
return builder.create()
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment