Rewrote Backup (#650)
* Rewrote Backup * Save automatic backups with datetime * Minor improvements * Remove suggested directories for backup and hardcoded strings. Rename JSON -> Backup * Bugfix * Fix tests * Run restore inside a transaction, use external cache dir for log and other minor changespull/724/head
parent
3094d084d6
commit
0642889b64
@ -0,0 +1,166 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.IntentService
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import com.github.salomonbrys.kotson.set
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
|
import timber.log.Timber
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [IntentService] used to backup [Manga] information to [JsonArray]
|
||||||
|
*/
|
||||||
|
class BackupCreateService : IntentService(NAME) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Name of class
|
||||||
|
private const val NAME = "BackupCreateService"
|
||||||
|
|
||||||
|
// Uri as string
|
||||||
|
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
|
// Backup called from job
|
||||||
|
private const val EXTRA_IS_JOB = "$ID.$NAME.EXTRA_IS_JOB"
|
||||||
|
// Options for backup
|
||||||
|
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
|
||||||
|
|
||||||
|
// Filter options
|
||||||
|
internal const val BACKUP_CATEGORY = 0x1
|
||||||
|
internal const val BACKUP_CATEGORY_MASK = 0x1
|
||||||
|
internal const val BACKUP_CHAPTER = 0x2
|
||||||
|
internal const val BACKUP_CHAPTER_MASK = 0x2
|
||||||
|
internal const val BACKUP_HISTORY = 0x4
|
||||||
|
internal const val BACKUP_HISTORY_MASK = 0x4
|
||||||
|
internal const val BACKUP_TRACK = 0x8
|
||||||
|
internal const val BACKUP_TRACK_MASK = 0x8
|
||||||
|
internal const val BACKUP_ALL = 0xF
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a backup from library
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param path path of Uri
|
||||||
|
* @param flags determines what to backup
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
fun makeBackup(context: Context, path: String, flags: Int, isJob: Boolean = false) {
|
||||||
|
val intent = Intent(context, BackupCreateService::class.java).apply {
|
||||||
|
putExtra(EXTRA_URI, path)
|
||||||
|
putExtra(EXTRA_IS_JOB, isJob)
|
||||||
|
putExtra(EXTRA_FLAGS, flags)
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val backupManager by lazy { BackupManager(this) }
|
||||||
|
|
||||||
|
override fun onHandleIntent(intent: Intent?) {
|
||||||
|
if (intent == null) return
|
||||||
|
|
||||||
|
// Get values
|
||||||
|
val uri = intent.getStringExtra(EXTRA_URI)
|
||||||
|
val isJob = intent.getBooleanExtra(EXTRA_IS_JOB, false)
|
||||||
|
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
|
||||||
|
// Create backup
|
||||||
|
createBackupFromApp(Uri.parse(uri), flags, isJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create backup Json file from database
|
||||||
|
*
|
||||||
|
* @param uri path of Uri
|
||||||
|
* @param isJob backup called from job
|
||||||
|
*/
|
||||||
|
fun createBackupFromApp(uri: Uri, flags: Int, isJob: Boolean) {
|
||||||
|
// Create root object
|
||||||
|
val root = JsonObject()
|
||||||
|
|
||||||
|
// Create information object
|
||||||
|
val information = JsonObject()
|
||||||
|
|
||||||
|
// Create manga array
|
||||||
|
val mangaEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create category array
|
||||||
|
val categoryEntries = JsonArray()
|
||||||
|
|
||||||
|
// Add value's to root
|
||||||
|
root[VERSION] = Backup.CURRENT_VERSION
|
||||||
|
root[MANGAS] = mangaEntries
|
||||||
|
root[CATEGORIES] = categoryEntries
|
||||||
|
|
||||||
|
backupManager.databaseHelper.inTransaction {
|
||||||
|
// Get manga from database
|
||||||
|
val mangas = backupManager.getFavoriteManga()
|
||||||
|
|
||||||
|
// Backup library manga and its dependencies
|
||||||
|
mangas.forEach { manga ->
|
||||||
|
mangaEntries.add(backupManager.backupMangaObject(manga, flags))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup categories
|
||||||
|
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
|
||||||
|
backupManager.backupCategories(categoryEntries)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// When BackupCreatorJob
|
||||||
|
if (isJob) {
|
||||||
|
// Get dir of file
|
||||||
|
val dir = UniFile.fromUri(this, uri)
|
||||||
|
|
||||||
|
// Delete older backups
|
||||||
|
val numberOfBackups = backupManager.numberOfBackups()
|
||||||
|
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
|
||||||
|
dir.listFiles { _, filename -> backupRegex.matches(filename) }
|
||||||
|
.orEmpty()
|
||||||
|
.sortedByDescending { it.name }
|
||||||
|
.drop(numberOfBackups - 1)
|
||||||
|
.forEach { it.delete() }
|
||||||
|
|
||||||
|
// Create new file to place backup
|
||||||
|
val newFile = dir.createFile(Backup.getDefaultFilename())
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
newFile.openOutputStream().bufferedWriter().use {
|
||||||
|
backupManager.parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val file = UniFile.fromUri(this, uri)
|
||||||
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
file.openOutputStream().bufferedWriter().use {
|
||||||
|
backupManager.parser.toJson(root, it)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show completed dialog
|
||||||
|
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_BACKUP_COMPLETED_DIALOG)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_URI, file.uri.toString())
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
if (!isJob) {
|
||||||
|
// Show error dialog
|
||||||
|
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_BACKUP_DIALOG)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, e.message)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,41 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import com.evernote.android.job.Job
|
||||||
|
import com.evernote.android.job.JobManager
|
||||||
|
import com.evernote.android.job.JobRequest
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class BackupCreatorJob : Job() {
|
||||||
|
|
||||||
|
override fun onRunJob(params: Params): Result {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val path = preferences.backupsDirectory().getOrDefault()
|
||||||
|
val flags = BackupCreateService.BACKUP_ALL
|
||||||
|
BackupCreateService.makeBackup(context,path,flags,true)
|
||||||
|
return Result.SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "BackupCreator"
|
||||||
|
|
||||||
|
fun setupTask(prefInterval: Int? = null) {
|
||||||
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
|
val interval = prefInterval ?: preferences.backupInterval().getOrDefault()
|
||||||
|
if (interval > 0) {
|
||||||
|
JobRequest.Builder(TAG)
|
||||||
|
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
|
||||||
|
.setPersisted(true)
|
||||||
|
.setUpdateCurrent(true)
|
||||||
|
.build()
|
||||||
|
.schedule()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun cancelTask() {
|
||||||
|
JobManager.instance().cancelAllForTag(TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,413 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.PowerManager
|
||||||
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
|
import com.google.gson.JsonArray
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import com.google.gson.stream.JsonReader
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.ui.setting.SettingsBackupFragment
|
||||||
|
import eu.kanade.tachiyomi.util.AndroidComponentUtil
|
||||||
|
import eu.kanade.tachiyomi.util.chop
|
||||||
|
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||||
|
import rx.Observable
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restores backup from json file
|
||||||
|
*/
|
||||||
|
class BackupRestoreService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
// Name of service
|
||||||
|
private const val NAME = "BackupRestoreService"
|
||||||
|
|
||||||
|
// Uri as string
|
||||||
|
private const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the status of the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
* @return true if the service is running, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isRunning(context: Context): Boolean {
|
||||||
|
return AndroidComponentUtil.isServiceRunning(context, BackupRestoreService::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts a service to restore a backup from Json
|
||||||
|
*
|
||||||
|
* @param context context of application
|
||||||
|
* @param uri path of Uri
|
||||||
|
*/
|
||||||
|
fun start(context: Context, uri: String) {
|
||||||
|
if (!isRunning(context)) {
|
||||||
|
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||||
|
putExtra(EXTRA_URI, uri)
|
||||||
|
}
|
||||||
|
context.startService(intent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stops the service.
|
||||||
|
*
|
||||||
|
* @param context the application context.
|
||||||
|
*/
|
||||||
|
fun stop(context: Context) {
|
||||||
|
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wake lock that will be held until the service is destroyed.
|
||||||
|
*/
|
||||||
|
private lateinit var wakeLock: PowerManager.WakeLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription where the update is done.
|
||||||
|
*/
|
||||||
|
private var subscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The progress of a backup restore
|
||||||
|
*/
|
||||||
|
private var restoreProgress = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Amount of manga in Json file (needed for restore)
|
||||||
|
*/
|
||||||
|
private var restoreAmount = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List containing errors
|
||||||
|
*/
|
||||||
|
private val errors = mutableListOf<Pair<Date, String>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup manager
|
||||||
|
*/
|
||||||
|
private lateinit var backupManager: BackupManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database
|
||||||
|
*/
|
||||||
|
private val db: DatabaseHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||||
|
*/
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||||
|
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
||||||
|
wakeLock.acquire()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service is destroyed. It destroys the running subscription and
|
||||||
|
* releases the wake lock.
|
||||||
|
*/
|
||||||
|
override fun onDestroy() {
|
||||||
|
subscription?.unsubscribe()
|
||||||
|
if (wakeLock.isHeld) {
|
||||||
|
wakeLock.release()
|
||||||
|
}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This method needs to be implemented, but it's not used/needed.
|
||||||
|
*/
|
||||||
|
override fun onBind(intent: Intent): IBinder? {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called when the service receives an intent.
|
||||||
|
*
|
||||||
|
* @param intent the start intent from.
|
||||||
|
* @param flags the flags of the command.
|
||||||
|
* @param startId the start id of this command.
|
||||||
|
* @return the start value of the command.
|
||||||
|
*/
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
if (intent == null) return Service.START_NOT_STICKY
|
||||||
|
|
||||||
|
// Unsubscribe from any previous subscription if needed.
|
||||||
|
subscription?.unsubscribe()
|
||||||
|
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
subscription = Observable.defer {
|
||||||
|
// Get URI
|
||||||
|
val uri = Uri.parse(intent.getStringExtra(EXTRA_URI))
|
||||||
|
// Get file from Uri
|
||||||
|
val file = UniFile.fromUri(this, uri)
|
||||||
|
|
||||||
|
// Clear errors
|
||||||
|
errors.clear()
|
||||||
|
|
||||||
|
// Reset progress
|
||||||
|
restoreProgress = 0
|
||||||
|
|
||||||
|
db.lowLevel().beginTransaction()
|
||||||
|
getRestoreObservable(file)
|
||||||
|
}
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.subscribe({
|
||||||
|
}, { error ->
|
||||||
|
db.lowLevel().endTransaction()
|
||||||
|
Timber.e(error)
|
||||||
|
writeErrorLog()
|
||||||
|
val errorIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_ERROR_RESTORE_DIALOG)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERROR_MESSAGE, error.message)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(errorIntent)
|
||||||
|
stopSelf(startId)
|
||||||
|
}, {
|
||||||
|
db.lowLevel().setTransactionSuccessful()
|
||||||
|
db.lowLevel().endTransaction()
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
val time = endTime - startTime
|
||||||
|
val file = writeErrorLog()
|
||||||
|
val completeIntent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_TIME, time)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors.size)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE_PATH, file.parent)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERROR_FILE, file.name)
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(completeIntent)
|
||||||
|
stopSelf(startId)
|
||||||
|
})
|
||||||
|
return Service.START_NOT_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an [Observable] containing restore process.
|
||||||
|
*
|
||||||
|
* @param file restore file
|
||||||
|
* @return [Observable<Manga>]
|
||||||
|
*/
|
||||||
|
private fun getRestoreObservable(file: UniFile): Observable<Manga> {
|
||||||
|
val reader = JsonReader(file.openInputStream().bufferedReader())
|
||||||
|
val json = JsonParser().parse(reader).asJsonObject
|
||||||
|
|
||||||
|
// Get parser version
|
||||||
|
val version = json.get(VERSION)?.asInt ?: 1
|
||||||
|
|
||||||
|
// Initialize manager
|
||||||
|
backupManager = BackupManager(this, version)
|
||||||
|
|
||||||
|
val mangasJson = json.get(MANGAS).asJsonArray
|
||||||
|
|
||||||
|
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||||
|
|
||||||
|
// Restore categories
|
||||||
|
json.get(CATEGORIES)?.let {
|
||||||
|
backupManager.restoreCategories(it.asJsonArray)
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Observable.from(mangasJson)
|
||||||
|
.concatMap {
|
||||||
|
val obj = it.asJsonObject
|
||||||
|
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
|
||||||
|
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
|
||||||
|
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
|
||||||
|
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
|
||||||
|
|
||||||
|
val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks)
|
||||||
|
if (observable != null) {
|
||||||
|
observable
|
||||||
|
} else {
|
||||||
|
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
|
||||||
|
restoreProgress += 1
|
||||||
|
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write errors to error log
|
||||||
|
*/
|
||||||
|
private fun writeErrorLog(): File {
|
||||||
|
try {
|
||||||
|
if (errors.isNotEmpty()) {
|
||||||
|
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
|
||||||
|
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
|
||||||
|
|
||||||
|
destFile.bufferedWriter().use { out ->
|
||||||
|
errors.forEach { (date, message) ->
|
||||||
|
out.write("[${sdf.format(date)}] $message\n")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return destFile
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// Empty
|
||||||
|
}
|
||||||
|
return File("")
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga restore observable
|
||||||
|
*
|
||||||
|
* @param manga manga data from json
|
||||||
|
* @param chapters chapters data from json
|
||||||
|
* @param categories categories data from json
|
||||||
|
* @param history history data from json
|
||||||
|
* @param tracks tracking data from json
|
||||||
|
* @return [Observable] containing manga restore information
|
||||||
|
*/
|
||||||
|
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga>? {
|
||||||
|
// Get source
|
||||||
|
val source = backupManager.sourceManager.get(manga.source) ?: return null
|
||||||
|
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||||
|
|
||||||
|
if (dbManga == null) {
|
||||||
|
// Manga not in database
|
||||||
|
return mangaFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||||
|
} else { // Manga in database
|
||||||
|
// Copy information from manga already in database
|
||||||
|
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||||
|
// Fetch rest of manga information
|
||||||
|
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches manga information
|
||||||
|
*
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @param chapters chapters of manga that needs updating
|
||||||
|
* @param categories categories that need updating
|
||||||
|
*/
|
||||||
|
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga> {
|
||||||
|
return backupManager.restoreMangaFetchObservable(source, manga)
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
.filter { it.id != null }
|
||||||
|
.flatMap { manga ->
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
// Convert to the manga that contains new chapters.
|
||||||
|
.map { manga }
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(it, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(it, tracks)
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
|
||||||
|
categories: List<String>, history: List<DHistory>,
|
||||||
|
tracks: List<Track>): Observable<Manga> {
|
||||||
|
|
||||||
|
return Observable.just(backupManga)
|
||||||
|
.flatMap { manga ->
|
||||||
|
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||||
|
chapterFetchObservable(source, manga, chapters)
|
||||||
|
.map { manga }
|
||||||
|
} else {
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.doOnNext {
|
||||||
|
// Restore categories
|
||||||
|
backupManager.restoreCategoriesForManga(it, categories)
|
||||||
|
|
||||||
|
// Restore history
|
||||||
|
backupManager.restoreHistoryForManga(history)
|
||||||
|
|
||||||
|
// Restore tracking
|
||||||
|
backupManager.restoreTrackForManga(it, tracks)
|
||||||
|
}
|
||||||
|
.doOnCompleted {
|
||||||
|
restoreProgress += 1
|
||||||
|
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [Observable] that fetches chapter information
|
||||||
|
*
|
||||||
|
* @param source source of manga
|
||||||
|
* @param manga manga that needs updating
|
||||||
|
* @return [Observable] that contains manga
|
||||||
|
*/
|
||||||
|
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||||
|
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
|
||||||
|
// If there's any error, return empty update and continue.
|
||||||
|
.onErrorReturn {
|
||||||
|
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||||
|
Pair(emptyList<Chapter>(), emptyList<Chapter>())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called to update dialog in [SettingsBackupFragment]
|
||||||
|
*
|
||||||
|
* @param progress restore progress
|
||||||
|
* @param amount total restoreAmount of manga
|
||||||
|
* @param title title of restored manga
|
||||||
|
*/
|
||||||
|
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
|
||||||
|
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
|
||||||
|
val intent = Intent(SettingsBackupFragment.INTENT_FILTER).apply {
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_PROGRESS, progress)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_AMOUNT, amount)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_CONTENT, content)
|
||||||
|
putExtra(SettingsBackupFragment.EXTRA_ERRORS, errors)
|
||||||
|
putExtra(SettingsBackupFragment.ACTION, SettingsBackupFragment.ACTION_SET_PROGRESS_DIALOG)
|
||||||
|
}
|
||||||
|
sendLocalBroadcast(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
import java.text.SimpleDateFormat
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Json values
|
||||||
|
*/
|
||||||
|
object Backup {
|
||||||
|
const val CURRENT_VERSION = 2
|
||||||
|
const val MANGA = "manga"
|
||||||
|
const val MANGAS = "mangas"
|
||||||
|
const val TRACK = "track"
|
||||||
|
const val CHAPTERS = "chapters"
|
||||||
|
const val CATEGORIES = "categories"
|
||||||
|
const val HISTORY = "history"
|
||||||
|
const val VERSION = "version"
|
||||||
|
|
||||||
|
fun getDefaultFilename(): String {
|
||||||
|
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
|
||||||
|
return "tachiyomi_$date.json"
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.models
|
||||||
|
|
||||||
|
data class DHistory(val url: String,val lastRead: Long)
|
@ -1,16 +0,0 @@
|
|||||||
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 BooleanSerializer : JsonSerializer<Boolean> {
|
|
||||||
|
|
||||||
override fun serialize(value: Boolean?, type: Type, context: JsonSerializationContext): JsonElement? {
|
|
||||||
if (value != null && value != false)
|
|
||||||
return JsonPrimitive(value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,31 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [CategoryImpl] to / from json
|
||||||
|
*/
|
||||||
|
object CategoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<CategoryImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.name)
|
||||||
|
value(it.order)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val category = CategoryImpl()
|
||||||
|
category.name = nextString()
|
||||||
|
category.order = nextInt()
|
||||||
|
endArray()
|
||||||
|
category
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,61 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [ChapterImpl] to / from json
|
||||||
|
*/
|
||||||
|
object ChapterTypeAdapter {
|
||||||
|
|
||||||
|
private const val URL = "u"
|
||||||
|
private const val READ = "r"
|
||||||
|
private const val BOOKMARK = "b"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<ChapterImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.read || it.bookmark || it.last_page_read != 0) {
|
||||||
|
beginObject()
|
||||||
|
name(URL)
|
||||||
|
value(it.url)
|
||||||
|
if (it.read) {
|
||||||
|
name(READ)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.bookmark) {
|
||||||
|
name(BOOKMARK)
|
||||||
|
value(1)
|
||||||
|
}
|
||||||
|
if (it.last_page_read != 0) {
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_page_read)
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val chapter = ChapterImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
val name = nextName()
|
||||||
|
|
||||||
|
when (name) {
|
||||||
|
URL -> chapter.url = nextString()
|
||||||
|
READ -> chapter.read = nextInt() == 1
|
||||||
|
BOOKMARK -> chapter.bookmark = nextInt() == 1
|
||||||
|
LAST_READ -> chapter.last_page_read = nextInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
chapter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,32 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [DHistory] to / from json
|
||||||
|
*/
|
||||||
|
object HistoryTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<DHistory> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
if (it.lastRead != 0L) {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.lastRead)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val url = nextString()
|
||||||
|
val lastRead = nextLong()
|
||||||
|
endArray()
|
||||||
|
DHistory(url, lastRead)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup.serializer
|
|
||||||
|
|
||||||
import com.google.gson.ExclusionStrategy
|
|
||||||
import com.google.gson.FieldAttributes
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
|
||||||
|
|
||||||
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) {
|
|
||||||
MangaImpl::class.java -> mangaExclusions.contains(f.name)
|
|
||||||
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
|
|
||||||
TrackImpl::class.java -> syncExclusions.contains(f.name)
|
|
||||||
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
|
|
||||||
else -> false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun shouldSkipClass(clazz: Class<*>) = false
|
|
||||||
|
|
||||||
}
|
|
@ -1,17 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,16 +0,0 @@
|
|||||||
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 LongSerializer : JsonSerializer<Long> {
|
|
||||||
|
|
||||||
override fun serialize(value: Long?, type: Type, context: JsonSerializationContext): JsonElement? {
|
|
||||||
if (value != null && value !== 0L)
|
|
||||||
return JsonPrimitive(value)
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,37 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [MangaImpl] to / from json
|
||||||
|
*/
|
||||||
|
object MangaTypeAdapter {
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<MangaImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginArray()
|
||||||
|
value(it.url)
|
||||||
|
value(it.title)
|
||||||
|
value(it.source)
|
||||||
|
value(it.viewer)
|
||||||
|
value(it.chapter_flags)
|
||||||
|
endArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
beginArray()
|
||||||
|
val manga = MangaImpl()
|
||||||
|
manga.url = nextString()
|
||||||
|
manga.title = nextString()
|
||||||
|
manga.source = nextLong()
|
||||||
|
manga.viewer = nextInt()
|
||||||
|
manga.chapter_flags = nextInt()
|
||||||
|
endArray()
|
||||||
|
manga
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,53 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.backup.serializer
|
||||||
|
|
||||||
|
import com.github.salomonbrys.kotson.typeAdapter
|
||||||
|
import com.google.gson.TypeAdapter
|
||||||
|
import com.google.gson.stream.JsonToken
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.TrackImpl
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON Serializer used to write / read [TrackImpl] to / from json
|
||||||
|
*/
|
||||||
|
object TrackTypeAdapter {
|
||||||
|
|
||||||
|
private const val SYNC = "s"
|
||||||
|
private const val REMOTE = "r"
|
||||||
|
private const val TITLE = "t"
|
||||||
|
private const val LAST_READ = "l"
|
||||||
|
|
||||||
|
fun build(): TypeAdapter<TrackImpl> {
|
||||||
|
return typeAdapter {
|
||||||
|
write {
|
||||||
|
beginObject()
|
||||||
|
name(TITLE)
|
||||||
|
value(it.title)
|
||||||
|
name(SYNC)
|
||||||
|
value(it.sync_id)
|
||||||
|
name(REMOTE)
|
||||||
|
value(it.remote_id)
|
||||||
|
name(LAST_READ)
|
||||||
|
value(it.last_chapter_read)
|
||||||
|
endObject()
|
||||||
|
}
|
||||||
|
|
||||||
|
read {
|
||||||
|
val track = TrackImpl()
|
||||||
|
beginObject()
|
||||||
|
while (hasNext()) {
|
||||||
|
if (peek() == JsonToken.NAME) {
|
||||||
|
val name = nextName()
|
||||||
|
|
||||||
|
when (name) {
|
||||||
|
TITLE -> track.title = nextString()
|
||||||
|
SYNC -> track.sync_id = nextInt()
|
||||||
|
REMOTE -> track.remote_id = nextInt()
|
||||||
|
LAST_READ -> track.last_chapter_read = nextInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
endObject()
|
||||||
|
track
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import com.pushtorefresh.storio.sqlite.StorIOSQLite
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
|
||||||
|
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
|
||||||
|
import eu.kanade.tachiyomi.data.database.inTransactionReturn
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
||||||
|
|
||||||
|
class ChapterBackupPutResolver : PutResolver<Chapter>() {
|
||||||
|
|
||||||
|
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
|
||||||
|
val updateQuery = mapToUpdateQuery(chapter)
|
||||||
|
val contentValues = mapToContentValues(chapter)
|
||||||
|
|
||||||
|
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
|
||||||
|
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
|
||||||
|
.table(ChapterTable.TABLE)
|
||||||
|
.where("${ChapterTable.COL_URL} = ?")
|
||||||
|
.whereArgs(chapter.url)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
|
||||||
|
put(ChapterTable.COL_READ, chapter.read)
|
||||||
|
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
|
||||||
|
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,163 +0,0 @@
|
|||||||
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.activity.ActivityMixin
|
|
||||||
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 rx.Observable
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.internal.util.SubscriptionList
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import timber.log.Timber
|
|
||||||
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
|
|
||||||
|
|
||||||
private lateinit var subscriptions: SubscriptionList
|
|
||||||
|
|
||||||
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?) {
|
|
||||||
setToolbarTitle(getString(R.string.label_backup))
|
|
||||||
|
|
||||||
(activity as ActivityMixin).requestPermissionsOnMarshmallow()
|
|
||||||
subscriptions = SubscriptionList()
|
|
||||||
|
|
||||||
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/*"
|
|
||||||
val chooser = Intent.createChooser(intent, getString(R.string.file_select_backup))
|
|
||||||
startActivityForResult(chooser, REQUEST_BACKUP_OPEN)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
subscriptions.unsubscribe()
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Called from the presenter when the backup is completed.
|
|
||||||
*
|
|
||||||
* @param file the file where the backup is saved.
|
|
||||||
*/
|
|
||||||
fun onBackupCompleted(file: File) {
|
|
||||||
dismissBackupDialog()
|
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
|
||||||
intent.type = "application/json"
|
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, Uri.parse("file://" + file))
|
|
||||||
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()
|
|
||||||
|
|
||||||
// When using cloud services, we have to open the input stream in a background thread.
|
|
||||||
Observable.fromCallable { context.contentResolver.openInputStream(data.data) }
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe({
|
|
||||||
presenter.restoreBackup(it)
|
|
||||||
}, { error ->
|
|
||||||
context.toast(error.message)
|
|
||||||
Timber.e(error)
|
|
||||||
})
|
|
||||||
.apply { subscriptions.add(this) }
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,94 +0,0 @@
|
|||||||
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 eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
|
||||||
import rx.Observable
|
|
||||||
import rx.Subscription
|
|
||||||
import rx.android.schedulers.AndroidSchedulers
|
|
||||||
import rx.schedulers.Schedulers
|
|
||||||
import uy.kohesive.injekt.injectLazy
|
|
||||||
import java.io.File
|
|
||||||
import java.io.InputStream
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Presenter of [BackupFragment].
|
|
||||||
*/
|
|
||||||
class BackupPresenter : BasePresenter<BackupFragment>() {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Database.
|
|
||||||
*/
|
|
||||||
val db: DatabaseHelper by injectLazy()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Backup manager.
|
|
||||||
*/
|
|
||||||
private lateinit var backupManager: BackupManager
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription where the backup is restored.
|
|
||||||
*/
|
|
||||||
private var restoreSubscription: Subscription? = null
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Subscription where the backup is created.
|
|
||||||
*/
|
|
||||||
private var backupSubscription: Subscription? = null
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
backupManager = BackupManager(db)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a backup and saves it to a file.
|
|
||||||
*
|
|
||||||
* @param file the path where the file will be saved.
|
|
||||||
*/
|
|
||||||
fun createBackup(file: File) {
|
|
||||||
if (backupSubscription.isNullOrUnsubscribed()) {
|
|
||||||
backupSubscription = getBackupObservable(file)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst(
|
|
||||||
{ view, result -> view.onBackupCompleted(file) },
|
|
||||||
BackupFragment::onBackupError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Restores a backup from a stream.
|
|
||||||
*
|
|
||||||
* @param stream the input stream of the backup file.
|
|
||||||
*/
|
|
||||||
fun restoreBackup(stream: InputStream) {
|
|
||||||
if (restoreSubscription.isNullOrUnsubscribed()) {
|
|
||||||
restoreSubscription = getRestoreObservable(stream)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribeFirst(
|
|
||||||
{ view, result -> view.onRestoreCompleted() },
|
|
||||||
BackupFragment::onRestoreError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the observable to save a backup.
|
|
||||||
*/
|
|
||||||
private fun getBackupObservable(file: File) = Observable.fromCallable {
|
|
||||||
backupManager.backupToFile(file)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the observable to restore a backup.
|
|
||||||
*/
|
|
||||||
private fun getRestoreObservable(stream: InputStream) = Observable.fromCallable {
|
|
||||||
backupManager.restoreFromStream(stream)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -0,0 +1,413 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.setting
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.BroadcastReceiver
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentFilter
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v7.preference.XpPreferenceFragment
|
||||||
|
import android.view.View
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreateService
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
|
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||||
|
import eu.kanade.tachiyomi.util.*
|
||||||
|
import eu.kanade.tachiyomi.widget.CustomLayoutPickerActivity
|
||||||
|
import eu.kanade.tachiyomi.widget.preference.IntListPreference
|
||||||
|
import net.xpece.android.support.preference.Preference
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
import java.io.File
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings for [BackupCreateService] and [BackupRestoreService]
|
||||||
|
*/
|
||||||
|
class SettingsBackupFragment : SettingsFragment() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val INTENT_FILTER = "SettingsBackupFragment"
|
||||||
|
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
|
||||||
|
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
|
||||||
|
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
|
||||||
|
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
|
||||||
|
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
|
||||||
|
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
|
||||||
|
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
|
||||||
|
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
|
||||||
|
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
|
||||||
|
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
|
||||||
|
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
|
||||||
|
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
|
||||||
|
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
|
||||||
|
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
|
||||||
|
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
|
||||||
|
|
||||||
|
private const val BACKUP_CREATE = 201
|
||||||
|
private const val BACKUP_RESTORE = 202
|
||||||
|
private const val BACKUP_DIR = 203
|
||||||
|
|
||||||
|
fun newInstance(rootKey: String): SettingsBackupFragment {
|
||||||
|
val args = Bundle()
|
||||||
|
args.putString(XpPreferenceFragment.ARG_PREFERENCE_ROOT, rootKey)
|
||||||
|
return SettingsBackupFragment().apply { arguments = args }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference selected to create backup
|
||||||
|
*/
|
||||||
|
private val createBackup: Preference by bindPref(R.string.pref_create_local_backup_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference selected to restore backup
|
||||||
|
*/
|
||||||
|
private val restoreBackup: Preference by bindPref(R.string.pref_restore_local_backup_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference which determines the frequency of automatic backups.
|
||||||
|
*/
|
||||||
|
private val automaticBackup: IntListPreference by bindPref(R.string.pref_backup_interval_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference containing number of automatic backups
|
||||||
|
*/
|
||||||
|
private val backupSlots: IntListPreference by bindPref(R.string.pref_backup_slots_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preference containing interval of automatic backups
|
||||||
|
*/
|
||||||
|
private val backupDirPref: Preference by bindPref(R.string.pref_backup_directory_key)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences
|
||||||
|
*/
|
||||||
|
private val preferences: PreferencesHelper by injectLazy()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Value containing information on what to backup
|
||||||
|
*/
|
||||||
|
private var backup_flags = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The root directory for backups..
|
||||||
|
*/
|
||||||
|
private var backupDir = preferences.backupsDirectory().getOrDefault().let {
|
||||||
|
UniFile.fromUri(context, Uri.parse(it))
|
||||||
|
}
|
||||||
|
|
||||||
|
val restoreDialog: MaterialDialog by lazy {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.backup)
|
||||||
|
.content(R.string.restoring_backup)
|
||||||
|
.progress(false, 100, true)
|
||||||
|
.cancelable(false)
|
||||||
|
.negativeText(R.string.action_stop)
|
||||||
|
.onNegative { materialDialog, _ ->
|
||||||
|
BackupRestoreService.stop(context)
|
||||||
|
materialDialog.dismiss()
|
||||||
|
}
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
val backupDialog: MaterialDialog by lazy {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.backup)
|
||||||
|
.content(R.string.creating_backup)
|
||||||
|
.progress(true, 0)
|
||||||
|
.cancelable(false)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val receiver = object : BroadcastReceiver() {
|
||||||
|
|
||||||
|
override fun onReceive(context: Context, intent: Intent) {
|
||||||
|
when (intent.getStringExtra(ACTION)) {
|
||||||
|
ACTION_BACKUP_COMPLETED_DIALOG -> {
|
||||||
|
backupDialog.dismiss()
|
||||||
|
val uri = Uri.parse(intent.getStringExtra(EXTRA_URI))
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
MaterialDialog.Builder(this@SettingsBackupFragment.context)
|
||||||
|
.title(getString(R.string.backup_created))
|
||||||
|
.content(getString(R.string.file_saved, file.filePath))
|
||||||
|
.positiveText(getString(R.string.action_close))
|
||||||
|
.negativeText(getString(R.string.action_export))
|
||||||
|
.onPositive { materialDialog, _ -> materialDialog.dismiss() }
|
||||||
|
.onNegative { _, _ ->
|
||||||
|
val sendIntent = Intent(Intent.ACTION_SEND)
|
||||||
|
sendIntent.type = "application/json"
|
||||||
|
sendIntent.putExtra(Intent.EXTRA_STREAM, file.uri)
|
||||||
|
startActivity(Intent.createChooser(sendIntent, ""))
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
|
||||||
|
}
|
||||||
|
ACTION_SET_PROGRESS_DIALOG -> {
|
||||||
|
val progress = intent.getIntExtra(EXTRA_PROGRESS, 0)
|
||||||
|
val amount = intent.getIntExtra(EXTRA_AMOUNT, 0)
|
||||||
|
val content = intent.getStringExtra(EXTRA_CONTENT)
|
||||||
|
restoreDialog.setContent(content)
|
||||||
|
restoreDialog.setProgress(progress)
|
||||||
|
restoreDialog.maxProgress = amount
|
||||||
|
}
|
||||||
|
ACTION_RESTORE_COMPLETED_DIALOG -> {
|
||||||
|
restoreDialog.dismiss()
|
||||||
|
val time = intent.getLongExtra(EXTRA_TIME, 0)
|
||||||
|
val errors = intent.getIntExtra(EXTRA_ERRORS, 0)
|
||||||
|
val path = intent.getStringExtra(EXTRA_ERROR_FILE_PATH)
|
||||||
|
val file = intent.getStringExtra(EXTRA_ERROR_FILE)
|
||||||
|
val timeString = String.format("%02d min, %02d sec",
|
||||||
|
TimeUnit.MILLISECONDS.toMinutes(time),
|
||||||
|
TimeUnit.MILLISECONDS.toSeconds(time) -
|
||||||
|
TimeUnit.MINUTES.toSeconds(TimeUnit.MILLISECONDS.toMinutes(time))
|
||||||
|
)
|
||||||
|
|
||||||
|
if (errors > 0) {
|
||||||
|
MaterialDialog.Builder(this@SettingsBackupFragment.context)
|
||||||
|
.title(getString(R.string.restore_completed))
|
||||||
|
.content(getString(R.string.restore_completed_content, timeString,
|
||||||
|
if (errors > 0) "$errors" else getString(android.R.string.no)))
|
||||||
|
.positiveText(getString(R.string.action_close))
|
||||||
|
.negativeText(getString(R.string.action_open_log))
|
||||||
|
.onPositive { materialDialog, _ -> materialDialog.dismiss() }
|
||||||
|
.onNegative { materialDialog, _ ->
|
||||||
|
if (!path.isEmpty()) {
|
||||||
|
val destFile = File(path, file)
|
||||||
|
val uri = destFile.getUriCompat(context)
|
||||||
|
val sendIntent = Intent(Intent.ACTION_VIEW).apply {
|
||||||
|
setDataAndType(uri, "text/plain")
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||||
|
}
|
||||||
|
startActivity(sendIntent)
|
||||||
|
} else {
|
||||||
|
context.toast(getString(R.string.error_opening_log))
|
||||||
|
}
|
||||||
|
materialDialog.dismiss()
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ACTION_ERROR_BACKUP_DIALOG -> {
|
||||||
|
context.toast(intent.getStringExtra(EXTRA_ERROR_MESSAGE))
|
||||||
|
backupDialog.dismiss()
|
||||||
|
}
|
||||||
|
ACTION_ERROR_RESTORE_DIALOG -> {
|
||||||
|
context.toast(intent.getStringExtra(EXTRA_ERROR_MESSAGE))
|
||||||
|
restoreDialog.dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onPause() {
|
||||||
|
context.unregisterLocalReceiver(receiver)
|
||||||
|
super.onPause()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
context.registerLocalReceiver(receiver, IntentFilter(INTENT_FILTER))
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
|
super.onViewCreated(view, savedState)
|
||||||
|
|
||||||
|
(activity as BaseActivity).requestPermissionsOnMarshmallow()
|
||||||
|
|
||||||
|
// Set onClickListeners
|
||||||
|
createBackup.setOnPreferenceClickListener {
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(R.string.pref_create_backup)
|
||||||
|
.content(R.string.backup_choice)
|
||||||
|
.items(R.array.backup_options)
|
||||||
|
.itemsCallbackMultiChoice(arrayOf(0, 1, 2, 3, 4 /*todo not hard code*/)) { _, positions, _ ->
|
||||||
|
// TODO not very happy with global value, but putExtra doesn't work
|
||||||
|
backup_flags = 0
|
||||||
|
for (i in 1..positions.size - 1) {
|
||||||
|
when (positions[i]) {
|
||||||
|
1 -> backup_flags = backup_flags or BackupCreateService.BACKUP_CATEGORY
|
||||||
|
2 -> backup_flags = backup_flags or BackupCreateService.BACKUP_CHAPTER
|
||||||
|
3 -> backup_flags = backup_flags or BackupCreateService.BACKUP_TRACK
|
||||||
|
4 -> backup_flags = backup_flags or BackupCreateService.BACKUP_HISTORY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If API lower as KitKat use custom dir picker
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
// Get dirs
|
||||||
|
val currentDir = preferences.backupsDirectory().getOrDefault()
|
||||||
|
|
||||||
|
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
||||||
|
startActivityForResult(i, BACKUP_CREATE)
|
||||||
|
} else {
|
||||||
|
// Use Androids build in file creator
|
||||||
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
|
||||||
|
// TODO create custom MIME data type? Will make older backups deprecated
|
||||||
|
intent.type = "application/*"
|
||||||
|
intent.putExtra(Intent.EXTRA_TITLE, Backup.getDefaultFilename())
|
||||||
|
startActivityForResult(intent, BACKUP_CREATE)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
.itemsDisabledIndices(0)
|
||||||
|
.positiveText(getString(R.string.action_create))
|
||||||
|
.negativeText(android.R.string.cancel)
|
||||||
|
.show()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
restoreBackup.setOnPreferenceClickListener {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
val intent = Intent()
|
||||||
|
intent.type = "application/*"
|
||||||
|
intent.action = Intent.ACTION_GET_CONTENT
|
||||||
|
startActivityForResult(Intent.createChooser(intent, getString(R.string.file_select_backup)), BACKUP_RESTORE)
|
||||||
|
} else {
|
||||||
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
|
intent.type = "application/*"
|
||||||
|
startActivityForResult(intent, BACKUP_RESTORE)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
automaticBackup.setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
// Always cancel the previous task, it seems that sometimes they are not updated.
|
||||||
|
BackupCreatorJob.cancelTask()
|
||||||
|
|
||||||
|
val interval = (newValue as String).toInt()
|
||||||
|
if (interval > 0) {
|
||||||
|
BackupCreatorJob.setupTask(interval)
|
||||||
|
}
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
backupSlots.setOnPreferenceChangeListener { preference, newValue ->
|
||||||
|
preferences.numberOfBackups().set((newValue as String).toInt())
|
||||||
|
preference.summary = newValue
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
backupDirPref.setOnPreferenceClickListener {
|
||||||
|
val currentDir = preferences.backupsDirectory().getOrDefault()
|
||||||
|
|
||||||
|
if (Build.VERSION.SDK_INT < 21) {
|
||||||
|
// Custom dir selected, open directory selector
|
||||||
|
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
||||||
|
i.putExtra(FilePickerActivity.EXTRA_START_PATH, currentDir)
|
||||||
|
|
||||||
|
startActivityForResult(i, BACKUP_DIR)
|
||||||
|
} else {
|
||||||
|
val i = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
|
startActivityForResult(i, BACKUP_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions += preferences.backupsDirectory().asObservable()
|
||||||
|
.subscribe { path ->
|
||||||
|
backupDir = UniFile.fromUri(context, Uri.parse(path))
|
||||||
|
backupDirPref.summary = backupDir.filePath ?: path
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriptions += preferences.backupInterval().asObservable()
|
||||||
|
.subscribe {
|
||||||
|
backupDirPref.isVisible = it > 0
|
||||||
|
backupSlots.isVisible = it > 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
|
when (requestCode) {
|
||||||
|
BACKUP_DIR -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||||
|
val uri = Uri.fromFile(File(data.data.path))
|
||||||
|
preferences.backupsDirectory().set(uri.toString())
|
||||||
|
} else {
|
||||||
|
val uri = data.data
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
preferences.backupsDirectory().set(file.uri.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BACKUP_CREATE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
val dir = data.data.path
|
||||||
|
val file = File(dir, Backup.getDefaultFilename())
|
||||||
|
|
||||||
|
backupDialog.show()
|
||||||
|
BackupCreateService.makeBackup(context, file.toURI().toString(), backup_flags)
|
||||||
|
} else {
|
||||||
|
val uri = data.data
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
|
||||||
|
backupDialog.show()
|
||||||
|
BackupCreateService.makeBackup(context, file.uri.toString(), backup_flags)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
BACKUP_RESTORE -> if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) {
|
||||||
|
val uri = Uri.fromFile(File(data.data.path))
|
||||||
|
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(getString(R.string.pref_restore_backup))
|
||||||
|
.content(getString(R.string.backup_restore_content))
|
||||||
|
.positiveText(getString(R.string.action_restore))
|
||||||
|
.onPositive { materialDialog, _ ->
|
||||||
|
materialDialog.dismiss()
|
||||||
|
restoreDialog.show()
|
||||||
|
BackupRestoreService.start(context, uri.toString())
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
} else {
|
||||||
|
val uri = data.data
|
||||||
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
|
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
|
val file = UniFile.fromUri(context, uri)
|
||||||
|
|
||||||
|
MaterialDialog.Builder(context)
|
||||||
|
.title(getString(R.string.pref_restore_backup))
|
||||||
|
.content(getString(R.string.backup_restore_content))
|
||||||
|
.positiveText(getString(R.string.action_restore))
|
||||||
|
.onPositive { materialDialog, _ ->
|
||||||
|
materialDialog.dismiss()
|
||||||
|
restoreDialog.show()
|
||||||
|
BackupRestoreService.start(context, file.uri.toString())
|
||||||
|
}
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,33 @@
|
|||||||
|
package eu.kanade.tachiyomi.widget
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
|
import com.nononsenseapps.filepicker.FilePickerFragment
|
||||||
|
import com.nononsenseapps.filepicker.LogicHandler
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class CustomLayoutPickerActivity : FilePickerActivity() {
|
||||||
|
|
||||||
|
override fun getFragment(startPath: String?, mode: Int, allowMultiple: Boolean, allowCreateDir: Boolean):
|
||||||
|
AbstractFilePickerFragment<File> {
|
||||||
|
val fragment = CustomLayoutFilePickerFragment()
|
||||||
|
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir)
|
||||||
|
return fragment
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class CustomLayoutFilePickerFragment : FilePickerFragment() {
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
|
||||||
|
when (viewType) {
|
||||||
|
LogicHandler.VIEWTYPE_DIR -> {
|
||||||
|
val view = parent.inflate(R.layout.listitem_dir)
|
||||||
|
return DirViewHolder(view)
|
||||||
|
}
|
||||||
|
else -> return super.onCreateViewHolder(parent, viewType)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,48 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<PreferenceScreen
|
||||||
|
android:icon="@drawable/ic_backup_black_24dp"
|
||||||
|
android:key="backup_screen"
|
||||||
|
android:persistent="false"
|
||||||
|
android:title="Backup"
|
||||||
|
app:asp_tintEnabled="true">
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/pref_create_local_backup_key"
|
||||||
|
android:summary="@string/pref_create_backup_summ"
|
||||||
|
android:title="@string/pref_create_backup" />
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/pref_restore_local_backup_key"
|
||||||
|
android:summary="@string/pref_restore_backup_summ"
|
||||||
|
android:title="@string/pref_restore_backup" />
|
||||||
|
|
||||||
|
<PreferenceCategory
|
||||||
|
android:persistent="false"
|
||||||
|
android:title="@string/pref_backup_service_category" />
|
||||||
|
|
||||||
|
<eu.kanade.tachiyomi.widget.preference.IntListPreference
|
||||||
|
android:defaultValue="0"
|
||||||
|
android:entries="@array/backup_update_interval"
|
||||||
|
android:entryValues="@array/backup_update_interval_values"
|
||||||
|
android:key="@string/pref_backup_interval_key"
|
||||||
|
android:summary="%s"
|
||||||
|
android:title="@string/pref_backup_interval"/>
|
||||||
|
|
||||||
|
<Preference
|
||||||
|
android:key="@string/pref_backup_directory_key"
|
||||||
|
android:title="@string/pref_backup_directory" />
|
||||||
|
|
||||||
|
<eu.kanade.tachiyomi.widget.preference.IntListPreference
|
||||||
|
android:defaultValue="1"
|
||||||
|
android:entries="@array/backup_slots"
|
||||||
|
android:entryValues="@array/backup_slots"
|
||||||
|
android:key="@string/pref_backup_slots_key"
|
||||||
|
android:summary="%s"
|
||||||
|
android:title="@string/pref_backup_slots" />
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
||||||
|
|
||||||
|
</PreferenceScreen>
|
@ -1,568 +1,412 @@
|
|||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import com.google.gson.Gson
|
import com.github.salomonbrys.kotson.fromJson
|
||||||
import com.google.gson.JsonElement
|
import com.google.gson.JsonArray
|
||||||
import com.google.gson.JsonObject
|
import com.google.gson.JsonObject
|
||||||
import eu.kanade.tachiyomi.BuildConfig
|
import eu.kanade.tachiyomi.BuildConfig
|
||||||
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
|
import eu.kanade.tachiyomi.CustomRobolectricGradleTestRunner
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.Backup
|
||||||
|
import eu.kanade.tachiyomi.data.backup.models.DHistory
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.data.database.models.*
|
import eu.kanade.tachiyomi.data.database.models.*
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
import org.assertj.core.api.Assertions.assertThat
|
import org.assertj.core.api.Assertions.assertThat
|
||||||
import org.junit.Before
|
import org.junit.Before
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
import org.junit.runner.RunWith
|
import org.junit.runner.RunWith
|
||||||
|
import org.mockito.Mockito
|
||||||
|
import org.mockito.Mockito.*
|
||||||
import org.robolectric.RuntimeEnvironment
|
import org.robolectric.RuntimeEnvironment
|
||||||
import org.robolectric.annotation.Config
|
import org.robolectric.annotation.Config
|
||||||
import uy.kohesive.injekt.injectLazy
|
import rx.Observable
|
||||||
import java.util.*
|
import rx.observers.TestSubscriber
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.InjektModule
|
||||||
|
import uy.kohesive.injekt.api.InjektRegistrar
|
||||||
|
import uy.kohesive.injekt.api.addSingleton
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test class for the [BackupManager].
|
||||||
|
* Note that this does not include the backup create/restore services.
|
||||||
|
*/
|
||||||
@Config(constants = BuildConfig::class, sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP))
|
@Config(constants = BuildConfig::class, sdk = intArrayOf(Build.VERSION_CODES.LOLLIPOP))
|
||||||
@RunWith(CustomRobolectricGradleTestRunner::class)
|
@RunWith(CustomRobolectricGradleTestRunner::class)
|
||||||
class BackupTest {
|
class BackupTest {
|
||||||
|
// Create root object
|
||||||
|
var root = JsonObject()
|
||||||
|
|
||||||
val gson: Gson by injectLazy()
|
// Create information object
|
||||||
|
var information = JsonObject()
|
||||||
|
|
||||||
lateinit var db: DatabaseHelper
|
// Create manga array
|
||||||
|
var mangaEntries = JsonArray()
|
||||||
|
|
||||||
|
// Create category array
|
||||||
|
var categoryEntries = JsonArray()
|
||||||
|
|
||||||
|
lateinit var app: Application
|
||||||
|
lateinit var context: Context
|
||||||
|
lateinit var source: HttpSource
|
||||||
|
|
||||||
lateinit var backupManager: BackupManager
|
lateinit var backupManager: BackupManager
|
||||||
|
|
||||||
lateinit var root: JsonObject
|
lateinit var db: DatabaseHelper
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
fun setup() {
|
fun setup() {
|
||||||
val app = RuntimeEnvironment.application
|
app = RuntimeEnvironment.application
|
||||||
db = DatabaseHelper(app)
|
context = app.applicationContext
|
||||||
backupManager = BackupManager(db)
|
backupManager = BackupManager(context)
|
||||||
root = JsonObject()
|
db = backupManager.databaseHelper
|
||||||
}
|
|
||||||
|
// Mock the source manager
|
||||||
|
val module = object : InjektModule {
|
||||||
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
|
addSingleton(Mockito.mock(SourceManager::class.java, RETURNS_DEEP_STUBS))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Injekt.importModule(module)
|
||||||
|
|
||||||
@Test
|
source = mock(HttpSource::class.java)
|
||||||
fun testRestoreCategory() {
|
`when`(backupManager.sourceManager.get(anyLong())).thenReturn(source)
|
||||||
val catName = "cat"
|
|
||||||
root = createRootJson(null, toJson(createCategories(catName)))
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbCats = db.getCategories().executeAsBlocking()
|
root.add(Backup.MANGAS, mangaEntries)
|
||||||
assertThat(dbCats).hasSize(1)
|
root.add(Backup.CATEGORIES, categoryEntries)
|
||||||
assertThat(dbCats[0].name).isEqualTo(catName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test that checks if no crashes when no categories in library.
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testRestoreEmptyCategory() {
|
fun testRestoreEmptyCategory() {
|
||||||
root = createRootJson(null, toJson(ArrayList<Any>()))
|
// Initialize json with version 2
|
||||||
backupManager.restoreFromJson(root)
|
initializeJsonTest(2)
|
||||||
val dbCats = db.getCategories().executeAsBlocking()
|
|
||||||
assertThat(dbCats).isEmpty()
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
// Create backup of empty database
|
||||||
fun testRestoreExistingCategory() {
|
backupManager.backupCategories(categoryEntries)
|
||||||
val catName = "cat"
|
|
||||||
db.insertCategory(createCategory(catName)).executeAsBlocking()
|
|
||||||
|
|
||||||
root = createRootJson(null, toJson(createCategories(catName)))
|
// Restore Json
|
||||||
backupManager.restoreFromJson(root)
|
backupManager.restoreCategories(categoryEntries)
|
||||||
|
|
||||||
|
// Check if empty
|
||||||
val dbCats = db.getCategories().executeAsBlocking()
|
val dbCats = db.getCategories().executeAsBlocking()
|
||||||
assertThat(dbCats).hasSize(1)
|
assertThat(dbCats).isEmpty()
|
||||||
assertThat(dbCats[0].name).isEqualTo(catName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test to check if single category gets restored
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testRestoreCategories() {
|
fun testRestoreSingleCategory() {
|
||||||
root = createRootJson(null, toJson(createCategories("cat", "cat2", "cat3")))
|
// Initialize json with version 2
|
||||||
backupManager.restoreFromJson(root)
|
initializeJsonTest(2)
|
||||||
|
|
||||||
val dbCats = db.getCategories().executeAsBlocking()
|
// Create category and add to json
|
||||||
assertThat(dbCats).hasSize(3)
|
val category = addSingleCategory("category")
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestoreExistingCategories() {
|
|
||||||
db.insertCategories(createCategories("cat", "cat2")).executeAsBlocking()
|
|
||||||
|
|
||||||
root = createRootJson(null, toJson(createCategories("cat", "cat2", "cat3")))
|
// Restore Json
|
||||||
backupManager.restoreFromJson(root)
|
backupManager.restoreCategories(categoryEntries)
|
||||||
|
|
||||||
val dbCats = db.getCategories().executeAsBlocking()
|
// Check if successful
|
||||||
assertThat(dbCats).hasSize(3)
|
val dbCats = backupManager.databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
assertThat(dbCats).hasSize(1)
|
||||||
|
assertThat(dbCats[0].name).isEqualTo(category.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test to check if multiple categories get restored.
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testRestoreExistingCategoriesAlt() {
|
fun testRestoreMultipleCategories() {
|
||||||
db.insertCategories(createCategories("cat", "cat2", "cat3")).executeAsBlocking()
|
// Initialize json with version 2
|
||||||
|
initializeJsonTest(2)
|
||||||
root = createRootJson(null, toJson(createCategories("cat", "cat2")))
|
|
||||||
backupManager.restoreFromJson(root)
|
// Create category and add to json
|
||||||
|
val category = addSingleCategory("category")
|
||||||
val dbCats = db.getCategories().executeAsBlocking()
|
val category2 = addSingleCategory("category2")
|
||||||
assertThat(dbCats).hasSize(3)
|
val category3 = addSingleCategory("category3")
|
||||||
|
val category4 = addSingleCategory("category4")
|
||||||
|
val category5 = addSingleCategory("category5")
|
||||||
|
|
||||||
|
// Insert category to test if no duplicates on restore.
|
||||||
|
db.insertCategory(category).executeAsBlocking()
|
||||||
|
|
||||||
|
// Restore Json
|
||||||
|
backupManager.restoreCategories(categoryEntries)
|
||||||
|
|
||||||
|
// Check if successful
|
||||||
|
val dbCats = backupManager.databaseHelper.getCategories().executeAsBlocking()
|
||||||
|
assertThat(dbCats).hasSize(5)
|
||||||
|
assertThat(dbCats[0].name).isEqualTo(category.name)
|
||||||
|
assertThat(dbCats[1].name).isEqualTo(category2.name)
|
||||||
|
assertThat(dbCats[2].name).isEqualTo(category3.name)
|
||||||
|
assertThat(dbCats[3].name).isEqualTo(category4.name)
|
||||||
|
assertThat(dbCats[4].name).isEqualTo(category5.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if restore of manga is successful
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testRestoreManga() {
|
fun testRestoreManga() {
|
||||||
val mangaName = "title"
|
// Initialize json with version 2
|
||||||
val mangas = createMangas(mangaName)
|
initializeJsonTest(2)
|
||||||
val elements = ArrayList<JsonElement>()
|
|
||||||
for (manga in mangas) {
|
|
||||||
val entry = JsonObject()
|
|
||||||
entry.add("manga", toJson(manga))
|
|
||||||
elements.add(entry)
|
|
||||||
}
|
|
||||||
root = createRootJson(toJson(elements), null)
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbMangas = db.getMangas().executeAsBlocking()
|
|
||||||
assertThat(dbMangas).hasSize(1)
|
|
||||||
assertThat(dbMangas[0].title).isEqualTo(mangaName)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestoreExistingManga() {
|
|
||||||
val mangaName = "title"
|
|
||||||
val manga = createManga(mangaName)
|
|
||||||
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
val elements = ArrayList<JsonElement>()
|
|
||||||
val entry = JsonObject()
|
|
||||||
entry.add("manga", toJson(manga))
|
|
||||||
elements.add(entry)
|
|
||||||
|
|
||||||
root = createRootJson(toJson(elements), null)
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbMangas = db.getMangas().executeAsBlocking()
|
|
||||||
assertThat(dbMangas).hasSize(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestoreExistingMangaWithUpdatedFields() {
|
|
||||||
// Store a manga in db
|
|
||||||
val mangaName = "title"
|
|
||||||
val updatedThumbnailUrl = "updated thumbnail url"
|
|
||||||
var 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
|
|
||||||
val entry = JsonObject()
|
|
||||||
entry.add("manga", toJson(manga))
|
|
||||||
|
|
||||||
// Append the entry to the backup list
|
|
||||||
val elements = ArrayList<JsonElement>()
|
|
||||||
elements.add(entry)
|
|
||||||
|
|
||||||
// Restore from json
|
|
||||||
root = createRootJson(toJson(elements), null)
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbMangas = db.getMangas().executeAsBlocking()
|
|
||||||
assertThat(dbMangas).hasSize(1)
|
|
||||||
assertThat(dbMangas[0].thumbnail_url).isEqualTo(updatedThumbnailUrl)
|
|
||||||
assertThat(dbMangas[0].chapter_flags).isEqualTo(512)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestoreChaptersForManga() {
|
|
||||||
// Create a manga and 3 chapters
|
|
||||||
val manga = createManga("title")
|
|
||||||
manga.id = 1L
|
|
||||||
val chapters = createChapters(manga, "1", "2", "3")
|
|
||||||
|
|
||||||
// Add an entry for the manga
|
|
||||||
val entry = JsonObject()
|
|
||||||
entry.add("manga", toJson(manga))
|
|
||||||
entry.add("chapters", toJson(chapters))
|
|
||||||
|
|
||||||
// Append the entry to the backup list
|
|
||||||
val mangas = ArrayList<JsonElement>()
|
|
||||||
mangas.add(entry)
|
|
||||||
|
|
||||||
// Restore from json
|
|
||||||
root = createRootJson(toJson(mangas), null)
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbManga = db.getManga(1).executeAsBlocking()
|
|
||||||
assertThat(dbManga).isNotNull()
|
|
||||||
|
|
||||||
val dbChapters = db.getChapters(dbManga!!).executeAsBlocking()
|
|
||||||
assertThat(dbChapters).hasSize(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
// Add manga to database
|
||||||
fun testRestoreChaptersForExistingManga() {
|
val manga = getSingleManga("One Piece")
|
||||||
val mangaId: Long = 3
|
manga.viewer = 3
|
||||||
// Create a manga and 3 chapters
|
manga.id = db.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
val manga = createManga("title")
|
|
||||||
manga.id = mangaId
|
|
||||||
val chapters = createChapters(manga, "1", "2", "3")
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
// Add an entry for the manga
|
|
||||||
val entry = JsonObject()
|
|
||||||
entry.add("manga", toJson(manga))
|
|
||||||
entry.add("chapters", toJson(chapters))
|
|
||||||
|
|
||||||
// Append the entry to the backup list
|
|
||||||
val mangas = ArrayList<JsonElement>()
|
|
||||||
mangas.add(entry)
|
|
||||||
|
|
||||||
// Restore from json
|
|
||||||
root = createRootJson(toJson(mangas), null)
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbManga = db.getManga(mangaId).executeAsBlocking()
|
|
||||||
assertThat(dbManga).isNotNull()
|
|
||||||
|
|
||||||
val dbChapters = db.getChapters(dbManga!!).executeAsBlocking()
|
|
||||||
assertThat(dbChapters).hasSize(3)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
var favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
fun testRestoreExistingChaptersForExistingManga() {
|
assertThat(favoriteManga).hasSize(1)
|
||||||
val mangaId: Long = 5
|
assertThat(favoriteManga[0].viewer).isEqualTo(3)
|
||||||
// Store a manga and 3 chapters
|
|
||||||
val manga = createManga("title")
|
|
||||||
manga.id = mangaId
|
|
||||||
var 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
|
|
||||||
val entry = JsonObject()
|
|
||||||
entry.add("manga", toJson(manga))
|
|
||||||
entry.add("chapters", toJson(chapters))
|
|
||||||
|
|
||||||
// Append the entry to the backup list
|
|
||||||
val mangas = ArrayList<JsonElement>()
|
|
||||||
mangas.add(entry)
|
|
||||||
|
|
||||||
// Restore from json
|
|
||||||
root = createRootJson(toJson(mangas), null)
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbManga = db.getManga(mangaId).executeAsBlocking()
|
|
||||||
assertThat(dbManga).isNotNull()
|
|
||||||
|
|
||||||
val dbChapters = db.getChapters(dbManga!!).executeAsBlocking()
|
|
||||||
assertThat(dbChapters).hasSize(4)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
// Update json with all options enabled
|
||||||
fun testRestoreCategoriesForManga() {
|
mangaEntries.add(backupManager.backupMangaObject(manga,1))
|
||||||
// Create a manga
|
|
||||||
val manga = createManga("title")
|
|
||||||
|
|
||||||
// Create categories
|
// Change manga in database to default values
|
||||||
val categories = createCategories("cat1", "cat2", "cat3")
|
val dbManga = getSingleManga("One Piece")
|
||||||
|
dbManga.id = manga.id
|
||||||
|
db.insertManga(dbManga).executeAsBlocking()
|
||||||
|
|
||||||
// Add an entry for the manga
|
favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
val entry = JsonObject()
|
assertThat(favoriteManga).hasSize(1)
|
||||||
entry.add("manga", toJson(manga))
|
assertThat(favoriteManga[0].viewer).isEqualTo(0)
|
||||||
entry.add("categories", toJson(createStringCategories("cat1")))
|
|
||||||
|
|
||||||
// Append the entry to the backup list
|
// Restore local manga
|
||||||
val mangas = ArrayList<JsonElement>()
|
backupManager.restoreMangaNoFetch(manga,dbManga)
|
||||||
mangas.add(entry)
|
|
||||||
|
|
||||||
// Restore from json
|
// Test if restore successful
|
||||||
root = createRootJson(toJson(mangas), toJson(categories))
|
favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
backupManager.restoreFromJson(root)
|
assertThat(favoriteManga).hasSize(1)
|
||||||
|
assertThat(favoriteManga[0].viewer).isEqualTo(3)
|
||||||
|
|
||||||
val dbManga = db.getManga(1).executeAsBlocking()
|
// Clear database to test manga fetch
|
||||||
assertThat(dbManga).isNotNull()
|
clearDatabase()
|
||||||
|
|
||||||
val result = db.getCategoriesForManga(dbManga!!).executeAsBlocking()
|
|
||||||
|
|
||||||
assertThat(result).hasSize(1)
|
|
||||||
assertThat(result).contains(Category.create("cat1"))
|
|
||||||
assertThat(result).doesNotContain(Category.create("cat2"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestoreCategoriesForExistingManga() {
|
|
||||||
// Store a manga
|
|
||||||
val manga = createManga("title")
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
// Create categories
|
|
||||||
val categories = createCategories("cat1", "cat2", "cat3")
|
|
||||||
|
|
||||||
// Add an entry for the manga
|
|
||||||
val entry = JsonObject()
|
|
||||||
entry.add("manga", toJson(manga))
|
|
||||||
entry.add("categories", toJson(createStringCategories("cat1")))
|
|
||||||
|
|
||||||
// Append the entry to the backup list
|
|
||||||
val mangas = ArrayList<JsonElement>()
|
|
||||||
mangas.add(entry)
|
|
||||||
|
|
||||||
// Restore from json
|
|
||||||
root = createRootJson(toJson(mangas), toJson(categories))
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbManga = db.getManga(1).executeAsBlocking()
|
|
||||||
assertThat(dbManga).isNotNull()
|
|
||||||
|
|
||||||
val result = db.getCategoriesForManga(dbManga!!).executeAsBlocking()
|
|
||||||
|
|
||||||
assertThat(result).hasSize(1)
|
|
||||||
assertThat(result).contains(Category.create("cat1"))
|
|
||||||
assertThat(result).doesNotContain(Category.create("cat2"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
fun testRestoreMultipleCategoriesForManga() {
|
|
||||||
// Create a manga
|
|
||||||
val manga = createManga("title")
|
|
||||||
|
|
||||||
// Create categories
|
// Test if successful
|
||||||
val categories = createCategories("cat1", "cat2", "cat3")
|
favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
|
assertThat(favoriteManga).hasSize(0)
|
||||||
|
|
||||||
// Add an entry for the manga
|
// Restore Json
|
||||||
val entry = JsonObject()
|
// Create JSON from manga to test parser
|
||||||
entry.add("manga", toJson(manga))
|
val json = backupManager.parser.toJsonTree(manga)
|
||||||
entry.add("categories", toJson(createStringCategories("cat1", "cat3")))
|
// Restore JSON from manga to test parser
|
||||||
|
val jsonManga = backupManager.parser.fromJson<MangaImpl>(json)
|
||||||
|
|
||||||
// Append the entry to the backup list
|
// Restore manga with fetch observable
|
||||||
val mangas = ArrayList<JsonElement>()
|
val networkManga = getSingleManga("One Piece")
|
||||||
mangas.add(entry)
|
networkManga.description = "This is a description"
|
||||||
|
`when`(source.fetchMangaDetails(jsonManga)).thenReturn(Observable.just(networkManga))
|
||||||
|
|
||||||
// Restore from json
|
val obs = backupManager.restoreMangaFetchObservable(source, jsonManga)
|
||||||
root = createRootJson(toJson(mangas), toJson(categories))
|
val testSubscriber = TestSubscriber<Manga>()
|
||||||
backupManager.restoreFromJson(root)
|
obs.subscribe(testSubscriber)
|
||||||
|
|
||||||
val dbManga = db.getManga(1).executeAsBlocking()
|
testSubscriber.assertNoErrors()
|
||||||
assertThat(dbManga).isNotNull()
|
|
||||||
|
|
||||||
val result = db.getCategoriesForManga(dbManga!!).executeAsBlocking()
|
// Check if restore successful
|
||||||
|
val dbCats = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||||
assertThat(result).hasSize(2)
|
assertThat(dbCats).hasSize(1)
|
||||||
assertThat(result).contains(Category.create("cat1"), Category.create("cat3"))
|
assertThat(dbCats[0].viewer).isEqualTo(3)
|
||||||
assertThat(result).doesNotContain(Category.create("cat2"))
|
assertThat(dbCats[0].description).isEqualTo("This is a description")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test if chapter restore is successful
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testRestoreMultipleCategoriesForExistingMangaAndCategory() {
|
fun testRestoreChapters() {
|
||||||
// Store a manga and a category
|
// Initialize json with version 2
|
||||||
val manga = createManga("title")
|
initializeJsonTest(2)
|
||||||
manga.id = 1L
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
val cat = createCategory("cat1")
|
// Insert manga
|
||||||
cat.id = 1
|
val manga = getSingleManga("One Piece")
|
||||||
db.insertCategory(cat).executeAsBlocking()
|
manga.id = backupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
db.insertMangaCategory(MangaCategory.create(manga, cat)).executeAsBlocking()
|
|
||||||
|
|
||||||
// Create categories
|
|
||||||
val categories = createCategories("cat1", "cat2", "cat3")
|
|
||||||
|
|
||||||
// Add an entry for the manga
|
// Create restore list
|
||||||
val entry = JsonObject()
|
val chapters = ArrayList<Chapter>()
|
||||||
entry.add("manga", toJson(manga))
|
for (i in 1..8){
|
||||||
entry.add("categories", toJson(createStringCategories("cat1", "cat2")))
|
val chapter = getSingleChapter("Chapter $i")
|
||||||
|
chapter.read = true
|
||||||
|
chapters.add(chapter)
|
||||||
|
}
|
||||||
|
|
||||||
// Append the entry to the backup list
|
// Check parser
|
||||||
val mangas = ArrayList<JsonElement>()
|
val chaptersJson = backupManager.parser.toJsonTree(chapters)
|
||||||
mangas.add(entry)
|
val restoredChapters = backupManager.parser.fromJson<List<ChapterImpl>>(chaptersJson)
|
||||||
|
|
||||||
// Restore from json
|
// Fetch chapters from upstream
|
||||||
root = createRootJson(toJson(mangas), toJson(categories))
|
// Create list
|
||||||
backupManager.restoreFromJson(root)
|
val chaptersRemote = ArrayList<Chapter>()
|
||||||
|
(1..10).mapTo(chaptersRemote) { getSingleChapter("Chapter $it") }
|
||||||
|
`when`(source.fetchChapterList(manga)).thenReturn(Observable.just(chaptersRemote))
|
||||||
|
|
||||||
val dbManga = db.getManga(1).executeAsBlocking()
|
// Call restoreChapterFetchObservable
|
||||||
assertThat(dbManga).isNotNull()
|
val obs = backupManager.restoreChapterFetchObservable(source, manga, restoredChapters)
|
||||||
|
val testSubscriber = TestSubscriber<Pair<List<Chapter>, List<Chapter>>>()
|
||||||
|
obs.subscribe(testSubscriber)
|
||||||
|
|
||||||
val result = db.getCategoriesForManga(dbManga!!).executeAsBlocking()
|
testSubscriber.assertNoErrors()
|
||||||
|
|
||||||
assertThat(result).hasSize(2)
|
val dbCats = backupManager.databaseHelper.getChapters(manga).executeAsBlocking()
|
||||||
assertThat(result).contains(Category.create("cat1"), Category.create("cat2"))
|
assertThat(dbCats).hasSize(10)
|
||||||
assertThat(result).doesNotContain(Category.create("cat3"))
|
assertThat(dbCats[0].read).isEqualTo(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test to check if history restore works
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testRestoreSyncForManga() {
|
fun restoreHistoryForManga(){
|
||||||
// Create a manga and track
|
// Initialize json with version 2
|
||||||
val manga = createManga("title")
|
initializeJsonTest(2)
|
||||||
manga.id = 1L
|
|
||||||
|
|
||||||
val track = createTrack(manga, 1, 2, 3)
|
val manga = getSingleManga("One Piece")
|
||||||
|
manga.id = backupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
// Add an entry for the manga
|
// Create chapter
|
||||||
val entry = JsonObject()
|
val chapter = getSingleChapter("Chapter 1")
|
||||||
entry.add("manga", toJson(manga))
|
chapter.manga_id = manga.id
|
||||||
entry.add("sync", toJson(track))
|
chapter.read = true
|
||||||
|
chapter.id = backupManager.databaseHelper.insertChapter(chapter).executeAsBlocking().insertedId()
|
||||||
|
|
||||||
// Append the entry to the backup list
|
val historyJson = getSingleHistory(chapter)
|
||||||
val mangas = ArrayList<JsonElement>()
|
|
||||||
mangas.add(entry)
|
|
||||||
|
|
||||||
// Restore from json
|
val historyList = ArrayList<DHistory>()
|
||||||
root = createRootJson(toJson(mangas), null)
|
historyList.add(historyJson)
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbManga = db.getManga(1).executeAsBlocking()
|
// Check parser
|
||||||
assertThat(dbManga).isNotNull()
|
val historyListJson = backupManager.parser.toJsonTree(historyList)
|
||||||
|
val history = backupManager.parser.fromJson<List<DHistory>>(historyListJson)
|
||||||
|
|
||||||
val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
|
// Restore categories
|
||||||
assertThat(dbSync).hasSize(3)
|
backupManager.restoreHistoryForManga(history)
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
val historyDB = backupManager.databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
|
||||||
fun testRestoreSyncForExistingManga() {
|
assertThat(historyDB).hasSize(1)
|
||||||
val mangaId: Long = 3
|
assertThat(historyDB[0].last_read).isEqualTo(1000)
|
||||||
// Create a manga and 3 sync
|
|
||||||
val manga = createManga("title")
|
|
||||||
manga.id = mangaId
|
|
||||||
val track = createTrack(manga, 1, 2, 3)
|
|
||||||
db.insertManga(manga).executeAsBlocking()
|
|
||||||
|
|
||||||
// Add an entry for the manga
|
|
||||||
val entry = JsonObject()
|
|
||||||
entry.add("manga", toJson(manga))
|
|
||||||
entry.add("sync", toJson(track))
|
|
||||||
|
|
||||||
// Append the entry to the backup list
|
|
||||||
val mangas = ArrayList<JsonElement>()
|
|
||||||
mangas.add(entry)
|
|
||||||
|
|
||||||
// Restore from json
|
|
||||||
root = createRootJson(toJson(mangas), null)
|
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
|
||||||
val dbManga = db.getManga(mangaId).executeAsBlocking()
|
|
||||||
assertThat(dbManga).isNotNull()
|
|
||||||
|
|
||||||
val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
|
|
||||||
assertThat(dbSync).hasSize(3)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test to check if tracking restore works
|
||||||
|
*/
|
||||||
@Test
|
@Test
|
||||||
fun testRestoreExistingSyncForExistingManga() {
|
fun restoreTrackForManga() {
|
||||||
val mangaId: Long = 5
|
// Initialize json with version 2
|
||||||
// Store a manga and 3 sync
|
initializeJsonTest(2)
|
||||||
val manga = createManga("title")
|
|
||||||
manga.id = mangaId
|
// Create mangas
|
||||||
var track = createTrack(manga, 1, 2, 3)
|
val manga = getSingleManga("One Piece")
|
||||||
db.insertManga(manga).executeAsBlocking()
|
val manga2 = getSingleManga("Bleach")
|
||||||
db.insertTracks(track).executeAsBlocking()
|
manga.id = backupManager.databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
|
||||||
|
manga2.id = backupManager.databaseHelper.insertManga(manga2).executeAsBlocking().insertedId()
|
||||||
// The backup contains a existing sync and a new one, so it should have 4 sync
|
|
||||||
track = createTrack(manga, 3, 4)
|
// Create track and add it to database
|
||||||
|
// This tests duplicate errors.
|
||||||
// Add an entry for the manga
|
val track = getSingleTrack(manga)
|
||||||
val entry = JsonObject()
|
track.last_chapter_read = 5
|
||||||
entry.add("manga", toJson(manga))
|
backupManager.databaseHelper.insertTrack(track).executeAsBlocking()
|
||||||
entry.add("sync", toJson(track))
|
var trackDB = backupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
|
assertThat(trackDB).hasSize(1)
|
||||||
// Append the entry to the backup list
|
assertThat(trackDB[0].last_chapter_read).isEqualTo(5)
|
||||||
val mangas = ArrayList<JsonElement>()
|
track.last_chapter_read = 7
|
||||||
mangas.add(entry)
|
|
||||||
|
// Create track for different manga to test track not in database
|
||||||
// Restore from json
|
val track2 = getSingleTrack(manga2)
|
||||||
root = createRootJson(toJson(mangas), null)
|
track2.last_chapter_read = 10
|
||||||
backupManager.restoreFromJson(root)
|
|
||||||
|
// Check parser and restore already in database
|
||||||
val dbManga = db.getManga(mangaId).executeAsBlocking()
|
var trackList = listOf(track)
|
||||||
assertThat(dbManga).isNotNull()
|
//Check parser
|
||||||
|
var trackListJson = backupManager.parser.toJsonTree(trackList)
|
||||||
val dbSync = db.getTracks(dbManga!!).executeAsBlocking()
|
var trackListRestore = backupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
|
||||||
assertThat(dbSync).hasSize(4)
|
backupManager.restoreTrackForManga(manga, trackListRestore)
|
||||||
}
|
|
||||||
|
// Assert if restore works.
|
||||||
private fun createRootJson(mangas: JsonElement?, categories: JsonElement?): JsonObject {
|
trackDB = backupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
val root = JsonObject()
|
assertThat(trackDB).hasSize(1)
|
||||||
if (mangas != null)
|
assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
|
||||||
root.add("mangas", mangas)
|
|
||||||
if (categories != null)
|
// Check parser and restore already in database with lower chapter_read
|
||||||
root.add("categories", categories)
|
track.last_chapter_read = 5
|
||||||
return root
|
trackList = listOf(track)
|
||||||
}
|
backupManager.restoreTrackForManga(manga, trackList)
|
||||||
|
|
||||||
private fun createCategory(name: String): Category {
|
// Assert if restore works.
|
||||||
val c = CategoryImpl()
|
trackDB = backupManager.databaseHelper.getTracks(manga).executeAsBlocking()
|
||||||
c.name = name
|
assertThat(trackDB).hasSize(1)
|
||||||
return c
|
assertThat(trackDB[0].last_chapter_read).isEqualTo(7)
|
||||||
|
|
||||||
|
// Check parser and restore, track not in database
|
||||||
|
trackList = listOf(track2)
|
||||||
|
|
||||||
|
//Check parser
|
||||||
|
trackListJson = backupManager.parser.toJsonTree(trackList)
|
||||||
|
trackListRestore = backupManager.parser.fromJson<List<TrackImpl>>(trackListJson)
|
||||||
|
backupManager.restoreTrackForManga(manga2, trackListRestore)
|
||||||
|
|
||||||
|
// Assert if restore works.
|
||||||
|
trackDB = backupManager.databaseHelper.getTracks(manga2).executeAsBlocking()
|
||||||
|
assertThat(trackDB).hasSize(1)
|
||||||
|
assertThat(trackDB[0].last_chapter_read).isEqualTo(10)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createCategories(vararg names: String): List<Category> {
|
fun clearJson() {
|
||||||
val cats = ArrayList<Category>()
|
root = JsonObject()
|
||||||
for (name in names) {
|
information = JsonObject()
|
||||||
cats.add(createCategory(name))
|
mangaEntries = JsonArray()
|
||||||
}
|
categoryEntries = JsonArray()
|
||||||
return cats
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun createStringCategories(vararg names: String): List<String> {
|
|
||||||
val cats = ArrayList<String>()
|
|
||||||
for (name in names) {
|
|
||||||
cats.add(name)
|
|
||||||
}
|
|
||||||
return cats
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createManga(title: String): Manga {
|
fun initializeJsonTest(version: Int) {
|
||||||
val m = Manga.create(1)
|
clearJson()
|
||||||
m.title = title
|
backupManager.setVersion(version)
|
||||||
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
|
|
||||||
return m
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createMangas(vararg titles: String): List<Manga> {
|
fun addSingleCategory(name: String): Category {
|
||||||
val mangas = ArrayList<Manga>()
|
val category = Category.create(name)
|
||||||
for (title in titles) {
|
val catJson = backupManager.parser.toJsonTree(category)
|
||||||
mangas.add(createManga(title))
|
categoryEntries.add(catJson)
|
||||||
}
|
return category
|
||||||
return mangas
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createChapter(manga: Manga, url: String): Chapter {
|
fun clearDatabase(){
|
||||||
val c = Chapter.create()
|
db.deleteMangas().executeAsBlocking()
|
||||||
c.url = url
|
db.deleteHistory().executeAsBlocking()
|
||||||
c.name = url
|
|
||||||
c.manga_id = manga.id
|
|
||||||
return c
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createChapters(manga: Manga, vararg urls: String): List<Chapter> {
|
fun getSingleHistory(chapter: Chapter): DHistory {
|
||||||
val chapters = ArrayList<Chapter>()
|
return DHistory(chapter.url, 1000)
|
||||||
for (url in urls) {
|
|
||||||
chapters.add(createChapter(manga, url))
|
|
||||||
}
|
|
||||||
return chapters
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTrack(manga: Manga, syncId: Int): Track {
|
private fun getSingleTrack(manga: Manga): TrackImpl {
|
||||||
val m = Track.create(syncId)
|
val track = TrackImpl()
|
||||||
m.manga_id = manga.id!!
|
track.title = manga.title
|
||||||
m.title = "title"
|
track.manga_id = manga.id!!
|
||||||
return m
|
track.remote_id = 1
|
||||||
|
track.sync_id = 1
|
||||||
|
return track
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTrack(manga: Manga, vararg syncIds: Int): List<Track> {
|
private fun getSingleManga(title: String): MangaImpl {
|
||||||
val ms = ArrayList<Track>()
|
val manga = MangaImpl()
|
||||||
for (title in syncIds) {
|
manga.source = 1
|
||||||
ms.add(createTrack(manga, title))
|
manga.title = title
|
||||||
}
|
manga.url = "/manga/$title"
|
||||||
return ms
|
manga.favorite = true
|
||||||
|
return manga
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun toJson(element: Any): JsonElement {
|
private fun getSingleChapter(name: String): ChapterImpl {
|
||||||
return gson.toJsonTree(element)
|
val chapter = ChapterImpl()
|
||||||
|
chapter.name = name
|
||||||
|
chapter.url = "/read-online/$name-page-1.html"
|
||||||
|
return chapter
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
Loading…
Reference in new issue