diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 2d3c76ebf..bf3dfbecf 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -13,6 +13,9 @@
+
+
+
().theme)
registerActivityLifecycleCallbacks(get())
- val widgetUpdater = WidgetUpdater(applicationContext)
- widgetUpdater.subscribeToFavourites(get())
- widgetUpdater.subscribeToHistory(get())
}
private fun initKoin() {
diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt
index 215d02259..18e206142 100644
--- a/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/core/db/DatabaseModule.kt
@@ -1,9 +1,16 @@
package org.koitharu.kotatsu.core.db
+import androidx.room.InvalidationTracker
import org.koin.android.ext.koin.androidContext
import org.koin.dsl.module
val databaseModule
get() = module {
- single { MangaDatabase(androidContext()) }
+ single {
+ MangaDatabase(androidContext()).also { db ->
+ getAll().forEach {
+ db.invalidationTracker.addObserver(it)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
index 49bde074a..4ec967ece 100644
--- a/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/main/ui/MainActivity.kt
@@ -27,6 +27,7 @@ import com.google.android.material.navigation.NavigationView
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
+import kotlinx.coroutines.yield
import org.koin.android.ext.android.get
import org.koin.androidx.viewmodel.ext.android.viewModel
import org.koitharu.kotatsu.R
@@ -56,6 +57,7 @@ import org.koitharu.kotatsu.settings.newsources.NewSourcesDialogFragment
import org.koitharu.kotatsu.settings.onboard.OnboardDialogFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
+import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.tracker.ui.FeedFragment
import org.koitharu.kotatsu.tracker.work.TrackWorker
import org.koitharu.kotatsu.utils.VoiceInputContract
@@ -440,6 +442,8 @@ class MainActivity :
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
}
+ yield()
+ get().requestFullSync()
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt
index d9de212d6..b471ea43d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt
@@ -1,5 +1,7 @@
package org.koitharu.kotatsu.settings
+import android.accounts.AccountManager
+import android.content.ActivityNotFoundException
import android.content.Intent
import android.content.SharedPreferences
import android.os.Bundle
@@ -7,7 +9,10 @@ import android.provider.Settings
import android.view.View
import androidx.preference.ListPreference
import androidx.preference.Preference
+import com.google.android.material.snackbar.Snackbar
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import org.koin.android.ext.android.inject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
@@ -17,6 +22,8 @@ import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.local.data.LocalStorageManager
import org.koitharu.kotatsu.parsers.util.names
import org.koitharu.kotatsu.settings.utils.SliderPreference
+import org.koitharu.kotatsu.sync.domain.AUTHORITY_FAVOURITES
+import org.koitharu.kotatsu.sync.domain.AUTHORITY_HISTORY
import org.koitharu.kotatsu.utils.ext.getStorageName
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
@@ -60,6 +67,11 @@ class ContentSettingsFragment :
settings.subscribe(this)
}
+ override fun onResume() {
+ super.onResume()
+ bindSyncSummary()
+ }
+
override fun onDestroyView() {
settings.unsubscribe(this)
super.onDestroyView()
@@ -78,10 +90,6 @@ class ContentSettingsFragment :
AppSettings.KEY_SOURCES_HIDDEN -> {
bindRemoteSourcesSummary()
}
- AppSettings.KEY_SYNC -> {
- val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
- startActivity(intent)
- }
}
}
@@ -96,6 +104,23 @@ class ContentSettingsFragment :
.show()
true
}
+ AppSettings.KEY_SYNC -> {
+ val am = AccountManager.get(requireContext())
+ val accountType = getString(R.string.account_type_sync)
+ if (am.getAccountsByType(accountType).firstOrNull() == null) {
+ am.addAccount(accountType, accountType, null, null, requireActivity(), null, null)
+ } else {
+ val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
+ intent.putExtra(Settings.EXTRA_ACCOUNT_TYPES, arrayOf(accountType))
+ intent.putExtra(Settings.EXTRA_AUTHORITIES, arrayOf(AUTHORITY_HISTORY, AUTHORITY_FAVOURITES))
+ try {
+ startActivity(intent)
+ } catch (_: ActivityNotFoundException) {
+ Snackbar.make(listView, R.string.operation_not_supported, Snackbar.LENGTH_SHORT).show()
+ }
+ }
+ true
+ }
else -> super.onPreferenceTreeClick(preference)
}
}
@@ -119,4 +144,16 @@ class ContentSettingsFragment :
)
}
}
+
+ private fun bindSyncSummary() {
+ viewLifecycleScope.launch {
+ val account = withContext(Dispatchers.Default) {
+ val type = getString(R.string.account_type_sync)
+ AccountManager.get(requireContext()).getAccountsByType(type).firstOrNull()
+ }
+ findPreference(AppSettings.KEY_SYNC)?.run {
+ summary = account?.name ?: getString(R.string.sync_title)
+ }
+ }
+ }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/SyncModule.kt b/app/src/main/java/org/koitharu/kotatsu/sync/SyncModule.kt
index 78688cac0..ba0d9f2a9 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/SyncModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/SyncModule.kt
@@ -1,14 +1,19 @@
package org.koitharu.kotatsu.sync
+import androidx.room.InvalidationTracker
import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
+import org.koin.dsl.bind
import org.koin.dsl.module
import org.koitharu.kotatsu.sync.data.SyncAuthApi
+import org.koitharu.kotatsu.sync.domain.SyncController
import org.koitharu.kotatsu.sync.ui.SyncAuthViewModel
val syncModule
get() = module {
+ single { SyncController(androidContext()) } bind InvalidationTracker.Observer::class
+
factory { SyncAuthApi(androidContext(), get()) }
viewModel { SyncAuthViewModel(get()) }
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
new file mode 100644
index 000000000..683c1fa63
--- /dev/null
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncController.kt
@@ -0,0 +1,71 @@
+package org.koitharu.kotatsu.sync.domain
+
+import android.accounts.Account
+import android.accounts.AccountManager
+import android.content.ContentResolver
+import android.content.Context
+import android.os.Bundle
+import androidx.room.InvalidationTracker
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import org.koitharu.kotatsu.R
+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.utils.ext.processLifecycleScope
+
+class SyncController(
+ context: Context,
+) : InvalidationTracker.Observer(arrayOf(TABLE_HISTORY, TABLE_FAVOURITES, TABLE_FAVOURITE_CATEGORIES)) {
+
+ private val am = AccountManager.get(context)
+ private val accountType = context.getString(R.string.account_type_sync)
+
+ override fun onInvalidated(tables: MutableSet) {
+ requestSync(
+ favourites = TABLE_FAVOURITES in tables || TABLE_FAVOURITE_CATEGORIES in tables,
+ history = TABLE_HISTORY in tables,
+ )
+ }
+
+ suspend fun requestFullSync() = withContext(Dispatchers.Default) {
+ requestSyncImpl(favourites = true, history = true)
+ }
+
+ private fun requestSync(favourites: Boolean, history: Boolean) = processLifecycleScope.launch(Dispatchers.Default) {
+ requestSyncImpl(favourites, history)
+ }
+
+ @Synchronized
+ private fun requestSyncImpl(favourites: Boolean, history: Boolean) {
+ if (!favourites && !history) {
+ return
+ }
+ val account = peekAccount() ?: return
+ if (!ContentResolver.getMasterSyncAutomatically()) {
+ return
+ }
+ // TODO limit frequency
+ if (favourites) {
+ requestSyncForAuthority(account, AUTHORITY_FAVOURITES)
+ }
+ if (history) {
+ requestSyncForAuthority(account, AUTHORITY_HISTORY)
+ }
+ }
+
+ private fun requestSyncForAuthority(account: Account, authority: String) {
+ if (
+ ContentResolver.getSyncAutomatically(account, AUTHORITY_FAVOURITES) &&
+ !ContentResolver.isSyncActive(account, authority) &&
+ !ContentResolver.isSyncPending(account, authority)
+ ) {
+ ContentResolver.requestSync(account, authority, Bundle.EMPTY)
+ }
+ }
+
+ private fun peekAccount(): Account? {
+ return am.getAccountsByType(accountType).firstOrNull()
+ }
+}
\ No newline at end of file
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 f5dbfb9d5..1c816cab2 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
@@ -13,17 +13,17 @@ import org.json.JSONObject
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.*
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
-import org.koitharu.kotatsu.parsers.util.parseJson
import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
import org.koitharu.kotatsu.sync.data.SyncInterceptor
import org.koitharu.kotatsu.utils.GZipInterceptor
+import org.koitharu.kotatsu.utils.ext.parseJsonOrNull
import org.koitharu.kotatsu.utils.ext.toContentValues
import org.koitharu.kotatsu.utils.ext.toJson
import org.koitharu.kotatsu.utils.ext.toRequestBody
-private const val AUTHORITY_HISTORY = "org.koitharu.kotatsu.history"
-private const val AUTHORITY_FAVOURITES = "org.koitharu.kotatsu.favourites"
+const val AUTHORITY_HISTORY = "org.koitharu.kotatsu.history"
+const val AUTHORITY_FAVOURITES = "org.koitharu.kotatsu.favourites"
private const val FIELD_TIMESTAMP = "timestamp"
@@ -53,7 +53,7 @@ class SyncHelper(
.url("$baseUrl/resource/$TABLE_FAVOURITES")
.post(data.toRequestBody())
.build()
- val response = httpClient.newCall(request).execute().parseJson()
+ val response = httpClient.newCall(request).execute().parseJsonOrNull() ?: return
val timestamp = response.getLong(FIELD_TIMESTAMP)
val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES), timestamp)
syncResult.stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
@@ -71,7 +71,7 @@ class SyncHelper(
.url("$baseUrl/resource/$TABLE_HISTORY")
.post(data.toRequestBody())
.build()
- val response = httpClient.newCall(request).execute().parseJson()
+ val response = httpClient.newCall(request).execute().parseJsonOrNull() ?: return
val result = upsertHistory(
json = response.getJSONArray(TABLE_HISTORY),
timestamp = response.getLong(FIELD_TIMESTAMP),
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticator.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAccountAuthenticator.kt
similarity index 93%
rename from app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticator.kt
rename to app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAccountAuthenticator.kt
index 48c0aef8d..d1e4eb05c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticator.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAccountAuthenticator.kt
@@ -9,7 +9,7 @@ import android.content.Intent
import android.os.Bundle
import android.text.TextUtils
-class SyncAuthenticator(private val context: Context) : AbstractAccountAuthenticator(context) {
+class SyncAccountAuthenticator(private val context: Context) : AbstractAccountAuthenticator(context) {
override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?): Bundle? = null
@@ -52,7 +52,6 @@ class SyncAuthenticator(private val context: Context) : AbstractAccountAuthentic
} else {
val intent = Intent(context, SyncAuthActivity::class.java)
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
- // intent.putExtra(SyncAuthActivity.EXTRA_TOKEN_TYPE, authTokenType)
val bundle = Bundle()
if (options != null) {
bundle.putAll(options)
diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticatorService.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticatorService.kt
index 6f7ca8161..1262a9e9c 100644
--- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticatorService.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncAuthenticatorService.kt
@@ -6,11 +6,11 @@ import android.os.IBinder
class SyncAuthenticatorService : Service() {
- private lateinit var authenticator: SyncAuthenticator
+ private lateinit var authenticator: SyncAccountAuthenticator
override fun onCreate() {
super.onCreate()
- authenticator = SyncAuthenticator(this)
+ authenticator = SyncAccountAuthenticator(this)
}
override fun onBind(intent: Intent?): IBinder? {
diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt
index 3f3b966cc..12d23183d 100644
--- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/HttpExt.kt
@@ -2,8 +2,19 @@ package org.koitharu.kotatsu.utils.ext
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
+import okhttp3.Response
import org.json.JSONObject
+import org.koitharu.kotatsu.parsers.util.parseJson
+import java.net.HttpURLConnection
private val TYPE_JSON = "application/json".toMediaType()
-fun JSONObject.toRequestBody() = toString().toRequestBody(TYPE_JSON)
\ No newline at end of file
+fun JSONObject.toRequestBody() = toString().toRequestBody(TYPE_JSON)
+
+fun Response.parseJsonOrNull(): JSONObject? {
+ return if (code == HttpURLConnection.HTTP_NO_CONTENT) {
+ null
+ } else {
+ parseJson()
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/AppWidgetModule.kt b/app/src/main/java/org/koitharu/kotatsu/widget/AppWidgetModule.kt
index 3023da8b0..d96fdc43a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/widget/AppWidgetModule.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/widget/AppWidgetModule.kt
@@ -1,10 +1,15 @@
package org.koitharu.kotatsu.widget
+import androidx.room.InvalidationTracker
+import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModel
import org.koin.dsl.module
import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel
val appWidgetModule
get() = module {
+
+ single { WidgetUpdater(androidContext()) }
+
viewModel { ShelfConfigViewModel(get()) }
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt b/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt
index ee11b02c6..984744650 100644
--- a/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/widget/WidgetUpdater.kt
@@ -4,36 +4,26 @@ import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
-import kotlinx.coroutines.CancellationException
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.retry
-import kotlinx.coroutines.plus
-import org.koitharu.kotatsu.favourites.domain.FavouritesRepository
-import org.koitharu.kotatsu.history.domain.HistoryRepository
-import org.koitharu.kotatsu.parsers.model.SortOrder
-import org.koitharu.kotatsu.utils.ext.processLifecycleScope
+import androidx.room.InvalidationTracker
+import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
+import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.widget.recent.RecentWidgetProvider
import org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider
-class WidgetUpdater(private val context: Context) {
+class WidgetUpdater(
+ private val context: Context
+) : InvalidationTracker.Observer(TABLE_HISTORY, TABLE_FAVOURITES) {
- fun subscribeToFavourites(repository: FavouritesRepository) {
- repository.observeAll(SortOrder.NEWEST)
- .onEach { updateWidget(ShelfWidgetProvider::class.java) }
- .retry { error -> error !is CancellationException }
- .launchIn(processLifecycleScope + Dispatchers.Default)
+ override fun onInvalidated(tables: MutableSet) {
+ if (TABLE_HISTORY in tables) {
+ updateWidgets(RecentWidgetProvider::class.java)
+ }
+ if (TABLE_FAVOURITES in tables) {
+ updateWidgets(ShelfWidgetProvider::class.java)
+ }
}
- fun subscribeToHistory(repository: HistoryRepository) {
- repository.observeAll()
- .onEach { updateWidget(RecentWidgetProvider::class.java) }
- .retry { error -> error !is CancellationException }
- .launchIn(processLifecycleScope + Dispatchers.Default)
- }
-
- private fun updateWidget(cls: Class<*>) {
+ private fun updateWidgets(cls: Class<*>) {
val intent = Intent(context, cls)
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
val ids = AppWidgetManager.getInstance(context)
diff --git a/app/src/main/res/layout/activity_sync_auth.xml b/app/src/main/res/layout/activity_sync_auth.xml
index a0c7fefcd..86ff29e22 100644
--- a/app/src/main/res/layout/activity_sync_auth.xml
+++ b/app/src/main/res/layout/activity_sync_auth.xml
@@ -107,7 +107,7 @@
android:layout_alignParentEnd="true"
android:layout_marginTop="12dp"
android:gravity="center_horizontal"
- android:text="Enter your email to continue"
+ android:text="@string/enter_email_text"
android:textAppearance="?textAppearanceSubtitle1" />
diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml
index 12c41ccbf..54757b66d 100644
--- a/app/src/main/res/values/constants.xml
+++ b/app/src/main/res/values/constants.xml
@@ -5,7 +5,7 @@
https://4pda.to/forum/index.php?showtopic=697669
https://hosted.weblate.org/engage/kotatsu
org.kotatsu.sync
- http://95.216.215.49:8080
+ http://95.216.215.49:8055
- -1
- 1
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index a0e4737a4..b1d5f2e89 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -303,4 +303,5 @@
Default mode
Autodetect reader mode
Automatically detect if manga is webtoon
+ Enter your email to continue
\ No newline at end of file
diff --git a/app/src/main/res/xml/pref_content.xml b/app/src/main/res/xml/pref_content.xml
index c7f6ef630..5247bce95 100644
--- a/app/src/main/res/xml/pref_content.xml
+++ b/app/src/main/res/xml/pref_content.xml
@@ -44,7 +44,8 @@
android:key="sync"
android:persistent="false"
android:summary="@string/sync_title"
- android:title="@string/sync" />
+ android:title="@string/sync"
+ app:allowDividerAbove="true" />
-
-
-
-
-
\ No newline at end of file
+
\ No newline at end of file