From 9eb5e699e12305c87bb837d2eea98da6715d5752 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 09:19:29 +0300 Subject: [PATCH 1/8] Unit tests for json (de)serialization --- app/build.gradle | 1 + .../kotatsu/bookmarks/data/BookmarkEntity.kt | 2 +- .../kotatsu/core/backup/BackupRepository.kt | 118 ++++++++---------- .../kotatsu/core/backup/JsonDeserializer.kt | 62 +++++++++ .../kotatsu/core/backup/JsonSerializer.kt | 70 +++++++++++ .../kotatsu/core/backup/RestoreRepository.kt | 113 ----------------- .../kotatsu/core/db/entity/MangaEntity.kt | 2 +- .../kotatsu/core/db/entity/TagEntity.kt | 2 +- .../data/FavouriteCategoryEntity.kt | 2 +- .../favourites/data/FavouriteEntity.kt | 2 +- .../kotatsu/history/data/HistoryEntity.kt | 2 +- .../kotatsu/settings/SettingsModule.kt | 2 - .../kotatsu/settings/backup/AppBackupAgent.kt | 13 +- .../settings/backup/RestoreViewModel.kt | 10 +- .../kotatsu/core/backup/JsonSerializerTest.kt | 93 ++++++++++++++ 15 files changed, 300 insertions(+), 194 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/backup/JsonSerializer.kt delete mode 100644 app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt create mode 100644 app/src/test/java/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt diff --git a/app/build.gradle b/app/build.gradle index daa6a7d41..9e40045c8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -123,6 +123,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/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 7212b8569..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,60 +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) - 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) - 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/JsonDeserializer.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt new file mode 100644 index 000000000..96463e4a5 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonDeserializer.kt @@ -0,0 +1,62 @@ +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 +import org.koitharu.kotatsu.parsers.model.SortOrder +import org.koitharu.kotatsu.parsers.util.json.getBooleanOrDefault +import org.koitharu.kotatsu.parsers.util.json.getFloatOrDefault +import org.koitharu.kotatsu.parsers.util.json.getStringOrNull + +class JsonDeserializer(private val json: JSONObject) { + + fun toFavouriteEntity() = FavouriteEntity( + mangaId = json.getLong("manga_id"), + categoryId = json.getLong("category_id"), + createdAt = json.getLong("created_at"), + ) + + fun toMangaEntity() = MangaEntity( + id = json.getLong("id"), + title = json.getString("title"), + altTitle = json.getStringOrNull("alt_title"), + url = json.getString("url"), + publicUrl = json.getStringOrNull("public_url").orEmpty(), + rating = json.getDouble("rating").toFloat(), + isNsfw = json.getBooleanOrDefault("nsfw", false), + coverUrl = json.getString("cover_url"), + largeCoverUrl = json.getStringOrNull("large_cover_url"), + state = json.getStringOrNull("state"), + author = json.getStringOrNull("author"), + source = json.getString("source") + ) + + fun toTagEntity() = TagEntity( + id = json.getLong("id"), + title = json.getString("title"), + key = json.getString("key"), + source = json.getString("source") + ) + + fun toHistoryEntity() = HistoryEntity( + mangaId = json.getLong("manga_id"), + createdAt = json.getLong("created_at"), + updatedAt = json.getLong("updated_at"), + chapterId = json.getLong("chapter_id"), + page = json.getInt("page"), + scroll = json.getDouble("scroll").toFloat(), + percent = json.getFloatOrDefault("percent", -1f), + ) + + fun toFavouriteCategoryEntity() = FavouriteCategoryEntity( + categoryId = json.getInt("category_id"), + createdAt = json.getLong("created_at"), + sortKey = json.getInt("sort_key"), + title = json.getString("title"), + order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name, + track = json.getBooleanOrDefault("track", true), + ) +} \ 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..b6a035e87 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/core/backup/JsonSerializer.kt @@ -0,0 +1,70 @@ +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("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) + } + ) + + 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/backup/RestoreRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt deleted file mode 100644 index 4e20b955f..000000000 --- a/app/src/main/java/org/koitharu/kotatsu/core/backup/RestoreRepository.kt +++ /dev/null @@ -1,113 +0,0 @@ -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.* - -class RestoreRepository(private val db: MangaDatabase) { - - 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 - } - - private fun parseManga(json: JSONObject) = MangaEntity( - id = json.getLong("id"), - title = json.getString("title"), - altTitle = json.getStringOrNull("alt_title"), - url = json.getString("url"), - publicUrl = json.getStringOrNull("public_url").orEmpty(), - rating = json.getDouble("rating").toFloat(), - isNsfw = json.getBooleanOrDefault("nsfw", false), - coverUrl = json.getString("cover_url"), - largeCoverUrl = json.getStringOrNull("large_cover_url"), - state = json.getStringOrNull("state"), - author = json.getStringOrNull("author"), - source = json.getString("source") - ) - - private fun parseTag(json: JSONObject) = TagEntity( - id = json.getLong("id"), - title = json.getString("title"), - key = json.getString("key"), - source = json.getString("source") - ) - - private fun parseHistory(json: JSONObject) = HistoryEntity( - mangaId = json.getLong("manga_id"), - createdAt = json.getLong("created_at"), - updatedAt = json.getLong("updated_at"), - chapterId = json.getLong("chapter_id"), - page = json.getInt("page"), - scroll = json.getDouble("scroll").toFloat(), - percent = json.getFloatOrDefault("percent", -1f), - ) - - private fun parseCategory(json: JSONObject) = FavouriteCategoryEntity( - categoryId = json.getInt("category_id"), - createdAt = json.getLong("created_at"), - sortKey = json.getInt("sort_key"), - title = json.getString("title"), - order = json.getStringOrNull("order") ?: SortOrder.NEWEST.name, - track = json.getBooleanOrDefault("track", true), - ) - - private fun parseFavourite(json: JSONObject) = FavouriteEntity( - mangaId = json.getLong("manga_id"), - categoryId = json.getLong("category_id"), - createdAt = json.getLong("created_at") - ) -} \ No newline at end of file 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 c425f0e68..bfe64375f 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 @@ -5,7 +5,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "manga") -class MangaEntity( +data class MangaEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "manga_id") val id: Long, @ColumnInfo(name = "title") val title: String, 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 fe813a02b..8c5e927c4 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 @@ -5,7 +5,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "tags") -class TagEntity( +data class TagEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "tag_id") val id: Long, @ColumnInfo(name = "title") val title: String, 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 5fe02f019..5cc62dbae 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 @@ -5,7 +5,7 @@ import androidx.room.Entity import androidx.room.PrimaryKey @Entity(tableName = "favourite_categories") -class FavouriteCategoryEntity( +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 d79660a12..95ae66e87 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 @@ -21,7 +21,7 @@ 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 = "created_at") val createdAt: Long 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..4842a36a7 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 @@ -17,7 +17,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/settings/SettingsModule.kt b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt index 230ab0d32..a4fff5d93 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -5,7 +5,6 @@ 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.BackupViewModel import org.koitharu.kotatsu.settings.backup.RestoreViewModel @@ -18,7 +17,6 @@ val settingsModule get() = module { 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..52eb86cf0 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 @@ -6,7 +6,10 @@ import android.app.backup.BackupDataOutput import android.app.backup.FullBackupDataOutput import android.os.ParcelFileDescriptor 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.* @@ -63,7 +66,7 @@ class AppBackupAgent : BackupAgent() { } private fun restoreBackupFile(fd: FileDescriptor, size: Long) { - val repository = RestoreRepository(MangaDatabase(applicationContext)) + val repository = BackupRepository(MangaDatabase(applicationContext)) val tempFile = File.createTempFile("backup_", ".tmp") FileInputStream(fd).use { input -> tempFile.outputStream().use { output -> @@ -73,9 +76,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/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/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..45fda17ec --- /dev/null +++ b/app/src/test/java/org/koitharu/kotatsu/core/backup/JsonSerializerTest.kt @@ -0,0 +1,93 @@ +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, + 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, + ) + val json = JsonSerializer(entity).toJson() + val result = JsonDeserializer(json).toFavouriteCategoryEntity() + assertEquals(entity, result) + } +} \ No newline at end of file From dfa413da6f2962de3965b26944faf4a5f94c4701 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 09:55:49 +0300 Subject: [PATCH 2/8] Observe database updates using InvalidationTracker --- .../java/org/koitharu/kotatsu/KotatsuApp.kt | 29 ++++++++++----- .../org/koitharu/kotatsu/core/db/Tables.kt | 9 +++++ .../kotatsu/core/db/entity/MangaEntity.kt | 5 +-- .../kotatsu/core/db/entity/MangaTagsEntity.kt | 6 ++-- .../kotatsu/core/db/entity/TagEntity.kt | 5 +-- .../data/FavouriteCategoryEntity.kt | 3 +- .../favourites/data/FavouriteEntity.kt | 7 ++-- .../kotatsu/history/data/HistoryEntity.kt | 3 +- .../org/koitharu/kotatsu/main/MainModule.kt | 6 ++-- .../kotatsu/settings/SettingsModule.kt | 7 ++++ .../kotatsu/settings/backup/BackupObserver.kt | 22 ++++++++++++ .../kotatsu/widget/AppWidgetModule.kt | 5 +++ .../koitharu/kotatsu/widget/WidgetUpdater.kt | 36 +++++++------------ 13 files changed, 99 insertions(+), 44 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/core/db/Tables.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/settings/backup/BackupObserver.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt b/app/src/main/java/org/koitharu/kotatsu/KotatsuApp.kt index d7f25396e..fcbc79742 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 @@ -27,7 +29,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 @@ -36,7 +37,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() { @@ -48,11 +48,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() { @@ -112,6 +109,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/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 bfe64375f..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,8 +3,9 @@ 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") +@Entity(tableName = TABLE_MANGA) data class MangaEntity( @PrimaryKey(autoGenerate = false) @ColumnInfo(name = "manga_id") val id: Long, @@ -18,5 +19,5 @@ data 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 + @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 8c5e927c4..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,12 +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") +@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 + @ColumnInfo(name = "source") val source: String, ) \ No newline at end of file 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 5cc62dbae..4f4f594e9 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,8 +3,9 @@ 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") +@Entity(tableName = TABLE_FAVOURITE_CATEGORIES) data class FavouriteCategoryEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "category_id") val categoryId: Int, 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 95ae66e87..860465814 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"], @@ -24,5 +27,5 @@ import org.koitharu.kotatsu.core.db.entity.MangaEntity data class FavouriteEntity( @ColumnInfo(name = "manga_id", index = true) val mangaId: Long, @ColumnInfo(name = "category_id", index = true) val categoryId: Long, - @ColumnInfo(name = "created_at") val createdAt: Long + @ColumnInfo(name = "created_at") val createdAt: Long, ) \ 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 4842a36a7..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, 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..c85c04f26 100644 --- a/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/main/MainModule.kt @@ -1,7 +1,9 @@ package org.koitharu.kotatsu.main +import android.app.Application 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 @@ -11,8 +13,8 @@ import org.koitharu.kotatsu.main.ui.protect.ProtectViewModel val mainModule get() = module { - single { AppProtectHelper(get()) } - single { ActivityRecreationHandle() } + single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class + single { ActivityRecreationHandle() } bind Application.ActivityLifecycleCallbacks::class factory { ShortcutsRepository(androidContext(), get(), get(), get()) } viewModel { MainViewModel(get(), get()) } viewModel { ProtectViewModel(get(), get()) } 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 a4fff5d93..ca3fd8a2d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/SettingsModule.kt @@ -1,11 +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.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 @@ -16,6 +19,10 @@ import org.koitharu.kotatsu.settings.sources.SourcesSettingsViewModel val settingsModule get() = module { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + single { BackupObserver(androidContext()) } + } + factory { BackupRepository(get()) } single(createdAtStart = true) { AppSettings(androidContext()) } 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/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) From f0ee64bafacad5e02aff7c67f9ed3b88ad6f69e5 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 11:27:56 +0300 Subject: [PATCH 3/8] Unit test for BackupAgent --- app/build.gradle | 7 +- .../java/org/koitharu/kotatsu/SampleData.kt | 102 ++++++++++++++++++ .../settings/backup/AppBackupAgentTest.kt | 67 ++++++++++++ .../kotatsu/settings/backup/AppBackupAgent.kt | 16 +-- 4 files changed, 183 insertions(+), 9 deletions(-) create mode 100644 app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt create mode 100644 app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt diff --git a/app/build.gradle b/app/build.gradle index 9e40045c8..7c904e60a 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -64,8 +64,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 { 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..baeb96e27 --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt @@ -0,0 +1,102 @@ +package org.koitharu.kotatsu + +import org.koitharu.kotatsu.core.model.FavouriteCategory +import org.koitharu.kotatsu.parsers.model.* +import java.util.* + +object SampleData { + + val manga = Manga( + id = 1105355890252749533, + title = "Sasurai Emanon", + altTitle = null, + url = "/manga/sasurai_emanon/", + publicUrl = "https://www.mangatown.com/manga/sasurai_emanon/", + rating = 1.0f, + isNsfw = false, + coverUrl = "https://fmcdn.mangahere.com/store/manga/10992/ocover.jpg?token=905148d2f052f9d3604135933b958771c8b00077&ttl=1658214000&v=1578490983", + tags = setOf( + MangaTag(title = "Adventure", key = "0-adventure-0-0-0-0", source = MangaSource.MANGATOWN), + MangaTag(title = "Mature", key = "0-mature-0-0-0-0", source = MangaSource.MANGATOWN), + MangaTag(title = "Psychological", key = "0-psychological-0-0-0-0", source = MangaSource.MANGATOWN), + MangaTag(title = "Slice Of Life", key = "0-slice_of_life-0-0-0-0", source = MangaSource.MANGATOWN), + MangaTag(title = "Supernatural", key = "0-supernatural-0-0-0-0", source = MangaSource.MANGATOWN), + ), + state = MangaState.ONGOING, + author = "Kajio Shinji", + largeCoverUrl = null, + source = MangaSource.MANGATOWN, + ) + + val mangaDetails = manga.copy( + tags = setOf( + MangaTag(title = "Adventure", key = "0-adventure-0-0-0-0", source = MangaSource.MANGATOWN), + MangaTag(title = "Mature", key = "0-mature-0-0-0-0", source = MangaSource.MANGATOWN), + MangaTag(title = "Psychological", key = "0-psychological-0-0-0-0", source = MangaSource.MANGATOWN), + MangaTag(title = "Slice Of Life", key = "0-slice_of_life-0-0-0-0", source = MangaSource.MANGATOWN), + MangaTag(title = "Supernatural", key = "0-supernatural-0-0-0-0", source = MangaSource.MANGATOWN), + ), + largeCoverUrl = null, + description = """ + Based on the award-winning novel by Shinji Kajio, Memories of Emanon tells the story of a mysterious girl + who holds a 3-billion-year old memory, dating back to the moment life first appeared on Earth. The first + half of the volume is the colored Wandering Emanon '67 chapters (published before as Emanon Episode: 1). + The second half is Wandering Emanon set before the '67 chapters. + """.trimIndent(), + chapters = listOf( + MangaChapter( + id = -7214407414868456892, + name = "Sasurai Emanon - 1", + number = 1, + url = "/manga/sasurai_emanon/c001/", + scanlator = null, + uploadDate = 1335906000000, + branch = null, + source = MangaSource.MANGATOWN, + ), + MangaChapter( + id = -7214407414868456861, + name = "Sasurai Emanon - 2", + number = 2, + url = "/manga/sasurai_emanon/c002/", + scanlator = null, + uploadDate = 1335906000000, + branch = null, + source = MangaSource.MANGATOWN, + ), + MangaChapter( + id = -7214407414868456830, + name = "Sasurai Emanon - 3", + number = 3, + url = "/manga/sasurai_emanon/c003/", + scanlator = null, + uploadDate = 1335906000000, + branch = null, + source = MangaSource.MANGATOWN, + ), + MangaChapter( + id = -7214407414868456799, + name = "Sasurai Emanon - 4", + number = 3, + url = "/manga/sasurai_emanon/c004/", + scanlator = null, + uploadDate = 1335906000000, + branch = null, + source = MangaSource.MANGATOWN, + ), + ), + ) + + val tag = mangaDetails.tags.elementAt(2) + + val chapter = checkNotNull(mangaDetails.chapters)[2] + + val favouriteCategory = FavouriteCategory( + id = 4, + title = "Read later", + sortKey = 1, + order = SortOrder.NEWEST, + createdAt = Date(1335906000000), + isTrackingEnabled = true, + ) +} \ 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..afd61d83a --- /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.mangaDetails)) + + 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/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt b/app/src/main/java/org/koitharu/kotatsu/settings/backup/AppBackupAgent.kt index 52eb86cf0..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,7 +4,9 @@ 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.BackupEntry import org.koitharu.kotatsu.core.backup.BackupRepository @@ -29,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 { @@ -46,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()) @@ -65,8 +67,8 @@ class AppBackupAgent : BackupAgent() { } } - private fun restoreBackupFile(fd: FileDescriptor, size: Long) { - val repository = BackupRepository(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 -> From aa5fd530d30f36eaf4d8632d0c78ed0471405eae Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 11:34:00 +0300 Subject: [PATCH 4/8] Fix database test --- .../koitharu/kotatsu/core/db/MangaDatabaseTest.kt | 12 ++++++------ .../org/koitharu/kotatsu/core/db/MangaDatabase.kt | 4 +++- 2 files changed, 9 insertions(+), 7 deletions(-) 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..63b302b47 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,7 @@ class MangaDatabaseTest { Migration8To9(), Migration9To10(), Migration10To11(), + Migration11To12(), ) } } \ 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 82d5052aa..a648911bf 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 = 12 + @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 = 12, + version = DATABASE_VERSION, ) abstract class MangaDatabase : RoomDatabase() { From 62088b36a4980b258e4f784f3b4a01214543f186 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 12:16:39 +0300 Subject: [PATCH 5/8] Update app shortcuts using InvalidationTracker.Callback --- .../kotatsu/core/os/ShortcutsUpdaterTest.kt | 56 ++++++++++++++++++ ...tcutsRepository.kt => ShortcutsUpdater.kt} | 58 ++++++++++++------- .../kotatsu/details/ui/DetailsActivity.kt | 4 +- .../koitharu/kotatsu/history/HistoryModule.kt | 2 +- .../history/ui/HistoryListViewModel.kt | 9 +-- .../org/koitharu/kotatsu/local/LocalModule.kt | 2 +- .../kotatsu/local/ui/LocalListViewModel.kt | 3 - .../org/koitharu/kotatsu/main/MainModule.kt | 12 +++- .../koitharu/kotatsu/reader/ReaderModule.kt | 1 - .../kotatsu/reader/ui/ReaderViewModel.kt | 3 - 10 files changed, 107 insertions(+), 43 deletions(-) create mode 100644 app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt rename app/src/main/java/org/koitharu/kotatsu/core/os/{ShortcutsRepository.kt => ShortcutsUpdater.kt} (70%) 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..1dd8925de --- /dev/null +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt @@ -0,0 +1,56 @@ +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.delay +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.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 { + shortcutsUpdater.await() + assertTrue(getShortcuts().isEmpty()) + historyRepository.addOrUpdate( + manga = SampleData.manga, + chapterId = SampleData.chapter.id, + page = 4, + scroll = 2, + percent = 0.3f + ) + delay(1000) + shortcutsUpdater.await() + + 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" } + } +} \ 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 70% 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..424e312a9 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() { + shortcutsUpdateJob?.join() + } + + 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 d4339645c..8e517b73d 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 @@ -224,7 +224,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/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/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/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 e8490f9d9..ebd347913 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 @@ -29,7 +28,6 @@ class LocalListViewModel( private val repository: LocalMangaRepository, private val historyRepository: HistoryRepository, settings: AppSettings, - private val shortcutsRepository: ShortcutsRepository, ) : MangaListViewModel(settings) { val onMangaRemoved = SingleLiveEvent() @@ -107,7 +105,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 c85c04f26..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,12 +1,14 @@ 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 @@ -15,7 +17,13 @@ val mainModule get() = module { single { AppProtectHelper(get()) } bind Application.ActivityLifecycleCallbacks::class single { ActivityRecreationHandle() } bind Application.ActivityLifecycleCallbacks::class - factory { ShortcutsRepository(androidContext(), get(), get(), get()) } + + 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 0384e44aa..ea93e7267 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)) From 206fb4e584081729ef2b9e061b4899910b31f632 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 12:40:28 +0300 Subject: [PATCH 6/8] Update test data --- .../androidTest/assets/categories/simple.json | 8 ++ app/src/androidTest/assets/manga/header.json | 35 +++++ .../java/org/koitharu/kotatsu/SampleData.kt | 131 ++++++------------ .../kotatsu/tracker/domain/TrackerTest.kt | 21 +-- 4 files changed, 91 insertions(+), 104 deletions(-) create mode 100644 app/src/androidTest/assets/categories/simple.json create mode 100644 app/src/androidTest/assets/manga/header.json 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..1ebbee769 --- /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": null, + "description": null, + "source": "READMANGA_RU" +} \ 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 index baeb96e27..5c592cba5 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt @@ -1,102 +1,57 @@ 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.* +import org.koitharu.kotatsu.parsers.model.Manga import java.util.* +import kotlin.reflect.KClass object SampleData { - val manga = Manga( - id = 1105355890252749533, - title = "Sasurai Emanon", - altTitle = null, - url = "/manga/sasurai_emanon/", - publicUrl = "https://www.mangatown.com/manga/sasurai_emanon/", - rating = 1.0f, - isNsfw = false, - coverUrl = "https://fmcdn.mangahere.com/store/manga/10992/ocover.jpg?token=905148d2f052f9d3604135933b958771c8b00077&ttl=1658214000&v=1578490983", - tags = setOf( - MangaTag(title = "Adventure", key = "0-adventure-0-0-0-0", source = MangaSource.MANGATOWN), - MangaTag(title = "Mature", key = "0-mature-0-0-0-0", source = MangaSource.MANGATOWN), - MangaTag(title = "Psychological", key = "0-psychological-0-0-0-0", source = MangaSource.MANGATOWN), - MangaTag(title = "Slice Of Life", key = "0-slice_of_life-0-0-0-0", source = MangaSource.MANGATOWN), - MangaTag(title = "Supernatural", key = "0-supernatural-0-0-0-0", source = MangaSource.MANGATOWN), - ), - state = MangaState.ONGOING, - author = "Kajio Shinji", - largeCoverUrl = null, - source = MangaSource.MANGATOWN, - ) + private val moshi = Moshi.Builder() + .add(DateAdapter()) + .add(KotlinJsonAdapterFactory()) + .build() - val mangaDetails = manga.copy( - tags = setOf( - MangaTag(title = "Adventure", key = "0-adventure-0-0-0-0", source = MangaSource.MANGATOWN), - MangaTag(title = "Mature", key = "0-mature-0-0-0-0", source = MangaSource.MANGATOWN), - MangaTag(title = "Psychological", key = "0-psychological-0-0-0-0", source = MangaSource.MANGATOWN), - MangaTag(title = "Slice Of Life", key = "0-slice_of_life-0-0-0-0", source = MangaSource.MANGATOWN), - MangaTag(title = "Supernatural", key = "0-supernatural-0-0-0-0", source = MangaSource.MANGATOWN), - ), - largeCoverUrl = null, - description = """ - Based on the award-winning novel by Shinji Kajio, Memories of Emanon tells the story of a mysterious girl - who holds a 3-billion-year old memory, dating back to the moment life first appeared on Earth. The first - half of the volume is the colored Wandering Emanon '67 chapters (published before as Emanon Episode: 1). - The second half is Wandering Emanon set before the '67 chapters. - """.trimIndent(), - chapters = listOf( - MangaChapter( - id = -7214407414868456892, - name = "Sasurai Emanon - 1", - number = 1, - url = "/manga/sasurai_emanon/c001/", - scanlator = null, - uploadDate = 1335906000000, - branch = null, - source = MangaSource.MANGATOWN, - ), - MangaChapter( - id = -7214407414868456861, - name = "Sasurai Emanon - 2", - number = 2, - url = "/manga/sasurai_emanon/c002/", - scanlator = null, - uploadDate = 1335906000000, - branch = null, - source = MangaSource.MANGATOWN, - ), - MangaChapter( - id = -7214407414868456830, - name = "Sasurai Emanon - 3", - number = 3, - url = "/manga/sasurai_emanon/c003/", - scanlator = null, - uploadDate = 1335906000000, - branch = null, - source = MangaSource.MANGATOWN, - ), - MangaChapter( - id = -7214407414868456799, - name = "Sasurai Emanon - 4", - number = 3, - url = "/manga/sasurai_emanon/c004/", - scanlator = null, - uploadDate = 1335906000000, - branch = null, - source = MangaSource.MANGATOWN, - ), - ), - ) + val manga: Manga + get() = loadAsset("manga/header.json", Manga::class) + + val mangaDetails: Manga + get() = loadAsset("manga/full.json", Manga::class) val tag = mangaDetails.tags.elementAt(2) val chapter = checkNotNull(mangaDetails.chapters)[2] - val favouriteCategory = FavouriteCategory( - id = 4, - title = "Read later", - sortKey = 1, - order = SortOrder.NEWEST, - createdAt = Date(1335906000000), - isTrackingEnabled = true, - ) + val favouriteCategory: FavouriteCategory + get() = 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/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 } From 35f450e4449a5b9c28edad0f89839db5d4128653 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 12:56:56 +0300 Subject: [PATCH 7/8] Fix android tests --- app/src/androidTest/assets/manga/header.json | 2 +- .../org/koitharu/kotatsu/Instrumentation.kt | 9 +++++++++ .../java/org/koitharu/kotatsu/SampleData.kt | 9 +++------ .../kotatsu/core/os/ShortcutsUpdaterTest.kt | 17 +++++++++++++---- .../settings/backup/AppBackupAgentTest.kt | 2 +- .../kotatsu/core/os/ShortcutsUpdater.kt | 4 ++-- 6 files changed, 29 insertions(+), 14 deletions(-) create mode 100644 app/src/androidTest/java/org/koitharu/kotatsu/Instrumentation.kt diff --git a/app/src/androidTest/assets/manga/header.json b/app/src/androidTest/assets/manga/header.json index 1ebbee769..dc56dbf8e 100644 --- a/app/src/androidTest/assets/manga/header.json +++ b/app/src/androidTest/assets/manga/header.json @@ -29,7 +29,7 @@ } ], "state": "FINISHED", - "largeCoverUrl": null, + "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 index 5c592cba5..b7d4ad7e3 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/SampleData.kt @@ -17,18 +17,15 @@ object SampleData { .add(KotlinJsonAdapterFactory()) .build() - val manga: Manga - get() = loadAsset("manga/header.json", Manga::class) + val manga: Manga = loadAsset("manga/header.json", Manga::class) - val mangaDetails: Manga - get() = loadAsset("manga/full.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 - get() = loadAsset("categories/simple.json", FavouriteCategory::class) + val favouriteCategory: FavouriteCategory = loadAsset("categories/simple.json", FavouriteCategory::class) fun loadAsset(name: String, cls: KClass): T { val assets = InstrumentationRegistry.getInstrumentation().context.assets 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 index 1dd8925de..8b68552da 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/core/os/ShortcutsUpdaterTest.kt @@ -5,7 +5,6 @@ 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.delay import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -13,6 +12,7 @@ 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 @@ -32,7 +32,7 @@ class ShortcutsUpdaterTest : KoinTest { @Test fun testUpdateShortcuts() = runTest { - shortcutsUpdater.await() + awaitUpdate() assertTrue(getShortcuts().isEmpty()) historyRepository.addOrUpdate( manga = SampleData.manga, @@ -41,8 +41,7 @@ class ShortcutsUpdaterTest : KoinTest { scroll = 2, percent = 0.3f ) - delay(1000) - shortcutsUpdater.await() + awaitUpdate() val shortcuts = getShortcuts() assertEquals(1, shortcuts.size) @@ -53,4 +52,14 @@ class ShortcutsUpdaterTest : KoinTest { 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 index afd61d83a..1d0ca5498 100644 --- a/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt +++ b/app/src/androidTest/java/org/koitharu/kotatsu/settings/backup/AppBackupAgentTest.kt @@ -44,7 +44,7 @@ class AppBackupAgentTest : KoinTest { scroll = 40, percent = 0.2f, ) - val history = checkNotNull(historyRepository.getOne(SampleData.mangaDetails)) + val history = checkNotNull(historyRepository.getOne(SampleData.manga)) val agent = AppBackupAgent() val backup = agent.createBackupFile(get(), backupRepository) diff --git a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt index 424e312a9..dd18378af 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/os/ShortcutsUpdater.kt @@ -52,8 +52,8 @@ class ShortcutsUpdater( } @VisibleForTesting - suspend fun await() { - shortcutsUpdateJob?.join() + suspend fun await(): Boolean { + return shortcutsUpdateJob?.join() != null } private suspend fun updateShortcutsImpl() { From d470ca4b4750e7d3b2799d5b709a062a96d32fd3 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Mon, 18 Jul 2022 13:09:11 +0300 Subject: [PATCH 8/8] Test for update checking --- .../kotatsu/core/github/GithubRepository.kt | 2 +- .../core/github/GithubRepositoryTest.kt | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 app/src/test/java/org/koitharu/kotatsu/core/github/GithubRepositoryTest.kt diff --git a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubRepository.kt b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubRepository.kt index 8b9f4e793..0176823db 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/github/GithubRepository.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/github/GithubRepository.kt @@ -10,7 +10,7 @@ class GithubRepository(private val okHttp: OkHttpClient) { suspend fun getLatestVersion(): AppVersion { val request = Request.Builder() .get() - .url("https://api.github.com/repos/nv95/Kotatsu/releases/latest") + .url("https://api.github.com/repos/KotatsuApp/Kotatsu/releases/latest") val json = okHttp.newCall(request.build()).await().parseJson() val asset = json.getJSONArray("assets").getJSONObject(0) return AppVersion( 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