Sync on demand
This commit is contained in:
@@ -13,6 +13,9 @@
|
|||||||
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
<uses-permission android:name="android.permission.MANAGE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
<uses-permission android:name="android.permission.AUTHENTICATE_ACCOUNTS" />
|
||||||
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
<uses-permission android:name="android.permission.USE_CREDENTIALS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_SYNC_STATS" />
|
||||||
|
<uses-permission android:name="android.permission.READ_SYNC_SETTINGS" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:name="org.koitharu.kotatsu.KotatsuApp"
|
android:name="org.koitharu.kotatsu.KotatsuApp"
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import org.koitharu.kotatsu.settings.settingsModule
|
|||||||
import org.koitharu.kotatsu.suggestions.suggestionsModule
|
import org.koitharu.kotatsu.suggestions.suggestionsModule
|
||||||
import org.koitharu.kotatsu.sync.syncModule
|
import org.koitharu.kotatsu.sync.syncModule
|
||||||
import org.koitharu.kotatsu.tracker.trackerModule
|
import org.koitharu.kotatsu.tracker.trackerModule
|
||||||
import org.koitharu.kotatsu.widget.WidgetUpdater
|
|
||||||
import org.koitharu.kotatsu.widget.appWidgetModule
|
import org.koitharu.kotatsu.widget.appWidgetModule
|
||||||
|
|
||||||
class KotatsuApp : Application() {
|
class KotatsuApp : Application() {
|
||||||
@@ -44,9 +43,6 @@ class KotatsuApp : Application() {
|
|||||||
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
Thread.setDefaultUncaughtExceptionHandler(AppCrashHandler(applicationContext))
|
||||||
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
|
||||||
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
|
registerActivityLifecycleCallbacks(get<AppProtectHelper>())
|
||||||
val widgetUpdater = WidgetUpdater(applicationContext)
|
|
||||||
widgetUpdater.subscribeToFavourites(get())
|
|
||||||
widgetUpdater.subscribeToHistory(get())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initKoin() {
|
private fun initKoin() {
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
package org.koitharu.kotatsu.core.db
|
package org.koitharu.kotatsu.core.db
|
||||||
|
|
||||||
|
import androidx.room.InvalidationTracker
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
|
||||||
val databaseModule
|
val databaseModule
|
||||||
get() = module {
|
get() = module {
|
||||||
single { MangaDatabase(androidContext()) }
|
single {
|
||||||
|
MangaDatabase(androidContext()).also { db ->
|
||||||
|
getAll<InvalidationTracker.Observer>().forEach {
|
||||||
|
db.invalidationTracker.addObserver(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -27,6 +27,7 @@ import com.google.android.material.navigation.NavigationView
|
|||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
import org.koin.android.ext.android.get
|
import org.koin.android.ext.android.get
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
import org.koin.androidx.viewmodel.ext.android.viewModel
|
||||||
import org.koitharu.kotatsu.R
|
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.settings.onboard.OnboardDialogFragment
|
||||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
|
import org.koitharu.kotatsu.suggestions.ui.SuggestionsFragment
|
||||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
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.ui.FeedFragment
|
||||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||||
import org.koitharu.kotatsu.utils.VoiceInputContract
|
import org.koitharu.kotatsu.utils.VoiceInputContract
|
||||||
@@ -440,6 +442,8 @@ class MainActivity :
|
|||||||
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
|
!settings.isSourcesSelected -> OnboardDialogFragment.showWelcome(supportFragmentManager)
|
||||||
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
|
settings.newSources.isNotEmpty() -> NewSourcesDialogFragment.show(supportFragmentManager)
|
||||||
}
|
}
|
||||||
|
yield()
|
||||||
|
get<SyncController>().requestFullSync()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package org.koitharu.kotatsu.settings
|
package org.koitharu.kotatsu.settings
|
||||||
|
|
||||||
|
import android.accounts.AccountManager
|
||||||
|
import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
@@ -7,7 +9,10 @@ import android.provider.Settings
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
|
import com.google.android.material.snackbar.Snackbar
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.koin.android.ext.android.inject
|
import org.koin.android.ext.android.inject
|
||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.base.ui.BasePreferenceFragment
|
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.local.data.LocalStorageManager
|
||||||
import org.koitharu.kotatsu.parsers.util.names
|
import org.koitharu.kotatsu.parsers.util.names
|
||||||
import org.koitharu.kotatsu.settings.utils.SliderPreference
|
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.getStorageName
|
||||||
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
import org.koitharu.kotatsu.utils.ext.setDefaultValueCompat
|
||||||
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
import org.koitharu.kotatsu.utils.ext.viewLifecycleScope
|
||||||
@@ -60,6 +67,11 @@ class ContentSettingsFragment :
|
|||||||
settings.subscribe(this)
|
settings.subscribe(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onResume() {
|
||||||
|
super.onResume()
|
||||||
|
bindSyncSummary()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onDestroyView() {
|
override fun onDestroyView() {
|
||||||
settings.unsubscribe(this)
|
settings.unsubscribe(this)
|
||||||
super.onDestroyView()
|
super.onDestroyView()
|
||||||
@@ -78,10 +90,6 @@ class ContentSettingsFragment :
|
|||||||
AppSettings.KEY_SOURCES_HIDDEN -> {
|
AppSettings.KEY_SOURCES_HIDDEN -> {
|
||||||
bindRemoteSourcesSummary()
|
bindRemoteSourcesSummary()
|
||||||
}
|
}
|
||||||
AppSettings.KEY_SYNC -> {
|
|
||||||
val intent = Intent(Settings.ACTION_SYNC_SETTINGS)
|
|
||||||
startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,6 +104,23 @@ class ContentSettingsFragment :
|
|||||||
.show()
|
.show()
|
||||||
true
|
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)
|
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<Preference>(AppSettings.KEY_SYNC)?.run {
|
||||||
|
summary = account?.name ?: getString(R.string.sync_title)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
package org.koitharu.kotatsu.sync
|
package org.koitharu.kotatsu.sync
|
||||||
|
|
||||||
|
import androidx.room.InvalidationTracker
|
||||||
import org.koin.android.ext.koin.androidContext
|
import org.koin.android.ext.koin.androidContext
|
||||||
import org.koin.androidx.viewmodel.dsl.viewModel
|
import org.koin.androidx.viewmodel.dsl.viewModel
|
||||||
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koitharu.kotatsu.sync.data.SyncAuthApi
|
import org.koitharu.kotatsu.sync.data.SyncAuthApi
|
||||||
|
import org.koitharu.kotatsu.sync.domain.SyncController
|
||||||
import org.koitharu.kotatsu.sync.ui.SyncAuthViewModel
|
import org.koitharu.kotatsu.sync.ui.SyncAuthViewModel
|
||||||
|
|
||||||
val syncModule
|
val syncModule
|
||||||
get() = module {
|
get() = module {
|
||||||
|
|
||||||
|
single { SyncController(androidContext()) } bind InvalidationTracker.Observer::class
|
||||||
|
|
||||||
factory { SyncAuthApi(androidContext(), get()) }
|
factory { SyncAuthApi(androidContext(), get()) }
|
||||||
|
|
||||||
viewModel { SyncAuthViewModel(get()) }
|
viewModel { SyncAuthViewModel(get()) }
|
||||||
|
|||||||
@@ -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<String>) {
|
||||||
|
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,17 +13,17 @@ import org.json.JSONObject
|
|||||||
import org.koitharu.kotatsu.R
|
import org.koitharu.kotatsu.R
|
||||||
import org.koitharu.kotatsu.core.db.*
|
import org.koitharu.kotatsu.core.db.*
|
||||||
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
|
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.SyncAuthApi
|
||||||
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
|
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
|
||||||
import org.koitharu.kotatsu.sync.data.SyncInterceptor
|
import org.koitharu.kotatsu.sync.data.SyncInterceptor
|
||||||
import org.koitharu.kotatsu.utils.GZipInterceptor
|
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.toContentValues
|
||||||
import org.koitharu.kotatsu.utils.ext.toJson
|
import org.koitharu.kotatsu.utils.ext.toJson
|
||||||
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
import org.koitharu.kotatsu.utils.ext.toRequestBody
|
||||||
|
|
||||||
private const val AUTHORITY_HISTORY = "org.koitharu.kotatsu.history"
|
const val AUTHORITY_HISTORY = "org.koitharu.kotatsu.history"
|
||||||
private const val AUTHORITY_FAVOURITES = "org.koitharu.kotatsu.favourites"
|
const val AUTHORITY_FAVOURITES = "org.koitharu.kotatsu.favourites"
|
||||||
|
|
||||||
private const val FIELD_TIMESTAMP = "timestamp"
|
private const val FIELD_TIMESTAMP = "timestamp"
|
||||||
|
|
||||||
@@ -53,7 +53,7 @@ class SyncHelper(
|
|||||||
.url("$baseUrl/resource/$TABLE_FAVOURITES")
|
.url("$baseUrl/resource/$TABLE_FAVOURITES")
|
||||||
.post(data.toRequestBody())
|
.post(data.toRequestBody())
|
||||||
.build()
|
.build()
|
||||||
val response = httpClient.newCall(request).execute().parseJson()
|
val response = httpClient.newCall(request).execute().parseJsonOrNull() ?: return
|
||||||
val timestamp = response.getLong(FIELD_TIMESTAMP)
|
val timestamp = response.getLong(FIELD_TIMESTAMP)
|
||||||
val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES), timestamp)
|
val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES), timestamp)
|
||||||
syncResult.stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
|
syncResult.stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
|
||||||
@@ -71,7 +71,7 @@ class SyncHelper(
|
|||||||
.url("$baseUrl/resource/$TABLE_HISTORY")
|
.url("$baseUrl/resource/$TABLE_HISTORY")
|
||||||
.post(data.toRequestBody())
|
.post(data.toRequestBody())
|
||||||
.build()
|
.build()
|
||||||
val response = httpClient.newCall(request).execute().parseJson()
|
val response = httpClient.newCall(request).execute().parseJsonOrNull() ?: return
|
||||||
val result = upsertHistory(
|
val result = upsertHistory(
|
||||||
json = response.getJSONArray(TABLE_HISTORY),
|
json = response.getJSONArray(TABLE_HISTORY),
|
||||||
timestamp = response.getLong(FIELD_TIMESTAMP),
|
timestamp = response.getLong(FIELD_TIMESTAMP),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import android.content.Intent
|
|||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.TextUtils
|
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
|
override fun editProperties(response: AccountAuthenticatorResponse?, accountType: String?): Bundle? = null
|
||||||
|
|
||||||
@@ -52,7 +52,6 @@ class SyncAuthenticator(private val context: Context) : AbstractAccountAuthentic
|
|||||||
} else {
|
} else {
|
||||||
val intent = Intent(context, SyncAuthActivity::class.java)
|
val intent = Intent(context, SyncAuthActivity::class.java)
|
||||||
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
|
intent.putExtra(AccountManager.KEY_ACCOUNT_AUTHENTICATOR_RESPONSE, response)
|
||||||
// intent.putExtra(SyncAuthActivity.EXTRA_TOKEN_TYPE, authTokenType)
|
|
||||||
val bundle = Bundle()
|
val bundle = Bundle()
|
||||||
if (options != null) {
|
if (options != null) {
|
||||||
bundle.putAll(options)
|
bundle.putAll(options)
|
||||||
@@ -6,11 +6,11 @@ import android.os.IBinder
|
|||||||
|
|
||||||
class SyncAuthenticatorService : Service() {
|
class SyncAuthenticatorService : Service() {
|
||||||
|
|
||||||
private lateinit var authenticator: SyncAuthenticator
|
private lateinit var authenticator: SyncAccountAuthenticator
|
||||||
|
|
||||||
override fun onCreate() {
|
override fun onCreate() {
|
||||||
super.onCreate()
|
super.onCreate()
|
||||||
authenticator = SyncAuthenticator(this)
|
authenticator = SyncAccountAuthenticator(this)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onBind(intent: Intent?): IBinder? {
|
override fun onBind(intent: Intent?): IBinder? {
|
||||||
|
|||||||
@@ -2,8 +2,19 @@ package org.koitharu.kotatsu.utils.ext
|
|||||||
|
|
||||||
import okhttp3.MediaType.Companion.toMediaType
|
import okhttp3.MediaType.Companion.toMediaType
|
||||||
import okhttp3.RequestBody.Companion.toRequestBody
|
import okhttp3.RequestBody.Companion.toRequestBody
|
||||||
|
import okhttp3.Response
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
|
import org.koitharu.kotatsu.parsers.util.parseJson
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
|
||||||
private val TYPE_JSON = "application/json".toMediaType()
|
private val TYPE_JSON = "application/json".toMediaType()
|
||||||
|
|
||||||
fun JSONObject.toRequestBody() = toString().toRequestBody(TYPE_JSON)
|
fun JSONObject.toRequestBody() = toString().toRequestBody(TYPE_JSON)
|
||||||
|
|
||||||
|
fun Response.parseJsonOrNull(): JSONObject? {
|
||||||
|
return if (code == HttpURLConnection.HTTP_NO_CONTENT) {
|
||||||
|
null
|
||||||
|
} else {
|
||||||
|
parseJson()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
package org.koitharu.kotatsu.widget
|
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.androidx.viewmodel.dsl.viewModel
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel
|
import org.koitharu.kotatsu.widget.shelf.ShelfConfigViewModel
|
||||||
|
|
||||||
val appWidgetModule
|
val appWidgetModule
|
||||||
get() = module {
|
get() = module {
|
||||||
|
|
||||||
|
single<InvalidationTracker.Observer> { WidgetUpdater(androidContext()) }
|
||||||
|
|
||||||
viewModel { ShelfConfigViewModel(get()) }
|
viewModel { ShelfConfigViewModel(get()) }
|
||||||
}
|
}
|
||||||
@@ -4,36 +4,26 @@ import android.appwidget.AppWidgetManager
|
|||||||
import android.content.ComponentName
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import kotlinx.coroutines.CancellationException
|
import androidx.room.InvalidationTracker
|
||||||
import kotlinx.coroutines.Dispatchers
|
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
|
||||||
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 org.koitharu.kotatsu.widget.recent.RecentWidgetProvider
|
import org.koitharu.kotatsu.widget.recent.RecentWidgetProvider
|
||||||
import org.koitharu.kotatsu.widget.shelf.ShelfWidgetProvider
|
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) {
|
override fun onInvalidated(tables: MutableSet<String>) {
|
||||||
repository.observeAll(SortOrder.NEWEST)
|
if (TABLE_HISTORY in tables) {
|
||||||
.onEach { updateWidget(ShelfWidgetProvider::class.java) }
|
updateWidgets(RecentWidgetProvider::class.java)
|
||||||
.retry { error -> error !is CancellationException }
|
}
|
||||||
.launchIn(processLifecycleScope + Dispatchers.Default)
|
if (TABLE_FAVOURITES in tables) {
|
||||||
|
updateWidgets(ShelfWidgetProvider::class.java)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun subscribeToHistory(repository: HistoryRepository) {
|
private fun updateWidgets(cls: Class<*>) {
|
||||||
repository.observeAll()
|
|
||||||
.onEach { updateWidget(RecentWidgetProvider::class.java) }
|
|
||||||
.retry { error -> error !is CancellationException }
|
|
||||||
.launchIn(processLifecycleScope + Dispatchers.Default)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateWidget(cls: Class<*>) {
|
|
||||||
val intent = Intent(context, cls)
|
val intent = Intent(context, cls)
|
||||||
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
|
||||||
val ids = AppWidgetManager.getInstance(context)
|
val ids = AppWidgetManager.getInstance(context)
|
||||||
|
|||||||
@@ -107,7 +107,7 @@
|
|||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:gravity="center_horizontal"
|
android:gravity="center_horizontal"
|
||||||
android:text="Enter your email to continue"
|
android:text="@string/enter_email_text"
|
||||||
android:textAppearance="?textAppearanceSubtitle1" />
|
android:textAppearance="?textAppearanceSubtitle1" />
|
||||||
|
|
||||||
<com.google.android.material.textfield.TextInputLayout
|
<com.google.android.material.textfield.TextInputLayout
|
||||||
@@ -119,6 +119,7 @@
|
|||||||
android:layout_alignParentStart="true"
|
android:layout_alignParentStart="true"
|
||||||
android:layout_alignParentEnd="true"
|
android:layout_alignParentEnd="true"
|
||||||
android:layout_marginTop="30dp"
|
android:layout_marginTop="30dp"
|
||||||
|
app:endIconMode="password_toggle"
|
||||||
app:errorIconDrawable="@null"
|
app:errorIconDrawable="@null"
|
||||||
app:helperText="You can sign in into an existing account or create a new one"
|
app:helperText="You can sign in into an existing account or create a new one"
|
||||||
app:hintEnabled="false">
|
app:hintEnabled="false">
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
<string name="url_forpda" translatable="false">https://4pda.to/forum/index.php?showtopic=697669</string>
|
<string name="url_forpda" translatable="false">https://4pda.to/forum/index.php?showtopic=697669</string>
|
||||||
<string name="url_weblate" translatable="false">https://hosted.weblate.org/engage/kotatsu</string>
|
<string name="url_weblate" translatable="false">https://hosted.weblate.org/engage/kotatsu</string>
|
||||||
<string name="account_type_sync" translatable="false">org.kotatsu.sync</string>
|
<string name="account_type_sync" translatable="false">org.kotatsu.sync</string>
|
||||||
<string name="url_sync_server" translatable="false">http://95.216.215.49:8080</string>
|
<string name="url_sync_server" translatable="false">http://95.216.215.49:8055</string>
|
||||||
<string-array name="values_theme" translatable="false">
|
<string-array name="values_theme" translatable="false">
|
||||||
<item>-1</item>
|
<item>-1</item>
|
||||||
<item>1</item>
|
<item>1</item>
|
||||||
|
|||||||
@@ -303,4 +303,5 @@
|
|||||||
<string name="default_mode">Default mode</string>
|
<string name="default_mode">Default mode</string>
|
||||||
<string name="detect_reader_mode">Autodetect reader mode</string>
|
<string name="detect_reader_mode">Autodetect reader mode</string>
|
||||||
<string name="detect_reader_mode_summary">Automatically detect if manga is webtoon</string>
|
<string name="detect_reader_mode_summary">Automatically detect if manga is webtoon</string>
|
||||||
|
<string name="enter_email_text">Enter your email to continue</string>
|
||||||
</resources>
|
</resources>
|
||||||
@@ -44,7 +44,8 @@
|
|||||||
android:key="sync"
|
android:key="sync"
|
||||||
android:persistent="false"
|
android:persistent="false"
|
||||||
android:summary="@string/sync_title"
|
android:summary="@string/sync_title"
|
||||||
android:title="@string/sync" />
|
android:title="@string/sync"
|
||||||
|
app:allowDividerAbove="true" />
|
||||||
|
|
||||||
<PreferenceScreen
|
<PreferenceScreen
|
||||||
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"
|
android:fragment="org.koitharu.kotatsu.settings.backup.BackupSettingsFragment"
|
||||||
|
|||||||
@@ -1,10 +1,2 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<PreferenceScreen
|
<PreferenceScreen />
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android">
|
|
||||||
|
|
||||||
<Preference
|
|
||||||
android:persistent="false"
|
|
||||||
android:summary="Preference stub"
|
|
||||||
android:title="TODO" />
|
|
||||||
|
|
||||||
</PreferenceScreen>
|
|
||||||
Reference in New Issue
Block a user