Schedule sync with rate limit

pull/163/head
Koitharu 4 years ago
parent 318486d62b
commit a9f3ab259a
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -5,15 +5,16 @@ import android.accounts.AccountManager
import android.content.ContentResolver import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.os.Bundle import android.os.Bundle
import android.util.ArrayMap
import androidx.room.InvalidationTracker import androidx.room.InvalidationTracker
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.launch import org.koitharu.kotatsu.BuildConfig
import kotlinx.coroutines.withContext
import org.koitharu.kotatsu.R import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import java.util.concurrent.TimeUnit
class SyncController( class SyncController(
context: Context, context: Context,
@ -21,6 +22,12 @@ class SyncController(
private val am = AccountManager.get(context) private val am = AccountManager.get(context)
private val accountType = context.getString(R.string.account_type_sync) private val accountType = context.getString(R.string.account_type_sync)
private val minSyncInterval = if (BuildConfig.DEBUG) {
TimeUnit.SECONDS.toMillis(5)
} else {
TimeUnit.MINUTES.toMillis(4)
}
private val jobs = ArrayMap<String, Job>(2)
override fun onInvalidated(tables: MutableSet<String>) { override fun onInvalidated(tables: MutableSet<String>) {
requestSync( requestSync(
@ -29,6 +36,17 @@ class SyncController(
) )
} }
fun getLastSync(account: Account, authority: String): Long {
val key = "last_sync_" + authority.substringAfterLast('.')
val rawValue = am.getUserData(account, key) ?: return 0L
return rawValue.toLongOrNull() ?: 0L
}
fun setLastSync(account: Account, authority: String, time: Long) {
val key = "last_sync_" + authority.substringAfterLast('.')
am.setUserData(account, key, time.toString())
}
suspend fun requestFullSync() = withContext(Dispatchers.Default) { suspend fun requestFullSync() = withContext(Dispatchers.Default) {
requestSyncImpl(favourites = true, history = true) requestSyncImpl(favourites = true, history = true)
} }
@ -46,22 +64,41 @@ class SyncController(
if (!ContentResolver.getMasterSyncAutomatically()) { if (!ContentResolver.getMasterSyncAutomatically()) {
return return
} }
// TODO limit frequency
if (favourites) { if (favourites) {
requestSyncForAuthority(account, AUTHORITY_FAVOURITES) scheduleSync(account, AUTHORITY_FAVOURITES)
} }
if (history) { if (history) {
requestSyncForAuthority(account, AUTHORITY_HISTORY) scheduleSync(account, AUTHORITY_HISTORY)
} }
} }
private fun requestSyncForAuthority(account: Account, authority: String) { private fun scheduleSync(account: Account, authority: String) {
if ( if (
ContentResolver.getSyncAutomatically(account, AUTHORITY_FAVOURITES) && !ContentResolver.getSyncAutomatically(account, AUTHORITY_FAVOURITES) ||
!ContentResolver.isSyncActive(account, authority) && ContentResolver.isSyncActive(account, authority) ||
!ContentResolver.isSyncPending(account, authority) ContentResolver.isSyncPending(account, authority)
) { ) {
return
}
val job = jobs[authority]
if (job?.isActive == true) {
// already scheduled
return
}
val lastSyncTime = getLastSync(account, authority)
val timeLeft = System.currentTimeMillis() - lastSyncTime + minSyncInterval
if (timeLeft <= 0) {
jobs.remove(authority)
ContentResolver.requestSync(account, authority, Bundle.EMPTY) ContentResolver.requestSync(account, authority, Bundle.EMPTY)
} else {
jobs[authority] = processLifecycleScope.launch(Dispatchers.Default) {
try {
delay(timeLeft)
} finally {
// run even if scope cancelled
ContentResolver.requestSync(account, authority, Bundle.EMPTY)
}
}
} }
} }

@ -6,6 +6,7 @@ import android.content.ContentProviderClient
import android.content.Context import android.content.Context
import android.content.SyncResult import android.content.SyncResult
import android.os.Bundle import android.os.Bundle
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.domain.SyncHelper import org.koitharu.kotatsu.sync.domain.SyncHelper
import org.koitharu.kotatsu.utils.ext.onError import org.koitharu.kotatsu.utils.ext.onError
@ -21,6 +22,7 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
val syncHelper = SyncHelper(context, account, provider) val syncHelper = SyncHelper(context, account, provider)
runCatching { runCatching {
syncHelper.syncFavourites(syncResult) syncHelper.syncFavourites(syncResult)
SyncController(context).setLastSync(account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError) }.onFailure(syncResult::onError)
} }
} }

@ -6,6 +6,7 @@ import android.content.ContentProviderClient
import android.content.Context import android.content.Context
import android.content.SyncResult import android.content.SyncResult
import android.os.Bundle import android.os.Bundle
import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.domain.SyncHelper import org.koitharu.kotatsu.sync.domain.SyncHelper
import org.koitharu.kotatsu.utils.ext.onError import org.koitharu.kotatsu.utils.ext.onError
@ -21,6 +22,7 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context
val syncHelper = SyncHelper(context, account, provider) val syncHelper = SyncHelper(context, account, provider)
runCatching { runCatching {
syncHelper.syncHistory(syncResult) syncHelper.syncHistory(syncResult)
SyncController(context).setLastSync(account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError) }.onFailure(syncResult::onError)
} }
} }

@ -15,13 +15,13 @@ import androidx.core.app.ActivityOptionsCompat
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.coroutineScope import androidx.lifecycle.coroutineScope
import androidx.work.CoroutineWorker import androidx.work.CoroutineWorker
import kotlin.coroutines.resume
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import okio.IOException import okio.IOException
import org.json.JSONException import org.json.JSONException
import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.BuildConfig
import kotlin.coroutines.resume
val Context.connectivityManager: ConnectivityManager val Context.connectivityManager: ConnectivityManager
get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager get() = getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
@ -82,4 +82,5 @@ fun SyncResult.onError(error: Throwable) {
is JSONException -> stats.numParseExceptions++ is JSONException -> stats.numParseExceptions++
else -> if (BuildConfig.DEBUG) throw error else -> if (BuildConfig.DEBUG) throw error
} }
error.printStackTraceDebug()
} }
Loading…
Cancel
Save