Bläddra i källkod

Legacy backup conversion to Kotlin Serialization (#5282)

* Legacy backup conversion to Kotlin Serialization

* Fix BackupTest compiling
jobobby04 3 år sedan
förälder
incheckning
597cec3064
15 ändrade filer med 416 tillägg och 365 borttagningar
  1. 34 27
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt
  2. 35 43
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt
  3. 18 20
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestoreValidator.kt
  4. 28 16
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/models/Backup.kt
  5. 0 31
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeAdapter.kt
  6. 49 0
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeSerializer.kt
  7. 0 59
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeAdapter.kt
  8. 66 0
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeSerializer.kt
  9. 0 32
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeAdapter.kt
  10. 41 0
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeSerializer.kt
  11. 0 37
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeAdapter.kt
  12. 56 0
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeSerializer.kt
  13. 0 59
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeAdapter.kt
  14. 67 0
      app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt
  15. 22 41
      app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt

+ 34 - 27
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt

@@ -2,44 +2,52 @@ package eu.kanade.tachiyomi.data.backup.legacy
 
 import android.content.Context
 import android.net.Uri
-import com.github.salomonbrys.kotson.fromJson
-import com.github.salomonbrys.kotson.registerTypeAdapter
-import com.github.salomonbrys.kotson.registerTypeHierarchyAdapter
-import com.google.gson.Gson
-import com.google.gson.GsonBuilder
-import com.google.gson.JsonArray
 import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
-import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
+import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION
 import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
-import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
-import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
-import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
-import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
-import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
-import eu.kanade.tachiyomi.data.database.models.CategoryImpl
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryImplTypeSerializer
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeSerializer
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterImplTypeSerializer
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeSerializer
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeSerializer
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaImplTypeSerializer
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeSerializer
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackImplTypeSerializer
+import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeSerializer
+import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.ChapterImpl
 import eu.kanade.tachiyomi.data.database.models.History
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.MangaCategory
-import eu.kanade.tachiyomi.data.database.models.MangaImpl
 import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.database.models.TrackImpl
 import eu.kanade.tachiyomi.data.database.models.toMangaInfo
 import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.source.model.toSManga
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.modules.SerializersModule
+import kotlinx.serialization.modules.contextual
 import kotlin.math.max
 
 class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
 
-    val parser: Gson = when (version) {
-        2 -> GsonBuilder()
-            .registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
-            .registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
-            .registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
-            .registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
-            .registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
-            .create()
+    val parser: Json = when (version) {
+        2 -> Json {
+            // Forks may have added items to backup
+            ignoreUnknownKeys = true
+
+            // Register custom serializers
+            serializersModule = SerializersModule {
+                contextual(MangaTypeSerializer)
+                contextual(MangaImplTypeSerializer)
+                contextual(ChapterTypeSerializer)
+                contextual(ChapterImplTypeSerializer)
+                contextual(CategoryTypeSerializer)
+                contextual(CategoryImplTypeSerializer)
+                contextual(TrackTypeSerializer)
+                contextual(TrackImplTypeSerializer)
+                contextual(HistoryTypeSerializer)
+            }
+        }
         else -> throw Exception("Unknown backup version")
     }
 
@@ -79,12 +87,11 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
     /**
      * Restore the categories from Json
      *
-     * @param jsonCategories array containing categories
+     * @param backupCategories array containing categories
      */
-    internal fun restoreCategories(jsonCategories: JsonArray) {
+    internal fun restoreCategories(backupCategories: List<Category>) {
         // Get categories from file and from db
         val dbCategories = databaseHelper.getCategories().executeAsBlocking()
-        val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
 
         // Iterate over them
         backupCategories.forEach { category ->

+ 35 - 43
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt

@@ -2,88 +2,80 @@ package eu.kanade.tachiyomi.data.backup.legacy
 
 import android.content.Context
 import android.net.Uri
-import com.github.salomonbrys.kotson.fromJson
-import com.google.gson.JsonArray
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import com.google.gson.JsonParser
-import com.google.gson.stream.JsonReader
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
 import eu.kanade.tachiyomi.data.backup.BackupNotifier
 import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
-import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
 import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
+import eu.kanade.tachiyomi.data.backup.legacy.models.MangaObject
+import eu.kanade.tachiyomi.data.database.models.Category
 import eu.kanade.tachiyomi.data.database.models.Chapter
-import eu.kanade.tachiyomi.data.database.models.ChapterImpl
 import eu.kanade.tachiyomi.data.database.models.Manga
-import eu.kanade.tachiyomi.data.database.models.MangaImpl
 import eu.kanade.tachiyomi.data.database.models.Track
-import eu.kanade.tachiyomi.data.database.models.TrackImpl
 import eu.kanade.tachiyomi.source.Source
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromJsonElement
+import kotlinx.serialization.json.intOrNull
+import kotlinx.serialization.json.jsonPrimitive
+import okio.buffer
+import okio.source
 import java.util.Date
 
 class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
 
     override suspend fun performRestore(uri: Uri): Boolean {
-        val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
-        val json = JsonParser.parseReader(reader).asJsonObject
+        // Read the json and create a Json Object,
+        // cannot use the backupManager json deserializer one because its not initialized yet
+        val backupObject = Json.decodeFromString<JsonObject>(
+            context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
+        )
+
+        // Get parser version
+        val version = backupObject["version"]?.jsonPrimitive?.intOrNull ?: 1
 
-        val version = json.get(Backup.VERSION)?.asInt ?: 1
+        // Initialize manager
         backupManager = LegacyBackupManager(context, version)
 
-        val mangasJson = json.get(MANGAS).asJsonArray
-        restoreAmount = mangasJson.size() + 1 // +1 for categories
+        // Decode the json object to a Backup object
+        val backup = backupManager.parser.decodeFromJsonElement<Backup>(backupObject)
+
+        restoreAmount = backup.mangas.size + 1 // +1 for categories
 
         // Restore categories
-        json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
+        backup.categories?.let { restoreCategories(it) }
 
         // Store source mapping for error messages
-        sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
+        sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(backup.extensions ?: emptyList())
 
         // Restore individual manga
-        mangasJson.forEach {
+        backup.mangas.forEach {
             if (job?.isActive != true) {
                 return false
             }
 
-            restoreManga(it.asJsonObject)
+            restoreManga(it)
         }
 
         return true
     }
 
-    private fun restoreCategories(categoriesJson: JsonElement) {
+    private fun restoreCategories(categoriesJson: List<Category>) {
         db.inTransaction {
-            backupManager.restoreCategories(categoriesJson.asJsonArray)
+            backupManager.restoreCategories(categoriesJson)
         }
 
         restoreProgress += 1
         showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
     }
 
-    private suspend fun restoreManga(mangaJson: JsonObject) {
-        val manga = backupManager.parser.fromJson<MangaImpl>(
-            mangaJson.get(
-                Backup.MANGA
-            )
-        )
-        val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
-            mangaJson.get(Backup.CHAPTERS)
-                ?: JsonArray()
-        )
-        val categories = backupManager.parser.fromJson<List<String>>(
-            mangaJson.get(Backup.CATEGORIES)
-                ?: JsonArray()
-        )
-        val history = backupManager.parser.fromJson<List<DHistory>>(
-            mangaJson.get(Backup.HISTORY)
-                ?: JsonArray()
-        )
-        val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
-            mangaJson.get(Backup.TRACK)
-                ?: JsonArray()
-        )
+    private suspend fun restoreManga(mangaJson: MangaObject) {
+        val manga = mangaJson.manga
+        val chapters = mangaJson.chapters ?: emptyList()
+        val categories = mangaJson.categories ?: emptyList()
+        val history = mangaJson.history ?: emptyList()
+        val tracks = mangaJson.track ?: emptyList()
 
         val source = backupManager.sourceManager.get(manga.source)
         val sourceName = sourceMapping[manga.source] ?: manga.source.toString()

+ 18 - 20
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestoreValidator.kt

@@ -2,12 +2,12 @@ package eu.kanade.tachiyomi.data.backup.legacy
 
 import android.content.Context
 import android.net.Uri
-import com.google.gson.JsonObject
-import com.google.gson.JsonParser
-import com.google.gson.stream.JsonReader
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
 import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
+import kotlinx.serialization.decodeFromString
+import okio.buffer
+import okio.source
 
 class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
     /**
@@ -17,30 +17,30 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
      * @return List of missing sources or missing trackers.
      */
     override fun validate(context: Context, uri: Uri): Results {
-        val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
-        val json = JsonParser.parseReader(reader).asJsonObject
+        val backupManager = LegacyBackupManager(context)
 
-        val version = json.get(Backup.VERSION)
-        val mangasJson = json.get(Backup.MANGAS)
-        if (version == null || mangasJson == null) {
+        val backup = backupManager.parser.decodeFromString<Backup>(
+            context.contentResolver.openInputStream(uri)!!.source().buffer().use { it.readUtf8() }
+        )
+
+        if (backup.version == null) {
             throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
         }
 
-        val mangas = mangasJson.asJsonArray
-        if (mangas.size() == 0) {
+        if (backup.mangas.isEmpty()) {
             throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
         }
 
-        val sources = getSourceMapping(json)
+        val sources = getSourceMapping(backup.extensions ?: emptyList())
         val missingSources = sources
             .filter { sourceManager.get(it.key) == null }
             .values
             .sorted()
 
-        val trackers = mangas
-            .filter { it.asJsonObject.has("track") }
-            .flatMap { it.asJsonObject["track"].asJsonArray }
-            .map { it.asJsonObject["s"].asInt }
+        val trackers = backup.mangas
+            .filterNot { it.track.isNullOrEmpty() }
+            .flatMap { it.track ?: emptyList() }
+            .map { it.sync_id }
             .distinct()
         val missingTrackers = trackers
             .mapNotNull { trackManager.getService(it) }
@@ -52,12 +52,10 @@ class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
     }
 
     companion object {
-        fun getSourceMapping(json: JsonObject): Map<Long, String> {
-            val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
-
-            return extensionsMapping.asJsonArray
+        fun getSourceMapping(extensionsMapping: List<String>): Map<Long, String> {
+            return extensionsMapping
                 .map {
-                    val items = it.asString.split(":")
+                    val items = it.split(":")
                     items[0].toLong() to items[1]
                 }
                 .toMap()

+ 28 - 16
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/models/Backup.kt

@@ -1,25 +1,37 @@
 package eu.kanade.tachiyomi.data.backup.legacy.models
 
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.Track
+import kotlinx.serialization.Contextual
+import kotlinx.serialization.Serializable
 import java.text.SimpleDateFormat
 import java.util.Date
 import java.util.Locale
 
-/**
- * Json values
- */
-object Backup {
-    const val CURRENT_VERSION = 2
-    const val MANGA = "manga"
-    const val MANGAS = "mangas"
-    const val TRACK = "track"
-    const val CHAPTERS = "chapters"
-    const val CATEGORIES = "categories"
-    const val EXTENSIONS = "extensions"
-    const val HISTORY = "history"
-    const val VERSION = "version"
+@Serializable
+data class Backup(
+    val version: Int? = null,
+    var mangas: MutableList<MangaObject> = mutableListOf(),
+    var categories: List<@Contextual Category>? = null,
+    var extensions: List<String>? = null
+) {
+    companion object {
+        const val CURRENT_VERSION = 2
 
-    fun getDefaultFilename(): String {
-        val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
-        return "tachiyomi_$date.json"
+        fun getDefaultFilename(): String {
+            val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
+            return "tachiyomi_$date.json"
+        }
     }
 }
+
+@Serializable
+data class MangaObject(
+    var manga: @Contextual Manga,
+    var chapters: List<@Contextual Chapter>? = null,
+    var categories: List<String>? = null,
+    var track: List<@Contextual Track>? = null,
+    var history: List<@Contextual DHistory>? = null
+)

+ 0 - 31
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeAdapter.kt

@@ -1,31 +0,0 @@
-package eu.kanade.tachiyomi.data.backup.legacy.serializer
-
-import com.github.salomonbrys.kotson.typeAdapter
-import com.google.gson.TypeAdapter
-import eu.kanade.tachiyomi.data.database.models.CategoryImpl
-
-/**
- * JSON Serializer used to write / read [CategoryImpl] to / from json
- */
-object CategoryTypeAdapter {
-
-    fun build(): TypeAdapter<CategoryImpl> {
-        return typeAdapter {
-            write {
-                beginArray()
-                value(it.name)
-                value(it.order)
-                endArray()
-            }
-
-            read {
-                beginArray()
-                val category = CategoryImpl()
-                category.name = nextString()
-                category.order = nextInt()
-                endArray()
-                category
-            }
-        }
-    }
-}

+ 49 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/CategoryTypeSerializer.kt

@@ -0,0 +1,49 @@
+package eu.kanade.tachiyomi.data.backup.legacy.serializer
+
+import eu.kanade.tachiyomi.data.database.models.Category
+import eu.kanade.tachiyomi.data.database.models.CategoryImpl
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.add
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonPrimitive
+
+/**
+ * JSON Serializer used to write / read [CategoryImpl] to / from json
+ */
+open class CategoryBaseSerializer<T : Category> : KSerializer<T> {
+    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Category")
+
+    override fun serialize(encoder: Encoder, value: T) {
+        encoder as JsonEncoder
+        encoder.encodeJsonElement(
+            buildJsonArray {
+                add(value.name)
+                add(value.order)
+            }
+        )
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    override fun deserialize(decoder: Decoder): T {
+        // make a category impl and cast as T so that the serializer accepts it
+        return CategoryImpl().apply {
+            decoder as JsonDecoder
+            val array = decoder.decodeJsonElement().jsonArray
+            name = array[0].jsonPrimitive.content
+            order = array[1].jsonPrimitive.int
+        } as T
+    }
+}
+
+// Allow for serialization of a category and category impl
+object CategoryTypeSerializer : CategoryBaseSerializer<Category>()
+
+object CategoryImplTypeSerializer : CategoryBaseSerializer<CategoryImpl>()

+ 0 - 59
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeAdapter.kt

@@ -1,59 +0,0 @@
-package eu.kanade.tachiyomi.data.backup.legacy.serializer
-
-import com.github.salomonbrys.kotson.typeAdapter
-import com.google.gson.TypeAdapter
-import com.google.gson.stream.JsonToken
-import eu.kanade.tachiyomi.data.database.models.ChapterImpl
-
-/**
- * JSON Serializer used to write / read [ChapterImpl] to / from json
- */
-object ChapterTypeAdapter {
-
-    private const val URL = "u"
-    private const val READ = "r"
-    private const val BOOKMARK = "b"
-    private const val LAST_READ = "l"
-
-    fun build(): TypeAdapter<ChapterImpl> {
-        return typeAdapter {
-            write {
-                if (it.read || it.bookmark || it.last_page_read != 0) {
-                    beginObject()
-                    name(URL)
-                    value(it.url)
-                    if (it.read) {
-                        name(READ)
-                        value(1)
-                    }
-                    if (it.bookmark) {
-                        name(BOOKMARK)
-                        value(1)
-                    }
-                    if (it.last_page_read != 0) {
-                        name(LAST_READ)
-                        value(it.last_page_read)
-                    }
-                    endObject()
-                }
-            }
-
-            read {
-                val chapter = ChapterImpl()
-                beginObject()
-                while (hasNext()) {
-                    if (peek() == JsonToken.NAME) {
-                        when (nextName()) {
-                            URL -> chapter.url = nextString()
-                            READ -> chapter.read = nextInt() == 1
-                            BOOKMARK -> chapter.bookmark = nextInt() == 1
-                            LAST_READ -> chapter.last_page_read = nextInt()
-                        }
-                    }
-                }
-                endObject()
-                chapter
-            }
-        }
-    }
-}

+ 66 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/ChapterTypeSerializer.kt

@@ -0,0 +1,66 @@
+package eu.kanade.tachiyomi.data.backup.legacy.serializer
+
+import eu.kanade.tachiyomi.data.database.models.Chapter
+import eu.kanade.tachiyomi.data.database.models.ChapterImpl
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.intOrNull
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.put
+
+/**
+ * JSON Serializer used to write / read [ChapterImpl] to / from json
+ */
+open class ChapterBaseSerializer<T : Chapter> : KSerializer<T> {
+
+    override val descriptor = buildClassSerialDescriptor("Chapter")
+
+    override fun serialize(encoder: Encoder, value: T) {
+        encoder as JsonEncoder
+        encoder.encodeJsonElement(
+            buildJsonObject {
+                put(URL, value.url)
+                if (value.read) {
+                    put(READ, 1)
+                }
+                if (value.bookmark) {
+                    put(BOOKMARK, 1)
+                }
+                if (value.last_page_read != 0) {
+                    put(LAST_READ, value.last_page_read)
+                }
+            }
+        )
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    override fun deserialize(decoder: Decoder): T {
+        // make a chapter impl and cast as T so that the serializer accepts it
+        return ChapterImpl().apply {
+            decoder as JsonDecoder
+            val jsonObject = decoder.decodeJsonElement().jsonObject
+            url = jsonObject[URL]!!.jsonPrimitive.content
+            read = jsonObject[READ]?.jsonPrimitive?.intOrNull == 1
+            bookmark = jsonObject[BOOKMARK]?.jsonPrimitive?.intOrNull == 1
+            last_page_read = jsonObject[LAST_READ]?.jsonPrimitive?.intOrNull ?: last_page_read
+        } as T
+    }
+
+    companion object {
+        private const val URL = "u"
+        private const val READ = "r"
+        private const val BOOKMARK = "b"
+        private const val LAST_READ = "l"
+    }
+}
+
+// Allow for serialization of a chapter and chapter impl
+object ChapterTypeSerializer : ChapterBaseSerializer<Chapter>()
+
+object ChapterImplTypeSerializer : ChapterBaseSerializer<ChapterImpl>()

+ 0 - 32
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeAdapter.kt

@@ -1,32 +0,0 @@
-package eu.kanade.tachiyomi.data.backup.legacy.serializer
-
-import com.github.salomonbrys.kotson.typeAdapter
-import com.google.gson.TypeAdapter
-import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
-
-/**
- * JSON Serializer used to write / read [DHistory] to / from json
- */
-object HistoryTypeAdapter {
-
-    fun build(): TypeAdapter<DHistory> {
-        return typeAdapter {
-            write {
-                if (it.lastRead != 0L) {
-                    beginArray()
-                    value(it.url)
-                    value(it.lastRead)
-                    endArray()
-                }
-            }
-
-            read {
-                beginArray()
-                val url = nextString()
-                val lastRead = nextLong()
-                endArray()
-                DHistory(url, lastRead)
-            }
-        }
-    }
-}

+ 41 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/HistoryTypeSerializer.kt

@@ -0,0 +1,41 @@
+package eu.kanade.tachiyomi.data.backup.legacy.serializer
+
+import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.add
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.long
+
+/**
+ * JSON Serializer used to write / read [DHistory] to / from json
+ */
+object HistoryTypeSerializer : KSerializer<DHistory> {
+    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("History")
+
+    override fun serialize(encoder: Encoder, value: DHistory) {
+        encoder as JsonEncoder
+        encoder.encodeJsonElement(
+            buildJsonArray {
+                add(value.url)
+                add(value.lastRead)
+            }
+        )
+    }
+
+    override fun deserialize(decoder: Decoder): DHistory {
+        decoder as JsonDecoder
+        val array = decoder.decodeJsonElement().jsonArray
+        return DHistory(
+            url = array[0].jsonPrimitive.content,
+            lastRead = array[1].jsonPrimitive.long
+        )
+    }
+}

+ 0 - 37
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeAdapter.kt

@@ -1,37 +0,0 @@
-package eu.kanade.tachiyomi.data.backup.legacy.serializer
-
-import com.github.salomonbrys.kotson.typeAdapter
-import com.google.gson.TypeAdapter
-import eu.kanade.tachiyomi.data.database.models.MangaImpl
-
-/**
- * JSON Serializer used to write / read [MangaImpl] to / from json
- */
-object MangaTypeAdapter {
-
-    fun build(): TypeAdapter<MangaImpl> {
-        return typeAdapter {
-            write {
-                beginArray()
-                value(it.url)
-                value(it.title)
-                value(it.source)
-                value(it.viewer_flags)
-                value(it.chapter_flags)
-                endArray()
-            }
-
-            read {
-                beginArray()
-                val manga = MangaImpl()
-                manga.url = nextString()
-                manga.title = nextString()
-                manga.source = nextLong()
-                manga.viewer_flags = nextInt()
-                manga.chapter_flags = nextInt()
-                endArray()
-                manga
-            }
-        }
-    }
-}

+ 56 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/MangaTypeSerializer.kt

@@ -0,0 +1,56 @@
+package eu.kanade.tachiyomi.data.backup.legacy.serializer
+
+import eu.kanade.tachiyomi.data.database.models.Manga
+import eu.kanade.tachiyomi.data.database.models.MangaImpl
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.add
+import kotlinx.serialization.json.buildJsonArray
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonArray
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.long
+
+/**
+ * JSON Serializer used to write / read [MangaImpl] to / from json
+ */
+open class MangaBaseSerializer<T : Manga> : KSerializer<T> {
+    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Manga")
+
+    override fun serialize(encoder: Encoder, value: T) {
+        encoder as JsonEncoder
+        encoder.encodeJsonElement(
+            buildJsonArray {
+                add(value.url)
+                add(value.title)
+                add(value.source)
+                add(value.viewer_flags)
+                add(value.chapter_flags)
+            }
+        )
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    override fun deserialize(decoder: Decoder): T {
+        // make a manga impl and cast as T so that the serializer accepts it
+        return MangaImpl().apply {
+            decoder as JsonDecoder
+            val array = decoder.decodeJsonElement().jsonArray
+            url = array[0].jsonPrimitive.content
+            title = array[1].jsonPrimitive.content
+            source = array[2].jsonPrimitive.long
+            viewer_flags = array[3].jsonPrimitive.int
+            chapter_flags = array[4].jsonPrimitive.int
+        } as T
+    }
+}
+
+// Allow for serialization of a manga and manga impl
+object MangaTypeSerializer : MangaBaseSerializer<Manga>()
+
+object MangaImplTypeSerializer : MangaBaseSerializer<MangaImpl>()

+ 0 - 59
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeAdapter.kt

@@ -1,59 +0,0 @@
-package eu.kanade.tachiyomi.data.backup.legacy.serializer
-
-import com.github.salomonbrys.kotson.typeAdapter
-import com.google.gson.TypeAdapter
-import com.google.gson.stream.JsonToken
-import eu.kanade.tachiyomi.data.database.models.TrackImpl
-
-/**
- * JSON Serializer used to write / read [TrackImpl] to / from json
- */
-object TrackTypeAdapter {
-
-    private const val SYNC = "s"
-    private const val MEDIA = "r"
-    private const val LIBRARY = "ml"
-    private const val TITLE = "t"
-    private const val LAST_READ = "l"
-    private const val TRACKING_URL = "u"
-
-    fun build(): TypeAdapter<TrackImpl> {
-        return typeAdapter {
-            write {
-                beginObject()
-                name(TITLE)
-                value(it.title)
-                name(SYNC)
-                value(it.sync_id)
-                name(MEDIA)
-                value(it.media_id)
-                name(LIBRARY)
-                value(it.library_id)
-                name(LAST_READ)
-                value(it.last_chapter_read)
-                name(TRACKING_URL)
-                value(it.tracking_url)
-                endObject()
-            }
-
-            read {
-                val track = TrackImpl()
-                beginObject()
-                while (hasNext()) {
-                    if (peek() == JsonToken.NAME) {
-                        when (nextName()) {
-                            TITLE -> track.title = nextString()
-                            SYNC -> track.sync_id = nextInt()
-                            MEDIA -> track.media_id = nextInt()
-                            LIBRARY -> track.library_id = nextLong()
-                            LAST_READ -> track.last_chapter_read = nextInt()
-                            TRACKING_URL -> track.tracking_url = nextString()
-                        }
-                    }
-                }
-                endObject()
-                track
-            }
-        }
-    }
-}

+ 67 - 0
app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/serializer/TrackTypeSerializer.kt

@@ -0,0 +1,67 @@
+package eu.kanade.tachiyomi.data.backup.legacy.serializer
+
+import eu.kanade.tachiyomi.data.database.models.Track
+import eu.kanade.tachiyomi.data.database.models.TrackImpl
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.descriptors.buildClassSerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonEncoder
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlinx.serialization.json.long
+import kotlinx.serialization.json.put
+
+/**
+ * JSON Serializer used to write / read [TrackImpl] to / from json
+ */
+open class TrackBaseSerializer<T : Track> : KSerializer<T> {
+    override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Track")
+
+    override fun serialize(encoder: Encoder, value: T) {
+        encoder as JsonEncoder
+        encoder.encodeJsonElement(
+            buildJsonObject {
+                put(TITLE, value.title)
+                put(SYNC, value.sync_id)
+                put(MEDIA, value.media_id)
+                put(LIBRARY, value.library_id)
+                put(LAST_READ, value.last_chapter_read)
+                put(TRACKING_URL, value.tracking_url)
+            }
+        )
+    }
+
+    @Suppress("UNCHECKED_CAST")
+    override fun deserialize(decoder: Decoder): T {
+        // make a track impl and cast as T so that the serializer accepts it
+        return TrackImpl().apply {
+            decoder as JsonDecoder
+            val jsonObject = decoder.decodeJsonElement().jsonObject
+            title = jsonObject[TITLE]!!.jsonPrimitive.content
+            sync_id = jsonObject[SYNC]!!.jsonPrimitive.int
+            media_id = jsonObject[MEDIA]!!.jsonPrimitive.int
+            library_id = jsonObject[LIBRARY]!!.jsonPrimitive.long
+            last_chapter_read = jsonObject[LAST_READ]!!.jsonPrimitive.int
+            tracking_url = jsonObject[TRACKING_URL]!!.jsonPrimitive.content
+        } as T
+    }
+
+    companion object {
+        private const val SYNC = "s"
+        private const val MEDIA = "r"
+        private const val LIBRARY = "ml"
+        private const val TITLE = "t"
+        private const val LAST_READ = "l"
+        private const val TRACKING_URL = "u"
+    }
+}
+
+// Allow for serialization of a track and track impl
+object TrackTypeSerializer : TrackBaseSerializer<Track>()
+
+object TrackImplTypeSerializer : TrackBaseSerializer<TrackImpl>()

+ 22 - 41
app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt

@@ -3,9 +3,6 @@ package eu.kanade.tachiyomi.data.backup
 import android.app.Application
 import android.content.Context
 import android.os.Build
-import com.github.salomonbrys.kotson.fromJson
-import com.google.gson.JsonArray
-import com.google.gson.JsonObject
 import eu.kanade.tachiyomi.BuildConfig
 import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
 import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
@@ -17,12 +14,16 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
 import eu.kanade.tachiyomi.data.database.models.ChapterImpl
 import eu.kanade.tachiyomi.data.database.models.Manga
 import eu.kanade.tachiyomi.data.database.models.MangaImpl
+import eu.kanade.tachiyomi.data.database.models.Track
 import eu.kanade.tachiyomi.data.database.models.TrackImpl
 import eu.kanade.tachiyomi.source.SourceManager
 import eu.kanade.tachiyomi.source.online.HttpSource
 import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
 import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
 import kotlinx.coroutines.runBlocking
+import kotlinx.serialization.decodeFromString
+import kotlinx.serialization.encodeToString
+import kotlinx.serialization.json.buildJsonObject
 import org.assertj.core.api.Assertions.assertThat
 import org.junit.Before
 import org.junit.Test
@@ -47,16 +48,10 @@ import uy.kohesive.injekt.api.addSingleton
 @RunWith(CustomRobolectricGradleTestRunner::class)
 class BackupTest {
     // Create root object
-    var root = JsonObject()
+    var root = Backup()
 
     // Create information object
-    var information = JsonObject()
-
-    // Create manga array
-    var mangaEntries = JsonArray()
-
-    // Create category array
-    var categoryEntries = JsonArray()
+    var information = buildJsonObject {}
 
     lateinit var app: Application
     lateinit var context: Context
@@ -83,11 +78,6 @@ class BackupTest {
 
         source = mock(HttpSource::class.java)
         `when`(legacyBackupManager.sourceManager.get(anyLong())).thenReturn(source)
-
-        root.add(Backup.MANGAS, mangaEntries)
-        root.add(Backup.CATEGORIES, categoryEntries)
-
-        clearJson()
     }
 
     /**
@@ -95,11 +85,8 @@ class BackupTest {
      */
     @Test
     fun testRestoreEmptyCategory() {
-        // Create backup of empty database
-        legacyBackupManager.backupCategories(categoryEntries)
-
         // Restore Json
-        legacyBackupManager.restoreCategories(categoryEntries)
+        legacyBackupManager.restoreCategories(root.categories ?: emptyList())
 
         // Check if empty
         val dbCats = db.getCategories().executeAsBlocking()
@@ -115,7 +102,7 @@ class BackupTest {
         val category = addSingleCategory("category")
 
         // Restore Json
-        legacyBackupManager.restoreCategories(categoryEntries)
+        legacyBackupManager.restoreCategories(root.categories ?: emptyList())
 
         // Check if successful
         val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
@@ -139,7 +126,7 @@ class BackupTest {
         db.insertCategory(category).executeAsBlocking()
 
         // Restore Json
-        legacyBackupManager.restoreCategories(categoryEntries)
+        legacyBackupManager.restoreCategories(root.categories ?: emptyList())
 
         // Check if successful
         val dbCats = legacyBackupManager.databaseHelper.getCategories().executeAsBlocking()
@@ -167,9 +154,6 @@ class BackupTest {
         assertThat(favoriteManga[0].readingModeType).isEqualTo(ReadingModeType.VERTICAL.flagValue)
         assertThat(favoriteManga[0].orientationType).isEqualTo(OrientationType.PORTRAIT.flagValue)
 
-        // Update json with all options enabled
-        mangaEntries.add(legacyBackupManager.backupMangaObject(manga, 1))
-
         // Change manga in database to default values
         val dbManga = getSingleManga("One Piece")
         dbManga.id = manga.id
@@ -198,9 +182,9 @@ class BackupTest {
 
         // Restore Json
         // Create JSON from manga to test parser
-        val json = legacyBackupManager.parser.toJsonTree(manga)
+        val json = legacyBackupManager.parser.encodeToString(manga)
         // Restore JSON from manga to test parser
-        val jsonManga = legacyBackupManager.parser.fromJson<MangaImpl>(json)
+        val jsonManga = legacyBackupManager.parser.decodeFromString<Manga>(json)
 
         // Restore manga with fetch observable
         val networkManga = getSingleManga("One Piece")
@@ -237,8 +221,8 @@ class BackupTest {
         }
 
         // Check parser
-        val chaptersJson = legacyBackupManager.parser.toJsonTree(chapters)
-        val restoredChapters = legacyBackupManager.parser.fromJson<List<ChapterImpl>>(chaptersJson)
+        val chaptersJson = legacyBackupManager.parser.encodeToString(chapters)
+        val restoredChapters = legacyBackupManager.parser.decodeFromString<List<Chapter>>(chaptersJson)
 
         // Fetch chapters from upstream
         // Create list
@@ -275,8 +259,8 @@ class BackupTest {
         historyList.add(historyJson)
 
         // Check parser
-        val historyListJson = legacyBackupManager.parser.toJsonTree(historyList)
-        val history = legacyBackupManager.parser.fromJson<List<DHistory>>(historyListJson)
+        val historyListJson = legacyBackupManager.parser.encodeToString(historyList)
+        val history = legacyBackupManager.parser.decodeFromString<List<DHistory>>(historyListJson)
 
         // Restore categories
         legacyBackupManager.restoreHistoryForManga(history)
@@ -314,8 +298,8 @@ class BackupTest {
         // Check parser and restore already in database
         var trackList = listOf(track)
         // Check parser
-        var trackListJson = legacyBackupManager.parser.toJsonTree(trackList)
-        var trackListRestore = legacyBackupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
+        var trackListJson = legacyBackupManager.parser.encodeToString(trackList)
+        var trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
         legacyBackupManager.restoreTrackForManga(manga, trackListRestore)
 
         // Assert if restore works.
@@ -337,8 +321,8 @@ class BackupTest {
         trackList = listOf(track2)
 
         // Check parser
-        trackListJson = legacyBackupManager.parser.toJsonTree(trackList)
-        trackListRestore = legacyBackupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
+        trackListJson = legacyBackupManager.parser.encodeToString(trackList)
+        trackListRestore = legacyBackupManager.parser.decodeFromString<List<Track>>(trackListJson)
         legacyBackupManager.restoreTrackForManga(manga2, trackListRestore)
 
         // Assert if restore works.
@@ -348,16 +332,13 @@ class BackupTest {
     }
 
     private fun clearJson() {
-        root = JsonObject()
-        information = JsonObject()
-        mangaEntries = JsonArray()
-        categoryEntries = JsonArray()
+        root = Backup()
+        information = buildJsonObject {}
     }
 
     private fun addSingleCategory(name: String): Category {
         val category = Category.create(name)
-        val catJson = legacyBackupManager.parser.toJsonTree(category)
-        categoryEntries.add(catJson)
+        root.categories = listOf(category)
         return category
     }