diff --git a/app/build.gradle b/app/build.gradle
index 46c5b98b9..777a43e3c 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,8 +15,8 @@ android {
applicationId 'org.koitharu.kotatsu'
minSdkVersion 21
targetSdkVersion 33
- versionCode 534
- versionName '5.0-b1'
+ versionCode 535
+ versionName '5.0-b2'
generatedDensities = []
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
diff --git a/app/src/debug/res/values/constants.xml b/app/src/debug/res/values/constants.xml
index 9950eed79..44e14a54f 100644
--- a/app/src/debug/res/values/constants.xml
+++ b/app/src/debug/res/values/constants.xml
@@ -1,4 +1,6 @@
org.kotatsu.debug.sync
+ org.koitharu.kotatsu.debug.history
+ org.koitharu.kotatsu.debug.favourites
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 5893d9188..f70692fb5 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -227,13 +227,13 @@
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
index 978e0d235..06d20579f 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainViewModel.kt
@@ -8,7 +8,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.combine
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BaseViewModel
-import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.exceptions.EmptyHistoryException
import org.koitharu.kotatsu.core.github.AppUpdateRepository
import org.koitharu.kotatsu.core.prefs.AppSettings
@@ -16,7 +15,6 @@ import org.koitharu.kotatsu.core.prefs.observeAsFlow
import org.koitharu.kotatsu.core.prefs.observeAsLiveData
import org.koitharu.kotatsu.history.domain.HistoryRepository
import org.koitharu.kotatsu.parsers.model.Manga
-import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
@@ -27,8 +25,6 @@ class MainViewModel @Inject constructor(
private val historyRepository: HistoryRepository,
private val appUpdateRepository: AppUpdateRepository,
private val trackingRepository: TrackingRepository,
- syncController: SyncController,
- database: MangaDatabase,
private val settings: AppSettings,
) : BaseViewModel() {
@@ -61,9 +57,6 @@ class MainViewModel @Inject constructor(
launchJob {
appUpdateRepository.fetchUpdate()
}
- launchJob {
- syncController.requestFullSyncAndGc(database)
- }
}
fun openLastReader() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
index b8d203f85..590352a16 100644
--- a/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/shelf/ui/ShelfViewModel.kt
@@ -34,6 +34,7 @@ import org.koitharu.kotatsu.shelf.domain.ShelfContent
import org.koitharu.kotatsu.shelf.domain.ShelfRepository
import org.koitharu.kotatsu.shelf.domain.ShelfSection
import org.koitharu.kotatsu.shelf.ui.model.ShelfSectionModel
+import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.tracker.domain.TrackingRepository
import org.koitharu.kotatsu.utils.SingleLiveEvent
import org.koitharu.kotatsu.utils.asFlowLiveData
@@ -46,6 +47,7 @@ class ShelfViewModel @Inject constructor(
private val favouritesRepository: FavouritesRepository,
private val trackingRepository: TrackingRepository,
private val settings: AppSettings,
+ syncController: SyncController,
networkState: NetworkState,
) : BaseViewModel(), ListExtraProvider {
@@ -63,6 +65,12 @@ class ShelfViewModel @Inject constructor(
emit(listOf(e.toErrorState(canRetry = false)))
}.asFlowLiveData(viewModelScope.coroutineContext + Dispatchers.Default, listOf(LoadingState))
+ init {
+ launchJob(Dispatchers.Default) {
+ syncController.requestFullSync()
+ }
+ }
+
override suspend fun getCounter(mangaId: Long): Int {
return if (settings.isTrackerEnabled) {
trackingRepository.getNewChaptersCount(mangaId)
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt
index 58ff58b2f..dc85a124a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt
@@ -3,20 +3,21 @@ package org.koitharu.kotatsu.sync.domain
import android.accounts.Account
import android.accounts.AccountManager
import android.content.ContentResolver
+import android.content.ContentResolver.SYNC_OBSERVER_TYPE_ACTIVE
import android.content.Context
import android.os.Bundle
-import androidx.collection.ArrayMap
import androidx.room.InvalidationTracker
import androidx.room.withTransaction
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.delay
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.channels.trySendBlocking
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.launch
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.coroutines.withContext
-import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
@@ -25,24 +26,21 @@ import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.utils.ext.processLifecycleScope
import java.util.concurrent.TimeUnit
import javax.inject.Inject
+import javax.inject.Provider
import javax.inject.Singleton
@Singleton
class SyncController @Inject constructor(
@ApplicationContext context: Context,
+ private val dbProvider: Provider,
) : InvalidationTracker.Observer(arrayOf(TABLE_HISTORY, TABLE_FAVOURITES, TABLE_FAVOURITE_CATEGORIES)) {
+ private val authorityHistory = context.getString(R.string.sync_authority_history)
+ private val authorityFavourites = context.getString(R.string.sync_authority_favourites)
private val am = AccountManager.get(context)
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 mutex = Mutex()
- private val jobs = ArrayMap(2)
- private val defaultGcPeriod: Long // gc period if sync disabled
- get() = TimeUnit.HOURS.toMillis(2)
+ private val defaultGcPeriod = TimeUnit.DAYS.toMillis(2) // gc period if sync disabled
override fun onInvalidated(tables: Set) {
requestSync(
@@ -57,79 +55,52 @@ class SyncController @Inject constructor(
return rawValue.toLongOrNull() ?: 0L
}
- fun setLastSync(account: Account, authority: String, time: Long) {
- val key = "last_sync_" + authority.substringAfterLast('.')
- am.setUserData(account, key, time.toString())
+ fun observeSyncStatus(): Flow = callbackFlow {
+ val handle = ContentResolver.addStatusChangeListener(SYNC_OBSERVER_TYPE_ACTIVE) { which ->
+ trySendBlocking(which and SYNC_OBSERVER_TYPE_ACTIVE != 0)
+ }
+ awaitClose { ContentResolver.removeStatusChangeListener(handle) }
}
suspend fun requestFullSync() = withContext(Dispatchers.Default) {
- requestSyncImpl(favourites = true, history = true, db = null)
- }
-
- suspend fun requestFullSyncAndGc(database: MangaDatabase) = withContext(Dispatchers.Default) {
- requestSyncImpl(favourites = true, history = true, db = database)
+ requestSyncImpl(favourites = true, history = true)
}
private fun requestSync(favourites: Boolean, history: Boolean) = processLifecycleScope.launch(Dispatchers.Default) {
- requestSyncImpl(favourites = favourites, history = history, db = null)
+ requestSyncImpl(favourites = favourites, history = history)
}
- private suspend fun requestSyncImpl(favourites: Boolean, history: Boolean, db: MangaDatabase?) = mutex.withLock {
+ private suspend fun requestSyncImpl(favourites: Boolean, history: Boolean) = mutex.withLock {
if (!favourites && !history) {
return
}
+ val db = dbProvider.get()
val account = peekAccount()
if (account == null || !ContentResolver.getMasterSyncAutomatically()) {
- db?.gc(favourites, history)
+ db.gc(favourites, history)
return
}
var gcHistory = false
var gcFavourites = false
if (favourites) {
- if (ContentResolver.getSyncAutomatically(account, AUTHORITY_FAVOURITES)) {
- scheduleSync(account, AUTHORITY_FAVOURITES)
+ if (ContentResolver.getSyncAutomatically(account, authorityFavourites)) {
+ ContentResolver.requestSync(account, authorityFavourites, Bundle.EMPTY)
} else {
gcFavourites = true
}
}
if (history) {
- if (ContentResolver.getSyncAutomatically(account, AUTHORITY_HISTORY)) {
- scheduleSync(account, AUTHORITY_HISTORY)
+ if (ContentResolver.getSyncAutomatically(account, authorityHistory)) {
+ ContentResolver.requestSync(account, authorityHistory, Bundle.EMPTY)
} else {
gcHistory = true
}
}
- if (db != null && (gcHistory || gcFavourites)) {
+ if (gcHistory || gcFavourites) {
db.gc(gcFavourites, gcHistory)
}
}
- private fun scheduleSync(account: Account, authority: String) {
- if (ContentResolver.isSyncActive(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)
- } else {
- jobs[authority] = processLifecycleScope.launch(Dispatchers.Default) {
- try {
- delay(timeLeft)
- } finally {
- // run even if scope cancelled
- ContentResolver.requestSync(account, authority, Bundle.EMPTY)
- }
- }
- }
- }
-
private fun peekAccount(): Account? {
return am.getAccountsByType(accountType).firstOrNull()
}
@@ -144,4 +115,14 @@ class SyncController @Inject constructor(
favouriteCategoriesDao.gc(deletedAt)
}
}
+
+ companion object {
+
+ @JvmStatic
+ fun setLastSync(context: Context, account: Account, authority: String, time: Long) {
+ val key = "last_sync_" + authority.substringAfterLast('.')
+ val am = AccountManager.get(context)
+ am.setUserData(account, key, time.toString())
+ }
+ }
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt
index 79d8a1d90..8b514a0b3 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt
@@ -1,7 +1,12 @@
package org.koitharu.kotatsu.sync.domain
import android.accounts.Account
-import android.content.*
+import android.content.ContentProviderClient
+import android.content.ContentProviderOperation
+import android.content.ContentProviderResult
+import android.content.Context
+import android.content.OperationApplicationException
+import android.content.SyncResult
import android.database.Cursor
import android.net.Uri
import androidx.annotation.WorkerThread
@@ -11,7 +16,12 @@ import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
import org.koitharu.kotatsu.R
-import org.koitharu.kotatsu.core.db.*
+import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
+import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
+import org.koitharu.kotatsu.core.db.TABLE_HISTORY
+import org.koitharu.kotatsu.core.db.TABLE_MANGA
+import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
+import org.koitharu.kotatsu.core.db.TABLE_TAGS
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
@@ -23,9 +33,6 @@ import org.koitharu.kotatsu.utils.ext.toJson
import org.koitharu.kotatsu.utils.ext.toRequestBody
import java.util.concurrent.TimeUnit
-const val AUTHORITY_HISTORY = "org.koitharu.kotatsu.history"
-const val AUTHORITY_FAVOURITES = "org.koitharu.kotatsu.favourites"
-
private const val FIELD_TIMESTAMP = "timestamp"
/**
@@ -38,6 +45,8 @@ class SyncHelper(
private val provider: ContentProviderClient,
) {
+ private val authorityHistory = context.getString(R.string.sync_authority_history)
+ private val authorityFavourites = context.getString(R.string.sync_authority_favourites)
private val httpClient = OkHttpClient.Builder()
.authenticator(SyncAuthenticator(context, account, SyncAuthApi(context, OkHttpClient())))
.addInterceptor(SyncInterceptor(context, account))
@@ -86,13 +95,13 @@ class SyncHelper(
}
private fun upsertHistory(json: JSONArray, timestamp: Long): Array {
- val uri = uri(AUTHORITY_HISTORY, TABLE_HISTORY)
+ val uri = uri(authorityHistory, TABLE_HISTORY)
val operations = ArrayList()
operations += ContentProviderOperation.newDelete(uri)
.withSelection("updated_at < ?", arrayOf(timestamp.toString()))
.build()
json.mapJSONTo(operations) { jo ->
- operations.addAll(upsertManga(jo.removeJSONObject("manga"), AUTHORITY_HISTORY))
+ operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityHistory))
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
.build()
@@ -101,7 +110,7 @@ class SyncHelper(
}
private fun upsertFavouriteCategories(json: JSONArray, timestamp: Long): Array {
- val uri = uri(AUTHORITY_FAVOURITES, TABLE_FAVOURITE_CATEGORIES)
+ val uri = uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES)
val operations = ArrayList()
operations += ContentProviderOperation.newDelete(uri)
.withSelection("created_at < ?", arrayOf(timestamp.toString()))
@@ -115,13 +124,13 @@ class SyncHelper(
}
private fun upsertFavourites(json: JSONArray, timestamp: Long): Array {
- val uri = uri(AUTHORITY_FAVOURITES, TABLE_FAVOURITES)
+ val uri = uri(authorityFavourites, TABLE_FAVOURITES)
val operations = ArrayList()
operations += ContentProviderOperation.newDelete(uri)
.withSelection("created_at < ?", arrayOf(timestamp.toString()))
.build()
json.mapJSONTo(operations) { jo ->
- operations.addAll(upsertManga(jo.removeJSONObject("manga"), AUTHORITY_FAVOURITES))
+ operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityFavourites))
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
.build()
@@ -142,25 +151,25 @@ class SyncHelper(
contentValuesOf(
"manga_id" to json.getLong("manga_id"),
"tag_id" to tag.getLong("tag_id"),
- )
+ ),
).build()
}
result.add(
0,
ContentProviderOperation.newInsert(uri(authority, TABLE_MANGA))
.withValues(json.toContentValues())
- .build()
+ .build(),
)
return result
}
private fun getHistory(): JSONArray {
- return provider.query(AUTHORITY_HISTORY, TABLE_HISTORY).use { cursor ->
+ return provider.query(authorityHistory, TABLE_HISTORY).use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
val jo = cursor.toJson()
- jo.put("manga", getManga(AUTHORITY_HISTORY, jo.getLong("manga_id")))
+ jo.put("manga", getManga(authorityHistory, jo.getLong("manga_id")))
json.put(jo)
} while (cursor.moveToNext())
}
@@ -169,12 +178,12 @@ class SyncHelper(
}
private fun getFavourites(): JSONArray {
- return provider.query(AUTHORITY_FAVOURITES, TABLE_FAVOURITES).use { cursor ->
+ return provider.query(authorityFavourites, TABLE_FAVOURITES).use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
val jo = cursor.toJson()
- jo.put("manga", getManga(AUTHORITY_FAVOURITES, jo.getLong("manga_id")))
+ jo.put("manga", getManga(authorityFavourites, jo.getLong("manga_id")))
json.put(jo)
} while (cursor.moveToNext())
}
@@ -183,7 +192,7 @@ class SyncHelper(
}
private fun getFavouriteCategories(): JSONArray {
- return provider.query(AUTHORITY_FAVOURITES, TABLE_FAVOURITE_CATEGORIES).use { cursor ->
+ return provider.query(authorityFavourites, TABLE_FAVOURITE_CATEGORIES).use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
@@ -247,15 +256,15 @@ class SyncHelper(
val deletedAt = System.currentTimeMillis() - defaultGcPeriod
val selection = "deleted_at != 0 AND deleted_at < ?"
val args = arrayOf(deletedAt.toString())
- provider.delete(uri(AUTHORITY_FAVOURITES, TABLE_FAVOURITES), selection, args)
- provider.delete(uri(AUTHORITY_FAVOURITES, TABLE_FAVOURITE_CATEGORIES), selection, args)
+ provider.delete(uri(authorityFavourites, TABLE_FAVOURITES), selection, args)
+ provider.delete(uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES), selection, args)
}
private fun gcHistory() {
val deletedAt = System.currentTimeMillis() - defaultGcPeriod
val selection = "deleted_at != 0 AND deleted_at < ?"
val args = arrayOf(deletedAt.toString())
- provider.delete(uri(AUTHORITY_HISTORY, TABLE_HISTORY), selection, args)
+ provider.delete(uri(authorityHistory, TABLE_HISTORY), selection, args)
}
private fun ContentProviderClient.query(authority: String, table: String): Cursor {
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt
index 8515f4735..cdeacf2db 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/favourites/FavouritesSyncAdapter.kt
@@ -27,7 +27,7 @@ class FavouritesSyncAdapter(context: Context) : AbstractThreadedSyncAdapter(cont
val syncHelper = SyncHelper(context, account, provider)
runCatchingCancellable {
syncHelper.syncFavourites(syncResult)
- SyncController(context).setLastSync(account, authority, System.currentTimeMillis())
+ SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt
index 8d0b3f4e8..d56529bb1 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/history/HistorySyncAdapter.kt
@@ -27,7 +27,7 @@ class HistorySyncAdapter(context: Context) : AbstractThreadedSyncAdapter(context
val syncHelper = SyncHelper(context, account, provider)
runCatchingCancellable {
syncHelper.syncHistory(syncResult)
- SyncController(context).setLastSync(account, authority, System.currentTimeMillis())
+ SyncController.setLastSync(context, account, authority, System.currentTimeMillis())
}.onFailure(syncResult::onError)
}
}
diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml
index cf8daf367..90dd945fb 100644
--- a/app/src/main/res/values/constants.xml
+++ b/app/src/main/res/values/constants.xml
@@ -15,6 +15,8 @@
6cd8e6349e9a36bc1fc1ab97703c9fd1
SxhkCVnqVLbGogvi
xPDACTLHnHU9Nfjv
+ org.koitharu.kotatsu.history
+ org.koitharu.kotatsu.favourites
- -1
- 1
diff --git a/app/src/main/res/xml/authenticator_sync.xml b/app/src/main/res/xml/authenticator_sync.xml
index 371460404..d40082822 100644
--- a/app/src/main/res/xml/authenticator_sync.xml
+++ b/app/src/main/res/xml/authenticator_sync.xml
@@ -3,5 +3,5 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountPreferences="@xml/pref_sync"
android:accountType="@string/account_type_sync"
- android:icon="@mipmap/ic_launcher"
- android:label="@string/app_name" />
\ No newline at end of file
+ android:icon="@mipmap/ic_launcher_round"
+ android:label="@string/app_name" />
diff --git a/app/src/main/res/xml/sync_favourites.xml b/app/src/main/res/xml/sync_favourites.xml
index 1365d5f03..8de3b0562 100644
--- a/app/src/main/res/xml/sync_favourites.xml
+++ b/app/src/main/res/xml/sync_favourites.xml
@@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type_sync"
android:allowParallelSyncs="false"
- android:contentAuthority="${applicationId}.favourites"
+ android:contentAuthority="@string/sync_authority_favourites"
android:isAlwaysSyncable="true"
android:supportsUploading="true"
android:userVisible="true" />
diff --git a/app/src/main/res/xml/sync_history.xml b/app/src/main/res/xml/sync_history.xml
index 6bca603ea..cfc96d27f 100644
--- a/app/src/main/res/xml/sync_history.xml
+++ b/app/src/main/res/xml/sync_history.xml
@@ -3,7 +3,7 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:accountType="@string/account_type_sync"
android:allowParallelSyncs="false"
- android:contentAuthority="${applicationId}.history"
+ android:contentAuthority="@string/sync_authority_history"
android:isAlwaysSyncable="true"
android:supportsUploading="true"
android:userVisible="true" />