diff --git a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt index b81fcbe7a..6b24b25e1 100644 --- a/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt +++ b/app/src/main/java/org/koitharu/kotatsu/core/prefs/AppSettings.kt @@ -312,6 +312,7 @@ class AppSettings(context: Context) { const val KEY_DOWNLOADS_SLOWDOWN = "downloads_slowdown" const val KEY_ALL_FAVOURITES_VISIBLE = "all_favourites_visible" const val KEY_DOH = "doh" + const val KEY_SYNC = "sync" // About const val KEY_APP_UPDATE = "app_update" diff --git a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt b/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt index 800599441..d9de212d6 100644 --- a/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/settings/ContentSettingsFragment.kt @@ -1,7 +1,9 @@ package org.koitharu.kotatsu.settings +import android.content.Intent import android.content.SharedPreferences import android.os.Bundle +import android.provider.Settings import android.view.View import androidx.preference.ListPreference import androidx.preference.Preference @@ -76,6 +78,10 @@ class ContentSettingsFragment : AppSettings.KEY_SOURCES_HIDDEN -> { bindRemoteSourcesSummary() } + AppSettings.KEY_SYNC -> { + val intent = Intent(Settings.ACTION_SYNC_SETTINGS) + startActivity(intent) + } } } diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt index 35b3c7408..f5dbfb9d5 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt +++ b/app/src/main/java/org/koitharu/kotatsu/sync/domain/SyncHelper.kt @@ -5,6 +5,7 @@ import android.content.* import android.database.Cursor import android.net.Uri import androidx.annotation.WorkerThread +import androidx.core.content.contentValuesOf import okhttp3.OkHttpClient import okhttp3.Request import org.json.JSONArray @@ -13,9 +14,9 @@ import org.koitharu.kotatsu.R import org.koitharu.kotatsu.core.db.* import org.koitharu.kotatsu.parsers.util.json.mapJSONTo import org.koitharu.kotatsu.parsers.util.parseJson +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.SyncAuthApi import org.koitharu.kotatsu.utils.GZipInterceptor import org.koitharu.kotatsu.utils.ext.toContentValues import org.koitharu.kotatsu.utils.ext.toJson @@ -86,6 +87,7 @@ class SyncHelper( .withSelection("updated_at < ?", arrayOf(timestamp.toString())) .build() json.mapJSONTo(operations) { jo -> + operations.addAll(upsertManga(jo.removeJSONObject("manga"), AUTHORITY_HISTORY)) ContentProviderOperation.newInsert(uri) .withValues(jo.toContentValues()) .build() @@ -114,6 +116,7 @@ class SyncHelper( .withSelection("created_at < ?", arrayOf(timestamp.toString())) .build() json.mapJSONTo(operations) { jo -> + operations.addAll(upsertManga(jo.removeJSONObject("manga"), AUTHORITY_FAVOURITES)) ContentProviderOperation.newInsert(uri) .withValues(jo.toContentValues()) .build() @@ -121,6 +124,31 @@ class SyncHelper( return provider.applyBatch(operations) } + private fun upsertManga(json: JSONObject, authority: String): List { + val tags = json.removeJSONArray(TABLE_TAGS) + val result = ArrayList(tags.length() * 2 + 1) + for (i in 0 until tags.length()) { + val tag = tags.getJSONObject(i) + 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"), + ) + ).build() + } + result.add( + 0, + ContentProviderOperation.newInsert(uri(authority, TABLE_MANGA)) + .withValues(json.toContentValues()) + .build() + ) + return result + } + private fun getHistory(): JSONArray { return provider.query(AUTHORITY_HISTORY, TABLE_HISTORY).use { cursor -> val json = JSONArray() @@ -217,4 +245,8 @@ class SyncHelper( } private fun uri(authority: String, table: String) = Uri.parse("content://$authority/$table") + + private fun JSONObject.removeJSONObject(name: String) = remove(name) as JSONObject + + private fun JSONObject.removeJSONArray(name: String) = remove(name) as JSONArray } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt index 02f296aac..604e6de9d 100644 --- a/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt +++ b/app/src/main/java/org/koitharu/kotatsu/sync/ui/SyncProvider.kt @@ -7,10 +7,11 @@ import android.content.ContentValues import android.database.Cursor import android.database.sqlite.SQLiteDatabase import android.net.Uri +import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQueryBuilder +import java.util.concurrent.Callable import org.koin.android.ext.android.inject import org.koitharu.kotatsu.core.db.* -import java.util.concurrent.Callable abstract class SyncProvider : ContentProvider() { @@ -55,15 +56,14 @@ abstract class SyncProvider : ContentProvider() { return null } val db = database.openHelper.writableDatabase - db.insert(table, SQLiteDatabase.CONFLICT_REPLACE, values) - return null + if (db.insert(table, SQLiteDatabase.CONFLICT_IGNORE, values) < 0) { + db.update(table, values) + } + return uri } override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - val table = getTableName(uri) - if (table == null) { - return 0 - } + val table = getTableName(uri) ?: return 0 return database.openHelper.writableDatabase.delete(table, selection, selectionArgs) } @@ -77,14 +77,35 @@ abstract class SyncProvider : ContentProvider() { } override fun applyBatch(operations: ArrayList): Array { - return database.runInTransaction(Callable { super.applyBatch(operations) }) + return runAtomicTransaction { super.applyBatch(operations) } } override fun bulkInsert(uri: Uri, values: Array): Int { - return database.runInTransaction(Callable { super.bulkInsert(uri, values) }) + return runAtomicTransaction { super.bulkInsert(uri, values) } } private fun getTableName(uri: Uri): String? { return uri.pathSegments.singleOrNull()?.takeIf { it in supportedTables } } + + private fun runAtomicTransaction(callable: Callable): R { + return synchronized(database) { + database.runInTransaction(callable) + } + } + + private fun SupportSQLiteDatabase.update(table: String, values: ContentValues) { + val keys = when (table) { + TABLE_TAGS -> listOf("tag_id") + TABLE_MANGA_TAGS -> listOf("tag_id", "manga_id") + TABLE_MANGA -> listOf("manga_id") + TABLE_FAVOURITES -> listOf("manga_id", "category_id") + TABLE_FAVOURITE_CATEGORIES -> listOf("category_id") + TABLE_HISTORY -> listOf("manga_id") + else -> throw IllegalArgumentException("Update for $table is not supported") + } + val whereClause = keys.joinToString(" AND ") { "`$it` = ?" } + val whereArgs = Array(keys.size) { i -> values.get("`${keys[i]}`") ?: values.get(keys[i]) } + this.update(table, SQLiteDatabase.CONFLICT_IGNORE, values, whereClause, whereArgs) + } } \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CursorExt.kt b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CursorExt.kt index 852d92e10..eeab153b0 100644 --- a/app/src/main/java/org/koitharu/kotatsu/utils/ext/CursorExt.kt +++ b/app/src/main/java/org/koitharu/kotatsu/utils/ext/CursorExt.kt @@ -24,7 +24,7 @@ fun JSONObject.toContentValues(): ContentValues { for (key in keys()) { val name = key.escapeName() when (val value = get(key)) { - null -> cv.putNull(name) + 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) diff --git a/app/src/main/res/values/constants.xml b/app/src/main/res/values/constants.xml index e63a0cb2d..12c41ccbf 100644 --- a/app/src/main/res/values/constants.xml +++ b/app/src/main/res/values/constants.xml @@ -5,7 +5,7 @@ https://4pda.to/forum/index.php?showtopic=697669 https://hosted.weblate.org/engage/kotatsu org.kotatsu.sync - http://192.168.0.113:8080 + http://95.216.215.49:8080 -1 1 diff --git a/app/src/main/res/xml/pref_content.xml b/app/src/main/res/xml/pref_content.xml index 7b48494ea..c7f6ef630 100644 --- a/app/src/main/res/xml/pref_content.xml +++ b/app/src/main/res/xml/pref_content.xml @@ -40,9 +40,14 @@ android:valueTo="5" app:defaultValue="2" /> + + + android:title="@string/backup_restore" /> \ No newline at end of file