Use kotlin serialization for sync

master
Koitharu 11 months ago
parent b854ca8807
commit 4ef6908e82
Signed by: Koitharu
GPG Key ID: 676DEE768C17A9D7

@ -7,6 +7,7 @@ plugins {
id 'kotlin-parcelize'
id 'dagger.hilt.android.plugin'
id 'androidx.room'
id 'org.jetbrains.kotlin.plugin.serialization'
}
android {
@ -152,6 +153,7 @@ dependencies {
implementation libs.okhttp.tls
implementation libs.okhttp.dnsoverhttps
implementation libs.okio
implementation libs.kotlinx.serialization.json
implementation libs.adapterdelegates
implementation libs.adapterdelegates.viewbinding

@ -2,40 +2,23 @@ package org.koitharu.kotatsu.core.util.ext
import android.content.ContentValues
import android.database.Cursor
import org.json.JSONObject
import androidx.collection.ArraySet
fun Cursor.toJson(): JSONObject {
val jo = JSONObject()
for (i in 0 until columnCount) {
val name = getColumnName(i)
when (getType(i)) {
Cursor.FIELD_TYPE_STRING -> jo.put(name, getString(i))
Cursor.FIELD_TYPE_FLOAT -> jo.put(name, getDouble(i))
Cursor.FIELD_TYPE_INTEGER -> jo.put(name, getLong(i))
Cursor.FIELD_TYPE_NULL -> jo.put(name, null)
Cursor.FIELD_TYPE_BLOB -> jo.put(name, getBlob(i))
}
}
return jo
}
fun Cursor.getBoolean(columnIndex: Int) = getInt(columnIndex) > 0
inline fun <T> Cursor.map(mapper: (Cursor) -> T): List<T> = mapTo(ArrayList(count), mapper)
fun JSONObject.toContentValues(): ContentValues {
val cv = ContentValues(length())
for (key in keys()) {
val name = key.escapeName()
when (val value = get(key)) {
JSONObject.NULL, "null", null -> cv.putNull(name)
is String -> cv.put(name, value)
is Float -> cv.put(name, value)
is Double -> cv.put(name, value)
is Int -> cv.put(name, value)
is Long -> cv.put(name, value)
else -> throw IllegalArgumentException("Value $value cannot be putted in ContentValues")
}
inline fun <T> Cursor.mapToSet(mapper: (Cursor) -> T): Set<T> = mapTo(ArraySet(count), mapper)
inline fun <T, C: MutableCollection<in T>> Cursor.mapTo(destination: C, mapper: (Cursor) -> T): C = use { c ->
if (c.moveToFirst()) {
do {
destination.add(mapper(c))
} while (c.moveToNext())
}
return cv
destination
}
private fun String.escapeName() = "`$this`"
fun Cursor.getBoolean(columnIndex: Int) = getInt(columnIndex) > 0
inline fun buildContentValues(capacity: Int, block: ContentValues.() -> Unit): ContentValues {
return ContentValues(capacity).apply(block)
}

@ -0,0 +1,42 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
import org.koitharu.kotatsu.core.util.ext.getBoolean
@Serializable
data class FavouriteCategorySyncDto(
@SerialName("category_id") val categoryId: Int,
@SerialName("created_at") val createdAt: Long,
@SerialName("sort_key") val sortKey: Int,
@SerialName("title") val title: String,
@SerialName("order") val order: String,
@SerialName("track") val track: Boolean,
@SerialName("show_in_lib") val isVisibleInLibrary: Boolean,
@SerialName("deleted_at") val deletedAt: Long,
) {
constructor(cursor: Cursor) : this(
categoryId = cursor.getInt(cursor.getColumnIndexOrThrow("category_id")),
createdAt = cursor.getLong(cursor.getColumnIndexOrThrow("created_at")),
sortKey = cursor.getInt(cursor.getColumnIndexOrThrow("sort_key")),
title = cursor.getString(cursor.getColumnIndexOrThrow("title")),
order = cursor.getString(cursor.getColumnIndexOrThrow("order")),
track = cursor.getBoolean(cursor.getColumnIndexOrThrow("track")),
isVisibleInLibrary = cursor.getBoolean(cursor.getColumnIndexOrThrow("show_in_lib")),
deletedAt = cursor.getLong(cursor.getColumnIndexOrThrow("deleted_at")),
)
fun toContentValues() = buildContentValues(8) {
put("category_id", categoryId)
put("created_at", createdAt)
put("sort_key", sortKey)
put("title", title)
put("order", order)
put("track", track)
put("show_in_lib", isVisibleInLibrary)
put("deleted_at", deletedAt)
}
}

@ -0,0 +1,38 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
import org.koitharu.kotatsu.core.util.ext.getBoolean
@Serializable
data class FavouriteSyncDto(
@SerialName("manga_id") val mangaId: Long,
@SerialName("manga") val manga: MangaSyncDto,
@SerialName("category_id") val categoryId: Int,
@SerialName("sort_key") val sortKey: Int,
@SerialName("pinned") val pinned: Boolean,
@SerialName("created_at") val createdAt: Long,
@SerialName("deleted_at") var deletedAt: Long,
) {
constructor(cursor: Cursor, manga: MangaSyncDto) : this(
mangaId = cursor.getLong(cursor.getColumnIndexOrThrow("manga_id")),
manga = manga,
categoryId = cursor.getInt(cursor.getColumnIndexOrThrow("category_id")),
sortKey = cursor.getInt(cursor.getColumnIndexOrThrow("sort_key")),
pinned = cursor.getBoolean(cursor.getColumnIndexOrThrow("pinned")),
createdAt = cursor.getLong(cursor.getColumnIndexOrThrow("created_at")),
deletedAt = cursor.getLong(cursor.getColumnIndexOrThrow("deleted_at")),
)
fun toContentValues() = buildContentValues(6) {
put("manga_id", mangaId)
put("category_id", categoryId)
put("sort_key", sortKey)
put("pinned", pinned)
put("created_at", createdAt)
put("deleted_at", deletedAt)
}
}

@ -0,0 +1,46 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
@Serializable
data class HistorySyncDto(
@SerialName("manga_id") val mangaId: Long,
@SerialName("created_at") val createdAt: Long,
@SerialName("updated_at") val updatedAt: Long,
@SerialName("chapter_id") val chapterId: Long,
@SerialName("page") val page: Int,
@SerialName("scroll") val scroll: Float,
@SerialName("percent") val percent: Float,
@SerialName("deleted_at") val deletedAt: Long,
@SerialName("chapters") val chaptersCount: Int,
@SerialName("manga") val manga: MangaSyncDto,
) {
constructor(cursor: Cursor, manga: MangaSyncDto) : this(
mangaId = cursor.getLong(cursor.getColumnIndexOrThrow("manga_id")),
createdAt = cursor.getLong(cursor.getColumnIndexOrThrow("created_at")),
updatedAt = cursor.getLong(cursor.getColumnIndexOrThrow("updated_at")),
chapterId = cursor.getLong(cursor.getColumnIndexOrThrow("chapter_id")),
page = cursor.getInt(cursor.getColumnIndexOrThrow("page")),
scroll = cursor.getFloat(cursor.getColumnIndexOrThrow("scroll")),
percent = cursor.getFloat(cursor.getColumnIndexOrThrow("percent")),
deletedAt = cursor.getLong(cursor.getColumnIndexOrThrow("deleted_at")),
chaptersCount = cursor.getInt(cursor.getColumnIndexOrThrow("chapters")),
manga = manga,
)
fun toContentValues() = buildContentValues(9) {
put("manga_id", mangaId)
put("created_at", createdAt)
put("updated_at", updatedAt)
put("chapter_id", chapterId)
put("page", page)
put("scroll", scroll)
put("percent", percent)
put("deleted_at", deletedAt)
put("chapters", chaptersCount)
}
}

@ -0,0 +1,56 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import androidx.core.database.getStringOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
@Serializable
data class MangaSyncDto(
@SerialName("manga_id") val id: Long,
@SerialName("title") val title: String,
@SerialName("alt_title") val altTitle: String?,
@SerialName("url") val url: String,
@SerialName("public_url") val publicUrl: String,
@SerialName("rating") val rating: Float,
@SerialName("content_rating") val contentRating: String?,
@SerialName("cover_url") val coverUrl: String,
@SerialName("large_cover_url") val largeCoverUrl: String?,
@SerialName("tags") val tags: Set<MangaTagSyncDto>,
@SerialName("state") val state: String?,
@SerialName("author") val author: String?,
@SerialName("source") val source: String,
) {
constructor(cursor: Cursor, tags: Set<MangaTagSyncDto>) : this(
id = cursor.getLong(cursor.getColumnIndexOrThrow("manga_id")),
title = cursor.getString(cursor.getColumnIndexOrThrow("title")),
altTitle = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("alt_title")),
url = cursor.getString(cursor.getColumnIndexOrThrow("url")),
publicUrl = cursor.getString(cursor.getColumnIndexOrThrow("public_url")),
rating = cursor.getFloat(cursor.getColumnIndexOrThrow("rating")),
contentRating = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("content_rating")),
coverUrl = cursor.getString(cursor.getColumnIndexOrThrow("cover_url")),
largeCoverUrl = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("large_cover_url")),
tags = tags,
state = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("state")),
author = cursor.getStringOrNull(cursor.getColumnIndexOrThrow("author")),
source = cursor.getString(cursor.getColumnIndexOrThrow("source")),
)
fun toContentValues() = buildContentValues(12) {
put("manga_id", id)
put("title", title)
put("alt_title", altTitle)
put("url", url)
put("public_url", publicUrl)
put("rating", rating)
put("content_rating", contentRating)
put("cover_url", coverUrl)
put("large_cover_url", largeCoverUrl)
put("state", state)
put("author", author)
put("source", source)
}
}

