diff --git a/app/build.gradle b/app/build.gradle index 11fd34f91..b9300fa75 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -68,8 +68,11 @@ android { disable 'MissingTranslation', 'PrivateResource', 'NotifyDataSetChanged' } testOptions { - unitTests.includeAndroidResources = true - unitTests.returnDefaultValues = false + unitTests.includeAndroidResources true + unitTests.returnDefaultValues false + kotlinOptions { + freeCompilerArgs += ['-opt-in=org.koitharu.kotatsu.parsers.InternalParsersApi'] + } } } afterEvaluate { @@ -128,6 +131,7 @@ dependencies { debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.9.1' testImplementation 'junit:junit:4.13.2' + testImplementation 'org.json:json:20220320' testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4' androidTestImplementation 'androidx.test:runner:1.4.0' diff --git a/app/src/androidTest/assets/categories/simple.json b/app/src/androidTest/assets/categories/simple.json new file mode 100644 index 000000000..90f6ecf1a --- /dev/null +++ b/app/src/androidTest/assets/categories/simple.json @@ -0,0 +1,8 @@ +{ + "id": 4, + "title": "Read later", + "sortKey": 1, + "order": "NEWEST", + "createdAt": 1335906000000, + "isTrackingEnabled": true +} \ No newline at end of file diff --git a/app/src/androidTest/assets/manga/header.json b/app/src/androidTest/assets/manga/header.json new file mode 100644 index 000000000..dc56dbf8e --- /dev/null +++ b/app/src/androidTest/assets/manga/header.json @@ -0,0 +1,35 @@ +{ + "id": -2096681732556647985, + "title": "Странствия Эманон", + "url": "/stranstviia_emanon", + "publicUrl": "https://readmanga.io/stranstviia_emanon", + "rating": 0.9400894, + "isNsfw": true, + "coverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_p.jpg", + "tags": [ + { + "title": "Сверхъестественное", + "key": "supernatural", + "source": "READMANGA_RU" + }, + { + "title": "Сэйнэн", + "key": "seinen", + "source": "READMANGA_RU" + }, + { + "title": "Повседневность", + "key": "slice_of_life", + "source": "READMANGA_RU" + }, + { + "title": "Приключения", + "key": "adventure", + "source": "READMANGA_RU" + } + ], + "state": "FINISHED", + "largeCoverUrl": "https://staticrm.rmr.rocks/uploads/pics/01/12/559_o.jpg", + "description": null, + "source": "READMANGA_RU" +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/Instrumentation.kt b/app/src/androidTest/java/org/koitharu/kotatsu/Instrumentation.kt new file mode 100644 index 000000000..b9ef582c1 --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/Instrumentation.kt @@ -0,0 +1,9 @@ +package org.koitharu.kotatsu + +import android.app.Instrumentation +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +suspend fun Instrumentation.awaitForIdle() = suspendCoroutine { cont -> + waitForIdle { cont.resume(Unit) } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt b/app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt new file mode 100644 index 000000000..b7d4ad7e3 --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt @@ -0,0 +1,54 @@ +package org.koitharu.kotatsu + +import androidx.test.platform.app.InstrumentationRegistry +import com.squareup.moshi.* +import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory +import okio.buffer +import okio.source +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.parsers.model.Manga +import java.util.* +import kotlin.reflect.KClass + +object SampleData { + + private val moshi = Moshi.Builder() + .add(DateAdapter()) + .add(KotlinJsonAdapterFactory()) + .build() + + val manga: Manga = loadAsset("manga/header.json", Manga::class) + + val mangaDetails: Manga = loadAsset("manga/full.json", Manga::class) + + val tag = mangaDetails.tags.elementAt(2) + + val chapter = checkNotNull(mangaDetails.chapters)[2] + + val favouriteCategory: FavouriteCategory = loadAsset("categories/simple.json", FavouriteCategory::class) + + fun loadAsset(name: String, cls: KClass): T { + val assets = InstrumentationRegistry.getInstrumentation().context.assets + return assets.open(name).use { + moshi.adapter(cls.java).fromJson(it.source().buffer()) + } ?: throw RuntimeException("Cannot read asset from json \"$name\"") + } + + private class DateAdapter : JsonAdapter() { + + @FromJson + override fun fromJson(reader: JsonReader): Date? { + val ms = reader.nextLong() + return if (ms == 0L) { + null + } else { + Date(ms) + } + } + + @ToJson + override fun toJson(writer: JsonWriter, value: Date?) { + writer.value(value?.time ?: 0L) + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt index 54141f3e6..78aa09903 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/db/MangaDatabaseTest.kt @@ -3,11 +3,12 @@ package org.koitharu.kotatsu.core.db import androidx.room.testing.MigrationTestHelper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.platform.app.InstrumentationRegistry -import java.io.IOException import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.koitharu.kotatsu.core.db.migrations.* +import java.io.IOException +import kotlin.test.assertEquals @RunWith(AndroidJUnit4::class) class MangaDatabaseTest { @@ -21,17 +22,15 @@ class MangaDatabaseTest { @Test @Throws(IOException::class) fun migrateAll() { - helper.createDatabase(TEST_DB, 1).apply { - // TODO execSQL("") - close() - } + assertEquals(DATABASE_VERSION, migrations.last().endVersion) + helper.createDatabase(TEST_DB, 1).close() for (migration in migrations) { helper.runMigrationsAndValidate( TEST_DB, migration.endVersion, true, migration - ) + ).close() } } @@ -50,6 +49,8 @@ class MangaDatabaseTest { Migration8To9(), Migration9To10(), Migration10To11(), + Migration11To12(), + Migration12To13(), ) } } \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt new file mode 100644 index 000000000..8b68552da --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt @@ -0,0 +1,65 @@ +package org.koitharu.kotatsu.core.os + +import android.content.pm.ShortcutInfo +import android.content.pm.ShortcutManager +import androidx.core.content.getSystemService +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.platform.app.InstrumentationRegistry +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.test.KoinTest +import org.koin.test.inject +import org.koitharu.kotatsu.SampleData +import org.koitharu.kotatsu.awaitForIdle +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.history.domain.HistoryRepository +import kotlin.test.assertEquals +import kotlin.test.assertTrue + +@RunWith(AndroidJUnit4::class) +class ShortcutsUpdaterTest : KoinTest { + + private val historyRepository by inject() + private val shortcutsUpdater by inject() + private val database by inject() + + @Before + fun setUp() { + database.clearAllTables() + } + + @Test + fun testUpdateShortcuts() = runTest { + awaitUpdate() + assertTrue(getShortcuts().isEmpty()) + historyRepository.addOrUpdate( + manga = SampleData.manga, + chapterId = SampleData.chapter.id, + page = 4, + scroll = 2, + percent = 0.3f + ) + awaitUpdate() + + val shortcuts = getShortcuts() + assertEquals(1, shortcuts.size) + } + + private fun getShortcuts(): List { + val context = InstrumentationRegistry.getInstrumentation().targetContext + val manager = checkNotNull(context.getSystemService()) + return manager.dynamicShortcuts.filterNot { it.id == "com.squareup.leakcanary.dynamic_shortcut" } + } + + private suspend fun awaitUpdate() { + val instrumentation = InstrumentationRegistry.getInstrumentation() + while (true) { + instrumentation.awaitForIdle() + if (shortcutsUpdater.await()) { + return + } + } + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt new file mode 100644 index 000000000..1d0ca5498 --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt @@ -0,0 +1,67 @@ +package org.koitharu.kotatsu.settings.backup + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.koin.test.KoinTest +import org.koin.test.get +import org.koin.test.inject +import org.koitharu.kotatsu.SampleData +import org.koitharu.kotatsu.core.backup.BackupRepository +import org.koitharu.kotatsu.core.db.MangaDatabase +import org.koitharu.kotatsu.core.db.entity.toMangaTags +import org.koitharu.kotatsu.favourites.domain.FavouritesRepository +import org.koitharu.kotatsu.history.domain.HistoryRepository +import kotlin.test.* + +@RunWith(AndroidJUnit4::class) +class AppBackupAgentTest : KoinTest { + + private val historyRepository by inject() + private val favouritesRepository by inject() + private val backupRepository by inject() + private val database by inject() + + @Before + fun setUp() { + database.clearAllTables() + } + + @Test + fun testBackupRestore() = runTest { + val category = favouritesRepository.createCategory( + title = SampleData.favouriteCategory.title, + sortOrder = SampleData.favouriteCategory.order, + isTrackerEnabled = SampleData.favouriteCategory.isTrackingEnabled, + ) + favouritesRepository.addToCategory(categoryId = category.id, mangas = listOf(SampleData.manga)) + historyRepository.addOrUpdate( + manga = SampleData.mangaDetails, + chapterId = SampleData.mangaDetails.chapters!![2].id, + page = 3, + scroll = 40, + percent = 0.2f, + ) + val history = checkNotNull(historyRepository.getOne(SampleData.manga)) + + val agent = AppBackupAgent() + val backup = agent.createBackupFile(get(), backupRepository) + + database.clearAllTables() + assertTrue(favouritesRepository.getAllManga().isEmpty()) + assertNull(historyRepository.getLastOrNull()) + + backup.inputStream().use { + agent.restoreBackupFile(it.fd, backup.length(), backupRepository) + } + + assertEquals(category, favouritesRepository.getCategory(category.id)) + assertEquals(history, historyRepository.getOne(SampleData.manga)) + assertContentEquals(listOf(SampleData.manga), favouritesRepository.getManga(category.id)) + + val allTags = database.tagsDao.findTags(SampleData.tag.source.name).toMangaTags() + assertContains(allTags, SampleData.tag) + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt b/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt index 3b63f429e..7d829332d 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/tracker/domain/TrackerTest.kt @@ -1,29 +1,21 @@ package org.koitharu.kotatsu.tracker.domain import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.platform.app.InstrumentationRegistry -import com.squareup.moshi.Moshi -import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory -import kotlin.test.assertEquals -import kotlin.test.assertFalse -import kotlin.test.assertTrue import kotlinx.coroutines.test.runTest -import okio.buffer -import okio.source import org.junit.Test import org.junit.runner.RunWith import org.koin.test.KoinTest import org.koin.test.inject +import org.koitharu.kotatsu.SampleData import org.koitharu.kotatsu.base.domain.MangaDataRepository -import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertTrue @RunWith(AndroidJUnit4::class) class TrackerTest : KoinTest { - private val moshi = Moshi.Builder().add(KotlinJsonAdapterFactory()).build() - private val mangaAdapter = moshi.adapter(Manga::class.java) - private val historyRegistry by inject() private val repository by inject() private val dataRepository by inject() private val tracker by inject() @@ -178,10 +170,7 @@ class TrackerTest : KoinTest { } private suspend fun loadManga(name: String): Manga { - val assets = InstrumentationRegistry.getInstrumentation().context.assets - val manga = assets.open("manga/$name").use { - mangaAdapter.fromJson(it.source().buffer()) - } ?: throw RuntimeException("Cannot read manga from json \"$name\"") + val manga = SampleData.loadAsset("manga/$name", Manga::class) dataRepository.storeManga(manga) return manga } diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index 33b999667..7c2d53226 100644 --- a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt +++ b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt @@ -5,16 +5,18 @@ import android.content.Context import android.os.StrictMode import androidx.appcompat.app.AppCompatDelegate import androidx.fragment.app.strictmode.FragmentStrictMode +import androidx.room.InvalidationTracker import org.acra.ReportField import org.acra.config.dialog import org.acra.config.mailSender import org.acra.data.StringFormat import org.acra.ktx.initAcra import org.koin.android.ext.android.get +import org.koin.android.ext.android.getKoin import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin -import org.koitharu.kotatsu.base.ui.util.ActivityRecreationHandle 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.github.githubModule import org.koitharu.kotatsu.core.network.networkModule @@ -29,7 +31,6 @@ import org.koitharu.kotatsu.local.data.PagesCache import org.koitharu.kotatsu.local.domain.LocalMangaRepository import org.koitharu.kotatsu.local.localModule import org.koitharu.kotatsu.main.mainModule -import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.parsers.MangaLoaderContext import org.koitharu.kotatsu.reader.readerModule import org.koitharu.kotatsu.remotelist.remoteListModule @@ -38,7 +39,6 @@ import org.koitharu.kotatsu.search.searchModule import org.koitharu.kotatsu.settings.settingsModule import org.koitharu.kotatsu.suggestions.suggestionsModule import org.koitharu.kotatsu.tracker.trackerModule -import org.koitharu.kotatsu.widget.WidgetUpdater import org.koitharu.kotatsu.widget.appWidgetModule class KotatsuApp : Application() { @@ -50,11 +50,8 @@ class KotatsuApp : Application() { } initKoin() AppCompatDelegate.setDefaultNightMode(get().theme) - registerActivityLifecycleCallbacks(get()) - registerActivityLifecycleCallbacks(get()) - val widgetUpdater = WidgetUpdater(applicationContext) - widgetUpdater.subscribeToFavourites(get()) - widgetUpdater.subscribeToHistory(get()) + setupActivityLifecycleCallbacks() + setupDatabaseObservers() } private fun initKoin() { @@ -116,6 +113,22 @@ class KotatsuApp : Application() { } } + private fun setupDatabaseObservers() { + val observers = getKoin().getAll() + val database = get() + val tracker = database.invalidationTracker + observers.forEach { + tracker.addObserver(it) + } + } + + private fun setupActivityLifecycleCallbacks() { + val callbacks = getKoin().getAll() + callbacks.forEach { + registerActivityLifecycleCallbacks(it) + } + } + private fun enableStrictMode() { StrictMode.setThreadPolicy( StrictMode.ThreadPolicy.Builder() diff --git a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt index 1f348899b..f4bbba185 100644 --- a/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/bookmarks/data/BookmarkEntity.kt @@ -17,7 +17,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity ), ] ) -class BookmarkEntity( +data class BookmarkEntity( @ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "page_id", index = true) val pageId: Long, @ColumnInfo(name = "chapter_id") val chapterId: Long, diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt index d0e60f54c..27dd10255 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/BackupRepository.kt @@ -1,14 +1,12 @@ package org.koitharu.kotatsu.core.backup +import androidx.room.withTransaction import org.json.JSONArray import org.json.JSONObject import org.koitharu.kotatsu.BuildConfig import org.koitharu.kotatsu.core.db.MangaDatabase -import org.koitharu.kotatsu.core.db.entity.MangaEntity -import org.koitharu.kotatsu.core.db.entity.TagEntity -import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity -import org.koitharu.kotatsu.favourites.data.FavouriteEntity -import org.koitharu.kotatsu.history.data.HistoryEntity +import org.koitharu.kotatsu.parsers.util.json.JSONIterator +import org.koitharu.kotatsu.parsers.util.json.mapJSON private const val PAGE_SIZE = 10 @@ -24,11 +22,11 @@ class BackupRepository(private val db: MangaDatabase) { } offset += history.size for (item in history) { - val manga = item.manga.toJson() + val manga = JsonSerializer(item.manga).toJson() val tags = JSONArray() - item.tags.forEach { tags.put(it.toJson()) } + item.tags.forEach { tags.put(JsonSerializer(it).toJson()) } manga.put("tags", tags) - val json = item.history.toJson() + val json = JsonSerializer(item.history).toJson() json.put("manga", manga) entry.data.put(json) } @@ -40,7 +38,7 @@ class BackupRepository(private val db: MangaDatabase) { val entry = BackupEntry(BackupEntry.CATEGORIES, JSONArray()) val categories = db.favouriteCategoriesDao.findAll() for (item in categories) { - entry.data.put(item.toJson()) + entry.data.put(JsonSerializer(item).toJson()) } return entry } @@ -55,11 +53,11 @@ class BackupRepository(private val db: MangaDatabase) { } offset += favourites.size for (item in favourites) { - val manga = item.manga.toJson() + val manga = JsonSerializer(item.manga).toJson() val tags = JSONArray() - item.tags.forEach { tags.put(it.toJson()) } + item.tags.forEach { tags.put(JsonSerializer(it).toJson()) } manga.put("tags", tags) - val json = item.favourite.toJson() + val json = JsonSerializer(item.favourite).toJson() json.put("manga", manga) entry.data.put(json) } @@ -77,62 +75,54 @@ class BackupRepository(private val db: MangaDatabase) { return entry } - private fun MangaEntity.toJson(): JSONObject { - val jo = JSONObject() - jo.put("id", id) - jo.put("title", title) - jo.put("alt_title", altTitle) - jo.put("url", url) - jo.put("public_url", publicUrl) - jo.put("rating", rating) - jo.put("nsfw", isNsfw) - jo.put("cover_url", coverUrl) - jo.put("large_cover_url", largeCoverUrl) - jo.put("state", state) - jo.put("author", author) - jo.put("source", source) - return jo - } - - private fun TagEntity.toJson(): JSONObject { - val jo = JSONObject() - jo.put("id", id) - jo.put("title", title) - jo.put("key", key) - jo.put("source", source) - return jo - } - - private fun HistoryEntity.toJson(): JSONObject { - val jo = JSONObject() - jo.put("manga_id", mangaId) - jo.put("created_at", createdAt) - jo.put("updated_at", updatedAt) - jo.put("chapter_id", chapterId) - jo.put("page", page) - jo.put("scroll", scroll) - jo.put("percent", percent) - return jo + suspend fun restoreHistory(entry: BackupEntry): CompositeResult { + val result = CompositeResult() + for (item in entry.data.JSONIterator()) { + val mangaJson = item.getJSONObject("manga") + val manga = JsonDeserializer(mangaJson).toMangaEntity() + val tags = mangaJson.getJSONArray("tags").mapJSON { + JsonDeserializer(it).toTagEntity() + } + val history = JsonDeserializer(item).toHistoryEntity() + result += runCatching { + db.withTransaction { + db.tagsDao.upsert(tags) + db.mangaDao.upsert(manga, tags) + db.historyDao.upsert(history) + } + } + } + return result } - private fun FavouriteCategoryEntity.toJson(): JSONObject { - val jo = JSONObject() - jo.put("category_id", categoryId) - jo.put("created_at", createdAt) - jo.put("sort_key", sortKey) - jo.put("title", title) - jo.put("order", order) - jo.put("track", track) - jo.put("show_in_lib", isVisibleInLibrary) - return jo + suspend fun restoreCategories(entry: BackupEntry): CompositeResult { + val result = CompositeResult() + for (item in entry.data.JSONIterator()) { + val category = JsonDeserializer(item).toFavouriteCategoryEntity() + result += runCatching { + db.favouriteCategoriesDao.upsert(category) + } + } + return result } - private fun FavouriteEntity.toJson(): JSONObject { - val jo = JSONObject() - jo.put("manga_id", mangaId) - jo.put("category_id", categoryId) - jo.put("created_at", createdAt) - jo.put("sort_key", sortKey) - return jo + suspend fun restoreFavourites(entry: BackupEntry): CompositeResult { + val result = CompositeResult() + for (item in entry.data.JSONIterator()) { + val mangaJson = item.getJSONObject("manga") + val manga = JsonDeserializer(mangaJson).toMangaEntity() + val tags = mangaJson.getJSONArray("tags").mapJSON { + JsonDeserializer(it).toTagEntity() + } + val favourite = JsonDeserializer(item).toFavouriteEntity() + result += runCatching { + db.withTransaction { + db.tagsDao.upsert(tags) + db.mangaDao.upsert(manga, tags) + db.favouritesDao.upsert(favourite) + } + } + } + return result } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt similarity index 50% rename from app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt rename to app/src/main/java/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt index 318d23fa6..9416afcb9 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt @@ -1,70 +1,27 @@ package org.koitharu.kotatsu.core.backup -import androidx.room.withTransaction import org.json.JSONObject -import org.koitharu.kotatsu.core.db.MangaDatabase import org.koitharu.kotatsu.core.db.entity.MangaEntity import org.koitharu.kotatsu.core.db.entity.TagEntity import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity import org.koitharu.kotatsu.favourites.data.FavouriteEntity import org.koitharu.kotatsu.history.data.HistoryEntity import org.koitharu.kotatsu.parsers.model.SortOrder -import org.koitharu.kotatsu.parsers.util.json.* +import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault +import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault +import org.koitharu.kotatsu.parsers.util.json.getIntOrDefault +import org.koitharu.kotatsu.parsers.util.json.getStringOrNull -class RestoreRepository(private val db: MangaDatabase) { +class JsonDeserializer(private val json: JSONObject) { - suspend fun upsertHistory(entry: BackupEntry): CompositeResult { - val result = CompositeResult() - for (item in entry.data.JSONIterator()) { - val mangaJson = item.getJSONObject("manga") - val manga = parseManga(mangaJson) - val tags = mangaJson.getJSONArray("tags").mapJSON { - parseTag(it) - } - val history = parseHistory(item) - result += runCatching { - db.withTransaction { - db.tagsDao.upsert(tags) - db.mangaDao.upsert(manga, tags) - db.historyDao.upsert(history) - } - } - } - return result - } - - suspend fun upsertCategories(entry: BackupEntry): CompositeResult { - val result = CompositeResult() - for (item in entry.data.JSONIterator()) { - val category = parseCategory(item) - result += runCatching { - db.favouriteCategoriesDao.upsert(category) - } - } - return result - } - - suspend fun upsertFavourites(entry: BackupEntry): CompositeResult { - val result = CompositeResult() - for (item in entry.data.JSONIterator()) { - val mangaJson = item.getJSONObject("manga") - val manga = parseManga(mangaJson) - val tags = mangaJson.getJSONArray("tags").mapJSON { - parseTag(it) - } - val favourite = parseFavourite(item) - result += runCatching { - db.withTransaction { - db.tagsDao.upsert(tags) - db.mangaDao.upsert(manga, tags) - db.favouritesDao.upsert(favourite) - } - } - } - return result - } + fun toFavouriteEntity() = FavouriteEntity( + mangaId = json.getLong("manga_id"), + categoryId = json.getLong("category_id"), + sortKey = json.getIntOrDefault("sort_key", 0), + createdAt = json.getLong("created_at"), + ) - private fun parseManga(json: JSONObject) = MangaEntity( + fun toMangaEntity() = MangaEntity( id = json.getLong("id"), title = json.getString("title"), altTitle = json.getStringOrNull("alt_title"), @@ -79,14 +36,14 @@ class RestoreRepository(private val db: MangaDatabase) { source = json.getString("source") ) - private fun parseTag(json: JSONObject) = TagEntity( + fun toTagEntity() = TagEntity( id = json.getLong("id"), title = json.getString("title"), key = json.getString("key"), source = json.getString("source") ) - private fun parseHistory(json: JSONObject) = HistoryEntity( + fun toHistoryEntity() = HistoryEntity( mangaId = json.getLong("manga_id"), createdAt = json.getLong("created_at"), updatedAt = json.getLong("updated_at"), @@ -96,7 +53,7 @@ class RestoreRepository(private val db: MangaDatabase) { percent = json.getFloatOrDefault("percent", -1f), ) - private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity( + fun toFavouriteCategoryEntity() = FavouriteCategoryEntity( categoryId = json.getInt("category_id"), createdAt = json.getLong("created_at"), sortKey = json.getInt("sort_key"), @@ -105,11 +62,4 @@ class RestoreRepository(private val db: MangaDatabase) { track = json.getBooleanOrDefault("track", true), isVisibleInLibrary = json.getBooleanOrDefault("show_in_lib", true), ) - - private fun parseFavourite(json: JSONObject) = FavouriteEntity( - mangaId = json.getLong("manga_id"), - categoryId = json.getLong("category_id"), - createdAt = json.getLong("created_at"), - sortKey = json.getIntOrDefault("sort_key", 0), - ) } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonSerializer.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonSerializer.kt new file mode 100644 index 000000000..3d4d2f068 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonSerializer.kt @@ -0,0 +1,72 @@ +package org.koitharu.kotatsu.core.backup + +import org.json.JSONObject +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity +import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity +import org.koitharu.kotatsu.favourites.data.FavouriteEntity +import org.koitharu.kotatsu.history.data.HistoryEntity + +class JsonSerializer private constructor(private val json: JSONObject) { + + constructor(e: FavouriteEntity) : this( + JSONObject().apply { + put("manga_id", e.mangaId) + put("category_id", e.categoryId) + put("sort_key", e.sortKey) + put("created_at", e.createdAt) + } + ) + + constructor(e: FavouriteCategoryEntity) : this( + JSONObject().apply { + put("category_id", e.categoryId) + put("created_at", e.createdAt) + put("sort_key", e.sortKey) + put("title", e.title) + put("order", e.order) + put("track", e.track) + put("show_in_lib", e.isVisibleInLibrary) + } + ) + + constructor(e: HistoryEntity) : this( + JSONObject().apply { + put("manga_id", e.mangaId) + put("created_at", e.createdAt) + put("updated_at", e.updatedAt) + put("chapter_id", e.chapterId) + put("page", e.page) + put("scroll", e.scroll) + put("percent", e.percent) + } + ) + + constructor(e: TagEntity) : this( + JSONObject().apply { + put("id", e.id) + put("title", e.title) + put("key", e.key) + put("source", e.source) + } + ) + + constructor(e: MangaEntity) : this( + JSONObject().apply { + put("id", e.id) + put("title", e.title) + put("alt_title", e.altTitle) + put("url", e.url) + put("public_url", e.publicUrl) + put("rating", e.rating) + put("nsfw", e.isNsfw) + put("cover_url", e.coverUrl) + put("large_cover_url", e.largeCoverUrl) + put("state", e.state) + put("author", e.author) + put("source", e.source) + } + ) + + fun toJson(): JSONObject = json +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt index 476dea694..3f48543fe 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/MangaDatabase.kt @@ -29,6 +29,8 @@ import org.koitharu.kotatsu.tracker.data.TrackEntity import org.koitharu.kotatsu.tracker.data.TrackLogEntity import org.koitharu.kotatsu.tracker.data.TracksDao +const val DATABASE_VERSION = 13 + @Database( entities = [ MangaEntity::class, TagEntity::class, HistoryEntity::class, MangaTagsEntity::class, @@ -36,7 +38,7 @@ import org.koitharu.kotatsu.tracker.data.TracksDao TrackEntity::class, TrackLogEntity::class, SuggestionEntity::class, BookmarkEntity::class, ScrobblingEntity::class, ], - version = 13, + version = DATABASE_VERSION, ) abstract class MangaDatabase : RoomDatabase() { diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt new file mode 100644 index 000000000..28920a626 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt @@ -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" diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt index d7bf917f4..36e534c71 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaEntity.kt @@ -3,9 +3,10 @@ package org.koitharu.kotatsu.core.db.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.koitharu.kotatsu.core.db.TABLE_MANGA -@Entity(tableName = "manga") -class MangaEntity( +@Entity(tableName = TABLE_MANGA) +data class MangaEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "manga_id") val id: Long, @ColumnInfo(name = "title") val title: String, @@ -18,44 +19,5 @@ class MangaEntity( @ColumnInfo(name = "large_cover_url") val largeCoverUrl: String?, @ColumnInfo(name = "state") val state: String?, @ColumnInfo(name = "author") val author: String?, - @ColumnInfo(name = "source") val source: String -) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as MangaEntity - - if (id != other.id) return false - if (title != other.title) return false - if (altTitle != other.altTitle) return false - if (url != other.url) return false - if (publicUrl != other.publicUrl) return false - if (rating != other.rating) return false - if (isNsfw != other.isNsfw) return false - if (coverUrl != other.coverUrl) return false - if (largeCoverUrl != other.largeCoverUrl) return false - if (state != other.state) return false - if (author != other.author) return false - if (source != other.source) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + title.hashCode() - result = 31 * result + (altTitle?.hashCode() ?: 0) - result = 31 * result + url.hashCode() - result = 31 * result + publicUrl.hashCode() - result = 31 * result + rating.hashCode() - result = 31 * result + isNsfw.hashCode() - result = 31 * result + coverUrl.hashCode() - result = 31 * result + (largeCoverUrl?.hashCode() ?: 0) - result = 31 * result + (state?.hashCode() ?: 0) - result = 31 * result + (author?.hashCode() ?: 0) - result = 31 * result + source.hashCode() - return result - } -} \ No newline at end of file + @ColumnInfo(name = "source") val source: String, +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt index d3ee401a6..e7a59c5d0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/MangaTagsEntity.kt @@ -3,9 +3,11 @@ package org.koitharu.kotatsu.core.db.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS @Entity( - tableName = "manga_tags", primaryKeys = ["manga_id", "tag_id"], + tableName = TABLE_MANGA_TAGS, + primaryKeys = ["manga_id", "tag_id"], foreignKeys = [ ForeignKey( entity = MangaEntity::class, @@ -23,5 +25,5 @@ import androidx.room.ForeignKey ) class MangaTagsEntity( @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, ) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt index 58bb45d5b..7f147c992 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/db/entity/TagEntity.kt @@ -3,35 +3,13 @@ package org.koitharu.kotatsu.core.db.entity import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.koitharu.kotatsu.core.db.TABLE_TAGS -@Entity(tableName = "tags") -class TagEntity( +@Entity(tableName = TABLE_TAGS) +data class TagEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "tag_id") val id: Long, @ColumnInfo(name = "title") val title: String, @ColumnInfo(name = "key") val key: String, - @ColumnInfo(name = "source") val source: String -) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as TagEntity - - if (id != other.id) return false - if (title != other.title) return false - if (key != other.key) return false - if (source != other.source) return false - - return true - } - - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + title.hashCode() - result = 31 * result + key.hashCode() - result = 31 * result + source.hashCode() - return result - } -} \ No newline at end of file + @ColumnInfo(name = "source") val source: String, +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt similarity index 69% rename from app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt rename to app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt index 9e8b18a52..dd18378af 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt @@ -6,38 +6,41 @@ import android.content.pm.ShortcutManager import android.media.ThumbnailUtils import android.os.Build import android.util.Size +import androidx.annotation.VisibleForTesting import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.graphics.drawable.IconCompat +import androidx.room.InvalidationTracker import coil.ImageLoader import coil.request.ImageRequest import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext +import kotlinx.coroutines.Job +import kotlinx.coroutines.launch import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.MangaDataRepository +import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.history.domain.HistoryRepository import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.reader.ui.ReaderActivity +import org.koitharu.kotatsu.utils.ext.processLifecycleScope import org.koitharu.kotatsu.utils.ext.requireBitmap -class ShortcutsRepository( +class ShortcutsUpdater( private val context: Context, private val coil: ImageLoader, private val historyRepository: HistoryRepository, private val mangaRepository: MangaDataRepository, -) { +) : InvalidationTracker.Observer(TABLE_HISTORY) { - private val iconSize by lazy { - getIconSize(context) - } + private val iconSize by lazy { getIconSize(context) } + private var shortcutsUpdateJob: Job? = null - suspend fun updateShortcuts() { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N_MR1) return - val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager - val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) - .filter { x -> x.title.isNotEmpty() } - .map { buildShortcutInfo(it).build().toShortcutInfo() } - manager.dynamicShortcuts = shortcuts + override fun onInvalidated(tables: MutableSet) { + val prevJob = shortcutsUpdateJob + shortcutsUpdateJob = processLifecycleScope.launch(Dispatchers.Default) { + prevJob?.join() + updateShortcutsImpl() + } } suspend fun requestPinShortcut(manga: Manga): Boolean { @@ -48,17 +51,28 @@ class ShortcutsRepository( ) } + @VisibleForTesting + suspend fun await(): Boolean { + return shortcutsUpdateJob?.join() != null + } + + private suspend fun updateShortcutsImpl() { + val manager = context.getSystemService(Context.SHORTCUT_SERVICE) as ShortcutManager + val shortcuts = historyRepository.getList(0, manager.maxShortcutCountPerActivity) + .filter { x -> x.title.isNotEmpty() } + .map { buildShortcutInfo(it).build().toShortcutInfo() } + manager.dynamicShortcuts = shortcuts + } + private suspend fun buildShortcutInfo(manga: Manga): ShortcutInfoCompat.Builder { val icon = runCatching { - withContext(Dispatchers.IO) { - val bmp = coil.execute( - ImageRequest.Builder(context) - .data(manga.coverUrl) - .size(iconSize.width, iconSize.height) - .build() - ).requireBitmap() - ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) - } + val bmp = coil.execute( + ImageRequest.Builder(context) + .data(manga.coverUrl) + .size(iconSize.width, iconSize.height) + .build() + ).requireBitmap() + ThumbnailUtils.extractThumbnail(bmp, iconSize.width, iconSize.height, 0) }.fold( onSuccess = { IconCompat.createWithAdaptiveBitmap(it) }, onFailure = { IconCompat.createWithResource(context, R.drawable.ic_shortcut_default) } diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index 14b71fecb..e821d08dc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -34,7 +34,7 @@ import org.koitharu.kotatsu.base.ui.BaseActivity import org.koitharu.kotatsu.browser.BrowserActivity import org.koitharu.kotatsu.core.exceptions.resolve.ExceptionResolver import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga -import org.koitharu.kotatsu.core.os.ShortcutsRepository +import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.databinding.ActivityDetailsBinding import org.koitharu.kotatsu.details.ui.adapter.BranchesAdapter import org.koitharu.kotatsu.download.ui.service.DownloadService @@ -222,7 +222,7 @@ class DetailsActivity : R.id.action_shortcut -> { viewModel.manga.value?.let { lifecycleScope.launch { - if (!get().requestPinShortcut(it)) { + if (!get().requestPinShortcut(it)) { binding.snackbar.show(getString(R.string.operation_not_supported)) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt index c6ff39f04..062454a82 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteCategoryEntity.kt @@ -3,9 +3,10 @@ package org.koitharu.kotatsu.favourites.data import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.PrimaryKey +import org.koitharu.kotatsu.core.db.TABLE_FAVOURITE_CATEGORIES -@Entity(tableName = "favourite_categories") -class FavouriteCategoryEntity( +@Entity(tableName = TABLE_FAVOURITE_CATEGORIES) +data class FavouriteCategoryEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "category_id") val categoryId: Int, @ColumnInfo(name = "created_at") val createdAt: Long, diff --git a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt index e2232b9b7..209707a3b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/favourites/data/FavouriteEntity.kt @@ -3,10 +3,13 @@ package org.koitharu.kotatsu.favourites.data import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey +import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES import org.koitharu.kotatsu.core.db.entity.MangaEntity @Entity( - tableName = "favourites", primaryKeys = ["manga_id", "category_id"], foreignKeys = [ + tableName = TABLE_FAVOURITES, + primaryKeys = ["manga_id", "category_id"], + foreignKeys = [ ForeignKey( entity = MangaEntity::class, parentColumns = ["manga_id"], @@ -21,32 +24,9 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity ) ] ) -class FavouriteEntity( +data class FavouriteEntity( @ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "category_id", index = true) val categoryId: Long, @ColumnInfo(name = "sort_key") val sortKey: Int, @ColumnInfo(name = "created_at") val createdAt: Long, -) { - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as FavouriteEntity - - if (mangaId != other.mangaId) return false - if (categoryId != other.categoryId) return false - if (sortKey != other.sortKey) return false - if (createdAt != other.createdAt) return false - - return true - } - - override fun hashCode(): Int { - var result = mangaId.hashCode() - result = 31 * result + categoryId.hashCode() - result = 31 * result + sortKey - result = 31 * result + createdAt.hashCode() - return result - } -} \ No newline at end of file +) \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt index 319353b69..029d024f0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/HistoryModule.kt @@ -10,5 +10,5 @@ val historyModule single { HistoryRepository(get(), get(), get(), getAll()) } - viewModel { HistoryListViewModel(get(), get(), get(), get()) } + viewModel { HistoryListViewModel(get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt index 181499fa4..38e2daa7b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/data/HistoryEntity.kt @@ -4,10 +4,11 @@ import androidx.room.ColumnInfo import androidx.room.Entity import androidx.room.ForeignKey import androidx.room.PrimaryKey +import org.koitharu.kotatsu.core.db.TABLE_HISTORY import org.koitharu.kotatsu.core.db.entity.MangaEntity @Entity( - tableName = "history", + tableName = TABLE_HISTORY, foreignKeys = [ ForeignKey( entity = MangaEntity::class, @@ -17,7 +18,7 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity ) ] ) -class HistoryEntity( +data class HistoryEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "manga_id") val mangaId: Long, @ColumnInfo(name = "created_at") val createdAt: Long, diff --git a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt index 2e930c529..7cb6d266e 100644 --- a/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/history/ui/HistoryListViewModel.kt @@ -9,8 +9,6 @@ import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onStart import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.domain.ReversibleHandle -import org.koitharu.kotatsu.base.domain.plus -import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.prefs.observeAsFlow @@ -31,7 +29,6 @@ import java.util.concurrent.TimeUnit class HistoryListViewModel( private val repository: HistoryRepository, private val settings: AppSettings, - private val shortcutsRepository: ShortcutsRepository, private val trackingRepository: TrackingRepository, ) : MangaListViewModel(settings) { @@ -72,7 +69,6 @@ class HistoryListViewModel( fun clearHistory() { launchLoadingJob { repository.clear() - shortcutsRepository.updateShortcuts() } } @@ -81,10 +77,7 @@ class HistoryListViewModel( return } launchJob(Dispatchers.Default) { - val handle = repository.deleteReversible(ids) + ReversibleHandle { - shortcutsRepository.updateShortcuts() - } - shortcutsRepository.updateShortcuts() + val handle = repository.deleteReversible(ids) onItemsRemoved.postCall(handle) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/library/LibraryModule.kt b/app/src/main/java/org/koitharu/kotatsu/library/LibraryModule.kt index a7dcb6be5..bac79d44b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/LibraryModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/LibraryModule.kt @@ -11,6 +11,6 @@ val libraryModule factory { LibraryRepository(get()) } - viewModel { LibraryViewModel(get(), get(), get(), get(), get()) } + viewModel { LibraryViewModel(get(), get(), get(), get()) } viewModel { LibraryCategoriesConfigViewModel(get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt index ea7fc5c13..d3024c8bc 100644 --- a/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/library/ui/LibraryViewModel.kt @@ -7,12 +7,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.combine import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.base.domain.ReversibleHandle -import org.koitharu.kotatsu.base.domain.plus import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.base.ui.util.ReversibleAction import org.koitharu.kotatsu.core.model.FavouriteCategory -import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.core.prefs.ListMode import org.koitharu.kotatsu.core.ui.DateTimeAgo @@ -36,7 +33,6 @@ private const val HISTORY_MAX_SEGMENTS = 2 class LibraryViewModel( private val repository: LibraryRepository, private val historyRepository: HistoryRepository, - private val shortcutsRepository: ShortcutsRepository, private val trackingRepository: TrackingRepository, private val settings: AppSettings, ) : BaseViewModel(), ListExtraProvider { @@ -88,10 +84,7 @@ class LibraryViewModel( return } launchJob(Dispatchers.Default) { - val handle = historyRepository.deleteReversible(ids) + ReversibleHandle { - shortcutsRepository.updateShortcuts() - } - shortcutsRepository.updateShortcuts() + val handle = historyRepository.deleteReversible(ids) onActionDone.postCall(ReversibleAction(R.string.removed_from_history, handle)) } } @@ -105,7 +98,6 @@ class LibraryViewModel( historyRepository.deleteAfter(minDate) R.string.removed_from_history } - shortcutsRepository.updateShortcuts() onActionDone.postCall(ReversibleAction(stringRes, null)) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt index f8cb28657..b116fdf97 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/LocalModule.kt @@ -16,5 +16,5 @@ val localModule factory { DownloadManager.Factory(androidContext(), get(), get(), get(), get(), get()) } - viewModel { LocalListViewModel(get(), get(), get(), get()) } + viewModel { LocalListViewModel(get(), get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt index c3a38bc9a..4498a06e2 100644 --- a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalListViewModel.kt @@ -11,7 +11,6 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.koitharu.kotatsu.R -import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.prefs.AppSettings import org.koitharu.kotatsu.download.ui.service.DownloadService import org.koitharu.kotatsu.history.domain.HistoryRepository @@ -32,7 +31,6 @@ class LocalListViewModel( private val repository: LocalMangaRepository, private val historyRepository: HistoryRepository, settings: AppSettings, - private val shortcutsRepository: ShortcutsRepository, ) : MangaListViewModel(settings) { val onMangaRemoved = SingleLiveEvent() @@ -103,7 +101,6 @@ class LocalListViewModel( } } } - shortcutsRepository.updateShortcuts() onMangaRemoved.call(Unit) } } diff --git a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt index 7bcca45d6..51aa633ec 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt @@ -1,19 +1,29 @@ package org.koitharu.kotatsu.main +import android.app.Application +import android.os.Build +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.base.ui.util.ActivityRecreationHandle -import org.koitharu.kotatsu.core.os.ShortcutsRepository +import org.koitharu.kotatsu.core.os.ShortcutsUpdater import org.koitharu.kotatsu.main.ui.MainViewModel import org.koitharu.kotatsu.main.ui.protect.AppProtectHelper import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel val mainModule get() = module { - single { AppProtectHelper(get()) } - single { ActivityRecreationHandle() } - factory { ShortcutsRepository(androidContext(), get(), get(), get()) } + single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class + single { ActivityRecreationHandle() } bind Application.ActivityLifecycleCallbacks::class + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) { + single { ShortcutsUpdater(androidContext(), get(), get(), get()) } bind InvalidationTracker.Observer::class + } else { + factory { ShortcutsUpdater(androidContext(), get(), get(), get()) } + } + viewModel { MainViewModel(get(), get()) } viewModel { ProtectViewModel(get(), get()) } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt index a27fb9e8d..5b8faf203 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ReaderModule.kt @@ -23,7 +23,6 @@ val readerModule preselectedBranch = params[2], dataRepository = get(), historyRepository = get(), - shortcutsRepository = get(), settings = get(), pageSaveHelper = get(), bookmarksRepository = get(), diff --git a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt index 0f18ea1a8..85acb6c89 100644 --- a/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/reader/ui/ReaderViewModel.kt @@ -17,7 +17,6 @@ import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.bookmarks.domain.Bookmark import org.koitharu.kotatsu.bookmarks.domain.BookmarksRepository import org.koitharu.kotatsu.core.exceptions.MangaNotFoundException -import org.koitharu.kotatsu.core.os.ShortcutsRepository import org.koitharu.kotatsu.core.parser.MangaRepository import org.koitharu.kotatsu.core.prefs.* import org.koitharu.kotatsu.history.domain.HistoryRepository @@ -46,7 +45,6 @@ class ReaderViewModel( private val preselectedBranch: String?, private val dataRepository: MangaDataRepository, private val historyRepository: HistoryRepository, - private val shortcutsRepository: ShortcutsRepository, private val bookmarksRepository: BookmarksRepository, private val settings: AppSettings, private val pageSaveHelper: PageSaveHelper, @@ -289,7 +287,6 @@ class ReaderViewModel( currentState.value?.let { val percent = computePercent(it.chapterId, it.page) historyRepository.addOrUpdate(manga, it.chapterId, it.page, it.scroll, percent) - shortcutsRepository.updateShortcuts() } content.postValue(ReaderContent(pages, currentState.value)) diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt index f205482d3..a888ad37b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -1,12 +1,14 @@ package org.koitharu.kotatsu.settings import android.net.Uri +import android.os.Build +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.core.backup.BackupRepository -import org.koitharu.kotatsu.core.backup.RestoreRepository 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.RestoreViewModel import org.koitharu.kotatsu.settings.newsources.NewSourcesViewModel @@ -18,8 +20,11 @@ import org.koitharu.kotatsu.settings.tools.ToolsViewModel val settingsModule get() = module { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + single { BackupObserver(androidContext()) } + } + factory { BackupRepository(get()) } - factory { RestoreRepository(get()) } single(createdAtStart = true) { AppSettings(androidContext()) } viewModel { BackupViewModel(get(), androidContext()) } diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt index baa2a5217..27c9bbcb0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt @@ -4,9 +4,14 @@ import android.app.backup.BackupAgent import android.app.backup.BackupDataInput import android.app.backup.BackupDataOutput import android.app.backup.FullBackupDataOutput +import android.content.Context import android.os.ParcelFileDescriptor +import androidx.annotation.VisibleForTesting import kotlinx.coroutines.runBlocking -import org.koitharu.kotatsu.core.backup.* +import org.koitharu.kotatsu.core.backup.BackupEntry +import org.koitharu.kotatsu.core.backup.BackupRepository +import org.koitharu.kotatsu.core.backup.BackupZipInput +import org.koitharu.kotatsu.core.backup.BackupZipOutput import org.koitharu.kotatsu.core.db.MangaDatabase import java.io.* @@ -26,7 +31,7 @@ class AppBackupAgent : BackupAgent() { override fun onFullBackup(data: FullBackupDataOutput) { super.onFullBackup(data) - val file = createBackupFile() + val file = createBackupFile(this, BackupRepository(MangaDatabase(applicationContext))) try { fullBackupFile(file, data) } finally { @@ -43,16 +48,16 @@ class AppBackupAgent : BackupAgent() { mtime: Long ) { if (destination?.name?.endsWith(".bk.zip") == true) { - restoreBackupFile(data.fileDescriptor, size) + restoreBackupFile(data.fileDescriptor, size, BackupRepository(MangaDatabase(applicationContext))) destination.delete() } else { super.onRestoreFile(data, size, destination, type, mode, mtime) } } - private fun createBackupFile() = runBlocking { - val repository = BackupRepository(MangaDatabase(applicationContext)) - BackupZipOutput(this@AppBackupAgent).use { backup -> + @VisibleForTesting + fun createBackupFile(context: Context, repository: BackupRepository) = runBlocking { + BackupZipOutput(context).use { backup -> backup.put(repository.createIndex()) backup.put(repository.dumpHistory()) backup.put(repository.dumpCategories()) @@ -62,8 +67,8 @@ class AppBackupAgent : BackupAgent() { } } - private fun restoreBackupFile(fd: FileDescriptor, size: Long) { - val repository = RestoreRepository(MangaDatabase(applicationContext)) + @VisibleForTesting + fun restoreBackupFile(fd: FileDescriptor, size: Long, repository: BackupRepository) { val tempFile = File.createTempFile("backup_", ".tmp") FileInputStream(fd).use { input -> tempFile.outputStream().use { output -> @@ -73,9 +78,9 @@ class AppBackupAgent : BackupAgent() { val backup = BackupZipInput(tempFile) try { runBlocking { - repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY)) - repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES)) - repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES)) + repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY)) + repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES)) + repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES)) } } finally { backup.close() diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupObserver.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupObserver.kt new file mode 100644 index 000000000..807e63e56 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupObserver.kt @@ -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) { + backupManager.dataChanged() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt index 79f2fc7c4..f63522140 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/backup/RestoreViewModel.kt @@ -7,9 +7,9 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.runInterruptible import org.koitharu.kotatsu.base.ui.BaseViewModel import org.koitharu.kotatsu.core.backup.BackupEntry +import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupZipInput import org.koitharu.kotatsu.core.backup.CompositeResult -import org.koitharu.kotatsu.core.backup.RestoreRepository import org.koitharu.kotatsu.utils.SingleLiveEvent import org.koitharu.kotatsu.utils.progress.Progress import java.io.File @@ -17,7 +17,7 @@ import java.io.FileNotFoundException class RestoreViewModel( uri: Uri?, - private val repository: RestoreRepository, + private val repository: BackupRepository, context: Context ) : BaseViewModel() { @@ -44,13 +44,13 @@ class RestoreViewModel( val result = CompositeResult() progress.value = Progress(0, 3) - result += repository.upsertHistory(backup.getEntry(BackupEntry.HISTORY)) + result += repository.restoreHistory(backup.getEntry(BackupEntry.HISTORY)) progress.value = Progress(1, 3) - result += repository.upsertCategories(backup.getEntry(BackupEntry.CATEGORIES)) + result += repository.restoreCategories(backup.getEntry(BackupEntry.CATEGORIES)) progress.value = Progress(2, 3) - result += repository.upsertFavourites(backup.getEntry(BackupEntry.FAVOURITES)) + result += repository.restoreFavourites(backup.getEntry(BackupEntry.FAVOURITES)) progress.value = Progress(3, 3) onRestoreDone.call(result) 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..fda8aba02 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..185d4d5b2 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,24 @@ 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/test/java/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt b/app/src/test/java/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt new file mode 100644 index 000000000..1417493f6 --- /dev/null +++ b/app/src/test/java/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt @@ -0,0 +1,95 @@ +package org.koitharu.kotatsu.core.backup + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.koitharu.kotatsu.core.db.entity.MangaEntity +import org.koitharu.kotatsu.core.db.entity.TagEntity +import org.koitharu.kotatsu.favourites.data.FavouriteCategoryEntity +import org.koitharu.kotatsu.favourites.data.FavouriteEntity +import org.koitharu.kotatsu.history.data.HistoryEntity +import org.koitharu.kotatsu.parsers.model.MangaSource +import org.koitharu.kotatsu.parsers.model.MangaState +import org.koitharu.kotatsu.parsers.model.SortOrder +import java.util.concurrent.TimeUnit + +class JsonSerializerTest { + + @Test + fun toFavouriteEntity() { + val entity = FavouriteEntity( + mangaId = 40, + categoryId = 20, + sortKey = 1, + createdAt = System.currentTimeMillis(), + ) + val json = JsonSerializer(entity).toJson() + val result = JsonDeserializer(json).toFavouriteEntity() + assertEquals(entity, result) + } + + @Test + fun toMangaEntity() { + val entity = MangaEntity( + id = 231, + title = "Lorem Ipsum", + altTitle = "Lorem Ispum 2", + url = "erw", + publicUrl = "hthth", + rating = 0.78f, + isNsfw = true, + coverUrl = "5345", + largeCoverUrl = null, + state = MangaState.FINISHED.name, + author = "RERE", + source = MangaSource.DUMMY.name, + ) + val json = JsonSerializer(entity).toJson() + val result = JsonDeserializer(json).toMangaEntity() + assertEquals(entity, result) + } + + @Test + fun toTagEntity() { + val entity = TagEntity( + id = 934023534, + title = "Adventure", + key = "adventure", + source = MangaSource.DUMMY.name, + ) + val json = JsonSerializer(entity).toJson() + val result = JsonDeserializer(json).toTagEntity() + assertEquals(entity, result) + } + + @Test + fun toHistoryEntity() { + val entity = HistoryEntity( + mangaId = 304135341, + createdAt = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(6), + updatedAt = System.currentTimeMillis(), + chapterId = 29014843034, + page = 35, + scroll = 24.0f, + percent = 0.6f, + ) + val json = JsonSerializer(entity).toJson() + val result = JsonDeserializer(json).toHistoryEntity() + assertEquals(entity, result) + } + + @Test + fun toFavouriteCategoryEntity() { + val entity = FavouriteCategoryEntity( + categoryId = 142, + createdAt = System.currentTimeMillis(), + sortKey = 14, + title = "Read later", + order = SortOrder.RATING.name, + track = false, + isVisibleInLibrary = true, + ) + val json = JsonSerializer(entity).toJson() + val result = JsonDeserializer(json).toFavouriteCategoryEntity() + assertEquals(entity, result) + } +} \ No newline at end of file diff --git a/app/src/test/java/org/koitharu/kotatsu/core/github/GithubRepositoryTest.kt b/app/src/test/java/org/koitharu/kotatsu/core/github/GithubRepositoryTest.kt new file mode 100644 index 000000000..b0c1d5678 --- /dev/null +++ b/app/src/test/java/org/koitharu/kotatsu/core/github/GithubRepositoryTest.kt @@ -0,0 +1,33 @@ +package org.koitharu.kotatsu.core.github + +import kotlinx.coroutines.test.runTest +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.internal.headersContentLength +import org.junit.Assert +import org.junit.Test +import org.koitharu.kotatsu.BuildConfig +import org.koitharu.kotatsu.parsers.util.await + +class GithubRepositoryTest { + + private val okHttpClient = OkHttpClient() + private val repository = GithubRepository(okHttpClient) + + @Test + fun getLatestVersion() = runTest { + val version = repository.getLatestVersion() + val versionId = VersionId(version.name) + + val apkHead = okHttpClient.newCall( + Request.Builder() + .url(version.apkUrl) + .head() + .build() + ).await() + + Assert.assertTrue(versionId <= VersionId(BuildConfig.VERSION_NAME)) + Assert.assertTrue(apkHead.isSuccessful) + Assert.assertEquals(version.apkSize, apkHead.headersContentLength()) + } +} \ No newline at end of file