Use kotlin serialization for sync
This commit is contained in:
@@ -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
|
||||
|
||||
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 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")
|
||||
}
|
||||
}
|
||||
return cv
|
||||
}
|
||||
|
||||
private fun String.escapeName() = "`$this`"
|
||||
import androidx.collection.ArraySet
|
||||
|
||||
fun Cursor.getBoolean(columnIndex: Int) = getInt(columnIndex) > 0
|
||||
|
||||
inline fun <T> Cursor.map(mapper: (Cursor) -> T): List<T> = mapTo(ArrayList(count), mapper)
|
||||
|
||||
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())
|
||||
}
|
||||
destination
|
||||
}
|
||||
|
||||
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): 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 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 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" }
|
||||
kotlin = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
|
||||
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" }
|
||||
room = { id = "androidx.room", version.ref = "room" }
|
||||
|
||||
Reference in New Issue
Block a user