@ -0,0 +1,29 @@
package org.koitharu.kotatsu.sync.data.model
import android.database.Cursor
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import org.koitharu.kotatsu.core.util.ext.buildContentValues
@Serializable
data class MangaTagSyncDto(
@SerialName("tag_id") val id: Long,
@SerialName("title") val title: String,
@SerialName("key") val key: String,
@SerialName("source") val source: String,
) {
constructor(cursor: Cursor) : this(
id = cursor.getLong(cursor.getColumnIndexOrThrow("tag_id")),
title = cursor.getString(cursor.getColumnIndexOrThrow("title")),
key = cursor.getString(cursor.getColumnIndexOrThrow("key")),
source = cursor.getString(cursor.getColumnIndexOrThrow("source")),
)
fun toContentValues() = buildContentValues(4) {
put("tag_id", id)
put("title", title)
put("key", key)
put("source", source)
}
}

@ -0,0 +1,12 @@
package org.koitharu.kotatsu.sync.data.model
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class SyncDto(
@SerialName("history") val history: List<HistorySyncDto>?,
@SerialName("categories") val categories: List<FavouriteCategorySyncDto>?,
@SerialName("favourites") val favourites: List<FavouriteSyncDto>?,
@SerialName("timestamp") val timestamp: Long,
)

