Support backups
This commit is contained in:
parent
06c63f1207
commit
da44dc3fb5
17 changed files with 1361 additions and 7 deletions
|
@ -0,0 +1,381 @@
|
|||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import com.google.gson.*
|
||||
import com.google.gson.reflect.TypeToken
|
||||
import com.google.gson.stream.JsonReader
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
|
||||
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import java.io.*
|
||||
import java.lang.reflect.Type
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* This class provides the necessary methods to create and restore backups for the data of the
|
||||
* application. The backup follows a JSON structure, with the following scheme:
|
||||
*
|
||||
* {
|
||||
* "mangas": [
|
||||
* {
|
||||
* "manga": {"id": 1, ...},
|
||||
* "chapters": [{"id": 1, ...}, {...}],
|
||||
* "sync": [{"id": 1, ...}, {...}],
|
||||
* "categories": ["cat1", "cat2", ...]
|
||||
* },
|
||||
* { ... }
|
||||
* ],
|
||||
* "categories": [
|
||||
* {"id": 1, ...},
|
||||
* {"id": 2, ...}
|
||||
* ]
|
||||
* }
|
||||
*
|
||||
* @param db the database helper.
|
||||
*/
|
||||
class BackupManager(private val db: DatabaseHelper) {
|
||||
|
||||
private val MANGA = "manga"
|
||||
private val MANGAS = "mangas"
|
||||
private val CHAPTERS = "chapters"
|
||||
private val MANGA_SYNC = "sync"
|
||||
private val CATEGORIES = "categories"
|
||||
|
||||
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
|
||||
private val gson = GsonBuilder()
|
||||
.registerTypeAdapter(Integer::class.java, IntegerSerializer())
|
||||
.setExclusionStrategies(IdExclusion())
|
||||
.create()
|
||||
|
||||
/**
|
||||
* Backups the data of the application to a file.
|
||||
*
|
||||
* @param file the file where the backup will be saved.
|
||||
* @throws IOException if there's any IO error.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun backupToFile(file: File) {
|
||||
val root = backupToJson()
|
||||
|
||||
FileWriter(file).use {
|
||||
gson.toJson(root, it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a JSON object containing the backup of the app's data.
|
||||
*
|
||||
* @return the backup as a JSON object.
|
||||
*/
|
||||
fun backupToJson(): JsonObject {
|
||||
val root = JsonObject()
|
||||
|
||||
// Backup library mangas and its dependencies
|
||||
val mangaEntries = JsonArray()
|
||||
root.add(MANGAS, mangaEntries)
|
||||
for (manga in db.getFavoriteMangas().executeAsBlocking()) {
|
||||
mangaEntries.add(backupManga(manga))
|
||||
}
|
||||
|
||||
// Backup categories
|
||||
val categoryEntries = JsonArray()
|
||||
root.add(CATEGORIES, categoryEntries)
|
||||
for (category in db.getCategories().executeAsBlocking()) {
|
||||
categoryEntries.add(backupCategory(category))
|
||||
}
|
||||
|
||||
return root
|
||||
}
|
||||
|
||||
/**
|
||||
* Backups a manga and its related data (chapters, categories this manga is in, sync...).
|
||||
*
|
||||
* @param manga the manga to backup.
|
||||
* @return a JSON object containing all the data of the manga.
|
||||
*/
|
||||
private fun backupManga(manga: Manga): JsonObject {
|
||||
// Entry for this manga
|
||||
val entry = JsonObject()
|
||||
|
||||
// Backup manga fields
|
||||
entry.add(MANGA, gson.toJsonTree(manga))
|
||||
|
||||
// Backup all the chapters
|
||||
val chapters = db.getChapters(manga).executeAsBlocking()
|
||||
if (!chapters.isEmpty()) {
|
||||
entry.add(CHAPTERS, gson.toJsonTree(chapters))
|
||||
}
|
||||
|
||||
// Backup manga sync
|
||||
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
|
||||
if (!mangaSync.isEmpty()) {
|
||||
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
|
||||
}
|
||||
|
||||
// Backup categories for this manga
|
||||
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
if (!categoriesForManga.isEmpty()) {
|
||||
val categoriesNames = ArrayList<String>()
|
||||
for (category in categoriesForManga) {
|
||||
categoriesNames.add(category.name)
|
||||
}
|
||||
entry.add(CATEGORIES, gson.toJsonTree(categoriesNames))
|
||||
}
|
||||
|
||||
return entry
|
||||
}
|
||||
|
||||
/**
|
||||
* Backups a category.
|
||||
*
|
||||
* @param category the category to backup.
|
||||
* @return a JSON object containing the data of the category.
|
||||
*/
|
||||
private fun backupCategory(category: Category): JsonElement {
|
||||
return gson.toJsonTree(category)
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup from a file.
|
||||
*
|
||||
* @param file the file containing the backup.
|
||||
* @throws IOException if there's any IO error.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun restoreFromFile(file: File) {
|
||||
JsonReader(FileReader(file)).use {
|
||||
val root = JsonParser().parse(it).asJsonObject
|
||||
restoreFromJson(root)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup from an input stream.
|
||||
*
|
||||
* @param stream the stream containing the backup.
|
||||
* @throws IOException if there's any IO error.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun restoreFromStream(stream: InputStream) {
|
||||
JsonReader(InputStreamReader(stream)).use {
|
||||
val root = JsonParser().parse(it).asJsonObject
|
||||
restoreFromJson(root)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup from a JSON object. Everything executes in a single transaction so that
|
||||
* nothing is modified if there's an error.
|
||||
*
|
||||
* @param root the root of the JSON.
|
||||
*/
|
||||
fun restoreFromJson(root: JsonObject) {
|
||||
db.inTransaction {
|
||||
// Restore categories
|
||||
root.get(CATEGORIES)?.let {
|
||||
restoreCategories(it.asJsonArray)
|
||||
}
|
||||
|
||||
// Restore mangas
|
||||
root.get(MANGAS)?.let {
|
||||
restoreMangas(it.asJsonArray)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories.
|
||||
*
|
||||
* @param jsonCategories the categories of the json.
|
||||
*/
|
||||
private fun restoreCategories(jsonCategories: JsonArray) {
|
||||
// Get categories from file and from db
|
||||
val dbCategories = db.getCategories().executeAsBlocking()
|
||||
val backupCategories = getArrayOrEmpty<Category>(jsonCategories,
|
||||
object : TypeToken<List<Category>>() {}.type)
|
||||
|
||||
// Iterate over them
|
||||
for (category in backupCategories) {
|
||||
// Used to know if the category is already in the db
|
||||
var found = false
|
||||
for (dbCategory in dbCategories) {
|
||||
// If the category is already in the db, assign the id to the file's category
|
||||
// and do nothing
|
||||
if (category.nameLower == dbCategory.nameLower) {
|
||||
category.id = dbCategory.id
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
// If the category isn't in the db, remove the id and insert a new category
|
||||
// Store the inserted id in the category
|
||||
if (!found) {
|
||||
// Let the db assign the id
|
||||
category.id = null
|
||||
val result = db.insertCategory(category).executeAsBlocking()
|
||||
category.id = result.insertedId()?.toInt()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores all the mangas and its related data.
|
||||
*
|
||||
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
|
||||
*/
|
||||
private fun restoreMangas(jsonMangas: JsonArray) {
|
||||
val chapterToken = object : TypeToken<List<Chapter>>() {}.type
|
||||
val mangaSyncToken = object : TypeToken<List<MangaSync>>() {}.type
|
||||
val categoriesNamesToken = object : TypeToken<List<String>>() {}.type
|
||||
|
||||
for (backupManga in jsonMangas) {
|
||||
// Map every entry to objects
|
||||
val element = backupManga.asJsonObject
|
||||
val manga = gson.fromJson(element.get(MANGA), Manga::class.java)
|
||||
val chapters = getArrayOrEmpty<Chapter>(element.get(CHAPTERS), chapterToken)
|
||||
val sync = getArrayOrEmpty<MangaSync>(element.get(MANGA_SYNC), mangaSyncToken)
|
||||
val categories = getArrayOrEmpty<String>(element.get(CATEGORIES), categoriesNamesToken)
|
||||
|
||||
// Restore everything related to this manga
|
||||
restoreManga(manga)
|
||||
restoreChaptersForManga(manga, chapters)
|
||||
restoreSyncForManga(manga, sync)
|
||||
restoreCategoriesForManga(manga, categories)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a manga.
|
||||
*
|
||||
* @param manga the manga to restore.
|
||||
*/
|
||||
private fun restoreManga(manga: Manga) {
|
||||
// Try to find existing manga in db
|
||||
val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking()
|
||||
if (dbManga == null) {
|
||||
// Let the db assign the id
|
||||
manga.id = null
|
||||
val result = db.insertManga(manga).executeAsBlocking()
|
||||
manga.id = result.insertedId()
|
||||
} else {
|
||||
// If it exists already, we copy only the values related to the source from the db
|
||||
// (they can be up to date). Local values (flags) are kept from the backup.
|
||||
manga.id = dbManga.id
|
||||
manga.copyFrom(dbManga)
|
||||
manga.favorite = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the chapters of a manga.
|
||||
*
|
||||
* @param manga the manga whose chapters have to be restored.
|
||||
* @param chapters the chapters to restore.
|
||||
*/
|
||||
private fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
for (chapter in chapters) {
|
||||
chapter.manga_id = manga.id
|
||||
}
|
||||
|
||||
val dbChapters = db.getChapters(manga).executeAsBlocking()
|
||||
val chaptersToUpdate = ArrayList<Chapter>()
|
||||
for (backupChapter in chapters) {
|
||||
// Try to find existing chapter in db
|
||||
val pos = dbChapters.indexOf(backupChapter)
|
||||
if (pos != -1) {
|
||||
// The chapter is already in the db, only update its fields
|
||||
val dbChapter = dbChapters[pos]
|
||||
// If one of them was read, the chapter will be marked as read
|
||||
dbChapter.read = backupChapter.read || dbChapter.read
|
||||
dbChapter.last_page_read = Math.max(backupChapter.last_page_read, dbChapter.last_page_read)
|
||||
chaptersToUpdate.add(dbChapter)
|
||||
} else {
|
||||
// Insert new chapter. Let the db assign the id
|
||||
backupChapter.id = null
|
||||
chaptersToUpdate.add(backupChapter)
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (!chaptersToUpdate.isEmpty()) {
|
||||
db.insertChapters(chaptersToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the categories a manga is in.
|
||||
*
|
||||
* @param manga the manga whose categories have to be restored.
|
||||
* @param categories the categories to restore.
|
||||
*/
|
||||
private fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
|
||||
val dbCategories = db.getCategories().executeAsBlocking()
|
||||
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
|
||||
for (backupCategoryStr in categories) {
|
||||
for (dbCategory in dbCategories) {
|
||||
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
|
||||
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (!mangaCategoriesToUpdate.isEmpty()) {
|
||||
val mangaAsList = ArrayList<Manga>()
|
||||
mangaAsList.add(manga)
|
||||
db.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
|
||||
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores the sync of a manga.
|
||||
*
|
||||
* @param manga the manga whose sync have to be restored.
|
||||
* @param sync the sync to restore.
|
||||
*/
|
||||
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
|
||||
// Fix foreign keys with the current manga id
|
||||
for (mangaSync in sync) {
|
||||
mangaSync.manga_id = manga.id
|
||||
}
|
||||
|
||||
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
|
||||
val syncToUpdate = ArrayList<MangaSync>()
|
||||
for (backupSync in sync) {
|
||||
// Try to find existing chapter in db
|
||||
val pos = dbSyncs.indexOf(backupSync)
|
||||
if (pos != -1) {
|
||||
// The sync is already in the db, only update its fields
|
||||
val dbSync = dbSyncs[pos]
|
||||
// Mark the max chapter as read and nothing else
|
||||
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read)
|
||||
syncToUpdate.add(dbSync)
|
||||
} else {
|
||||
// Insert new sync. Let the db assign the id
|
||||
backupSync.id = null
|
||||
syncToUpdate.add(backupSync)
|
||||
}
|
||||
}
|
||||
|
||||
// Update database
|
||||
if (!syncToUpdate.isEmpty()) {
|
||||
db.insertMangasSync(syncToUpdate).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of items from a json element, or an empty list if the element is null.
|
||||
*
|
||||
* @param element the json to be mapped to a list of items.
|
||||
* @param type the gson mapping to restore the list.
|
||||
* @return a list of items.
|
||||
*/
|
||||
private fun <T> getArrayOrEmpty(element: JsonElement?, type: Type): List<T> {
|
||||
return gson.fromJson<List<T>>(element, type) ?: ArrayList<T>()
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.google.gson.ExclusionStrategy
|
||||
import com.google.gson.FieldAttributes
|
||||
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.MangaSync
|
||||
|
||||
class IdExclusion : ExclusionStrategy {
|
||||
|
||||
private val categoryExclusions = listOf("id")
|
||||
private val mangaExclusions = listOf("id")
|
||||
private val chapterExclusions = listOf("id", "manga_id")
|
||||
private val syncExclusions = listOf("id", "manga_id", "update")
|
||||
|
||||
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
|
||||
Manga::class.java -> mangaExclusions.contains(f.name)
|
||||
Chapter::class.java -> chapterExclusions.contains(f.name)
|
||||
MangaSync::class.java -> syncExclusions.contains(f.name)
|
||||
Category::class.java -> categoryExclusions.contains(f.name)
|
||||
else -> false
|
||||
}
|
||||
|
||||
override fun shouldSkipClass(clazz: Class<*>) = false
|
||||
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package eu.kanade.tachiyomi.data.backup.serializer
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.google.gson.JsonSerializationContext
|
||||
import com.google.gson.JsonSerializer
|
||||
|
||||
import java.lang.reflect.Type
|
||||
|
||||
class IntegerSerializer : JsonSerializer<Int> {
|
||||
|
||||
override fun serialize(value: Int?, type: Type, context: JsonSerializationContext): JsonElement? {
|
||||
if (value != null && value !== 0)
|
||||
return JsonPrimitive(value)
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -256,6 +256,8 @@ open class DatabaseHelper(context: Context) {
|
|||
|
||||
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
|
||||
|
||||
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
|
||||
|
||||
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
|
||||
|
||||
// Categories related queries
|
||||
|
@ -268,6 +270,13 @@ open class DatabaseHelper(context: Context) {
|
|||
.build())
|
||||
.prepare()
|
||||
|
||||
fun getCategoriesForManga(manga: Manga) = db.get()
|
||||
.listOfObjects(Category::class.java)
|
||||
.withQuery(RawQuery.builder()
|
||||
.query(getCategoriesForMangaQuery(manga))
|
||||
.build())
|
||||
.prepare()
|
||||
|
||||
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
|
||||
|
||||
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import java.util.*
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga as MangaModel
|
||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
|
||||
|
@ -38,3 +40,15 @@ fun getRecentsQuery(date: Date): String =
|
|||
"ON ${Manga.TABLE}.${Manga.COLUMN_ID} = ${Chapter.TABLE}.${Chapter.COLUMN_MANGA_ID} " +
|
||||
"WHERE ${Manga.COLUMN_FAVORITE} = 1 AND ${Chapter.COLUMN_DATE_UPLOAD} > ${date.time} " +
|
||||
"ORDER BY ${Chapter.COLUMN_DATE_UPLOAD} DESC"
|
||||
|
||||
|
||||
/**
|
||||
* Query to get the categorias for a manga.
|
||||
*
|
||||
* @param manga the manga.
|
||||
*/
|
||||
fun getCategoriesForMangaQuery(manga: MangaModel) =
|
||||
"SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " +
|
||||
"JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " +
|
||||
"${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " +
|
||||
"WHERE ${MangaCategory.COLUMN_MANGA_ID} = ${manga.id}"
|
|
@ -35,4 +35,23 @@ public class Category implements Serializable {
|
|||
c.id = 0;
|
||||
return c;
|
||||
}
|
||||
|
||||
public String getNameLower() {
|
||||
return name.toLowerCase();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
Category category = (Category) o;
|
||||
|
||||
return name.equals(category.name);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return name.hashCode();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,9 +59,9 @@ public class Manga implements Serializable {
|
|||
@StorIOSQLiteColumn(name = MangaTable.COLUMN_CHAPTER_FLAGS)
|
||||
public int chapter_flags;
|
||||
|
||||
public int unread;
|
||||
public transient int unread;
|
||||
|
||||
public int category;
|
||||
public transient int category;
|
||||
|
||||
public static final int UNKNOWN = 0;
|
||||
public static final int ONGOING = 1;
|
||||
|
|
|
@ -40,6 +40,10 @@ public class MangaSync implements Serializable {
|
|||
|
||||
public boolean update;
|
||||
|
||||
public static MangaSync create() {
|
||||
return new MangaSync();
|
||||
}
|
||||
|
||||
public static MangaSync create(MangaSyncService service) {
|
||||
MangaSync mangasync = new MangaSync();
|
||||
mangasync.sync_id = service.getId();
|
||||
|
@ -52,4 +56,23 @@ public class MangaSync implements Serializable {
|
|||
status = other.status;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
|
||||
MangaSync mangaSync = (MangaSync) o;
|
||||
|
||||
if (manga_id != mangaSync.manga_id) return false;
|
||||
if (sync_id != mangaSync.sync_id) return false;
|
||||
return remote_id == mangaSync.remote_id;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
int result = (int) (manga_id ^ (manga_id >>> 32));
|
||||
result = 31 * result + sync_id;
|
||||
result = 31 * result + remote_id;
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.source.base.Source
|
|||
import eu.kanade.tachiyomi.data.updater.UpdateDownloader
|
||||
import eu.kanade.tachiyomi.injection.module.AppModule
|
||||
import eu.kanade.tachiyomi.injection.module.DataModule
|
||||
import eu.kanade.tachiyomi.ui.backup.BackupPresenter
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter
|
||||
import eu.kanade.tachiyomi.ui.category.CategoryPresenter
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadPresenter
|
||||
|
@ -38,6 +39,7 @@ interface AppComponent {
|
|||
fun inject(myAnimeListPresenter: MyAnimeListPresenter)
|
||||
fun inject(categoryPresenter: CategoryPresenter)
|
||||
fun inject(recentChaptersPresenter: RecentChaptersPresenter)
|
||||
fun inject(backupPresenter: BackupPresenter)
|
||||
|
||||
fun inject(mangaActivity: MangaActivity)
|
||||
fun inject(settingsActivity: SettingsActivity)
|
||||
|
|
|
@ -0,0 +1,133 @@
|
|||
package eu.kanade.tachiyomi.ui.backup
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.fragment_backup.*
|
||||
import nucleus.factory.RequiresPresenter
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Fragment to create and restore backups of the application's data.
|
||||
* Uses R.layout.fragment_backup.
|
||||
*/
|
||||
@RequiresPresenter(BackupPresenter::class)
|
||||
class BackupFragment : BaseRxFragment<BackupPresenter>() {
|
||||
|
||||
private var backupDialog: Dialog? = null
|
||||
private var restoreDialog: Dialog? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View {
|
||||
return inflater.inflate(R.layout.fragment_backup, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View?, savedState: Bundle?) {
|
||||
backup_button.setOnClickListener {
|
||||
val today = SimpleDateFormat("yyyy-MM-dd").format(Date())
|
||||
val file = File(activity.externalCacheDir, "tachiyomi-$today.json")
|
||||
presenter.createBackup(file)
|
||||
|
||||
backupDialog = MaterialDialog.Builder(activity)
|
||||
.content(R.string.backup_please_wait)
|
||||
.progress(true, 0)
|
||||
.show()
|
||||
}
|
||||
|
||||
restore_button.setOnClickListener {
|
||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||
intent.type = "application/octet-stream"
|
||||
val chooser = Intent.createChooser(intent, getString(R.string.file_select_cover))
|
||||
startActivityForResult(chooser, REQUEST_BACKUP_OPEN)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the backup is completed.
|
||||
*/
|
||||
fun onBackupCompleted() {
|
||||
dismissBackupDialog()
|
||||
val intent = Intent(Intent.ACTION_SEND)
|
||||
intent.type = "text/plain"
|
||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + presenter.backupFile))
|
||||
startActivity(Intent.createChooser(intent, ""))
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when the restore is completed.
|
||||
*/
|
||||
fun onRestoreCompleted() {
|
||||
dismissRestoreDialog()
|
||||
context.toast(R.string.backup_completed)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when there's an error doing the backup.
|
||||
* @param error the exception thrown.
|
||||
*/
|
||||
fun onBackupError(error: Throwable) {
|
||||
dismissBackupDialog()
|
||||
context.toast(error.message)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called from the presenter when there's an error restoring the backup.
|
||||
* @param error the exception thrown.
|
||||
*/
|
||||
fun onRestoreError(error: Throwable) {
|
||||
dismissRestoreDialog()
|
||||
context.toast(error.message)
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
if (data != null && resultCode == Activity.RESULT_OK && requestCode == REQUEST_BACKUP_OPEN) {
|
||||
restoreDialog = MaterialDialog.Builder(activity)
|
||||
.content(R.string.restore_please_wait)
|
||||
.progress(true, 0)
|
||||
.show()
|
||||
|
||||
val stream = context.contentResolver.openInputStream(data.data)
|
||||
presenter.restoreBackup(stream)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses the backup dialog.
|
||||
*/
|
||||
fun dismissBackupDialog() {
|
||||
backupDialog?.let {
|
||||
it.dismiss()
|
||||
backupDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses the restore dialog.
|
||||
*/
|
||||
fun dismissRestoreDialog() {
|
||||
restoreDialog?.let {
|
||||
it.dismiss()
|
||||
restoreDialog = null
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private val REQUEST_BACKUP_OPEN = 102
|
||||
|
||||
fun newInstance(): BackupFragment {
|
||||
return BackupFragment()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
package eu.kanade.tachiyomi.ui.backup
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.backup.BackupManager
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import rx.Observable
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import javax.inject.Inject
|
||||
|
||||
/**
|
||||
* Presenter of [BackupFragment].
|
||||
*/
|
||||
class BackupPresenter : BasePresenter<BackupFragment>() {
|
||||
|
||||
/**
|
||||
* Database.
|
||||
*/
|
||||
@Inject lateinit var db: DatabaseHelper
|
||||
|
||||
/**
|
||||
* Backup manager.
|
||||
*/
|
||||
private lateinit var backupManager: BackupManager
|
||||
|
||||
/**
|
||||
* File where the backup is saved.
|
||||
*/
|
||||
var backupFile: File? = null
|
||||
private set
|
||||
|
||||
/**
|
||||
* Stream to restore a backup.
|
||||
*/
|
||||
private var restoreStream: InputStream? = null
|
||||
|
||||
/**
|
||||
* Id of the restartable that creates a backup.
|
||||
*/
|
||||
private val CREATE_BACKUP = 1
|
||||
|
||||
/**
|
||||
* Id of the restartable that restores a backup.
|
||||
*/
|
||||
private val RESTORE_BACKUP = 2
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
backupManager = BackupManager(db)
|
||||
|
||||
startableFirst(CREATE_BACKUP,
|
||||
{ getBackupObservable() },
|
||||
{ view, next -> view.onBackupCompleted() },
|
||||
{ view, error -> view.onBackupError(error) })
|
||||
|
||||
startableFirst(RESTORE_BACKUP,
|
||||
{ getRestoreObservable() },
|
||||
{ view, next -> view.onRestoreCompleted() },
|
||||
{ view, error -> view.onRestoreError(error) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a backup and saves it to a file.
|
||||
*
|
||||
* @param file the path where the file will be saved.
|
||||
*/
|
||||
fun createBackup(file: File) {
|
||||
if (isUnsubscribed(CREATE_BACKUP)) {
|
||||
backupFile = file
|
||||
start(CREATE_BACKUP)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restores a backup from a stream.
|
||||
*
|
||||
* @param stream the input stream of the backup file.
|
||||
*/
|
||||
fun restoreBackup(stream: InputStream) {
|
||||
if (isUnsubscribed(RESTORE_BACKUP)) {
|
||||
restoreStream = stream
|
||||
start(RESTORE_BACKUP)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the observable to save a backup.
|
||||
*/
|
||||
private fun getBackupObservable(): Observable<Boolean> {
|
||||
return Observable.fromCallable {
|
||||
backupManager.backupToFile(backupFile!!)
|
||||
true
|
||||
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the observable to restore a backup.
|
||||
*/
|
||||
private fun getRestoreObservable(): Observable<Boolean> {
|
||||
return Observable.fromCallable {
|
||||
backupManager.restoreFromStream(restoreStream!!)
|
||||
true
|
||||
}.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
}
|
|
@ -9,6 +9,7 @@ import android.support.v4.widget.DrawerLayout
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.backup.BackupFragment
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment
|
||||
import eu.kanade.tachiyomi.ui.download.DownloadFragment
|
||||
|
@ -80,6 +81,10 @@ class MainActivity : BaseActivity() {
|
|||
item.isChecked = false
|
||||
startActivity(Intent(this, SettingsActivity::class.java))
|
||||
}
|
||||
R.id.nav_drawer_backup -> {
|
||||
setFragment(BackupFragment.newInstance())
|
||||
item.isChecked = true
|
||||
}
|
||||
}
|
||||
drawer.closeDrawer(GravityCompat.START)
|
||||
true
|
||||
|
|
9
app/src/main/res/drawable/ic_backup_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_backup_black_24dp.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24.0"
|
||||
android:viewportHeight="24.0">
|
||||
<path
|
||||
android:fillColor="#FF000000"
|
||||
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM14,13v4h-4v-4H7l5,-5 5,5h-3z"/>
|
||||
</vector>
|
21
app/src/main/res/layout/fragment_backup.xml
Normal file
21
app/src/main/res/layout/fragment_backup.xml
Normal file
|
@ -0,0 +1,21 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center">
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/backup_button"
|
||||
android:layout_marginBottom="16dp"
|
||||
android:text="@string/backup"/>
|
||||
|
||||
<Button
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:id="@+id/restore_button"
|
||||
android:text="@string/restore"/>
|
||||
|
||||
</LinearLayout>
|
|
@ -21,10 +21,14 @@
|
|||
android:title="@string/label_download_queue" />
|
||||
</group>
|
||||
<group android:id="@+id/group_settings"
|
||||
android:checkableBehavior="none">
|
||||
<item
|
||||
android:id="@+id/nav_drawer_settings"
|
||||
android:icon="@drawable/ic_settings_black_24dp"
|
||||
android:title="@string/label_settings" />
|
||||
android:checkableBehavior="single">
|
||||
<item
|
||||
android:id="@+id/nav_drawer_settings"
|
||||
android:icon="@drawable/ic_settings_black_24dp"
|
||||
android:title="@string/label_settings" />
|
||||
<item
|
||||
android:id="@+id/nav_drawer_backup"
|
||||
android:icon="@drawable/ic_backup_black_24dp"
|
||||
android:title="@string/label_backup" />
|
||||
</group>
|
||||
</menu>
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
<string name="label_catalogues">Catalogues</string>
|
||||
<string name="label_categories">Categories</string>
|
||||
<string name="label_selected">Selected: %1$d</string>
|
||||
<string name="label_backup">Backup</string>
|
||||
|
||||
<!-- Actions -->
|
||||
<string name="action_settings">Settings</string>
|
||||
|
@ -243,6 +244,13 @@
|
|||
<string name="decode_image_error">Image could not be loaded.\nTry changing the image decoder or with one of the options below</string>
|
||||
<string name="confirm_update_manga_sync">Update last chapter read in enabled services to %1$d?</string>
|
||||
|
||||
<!-- Backup fragment -->
|
||||
<string name="backup">Backup</string>
|
||||
<string name="restore">Restore</string>
|
||||
<string name="backup_please_wait">Backup in progress. Please wait…</string>
|
||||
<string name="backup_completed">Backup successfully restored</string>
|
||||
<string name="restore_please_wait">Restoring backup. Please wait…</string>
|
||||
|
||||
<!-- Downloads activity and service -->
|
||||
<string name="download_queue_error">An error occurred while downloading chapters. You can try again in the downloads section</string>
|
||||
|
||||
|
|
573
app/src/test/java/eu/kanade/tachiyomi/BackupTest.java
Normal file
573
app/src/test/java/eu/kanade/tachiyomi/BackupTest.java
Normal file
|
@ -0,0 +1,573 @@
|
|||
package eu.kanade.tachiyomi;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.Build;
|
||||
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonElement;
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.RuntimeEnvironment;
|
||||
import org.robolectric.annotation.Config;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.backup.BackupManager;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
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.MangaCategory;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@Config(constants = BuildConfig.class, sdk = Build.VERSION_CODES.LOLLIPOP)
|
||||
@RunWith(CustomRobolectricGradleTestRunner.class)
|
||||
public class BackupTest {
|
||||
|
||||
DatabaseHelper db;
|
||||
BackupManager backupManager;
|
||||
Gson gson;
|
||||
JsonObject root;
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
Application app = RuntimeEnvironment.application;
|
||||
db = new DatabaseHelper(app);
|
||||
backupManager = new BackupManager(db);
|
||||
gson = new Gson();
|
||||
root = new JsonObject();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreCategory() {
|
||||
String catName = "cat";
|
||||
root = createRootJson(null, toJson(createCategories(catName)));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
List<Category> dbCats = db.getCategories().executeAsBlocking();
|
||||
assertThat(dbCats).hasSize(1);
|
||||
assertThat(dbCats.get(0).name).isEqualTo(catName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreEmptyCategory() {
|
||||
root = createRootJson(null, toJson(new ArrayList<>()));
|
||||
backupManager.restoreFromJson(root);
|
||||
List<Category> dbCats = db.getCategories().executeAsBlocking();
|
||||
assertThat(dbCats).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreExistingCategory() {
|
||||
String catName = "cat";
|
||||
db.insertCategory(createCategory(catName)).executeAsBlocking();
|
||||
|
||||
root = createRootJson(null, toJson(createCategories(catName)));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
List<Category> dbCats = db.getCategories().executeAsBlocking();
|
||||
assertThat(dbCats).hasSize(1);
|
||||
assertThat(dbCats.get(0).name).isEqualTo(catName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreCategories() {
|
||||
root = createRootJson(null, toJson(createCategories("cat", "cat2", "cat3")));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
List<Category> dbCats = db.getCategories().executeAsBlocking();
|
||||
assertThat(dbCats).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreExistingCategories() {
|
||||
db.insertCategories(createCategories("cat", "cat2")).executeAsBlocking();
|
||||
|
||||
root = createRootJson(null, toJson(createCategories("cat", "cat2", "cat3")));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
List<Category> dbCats = db.getCategories().executeAsBlocking();
|
||||
assertThat(dbCats).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreExistingCategoriesAlt() {
|
||||
db.insertCategories(createCategories("cat", "cat2", "cat3")).executeAsBlocking();
|
||||
|
||||
root = createRootJson(null, toJson(createCategories("cat", "cat2")));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
List<Category> dbCats = db.getCategories().executeAsBlocking();
|
||||
assertThat(dbCats).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreManga() {
|
||||
String mangaName = "title";
|
||||
List<Manga> mangas = createMangas(mangaName);
|
||||
List<JsonElement> elements = new ArrayList<>();
|
||||
for (Manga manga : mangas) {
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
elements.add(entry);
|
||||
}
|
||||
root = createRootJson(toJson(elements), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
List<Manga> dbMangas = db.getMangas().executeAsBlocking();
|
||||
assertThat(dbMangas).hasSize(1);
|
||||
assertThat(dbMangas.get(0).title).isEqualTo(mangaName);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreExistingManga() {
|
||||
String mangaName = "title";
|
||||
Manga manga = createManga(mangaName);
|
||||
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
|
||||
List<JsonElement> elements = new ArrayList<>();
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
elements.add(entry);
|
||||
|
||||
root = createRootJson(toJson(elements), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
List<Manga> dbMangas = db.getMangas().executeAsBlocking();
|
||||
assertThat(dbMangas).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreExistingMangaWithUpdatedFields() {
|
||||
// Store a manga in db
|
||||
String mangaName = "title";
|
||||
String updatedThumbnailUrl = "updated thumbnail url";
|
||||
Manga manga = createManga(mangaName);
|
||||
manga.chapter_flags = 1024;
|
||||
manga.thumbnail_url = updatedThumbnailUrl;
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
|
||||
// Add an entry for a new manga with different attributes
|
||||
manga = createManga(mangaName);
|
||||
manga.chapter_flags = 512;
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> elements = new ArrayList<>();
|
||||
elements.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(elements), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
List<Manga> dbMangas = db.getMangas().executeAsBlocking();
|
||||
assertThat(dbMangas).hasSize(1);
|
||||
assertThat(dbMangas.get(0).thumbnail_url).isEqualTo(updatedThumbnailUrl);
|
||||
assertThat(dbMangas.get(0).chapter_flags).isEqualTo(512);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreChaptersForManga() {
|
||||
// Create a manga and 3 chapters
|
||||
Manga manga = createManga("title");
|
||||
manga.id = 1L;
|
||||
List<Chapter> chapters = createChapters(manga, "1", "2", "3");
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("chapters", toJson(chapters));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(1).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
List<Chapter> dbChapters = db.getChapters(dbManga).executeAsBlocking();
|
||||
assertThat(dbChapters).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreChaptersForExistingManga() {
|
||||
long mangaId = 3;
|
||||
// Create a manga and 3 chapters
|
||||
Manga manga = createManga("title");
|
||||
manga.id = mangaId;
|
||||
List<Chapter> chapters = createChapters(manga, "1", "2", "3");
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("chapters", toJson(chapters));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(mangaId).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
List<Chapter> dbChapters = db.getChapters(dbManga).executeAsBlocking();
|
||||
assertThat(dbChapters).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreExistingChaptersForExistingManga() {
|
||||
long mangaId = 5;
|
||||
// Store a manga and 3 chapters
|
||||
Manga manga = createManga("title");
|
||||
manga.id = mangaId;
|
||||
List<Chapter> chapters = createChapters(manga, "1", "2", "3");
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
db.insertChapters(chapters).executeAsBlocking();
|
||||
|
||||
// The backup contains a existing chapter and a new one, so it should have 4 chapters
|
||||
chapters = createChapters(manga, "3", "4");
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("chapters", toJson(chapters));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(mangaId).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
List<Chapter> dbChapters = db.getChapters(dbManga).executeAsBlocking();
|
||||
assertThat(dbChapters).hasSize(4);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreCategoriesForManga() {
|
||||
// Create a manga
|
||||
Manga manga = createManga("title");
|
||||
|
||||
// Create categories
|
||||
List<Category> categories = createCategories("cat1", "cat2", "cat3");
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("categories", toJson(createStringCategories("cat1")));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), toJson(categories));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(1).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
assertThat(db.getCategoriesForManga(dbManga).executeAsBlocking())
|
||||
.hasSize(1)
|
||||
.contains(Category.create("cat1"))
|
||||
.doesNotContain(Category.create("cat2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreCategoriesForExistingManga() {
|
||||
// Store a manga
|
||||
Manga manga = createManga("title");
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
|
||||
// Create categories
|
||||
List<Category> categories = createCategories("cat1", "cat2", "cat3");
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("categories", toJson(createStringCategories("cat1")));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), toJson(categories));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(1).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
assertThat(db.getCategoriesForManga(dbManga).executeAsBlocking())
|
||||
.hasSize(1)
|
||||
.contains(Category.create("cat1"))
|
||||
.doesNotContain(Category.create("cat2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreMultipleCategoriesForManga() {
|
||||
// Create a manga
|
||||
Manga manga = createManga("title");
|
||||
|
||||
// Create categories
|
||||
List<Category> categories = createCategories("cat1", "cat2", "cat3");
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("categories", toJson(createStringCategories("cat1", "cat3")));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), toJson(categories));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(1).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
assertThat(db.getCategoriesForManga(dbManga).executeAsBlocking())
|
||||
.hasSize(2)
|
||||
.contains(Category.create("cat1"), Category.create("cat3"))
|
||||
.doesNotContain(Category.create("cat2"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreMultipleCategoriesForExistingMangaAndCategory() {
|
||||
// Store a manga and a category
|
||||
Manga manga = createManga("title");
|
||||
manga.id = 1L;
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
|
||||
Category cat = createCategory("cat1");
|
||||
cat.id = 1;
|
||||
db.insertCategory(cat).executeAsBlocking();
|
||||
db.insertMangaCategory(MangaCategory.create(manga, cat)).executeAsBlocking();
|
||||
|
||||
// Create categories
|
||||
List<Category> categories = createCategories("cat1", "cat2", "cat3");
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("categories", toJson(createStringCategories("cat1", "cat2")));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), toJson(categories));
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(1).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
assertThat(db.getCategoriesForManga(dbManga).executeAsBlocking())
|
||||
.hasSize(2)
|
||||
.contains(Category.create("cat1"), Category.create("cat2"))
|
||||
.doesNotContain(Category.create("cat3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreSyncForManga() {
|
||||
// Create a manga and mangaSync
|
||||
Manga manga = createManga("title");
|
||||
manga.id = 1L;
|
||||
|
||||
List<MangaSync> mangaSync = createMangaSync(manga, 1, 2, 3);
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("sync", toJson(mangaSync));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(1).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
List<MangaSync> dbSync = db.getMangasSync(dbManga).executeAsBlocking();
|
||||
assertThat(dbSync).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreSyncForExistingManga() {
|
||||
long mangaId = 3;
|
||||
// Create a manga and 3 sync
|
||||
Manga manga = createManga("title");
|
||||
manga.id = mangaId;
|
||||
List<MangaSync> mangaSync = createMangaSync(manga, 1, 2, 3);
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("sync", toJson(mangaSync));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(mangaId).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
List<MangaSync> dbSync = db.getMangasSync(dbManga).executeAsBlocking();
|
||||
assertThat(dbSync).hasSize(3);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testRestoreExistingSyncForExistingManga() {
|
||||
long mangaId = 5;
|
||||
// Store a manga and 3 sync
|
||||
Manga manga = createManga("title");
|
||||
manga.id = mangaId;
|
||||
List<MangaSync> mangaSync = createMangaSync(manga, 1, 2, 3);
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
db.insertMangasSync(mangaSync).executeAsBlocking();
|
||||
|
||||
// The backup contains a existing sync and a new one, so it should have 4 sync
|
||||
mangaSync = createMangaSync(manga, 3, 4);
|
||||
|
||||
// Add an entry for the manga
|
||||
JsonObject entry = new JsonObject();
|
||||
entry.add("manga", toJson(manga));
|
||||
entry.add("sync", toJson(mangaSync));
|
||||
|
||||
// Append the entry to the backup list
|
||||
List<JsonElement> mangas = new ArrayList<>();
|
||||
mangas.add(entry);
|
||||
|
||||
// Restore from json
|
||||
root = createRootJson(toJson(mangas), null);
|
||||
backupManager.restoreFromJson(root);
|
||||
|
||||
Manga dbManga = db.getManga(mangaId).executeAsBlocking();
|
||||
assertThat(dbManga).isNotNull();
|
||||
|
||||
List<MangaSync> dbSync = db.getMangasSync(dbManga).executeAsBlocking();
|
||||
assertThat(dbSync).hasSize(4);
|
||||
}
|
||||
|
||||
private JsonObject createRootJson(JsonElement mangas, JsonElement categories) {
|
||||
JsonObject root = new JsonObject();
|
||||
if (mangas != null)
|
||||
root.add("mangas", mangas);
|
||||
if (categories != null)
|
||||
root.add("categories", categories);
|
||||
return root;
|
||||
}
|
||||
|
||||
private Category createCategory(String name) {
|
||||
Category c = new Category();
|
||||
c.name = name;
|
||||
return c;
|
||||
}
|
||||
|
||||
private List<Category> createCategories(String... names) {
|
||||
List<Category> cats = new ArrayList<>();
|
||||
for (String name : names) {
|
||||
cats.add(createCategory(name));
|
||||
}
|
||||
return cats;
|
||||
}
|
||||
|
||||
private List<String> createStringCategories(String... names) {
|
||||
List<String> cats = new ArrayList<>();
|
||||
for (String name : names) {
|
||||
cats.add(name);
|
||||
}
|
||||
return cats;
|
||||
}
|
||||
|
||||
private Manga createManga(String title) {
|
||||
Manga m = new Manga();
|
||||
m.title = title;
|
||||
m.author = "";
|
||||
m.artist = "";
|
||||
m.thumbnail_url = "";
|
||||
m.genre = "a list of genres";
|
||||
m.description = "long description";
|
||||
m.url = "url to manga";
|
||||
m.favorite = true;
|
||||
m.source = 1;
|
||||
return m;
|
||||
}
|
||||
|
||||
private List<Manga> createMangas(String... titles) {
|
||||
List<Manga> mangas = new ArrayList<>();
|
||||
for (String title : titles) {
|
||||
mangas.add(createManga(title));
|
||||
}
|
||||
return mangas;
|
||||
}
|
||||
|
||||
private Chapter createChapter(Manga manga, String url) {
|
||||
Chapter c = Chapter.create();
|
||||
c.url = url;
|
||||
c.name = url;
|
||||
c.manga_id = manga.id;
|
||||
return c;
|
||||
}
|
||||
|
||||
private List<Chapter> createChapters(Manga manga, String... urls) {
|
||||
List<Chapter> chapters = new ArrayList<>();
|
||||
for (String url : urls) {
|
||||
chapters.add(createChapter(manga, url));
|
||||
}
|
||||
return chapters;
|
||||
}
|
||||
|
||||
private MangaSync createMangaSync(Manga manga, int syncId) {
|
||||
MangaSync m = MangaSync.create();
|
||||
m.manga_id = manga.id;
|
||||
m.sync_id = syncId;
|
||||
m.title = "title";
|
||||
return m;
|
||||
}
|
||||
|
||||
private List<MangaSync> createMangaSync(Manga manga, Integer... syncIds) {
|
||||
List<MangaSync> ms = new ArrayList<>();
|
||||
for (int title : syncIds) {
|
||||
ms.add(createMangaSync(manga, title));
|
||||
}
|
||||
return ms;
|
||||
}
|
||||
|
||||
private JsonElement toJson(Object element) {
|
||||
return gson.toJsonTree(element);
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue