Observe database updates using InvalidationTracker

pull/203/head
Koitharu 4 years ago
parent 9eb5e699e1
commit dfa413da6f
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -5,16 +5,18 @@ import android.content.Context
import android.os.StrictMode import android.os.StrictMode
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.fragment.app.strictmode.FragmentStrictMode import androidx.fragment.app.strictmode.FragmentStrictMode
import androidx.room.InvalidationTracker
import org.acra.ReportField import org.acra.ReportField
import org.acra.config.dialog import org.acra.config.dialog
import org.acra.config.mailSender import org.acra.config.mailSender
import org.acra.data.StringFormat import org.acra.data.StringFormat
import org.acra.ktx.initAcra import org.acra.ktx.initAcra
import org.koin.android.ext.android.get import org.koin.android.ext.android.get
import org.koin.android.ext.android.getKoin
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.bookmarks.bookmarksModule import org.koitharu.kotatsu.bookmarks.bookmarksModule
import org.koitharu.kotatsu.core.db.MangaDatabase
import org.koitharu.kotatsu.core.db.databaseModule import org.koitharu.kotatsu.core.db.databaseModule
import org.koitharu.kotatsu.core.github.githubModule import org.koitharu.kotatsu.core.github.githubModule
import org.koitharu.kotatsu.core.network.networkModule import org.koitharu.kotatsu.core.network.networkModule
@ -27,7 +29,6 @@ import org.koitharu.kotatsu.local.data.PagesCache
import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.domain.LocalMangaRepository
import org.koitharu.kotatsu.local.localModule import org.koitharu.kotatsu.local.localModule
import org.koitharu.kotatsu.main.mainModule import org.koitharu.kotatsu.main.mainModule
import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper
import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.parsers.MangaLoaderContext
import org.koitharu.kotatsu.reader.readerModule import org.koitharu.kotatsu.reader.readerModule
import org.koitharu.kotatsu.remotelist.remoteListModule import org.koitharu.kotatsu.remotelist.remoteListModule
@ -36,7 +37,6 @@ import org.koitharu.kotatsu.search.searchModule
import org.koitharu.kotatsu.settings.settingsModule import org.koitharu.kotatsu.settings.settingsModule
import org.koitharu.kotatsu.suggestions.suggestionsModule import org.koitharu.kotatsu.suggestions.suggestionsModule
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() {
@ -48,11 +48,8 @@ class KotatsuApp : Application() {
} }
initKoin() initKoin()
AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme) AppCompatDelegate.setDefaultNightMode(get<AppSettings>().theme)
registerActivityLifecycleCallbacks(get<AppProtectHelper>()) setupActivityLifecycleCallbacks()
registerActivityLifecycleCallbacks(get<ActivityRecreationHandle>()) setupDatabaseObservers()
val widgetUpdater = WidgetUpdater(applicationContext)
widgetUpdater.subscribeToFavourites(get())
widgetUpdater.subscribeToHistory(get())
} }
private fun initKoin() { private fun initKoin() {
@ -112,6 +109,22 @@ class KotatsuApp : Application() {
} }
} }
private fun setupDatabaseObservers() {
val observers = getKoin().getAll<InvalidationTracker.Observer>()
val database = get<MangaDatabase>()
val tracker = database.invalidationTracker
observers.forEach {
tracker.addObserver(it)
}
}
private fun setupActivityLifecycleCallbacks() {
val callbacks = getKoin().getAll<ActivityLifecycleCallbacks>()
callbacks.forEach {
registerActivityLifecycleCallbacks(it)
}
}
private fun enableStrictMode() { private fun enableStrictMode() {
StrictMode.setThreadPolicy( StrictMode.setThreadPolicy(
StrictMode.ThreadPolicy.Builder() StrictMode.ThreadPolicy.Builder()

@ -0,0 +1,9 @@
package org.koitharu.kotatsu.core.db
const val TABLE_FAVOURITES = "favourites"
const val TABLE_MANGA = "manga"
const val TABLE_TAGS = "tags"
const val TABLE_FAVOURITE_CATEGORIES = "favourite_categories"
const val TABLE_HISTORY = "history"
const val TABLE_MANGA_TAGS = "manga_tags"

@ -3,8 +3,9 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_MANGA
@Entity(tableName = "manga") @Entity(tableName = TABLE_MANGA)
data class MangaEntity( data class MangaEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "manga_id") val id: Long, @ColumnInfo(name = "manga_id") val id: Long,
@ -18,5 +19,5 @@ data class MangaEntity(
@ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?, @ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?,
@ColumnInfo(name = "state") val state: String?, @ColumnInfo(name = "state") val state: String?,
@ColumnInfo(name = "author") val author: String?, @ColumnInfo(name = "author") val author: String?,
@ColumnInfo(name = "source") val source: String @ColumnInfo(name = "source") val source: String,
) )

@ -3,9 +3,11 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
@Entity( @Entity(
tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], tableName = TABLE_MANGA_TAGS,
primaryKeys = ["manga_id", "tag_id"],
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = MangaEntity::class, entity = MangaEntity::class,
@ -23,5 +25,5 @@ import androidx.room.ForeignKey
) )
class MangaTagsEntity( class MangaTagsEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "tag_id", index = true) val tagId: Long @ColumnInfo(name = "tag_id", index = true) val tagId: Long,
) )