@ -11,16 +11,20 @@ import android.content.SyncStats
import android.database.Cursor
import android.util.Log
import androidx.annotation.WorkerThread
import androidx.core.content.contentValuesOf
import androidx.core.net.toUri
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import okhttp3.Request
import org.json.JSONArray
import org.json.JSONObject
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.Response
import okhttp3.internal.closeQuietly
import okio.IOException
import org.jetbrains.annotations.Blocking
import org.koitharu.kotatsu.BuildConfig
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.core.db.TABLE_FAVOURITES
@ -30,20 +34,23 @@ import org.koitharu.kotatsu.core.db.TABLE_MANGA
import org.koitharu.kotatsu.core.db.TABLE_MANGA_TAGS
import org.koitharu.kotatsu.core.db.TABLE_TAGS
import org.koitharu.kotatsu.core.network.BaseHttpClient
import org.koitharu.kotatsu.core.util.ext.parseJsonOrNull
import org.koitharu.kotatsu.core.util.ext.buildContentValues
import org.koitharu.kotatsu.core.util.ext.map
import org.koitharu.kotatsu.core.util.ext.mapToSet
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toContentValues
import org.koitharu.kotatsu.core.util.ext.toJson
import org.koitharu.kotatsu.core.util.ext.toRequestBody
import org.koitharu.kotatsu.parsers.util.json.mapJSONTo
import org.koitharu.kotatsu.sync.data.SyncAuthApi
import org.koitharu.kotatsu.sync.data.SyncAuthenticator
import org.koitharu.kotatsu.sync.data.SyncInterceptor
import org.koitharu.kotatsu.sync.data.SyncSettings
import org.koitharu.kotatsu.sync.data.model.FavouriteCategorySyncDto
import org.koitharu.kotatsu.sync.data.model.FavouriteSyncDto
import org.koitharu.kotatsu.sync.data.model.HistorySyncDto
import org.koitharu.kotatsu.sync.data.model.MangaSyncDto
import org.koitharu.kotatsu.sync.data.model.MangaTagSyncDto
import org.koitharu.kotatsu.sync.data.model.SyncDto
import java.net.HttpURLConnection
import java.util.concurrent.TimeUnit
private const val FIELD_TIMESTAMP = "timestamp"
class SyncHelper @AssistedInject constructor(
@ApplicationContext context: Context,
@BaseHttpClient baseHttpClient: OkHttpClient,
@ -54,6 +61,7 @@ class SyncHelper @AssistedInject constructor(
private val authorityHistory = context.getString(R.string.sync_authority_history)
private val authorityFavourites = context.getString(R.string.sync_authority_favourites)
private val mediaTypeJson = "application/json".toMediaType()
private val httpClient = baseHttpClient.newBuilder()
.authenticator(SyncAuthenticator(context, account, settings, SyncAuthApi(OkHttpClient())))
.addInterceptor(SyncInterceptor(context, account))
@ -66,20 +74,26 @@ class SyncHelper @AssistedInject constructor(
@WorkerThread
fun syncFavourites(stats: SyncStats) {
val data = JSONObject()
data.put(TABLE_FAVOURITE_CATEGORIES, getFavouriteCategories())
data.put(TABLE_FAVOURITES, getFavourites())
data.put(FIELD_TIMESTAMP, System.currentTimeMillis())
val payload = Json.encodeToString(
SyncDto(
history = null,
favourites = getFavourites(),
categories = getFavouriteCategories(),
timestamp = System.currentTimeMillis(),
),
)
val request = Request.Builder()
.url("$baseUrl/resource/$TABLE_FAVOURITES")
.post(data.toRequestBody())
.post(payload.toRequestBody(mediaTypeJson))
.build()
val response = httpClient.newCall(request).execute().parseJsonOrNull()
if (response != null) {
val categoriesResult = upsertFavouriteCategories(response.getJSONArray(TABLE_FAVOURITE_CATEGORIES))
val response = httpClient.newCall(request).execute().parseDtoOrNull()
response?.categories?.let { categories ->
val categoriesResult = upsertFavouriteCategories(categories)
stats.numDeletes += categoriesResult.first().count?.toLong() ?: 0L
stats.numInserts += categoriesResult.drop(1).sumOf { it.count?.toLong() ?: 0L }
val favouritesResult = upsertFavourites(response.getJSONArray(TABLE_FAVOURITES))
}
response?.favourites?.let { favourites ->
val favouritesResult = upsertFavourites(favourites)
stats.numDeletes += favouritesResult.first().count?.toLong() ?: 0L
stats.numInserts += favouritesResult.drop(1).sumOf { it.count?.toLong() ?: 0L }
stats.numEntries += stats.numInserts + stats.numDeletes
@ -87,20 +101,24 @@ class SyncHelper @AssistedInject constructor(
gcFavourites()
}
@Blocking
@WorkerThread
fun syncHistory(stats: SyncStats) {
val data = JSONObject()
data.put(TABLE_HISTORY, getHistory())
data.put(FIELD_TIMESTAMP, System.currentTimeMillis())
val payload = Json.encodeToString(
SyncDto(
history = getHistory(),
favourites = null,
categories = null,
timestamp = System.currentTimeMillis(),
),
)
val request = Request.Builder()
.url("$baseUrl/resource/$TABLE_HISTORY")
.post(data.toRequestBody())
.post(payload.toRequestBody(mediaTypeJson))
.build()
val response = httpClient.newCall(request).execute().parseJsonOrNull()
if (response != null) {
val result = upsertHistory(
json = response.getJSONArray(TABLE_HISTORY),
)
val response = httpClient.newCall(request).execute().parseDtoOrNull()
response?.history?.let { history ->
val result = upsertHistory(history)
stats.numDeletes += result.first().count?.toLong() ?: 0L
stats.numInserts += result.drop(1).sumOf { it.count?.toLong() ?: 0L }
stats.numEntries += stats.numInserts + stats.numDeletes
@ -118,140 +136,119 @@ class SyncHelper @AssistedInject constructor(
}
}
private fun upsertHistory(json: JSONArray): Array<ContentProviderResult> {
private fun upsertHistory(history: List<HistorySyncDto>): Array<ContentProviderResult> {
val uri = uri(authorityHistory, TABLE_HISTORY)
val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo ->
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityHistory))
history.mapTo(operations) {
operations.addAll(upsertManga(it.manga, authorityHistory))
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
.withValues(it.toContentValues())
.build()
}
return provider.applyBatch(operations)
}
private fun upsertFavouriteCategories(json: JSONArray): Array<ContentProviderResult> {
private fun upsertFavouriteCategories(categories: List<FavouriteCategorySyncDto>): Array<ContentProviderResult> {
val uri = uri(authorityFavourites, TABLE_FAVOURITE_CATEGORIES)
val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo ->
categories.mapTo(operations) {
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
.withValues(it.toContentValues())
.build()
}
return provider.applyBatch(operations)
}
private fun upsertFavourites(json: JSONArray): Array<ContentProviderResult> {
private fun upsertFavourites(favourites: List<FavouriteSyncDto>): Array<ContentProviderResult> {
val uri = uri(authorityFavourites, TABLE_FAVOURITES)
val operations = ArrayList<ContentProviderOperation>()
json.mapJSONTo(operations) { jo ->
operations.addAll(upsertManga(jo.removeJSONObject("manga"), authorityFavourites))
favourites.mapTo(operations) {
operations.addAll(upsertManga(it.manga, authorityFavourites))
ContentProviderOperation.newInsert(uri)
.withValues(jo.toContentValues())
.withValues(it.toContentValues())
.build()
}
return provider.applyBatch(operations)
}
private fun upsertManga(json: JSONObject, authority: String): List<ContentProviderOperation> {
val tags = json.removeJSONArray(TABLE_TAGS)
val result = ArrayList<ContentProviderOperation>(tags.length() * 2 + 1)
for (i in 0 until tags.length()) {
val tag = tags.getJSONObject(i)
private fun upsertManga(manga: MangaSyncDto, authority: String): List<ContentProviderOperation> {
val tags = manga.tags
val result = ArrayList<ContentProviderOperation>(tags.size * 2 + 1)
for (tag in tags) {
result += ContentProviderOperation.newInsert(uri(authority, TABLE_TAGS))
.withValues(tag.toContentValues())
.build()
result += ContentProviderOperation.newInsert(uri(authority, TABLE_MANGA_TAGS))
.withValues(
contentValuesOf(
"manga_id" to json.getLong("manga_id"),
"tag_id" to tag.getLong("tag_id"),
),
buildContentValues(2) {
put("manga_id", manga.id)
put("tag_id", tag.id)
},
).build()
}
result.add(
0,
ContentProviderOperation.newInsert(uri(authority, TABLE_MANGA))
.withValues(json.toContentValues())
.withValues(manga.toContentValues())
.build(),
)
return result
}
private fun getHistory(): JSONArray {
private fun getHistory(): List<HistorySyncDto> {
return provider.query(authorityHistory, TABLE_HISTORY).use { cursor ->
val json = JSONArray()
val result = ArrayList<HistorySyncDto>(cursor.count)
if (cursor.moveToFirst()) {
do {
val jo = cursor.toJson()
jo.put("manga", getManga(authorityHistory, jo.getLong("manga_id")))
json.put(jo)
val mangaId = cursor.getLong(cursor.getColumnIndexOrThrow("manga_id"))
result.add(HistorySyncDto(cursor, getManga(authorityHistory, mangaId)))
} while (cursor.moveToNext())
}
json
result
}
}
private fun getFavourites(): JSONArray {
return provider.query(authorityFavourites, TABLE_FAVOURITES).use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
val jo = cursor.toJson()
jo.put("manga", getManga(authorityFavourites, jo.getLong("manga_id")))
json.put(jo)
} while (cursor.moveToNext())
}
json
private fun getFavourites(): List<FavouriteSyncDto> {
return provider.query(authorityFavourites, TABLE_FAVOURITES).map { cursor ->
val manga = getManga(authorityFavourites, cursor.getLong(cursor.getColumnIndexOrThrow("manga_id")))
FavouriteSyncDto(cursor, manga)
}
}
private fun getFavouriteCategories(): JSONArray {
return provider.query(authorityFavourites, TABLE_FAVOURITE_CATEGORIES).use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
json.put(cursor.toJson())
} while (cursor.moveToNext())
}
json
private fun getFavouriteCategories(): List<FavouriteCategorySyncDto> =
provider.query(authorityFavourites, TABLE_FAVOURITE_CATEGORIES).map { cursor ->
FavouriteCategorySyncDto(cursor)
}
}
private fun getManga(authority: String, id: Long): JSONObject {
val manga = provider.query(
uri(authority, TABLE_MANGA),
null,
"manga_id = ?",
arrayOf(id.toString()),
null,
)?.use { cursor ->
cursor.moveToFirst()
cursor.toJson()
}
requireNotNull(manga)
val tags = provider.query(
uri(authority, TABLE_MANGA_TAGS),
arrayOf("tag_id"),
"manga_id = ?",
arrayOf(id.toString()),
null,
)?.use { cursor ->
val json = JSONArray()
if (cursor.moveToFirst()) {
do {
val tagId = cursor.getLong(0)
json.put(getTag(authority, tagId))
} while (cursor.moveToNext())
}
json
}
manga.put("tags", requireNotNull(tags))
return manga
private fun getManga(authority: String, id: Long): MangaSyncDto {
val tags = requireNotNull(
provider.query(
uri(authority, TABLE_MANGA_TAGS),
arrayOf("tag_id"),
"manga_id = ?",
arrayOf(id.toString()),
null,
)?.mapToSet {
val tagId = it.getLong(it.getColumnIndexOrThrow("tag_id"))
getTag(authority, tagId)
},
)
return requireNotNull(
provider.query(
uri(authority, TABLE_MANGA),
null,
"manga_id = ?",
arrayOf(id.toString()),
null,
)?.use { cursor ->
cursor.moveToFirst()
MangaSyncDto(cursor, tags)
},
)
}
private fun getTag(authority: String, tagId: Long): JSONObject {
val tag = provider.query(
private fun getTag(authority: String, tagId: Long): MangaTagSyncDto = requireNotNull(
provider.query(
uri(authority, TABLE_TAGS),
null,
"tag_id = ?",
@ -259,13 +256,12 @@ class SyncHelper @AssistedInject constructor(
null,
)?.use { cursor ->
if (cursor.moveToFirst()) {
cursor.toJson()
MangaTagSyncDto(cursor)
} else {
null
}
}
return requireNotNull(tag)
}
},
)
private fun gcFavourites() {
val deletedAt = System.currentTimeMillis() - defaultGcPeriod
@ -290,9 +286,17 @@ class SyncHelper @AssistedInject constructor(
private fun uri(authority: String, table: String) = "content://$authority/$table".toUri()
private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject
private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray
private fun Response.parseDtoOrNull(): SyncDto? {
return try {
when {
!isSuccessful -> throw IOException(body?.string())
code == HttpURLConnection.HTTP_NO_CONTENT -> null
else -> Json.decodeFromString<SyncDto>(body?.string() ?: return null)
}
} finally {
closeQuietly()
}
}
@AssistedFactory
interface Factory {

@ -44,8 +44,8 @@
<item>1</item>
</string-array>
<string-array name="sync_url_list" translatable="false">
<item>https://moe.shirizu.org</item>
<item>https://sync.kotatsu.app</item>
<item>https://moe.shirizu.org</item>
<item>http://54.254.71.100</item>
<item>http://86.57.183.214:8081</item>
</string-array>

@ -4,4 +4,5 @@ plugins {
alias(libs.plugins.hilt) apply false
alias(libs.plugins.ksp) apply false
alias(libs.plugins.room) apply false
alias(libs.plugins.kotlinx.serizliation) apply false
}

@ -36,6 +36,7 @@ parsers = "bcde8ef2a2"
preference = "1.2.1"
recyclerview = "1.4.0"
room = "2.7.1"
serialization = "1.8.1"
ssiv = "9a67b6a7c9"
swiperefreshlayout = "1.1.0"
testRules = "1.6.1"
@ -96,6 +97,7 @@ kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "coroutines" }
kotlinx-coroutines-guava = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-guava", version.ref = "coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "coroutines" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "serialization" }
leakcanary-android = { module = "com.squareup.leakcanary:leakcanary-android", version.ref = "leakcanary" }
lifecycle-process = { module = "androidx.lifecycle:lifecycle-process", version.ref = "lifecycle" }
lifecycle-service = { module = "androidx.lifecycle:lifecycle-service", version.ref = "lifecycle" }
@ -112,7 +114,8 @@ workinspector = { module = "com.github.Koitharu:WorkInspector", version.ref = "w
[plugins]
android-application = { id = "com.android.application", version.ref = "gradle" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinx-serizliation = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
room = { id = "androidx.room", version.ref = "room" }

Loading…
Cancel
Save