@ -3,12 +3,13 @@ package org.koitharu.kotatsu.core.db.entity
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_TAGS
@Entity(tableName = "tags") @Entity(tableName = TABLE_TAGS)
data class TagEntity( data class TagEntity(
@PrimaryKey(autoGenerate = false) @PrimaryKey(autoGenerate = false)
@ColumnInfo(name = "tag_id") val id: Long, @ColumnInfo(name = "tag_id") val id: Long,
@ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "key") val key: String, @ColumnInfo(name = "key") val key: String,
@ColumnInfo(name = "source") val source: String @ColumnInfo(name = "source") val source: String,
) )

@ -3,8 +3,9 @@ package org.koitharu.kotatsu.favourites.data
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES
@Entity(tableName = "favourite_categories") @Entity(tableName = TABLE_FAVOURITE_CATEGORIES)
data class FavouriteCategoryEntity( data class FavouriteCategoryEntity(
@PrimaryKey(autoGenerate = true) @PrimaryKey(autoGenerate = true)
@ColumnInfo(name = "category_id") val categoryId: Int, @ColumnInfo(name = "category_id") val categoryId: Int,

@ -3,10 +3,13 @@ package org.koitharu.kotatsu.favourites.data
import androidx.room.ColumnInfo import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity( @Entity(
tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [ tableName = TABLE_FAVOURITES,
primaryKeys = ["manga_id", "category_id"],
foreignKeys = [
ForeignKey( ForeignKey(
entity = MangaEntity::class, entity = MangaEntity::class,
parentColumns = ["manga_id"], parentColumns = ["manga_id"],
@ -24,5 +27,5 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity
data class FavouriteEntity( data class FavouriteEntity(
@ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "manga_id", index = true) val mangaId: Long,
@ColumnInfo(name = "category_id", index = true) val categoryId: Long, @ColumnInfo(name = "category_id", index = true) val categoryId: Long,
@ColumnInfo(name = "created_at") val createdAt: Long @ColumnInfo(name = "created_at") val createdAt: Long,
) )

@ -4,10 +4,11 @@ import androidx.room.ColumnInfo
import androidx.room.Entity import androidx.room.Entity
import androidx.room.ForeignKey import androidx.room.ForeignKey
import androidx.room.PrimaryKey import androidx.room.PrimaryKey
import org.koitharu.kotatsu.core.db.TABLE_HISTORY
import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.MangaEntity
@Entity( @Entity(
tableName = "history", tableName = TABLE_HISTORY,
foreignKeys = [ foreignKeys = [
ForeignKey( ForeignKey(
entity = MangaEntity::class, entity = MangaEntity::class,

@ -1,7 +1,9 @@
package org.koitharu.kotatsu.main package org.koitharu.kotatsu.main
import android.app.Application
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.base.ui.util.ActivityRecreationHandle import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle
import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.os.ShortcutsRepository
@ -11,8 +13,8 @@ import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel
val mainModule val mainModule
get() = module { get() = module {
single { AppProtectHelper(get()) } single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class
single { ActivityRecreationHandle() } single { ActivityRecreationHandle() } bind Application.ActivityLifecycleCallbacks::class
factory { ShortcutsRepository(androidContext(), get(), get(), get()) } factory { ShortcutsRepository(androidContext(), get(), get(), get()) }
viewModel { MainViewModel(get(), get()) } viewModel { MainViewModel(get(), get()) }
viewModel { ProtectViewModel(get(), get()) } viewModel { ProtectViewModel(get(), get()) }

@ -1,11 +1,14 @@
package org.koitharu.kotatsu.settings package org.koitharu.kotatsu.settings
import android.net.Uri import android.net.Uri
import android.os.Build
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.module import org.koin.dsl.module
import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.AppSettings
import org.koitharu.kotatsu.settings.backup.BackupObserver
import org.koitharu.kotatsu.settings.backup.BackupViewModel import org.koitharu.kotatsu.settings.backup.BackupViewModel
import org.koitharu.kotatsu.settings.backup.RestoreViewModel import org.koitharu.kotatsu.settings.backup.RestoreViewModel
import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel
@ -16,6 +19,10 @@ import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel
val settingsModule val settingsModule
get() = module { get() = module {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
single<InvalidationTracker.Observer> { BackupObserver(androidContext()) }
}
factory { BackupRepository(get()) } factory { BackupRepository(get()) }
single(createdAtStart = true) { AppSettings(androidContext()) } single(createdAtStart = true) { AppSettings(androidContext()) }

@ -0,0 +1,22 @@
package org.koitharu.kotatsu.settings.backup
import android.app.backup.BackupManager
import android.content.Context
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.room.InvalidationTracker
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
@RequiresApi(Build.VERSION_CODES.M)
class BackupObserver(
context: Context,
) : InvalidationTracker.Observer(arrayOf(TABLE_HISTORY, TABLE_FAVOURITES, TABLE_FAVOURITE_CATEGORIES)) {
private val backupManager = BackupManager(context)
override fun onInvalidated(tables: MutableSet<String>) {
backupManager.dataChanged()
}
}

@ -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,24 @@ 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)

Loading…
Cancel
Save