mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-12-23 15:41:52 +01:00
Update restore to use a notification
Also added cleanup option to downloads
This commit is contained in:
parent
5368e37988
commit
b44ec4bfab
@ -32,6 +32,8 @@ import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import rx.Observable
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
@ -263,15 +265,15 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
*/
|
||||
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
|
||||
return source.fetchMangaDetails(manga)
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.favorite = true
|
||||
manga.initialized = true
|
||||
manga.id = insertManga(manga)
|
||||
manga
|
||||
}
|
||||
suspend fun restoreMangaFetch(source: Source, manga: Manga): Manga {
|
||||
return withContext(Dispatchers.IO) {
|
||||
val networkManga = source.fetchMangaDetails(manga).toBlocking().single()
|
||||
manga.copyFrom(networkManga)
|
||||
manga.favorite = true
|
||||
manga.initialized = true
|
||||
manga.id = insertManga(manga)
|
||||
manga
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -281,15 +283,16 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
|
||||
* @param manga manga that needs updating
|
||||
* @return [Observable] that contains manga
|
||||
*/
|
||||
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
|
||||
return source.fetchChapterList(manga)
|
||||
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
|
||||
.doOnNext {
|
||||
if (it.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
insertChapters(chapters)
|
||||
}
|
||||
}
|
||||
suspend fun restoreChapterFetch(source: Source, manga: Manga, chapters: List<Chapter>) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val fetchChapters = source.fetchChapterList(manga).toBlocking().single()
|
||||
val syncChaptersWithSource =
|
||||
syncChaptersWithSource(databaseHelper, fetchChapters, manga, source)
|
||||
if (syncChaptersWithSource.first.isNotEmpty()) {
|
||||
chapters.forEach { it.manga_id = manga.id }
|
||||
insertChapters(chapters)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,13 +1,18 @@
|
||||
package eu.kanade.tachiyomi.data.backup
|
||||
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.github.salomonbrys.kotson.fromJson
|
||||
import com.google.gson.JsonArray
|
||||
import com.google.gson.JsonObject
|
||||
import com.google.gson.JsonParser
|
||||
import com.google.gson.stream.JsonReader
|
||||
import eu.kanade.tachiyomi.R
|
||||
@ -21,64 +26,28 @@ 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.data.notification.NotificationReceiver
|
||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceNotFoundException
|
||||
import eu.kanade.tachiyomi.util.chop
|
||||
import eu.kanade.tachiyomi.util.getUriCompat
|
||||
import eu.kanade.tachiyomi.util.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.sendLocalBroadcast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.schedulers.Schedulers
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.ExecutorService
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
/**
|
||||
* Restores backup from json file
|
||||
*/
|
||||
class BackupRestoreService : Service() {
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
private fun isRunning(context: Context): Boolean =
|
||||
context.isServiceRunning(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: Uri) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||
putExtra(BackupConst.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.
|
||||
@ -88,27 +57,29 @@ class BackupRestoreService : Service() {
|
||||
/**
|
||||
* Subscription where the update is done.
|
||||
*/
|
||||
private var subscription: Subscription? = null
|
||||
private var job: Job? = null
|
||||
|
||||
/**
|
||||
* The progress of a backup restore
|
||||
*/
|
||||
private var restoreProgress = 0
|
||||
|
||||
/**
|
||||
* Amount of manga in Json file (needed for restore)
|
||||
*/
|
||||
private var restoreAmount = 0
|
||||
private var totalAmount = 0
|
||||
|
||||
/**
|
||||
* List containing errors
|
||||
*/
|
||||
private val errors = mutableListOf<Pair<Date, String>>()
|
||||
private val errors = mutableListOf<String>()
|
||||
|
||||
/**
|
||||
* count of cancelled
|
||||
*/
|
||||
private var cancelled = 0
|
||||
|
||||
/**
|
||||
* List containing distinct errors
|
||||
*/
|
||||
private val errorsMini = mutableListOf<String>()
|
||||
private val trackingErrors = mutableListOf<String>()
|
||||
|
||||
|
||||
/**
|
||||
@ -116,6 +87,11 @@ class BackupRestoreService : Service() {
|
||||
*/
|
||||
private val sourcesMissing = mutableListOf<Long>()
|
||||
|
||||
/**
|
||||
* List containing missing sources
|
||||
*/
|
||||
private var lincensedManga = 0
|
||||
|
||||
/**
|
||||
* Backup manager
|
||||
*/
|
||||
@ -132,17 +108,15 @@ class BackupRestoreService : Service() {
|
||||
internal val trackManager: TrackManager by injectLazy()
|
||||
|
||||
|
||||
private lateinit var executor: ExecutorService
|
||||
|
||||
/**
|
||||
* Method called when the service is created. It injects dependencies and acquire the wake lock.
|
||||
*/
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
startForeground(Notifications.ID_RESTORE_PROGRESS, progressNotification.build())
|
||||
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
|
||||
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
|
||||
wakeLock.acquire()
|
||||
executor = Executors.newSingleThreadExecutor()
|
||||
wakeLock.acquire(10 * 60 * 1000L /*10 minutes*/)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -150,8 +124,7 @@ class BackupRestoreService : Service() {
|
||||
* releases the wake lock.
|
||||
*/
|
||||
override fun onDestroy() {
|
||||
subscription?.unsubscribe()
|
||||
executor.shutdown() // must be called after unsubscribe
|
||||
job?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
@ -177,108 +150,164 @@ class BackupRestoreService : Service() {
|
||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
subscription?.unsubscribe()
|
||||
|
||||
subscription = Observable.using(
|
||||
{ db.lowLevel().beginTransaction() },
|
||||
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
|
||||
{ executor.execute { db.lowLevel().endTransaction() } })
|
||||
.doAfterTerminate { stopSelf(startId) }
|
||||
.subscribeOn(Schedulers.from(executor))
|
||||
.subscribe()
|
||||
job?.cancel()
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
showErrorNotification(exception.message!!)
|
||||
stopSelf(startId)
|
||||
}
|
||||
job = GlobalScope.launch(handler) {
|
||||
restoreBackup(uri!!)
|
||||
}
|
||||
job?.invokeOnCompletion { stopSelf(startId) }
|
||||
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
override fun stopService(name: Intent?): Boolean {
|
||||
job?.cancel()
|
||||
if (wakeLock.isHeld) {
|
||||
wakeLock.release()
|
||||
}
|
||||
return super.stopService(name)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an [Observable] containing restore process.
|
||||
*
|
||||
* @param uri restore file
|
||||
* @return [Observable<Manga>]
|
||||
* Restore a backup json file
|
||||
*/
|
||||
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
|
||||
val startTime = System.currentTimeMillis()
|
||||
private suspend fun restoreBackup(uri: Uri) {
|
||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser().parse(reader).asJsonObject
|
||||
|
||||
return Observable.just(Unit)
|
||||
.map {
|
||||
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
|
||||
val json = JsonParser().parse(reader).asJsonObject
|
||||
// Get parser version
|
||||
val version = json.get(VERSION)?.asInt ?: 1
|
||||
|
||||
// Get parser version
|
||||
val version = json.get(VERSION)?.asInt ?: 1
|
||||
// Initialize manager
|
||||
backupManager = BackupManager(this, version)
|
||||
|
||||
// Initialize manager
|
||||
backupManager = BackupManager(this, version)
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
|
||||
val mangasJson = json.get(MANGAS).asJsonArray
|
||||
// +1 for categories
|
||||
totalAmount = mangasJson.size() + 1
|
||||
trackingErrors.clear()
|
||||
sourcesMissing.clear()
|
||||
lincensedManga = 0
|
||||
errors.clear()
|
||||
cancelled = 0
|
||||
// Restore categories
|
||||
restoreCategories(json, backupManager)
|
||||
|
||||
restoreAmount = mangasJson.size() + 1 // +1 for categories
|
||||
restoreProgress = 0
|
||||
errors.clear()
|
||||
errorsMini.clear()
|
||||
mangasJson.forEach {
|
||||
restoreManga(it.asJsonObject, backupManager)
|
||||
}
|
||||
|
||||
// Restore categories
|
||||
json.get(CATEGORIES)?.let {
|
||||
backupManager.restoreCategories(it.asJsonArray)
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
|
||||
}
|
||||
notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
|
||||
|
||||
mangasJson
|
||||
cancelled = errors.count { it.contains("cancelled", true) }
|
||||
val tmpErrors = errors.filter { !it.contains("cancelled", true) }
|
||||
errors.clear()
|
||||
errors.addAll(tmpErrors)
|
||||
|
||||
val logFile = writeErrorLog()
|
||||
showResultNotification(logFile.parent, logFile.name)
|
||||
}
|
||||
|
||||
|
||||
/**Restore categories if they were backed up
|
||||
*
|
||||
*/
|
||||
private fun restoreCategories(json: JsonObject, backupManager: BackupManager) {
|
||||
val element = json.get(CATEGORIES)
|
||||
if (element != null) {
|
||||
backupManager.restoreCategories(element.asJsonArray)
|
||||
restoreProgress += 1
|
||||
showProgressNotification(restoreProgress, totalAmount, "Categories added")
|
||||
} else {
|
||||
totalAmount -= 1
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore manga from json this should be refactored more at some point to prevent the manga object from being mutable
|
||||
*/
|
||||
private suspend fun restoreManga(obj: JsonObject, backupManager: BackupManager) {
|
||||
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 source = backupManager.sourceManager.getOrStub(manga.source)
|
||||
|
||||
try {
|
||||
if (job?.isCancelled == false) {
|
||||
showProgressNotification(restoreProgress, totalAmount, manga.title)
|
||||
restoreProgress += 1
|
||||
}
|
||||
else {
|
||||
throw java.lang.Exception("Job was cancelled")
|
||||
}
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
val dbMangaExists = dbManga != null
|
||||
|
||||
if (dbMangaExists) {
|
||||
// Manga in database copy information from manga already in database
|
||||
backupManager.restoreMangaNoFetch(manga, dbManga!!)
|
||||
} else {
|
||||
// manga gets details from network
|
||||
backupManager.restoreMangaFetch(source, manga)
|
||||
}
|
||||
|
||||
if (!dbMangaExists || !backupManager.restoreChaptersForManga(manga, chapters)) {
|
||||
//manga gets chapters added
|
||||
backupManager.restoreChapterFetch(source, manga, chapters)
|
||||
}
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(manga, categories)
|
||||
// Restore history
|
||||
backupManager.restoreHistoryForManga(history)
|
||||
// Restore tracking
|
||||
backupManager.restoreTrackForManga(manga, tracks)
|
||||
|
||||
trackingFetch(manga, tracks)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
|
||||
if (e is RuntimeException) {
|
||||
val cause = e.cause
|
||||
if (cause is SourceNotFoundException) {
|
||||
sourcesMissing.add(cause.id)
|
||||
}
|
||||
.flatMap { Observable.from(it) }
|
||||
.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())
|
||||
else if (e.message?.contains("licensed", true) == true) {
|
||||
lincensedManga++
|
||||
}
|
||||
errors.add("${manga.title} - ${cause?.message ?: e.message}")
|
||||
return
|
||||
}
|
||||
errors.add("${manga.title} - ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
.doOnNext {
|
||||
val endTime = System.currentTimeMillis()
|
||||
val time = endTime - startTime
|
||||
val logFile = writeErrorLog()
|
||||
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.EXTRA_TIME, time)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
||||
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
|
||||
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
|
||||
val sourceMissingCount = sourcesMissing.distinct().size
|
||||
val sourceErrors = getString(R.string.sources_missing, sourceMissingCount)
|
||||
val otherErrors = errorsMini.distinct().joinToString("\n")
|
||||
putExtra(BackupConst.EXTRA_MINI_ERROR,
|
||||
if (sourceMissingCount > 0) sourceErrors + "\n" + otherErrors
|
||||
else otherErrors
|
||||
)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
|
||||
}
|
||||
sendLocalBroadcast(completeIntent)
|
||||
|
||||
}
|
||||
.doOnError { error ->
|
||||
Timber.e(error)
|
||||
writeErrorLog()
|
||||
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
|
||||
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
|
||||
}
|
||||
sendLocalBroadcast(errorIntent)
|
||||
}
|
||||
.onErrorReturn { emptyList() }
|
||||
/**
|
||||
* [refreshes tracking information
|
||||
* @param manga manga that needs updating.
|
||||
* @param tracks list containing tracks from restore file.
|
||||
*/
|
||||
private fun trackingFetch(manga: Manga, tracks: List<Track>) {
|
||||
tracks.forEach { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
service.refresh(track)
|
||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||
.onErrorReturn {
|
||||
errors.add("${manga.title} - ${it.message}")
|
||||
track
|
||||
}
|
||||
} else {
|
||||
errors.add("${manga.title} - ${service?.name} not logged in")
|
||||
val notLoggedIn = getString(R.string.not_logged_into, service?.name)
|
||||
trackingErrors.add(notLoggedIn)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -291,186 +320,149 @@ class BackupRestoreService : Service() {
|
||||
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")
|
||||
errors.forEach { message ->
|
||||
out.write("$message\n")
|
||||
}
|
||||
}
|
||||
return destFile
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Empty
|
||||
Timber.e(e)
|
||||
}
|
||||
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
|
||||
* keep a partially constructed progress notification for resuse
|
||||
*/
|
||||
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.getOrStub(manga.source)
|
||||
val dbManga = backupManager.getMangaFromDatabase(manga)
|
||||
private val progressNotification by lazy {
|
||||
NotificationCompat.Builder(this, Notifications.CHANNEL_RESTORE)
|
||||
.setContentTitle(getString(R.string.app_name))
|
||||
.setSmallIcon(R.drawable.ic_tachi)
|
||||
.setOngoing(true)
|
||||
.setOnlyAlertOnce(true)
|
||||
.setAutoCancel(false)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
|
||||
}
|
||||
|
||||
return if (dbManga == null) {
|
||||
// Manga not in database
|
||||
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
|
||||
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
|
||||
/**
|
||||
* Pending intent of action that cancels the library update
|
||||
*/
|
||||
private val cancelIntent by lazy {
|
||||
NotificationReceiver.cancelRestorePendingBroadcast(this)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Shows the notification containing the currently updating manga and the progress.
|
||||
*
|
||||
* @param manga the manga that's being updated.
|
||||
* @param current the current progress.
|
||||
* @param total the total progress.
|
||||
*/
|
||||
private fun showProgressNotification(current: Int, total: Int, title: String) {
|
||||
notificationManager.notify(Notifications.ID_RESTORE_PROGRESS, progressNotification
|
||||
.setContentTitle(title)
|
||||
.setProgress(total, current, false)
|
||||
.build())
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the result notification with option to show the error log
|
||||
*/
|
||||
private fun showResultNotification(path: String?, file: String?) {
|
||||
|
||||
val content = mutableListOf(getString(R.string.restore_completed_content, restoreProgress
|
||||
.toString(), errors.size.toString()))
|
||||
val sourceMissingCount = sourcesMissing.distinct().size
|
||||
if (sourceMissingCount > 0)
|
||||
content.add(getString(R.string.sources_missing, sourceMissingCount))
|
||||
if (lincensedManga > 0)
|
||||
content.add(getString(R.string.x_licensed_manga, lincensedManga))
|
||||
val trackingErrors = trackingErrors.distinct()
|
||||
if (trackingErrors.isNotEmpty()) {
|
||||
val trackingErrorsString = trackingErrors.distinct().joinToString("\n")
|
||||
content.add(trackingErrorsString)
|
||||
}
|
||||
if (cancelled > 0)
|
||||
content.add(getString(R.string.restore_completed_content_2, cancelled))
|
||||
|
||||
val restoreString = content.joinToString("\n")
|
||||
|
||||
val resultNotification = NotificationCompat.Builder(this, Notifications.CHANNEL_RESTORE)
|
||||
.setContentTitle(getString(R.string.restore_completed))
|
||||
.setContentText(restoreString)
|
||||
.setStyle(NotificationCompat.BigTextStyle().bigText(restoreString))
|
||||
.setSmallIcon(R.drawable.ic_tachi)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setColor(ContextCompat.getColor(this, R.color.colorAccent))
|
||||
if (errors.size > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
|
||||
resultNotification.addAction(R.drawable.ic_clear_grey_24dp_img, getString(R.string
|
||||
.notification_action_error_log), getErrorLogIntent(path, file))
|
||||
}
|
||||
notificationManager.notify(Notifications.ID_RESTORE_COMPLETE, resultNotification.build())
|
||||
}
|
||||
|
||||
/**Show an error notification if something happens that prevents the restore from starting/working
|
||||
*
|
||||
*/
|
||||
private fun showErrorNotification(errorMessage: String) {
|
||||
val resultNotification = NotificationCompat.Builder(this, Notifications.CHANNEL_RESTORE)
|
||||
.setContentTitle(getString(R.string.restore_error))
|
||||
.setContentText(errorMessage)
|
||||
.setSmallIcon(R.drawable.ic_error_grey)
|
||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||
.setColor(ContextCompat.getColor(this, R.color.md_red_500))
|
||||
notificationManager.notify(Notifications.ID_RESTORE_ERROR, resultNotification.build())
|
||||
}
|
||||
|
||||
/**Get the PendingIntent for the error log
|
||||
*
|
||||
*/
|
||||
private fun getErrorLogIntent(path: String, file: String): PendingIntent {
|
||||
val destFile = File(path, file!!)
|
||||
val uri = destFile.getUriCompat(applicationContext)
|
||||
return NotificationReceiver.openFileExplorerPendingActivity(this@BackupRestoreService, uri)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
/**
|
||||
* Returns the status of the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
* @return true if the service is running, false otherwise.
|
||||
*/
|
||||
private fun isRunning(context: Context): Boolean =
|
||||
context.isServiceRunning(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: Uri) {
|
||||
if (!isRunning(context)) {
|
||||
val intent = Intent(context, BackupRestoreService::class.java).apply {
|
||||
putExtra(BackupConst.EXTRA_URI, uri)
|
||||
}
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
|
||||
context.startService(intent)
|
||||
} else {
|
||||
context.startForegroundService(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops the service.
|
||||
*
|
||||
* @param context the application context.
|
||||
*/
|
||||
fun stop(context: Context) {
|
||||
context.stopService(Intent(context, BackupRestoreService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [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}")
|
||||
if (it is SourceNotFoundException) {
|
||||
sourcesMissing.add(it.id)
|
||||
}
|
||||
else {
|
||||
errorsMini.add(it.message ?: "")
|
||||
}
|
||||
manga
|
||||
}
|
||||
.filter { it.id != null }
|
||||
.flatMap {
|
||||
chapterFetchObservable(source, it, chapters)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
.doOnNext {
|
||||
restoreExtraForManga(it, categories, history, tracks)
|
||||
}
|
||||
.flatMap {
|
||||
trackingFetchObservable(it, tracks)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
.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 {
|
||||
restoreExtraForManga(it, categories, history, tracks)
|
||||
}
|
||||
.flatMap { manga ->
|
||||
trackingFetchObservable(manga, tracks)
|
||||
// Convert to the manga that contains new chapters.
|
||||
.map { manga }
|
||||
}
|
||||
.doOnCompleted {
|
||||
restoreProgress += 1
|
||||
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
|
||||
// Restore categories
|
||||
backupManager.restoreCategoriesForManga(manga, categories)
|
||||
|
||||
// Restore history
|
||||
backupManager.restoreHistoryForManga(history)
|
||||
|
||||
// Restore tracking
|
||||
backupManager.restoreTrackForManga(manga, tracks)
|
||||
}
|
||||
|
||||
/**
|
||||
* [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}")
|
||||
errorsMini.add(it.message ?: "")
|
||||
Pair(emptyList(), emptyList())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* [Observable] that refreshes tracking information
|
||||
* @param manga manga that needs updating.
|
||||
* @param tracks list containing tracks from restore file.
|
||||
* @return [Observable] that contains updated track item
|
||||
*/
|
||||
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
|
||||
return Observable.from(tracks)
|
||||
.concatMap { track ->
|
||||
val service = trackManager.getService(track.sync_id)
|
||||
if (service != null && service.isLogged) {
|
||||
service.refresh(track)
|
||||
.doOnNext { db.insertTrack(it).executeAsBlocking() }
|
||||
.onErrorReturn {
|
||||
errors.add(Date() to "${manga.title} - ${it.message}")
|
||||
errorsMini.add(it.message ?: "")
|
||||
track
|
||||
}
|
||||
} else {
|
||||
val notLoggedIn = getString(R.string.not_logged_into, service?.name)
|
||||
errors.add(Date() to "${manga.title} - $notLoggedIn")
|
||||
errorsMini.add(notLoggedIn)
|
||||
Observable.empty()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called to update dialog in [BackupConst]
|
||||
*
|
||||
* @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(BackupConst.INTENT_FILTER).apply {
|
||||
putExtra(BackupConst.EXTRA_PROGRESS, progress)
|
||||
putExtra(BackupConst.EXTRA_AMOUNT, amount)
|
||||
putExtra(BackupConst.EXTRA_CONTENT, content)
|
||||
putExtra(BackupConst.EXTRA_ERRORS, errors)
|
||||
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
|
||||
}
|
||||
sendLocalBroadcast(intent)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -212,6 +212,25 @@ class DownloadManager(val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the directories of chapters that were read or have no match
|
||||
*
|
||||
* @param chapters the list of chapters to delete.
|
||||
* @param manga the manga of the chapters.
|
||||
* @param source the source of the chapters.
|
||||
*/
|
||||
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source) {
|
||||
val filesWithNoChapter = provider.findUnmatchedChapterDirs(allChapters, manga, source)
|
||||
filesWithNoChapter.forEach { it.delete() }
|
||||
val readChapters = allChapters.filter { it.read }
|
||||
val readChapterDirs = provider.findChapterDirs(readChapters, manga, source)
|
||||
readChapterDirs.forEach { it.delete() }
|
||||
cache.removeChapters(readChapters, manga)
|
||||
if (cache.getDownloadCount(manga) == 0) {
|
||||
provider.findChapterDirs(allChapters, manga, source).firstOrNull()?.parentFile?.delete()// Delete manga directory if empty
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the directory of a downloaded manga.
|
||||
*
|
||||
|
@ -100,6 +100,26 @@ class DownloadProvider(private val context: Context) {
|
||||
return chapters.flatMap { getValidChapterDirNames(it) }.mapNotNull { mangaDir.findFile(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of all files in manga directory
|
||||
*
|
||||
* @param chapters the chapters to query.
|
||||
* @param manga the manga of the chapter.
|
||||
* @param source the source of the chapter.
|
||||
*/
|
||||
fun findUnmatchedChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
|
||||
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
|
||||
return mangaDir.listFiles()!!.asList().filter {
|
||||
chapters.find { chp ->
|
||||
(getValidChapterDirNames(chp) + "${getChapterDirName(chp)}_tmp").any { dir ->
|
||||
mangaDir.findFile(
|
||||
dir
|
||||
) != null
|
||||
}
|
||||
} == null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of downloaded directories for the chapters that exist.
|
||||
*
|
||||
|
@ -37,6 +37,10 @@ import eu.kanade.tachiyomi.util.isServiceRunning
|
||||
import eu.kanade.tachiyomi.util.notification
|
||||
import eu.kanade.tachiyomi.util.notificationManager
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.schedulers.Schedulers
|
||||
@ -72,6 +76,9 @@ class LibraryUpdateService(
|
||||
*/
|
||||
private var subscription: Subscription? = null
|
||||
|
||||
var job: Job? = null
|
||||
|
||||
|
||||
/**
|
||||
* Pending intent of action that cancels the library update
|
||||
*/
|
||||
@ -105,7 +112,8 @@ class LibraryUpdateService(
|
||||
enum class Target {
|
||||
CHAPTERS, // Manga chapters
|
||||
DETAILS, // Manga metadata
|
||||
TRACKING // Tracking metadata
|
||||
TRACKING, // Tracking metadata
|
||||
CLEANUP // Clean up downloads
|
||||
}
|
||||
|
||||
companion object {
|
||||
@ -203,37 +211,43 @@ class LibraryUpdateService(
|
||||
* @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
|
||||
if (intent == null) return START_NOT_STICKY
|
||||
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
|
||||
?: return Service.START_NOT_STICKY
|
||||
?: return START_NOT_STICKY
|
||||
|
||||
// Unsubscribe from any previous subscription if needed.
|
||||
subscription?.unsubscribe()
|
||||
|
||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||
// Update favorite manga. Destroy service when completed or in case of an error.
|
||||
subscription = Observable
|
||||
.defer {
|
||||
val selectedScheme = preferences.libraryUpdatePrioritization().getOrDefault()
|
||||
val mangaList = getMangaToUpdate(intent, target)
|
||||
.sortedWith(rankingScheme[selectedScheme])
|
||||
val mangaList = getMangaToUpdate(intent, target)
|
||||
.sortedWith(rankingScheme[selectedScheme])
|
||||
|
||||
// Update either chapter list or manga details.
|
||||
when (target) {
|
||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||
Target.DETAILS -> updateDetails(mangaList)
|
||||
Target.TRACKING -> updateTrackings(mangaList)
|
||||
}
|
||||
val handler = CoroutineExceptionHandler { _, exception ->
|
||||
Timber.e(exception)
|
||||
stopSelf(startId)
|
||||
}
|
||||
// Update either chapter list or manga details.
|
||||
if (target == Target.CLEANUP) {
|
||||
job = GlobalScope.launch(handler) {
|
||||
cleanupDownloads()
|
||||
}
|
||||
job?.invokeOnCompletion { stopSelf(startId) }
|
||||
} else {
|
||||
subscription = Observable.defer {
|
||||
when (target) {
|
||||
Target.CHAPTERS -> updateChapterList(mangaList)
|
||||
Target.DETAILS -> updateDetails(mangaList)
|
||||
else -> updateTrackings(mangaList)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe({
|
||||
}, {
|
||||
}.subscribeOn(Schedulers.io()).subscribe({}, {
|
||||
Timber.e(it)
|
||||
stopSelf(startId)
|
||||
}, {
|
||||
stopSelf(startId)
|
||||
})
|
||||
|
||||
return Service.START_REDELIVER_INTENT
|
||||
}
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
/**
|
||||
@ -336,6 +350,15 @@ class LibraryUpdateService(
|
||||
.map { manga -> manga.first }
|
||||
}
|
||||
|
||||
private fun cleanupDownloads() {
|
||||
val mangaList = db.getMangas().executeAsBlocking()
|
||||
for (manga in mangaList) {
|
||||
val chapterList = db.getChapters(manga).executeAsBlocking()
|
||||
val source = sourceManager.getOrStub(manga.source)
|
||||
downloadManager.cleanupChapters(chapterList, manga, source)
|
||||
}
|
||||
}
|
||||
|
||||
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
|
||||
// we need to get the chapters from the db so we have chapter ids
|
||||
val mangaChapters = db.getChapters(manga).executeAsBlocking()
|
||||
|
@ -7,9 +7,11 @@ import android.content.BroadcastReceiver
|
||||
import android.content.ClipData
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
@ -31,6 +33,7 @@ import uy.kohesive.injekt.injectLazy
|
||||
import java.io.File
|
||||
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
|
||||
|
||||
|
||||
/**
|
||||
* Global [BroadcastReceiver] that runs on UI thread
|
||||
* Pending Broadcasts should be made from here.
|
||||
@ -65,6 +68,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
|
||||
// Cancel library update and dismiss notification
|
||||
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context)
|
||||
ACTION_CANCEL_RESTORE -> cancelRestoreUpdate(context)
|
||||
// Open reader activity
|
||||
ACTION_OPEN_CHAPTER -> {
|
||||
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
|
||||
@ -164,7 +168,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Method called when user wants to stop a library update
|
||||
* Method called when user wants to mark as read
|
||||
*
|
||||
* @param context context of application
|
||||
* @param notificationId id of notification
|
||||
@ -185,6 +189,16 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
}
|
||||
}
|
||||
|
||||
/* Method called when user wants to stop a restore
|
||||
*
|
||||
* @param context context of application
|
||||
* @param notificationId id of notification
|
||||
*/
|
||||
private fun cancelRestoreUpdate(context: Context) {
|
||||
BackupRestoreService.stop(context)
|
||||
Handler().post { dismissNotification(context, Notifications.ID_RESTORE_PROGRESS) }
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val NAME = "NotificationReceiver"
|
||||
|
||||
@ -197,9 +211,13 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
// Called to cancel library update.
|
||||
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
|
||||
|
||||
// Called to cancel library update.
|
||||
// Called to mark as read
|
||||
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
|
||||
|
||||
// Called to cancel restore
|
||||
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
|
||||
|
||||
|
||||
// Called to open chapter
|
||||
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
|
||||
|
||||
@ -410,6 +428,19 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
)
|
||||
}
|
||||
|
||||
/**Returns the PendingIntent that will open the error log in an external text viewer
|
||||
*
|
||||
*/
|
||||
internal fun openFileExplorerPendingActivity(context: Context, uri: Uri): PendingIntent {
|
||||
val toLaunch = Intent(Intent.ACTION_VIEW).apply {
|
||||
setDataAndType(uri, "text/plain")
|
||||
flags = Intent.FLAG_ACTIVITY_NEW_TASK or
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
}
|
||||
return PendingIntent.getActivity(context, 0, toLaunch, 0)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that marks a chapter as read and deletes it if preferred
|
||||
*
|
||||
@ -441,5 +472,18 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns [PendingIntent] that starts a service which stops the restore service
|
||||
*
|
||||
* @param context context of application
|
||||
* @return [PendingIntent]
|
||||
*/
|
||||
internal fun cancelRestorePendingBroadcast(context: Context): PendingIntent {
|
||||
val intent = Intent(context, NotificationReceiver::class.java).apply {
|
||||
action = ACTION_CANCEL_RESTORE
|
||||
}
|
||||
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
}
|
||||
}
|
||||
}
|
@ -44,6 +44,12 @@ object Notifications {
|
||||
const val CHANNEL_UPDATES_TO_EXTS = "updates_ext_channel"
|
||||
const val ID_UPDATES_TO_EXTS = -401
|
||||
|
||||
const val CHANNEL_RESTORE = "backup_restore_channel"
|
||||
const val ID_RESTORE_PROGRESS = -501
|
||||
const val ID_RESTORE_COMPLETE = -502
|
||||
const val ID_RESTORE_ERROR = -503
|
||||
|
||||
|
||||
/**
|
||||
* Creates the notification channels introduced in Android Oreo.
|
||||
*
|
||||
@ -77,7 +83,10 @@ object Notifications {
|
||||
CHANNEL_NEW_CHAPTERS,
|
||||
context.getString(R.string.channel_new_chapters),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
)
|
||||
), NotificationChannel(CHANNEL_RESTORE, context.getString(R.string.channel_backup_restore),
|
||||
NotificationManager.IMPORTANCE_LOW).apply {
|
||||
setShowBadge(false)
|
||||
}
|
||||
)
|
||||
context.notificationManager.createNotificationChannels(channels)
|
||||
}
|
||||
|
@ -176,8 +176,7 @@ class ChaptersPresenter(
|
||||
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
||||
if (onlyUnread()) {
|
||||
observable = observable.filter { !it.read }
|
||||
}
|
||||
else if (onlyRead()) {
|
||||
} else if (onlyRead()) {
|
||||
observable = observable.filter { it.read }
|
||||
}
|
||||
if (onlyDownloaded()) {
|
||||
|
@ -126,7 +126,7 @@ class MangaInfoPresenter(
|
||||
toggleFavorite()
|
||||
}
|
||||
|
||||
fun shareManga(cover:Bitmap) {
|
||||
fun shareManga(cover: Bitmap) {
|
||||
val context = Injekt.get<Application>()
|
||||
|
||||
val destDir = File(context.cacheDir, "shared_image")
|
||||
@ -149,7 +149,7 @@ class MangaInfoPresenter(
|
||||
|
||||
val destFile = File(directory, filename)
|
||||
val stream: OutputStream = FileOutputStream(destFile)
|
||||
cover.compress(Bitmap.CompressFormat.JPEG,75,stream)
|
||||
cover.compress(Bitmap.CompressFormat.JPEG, 75, stream)
|
||||
stream.flush()
|
||||
stream.close()
|
||||
return destFile
|
||||
|
@ -69,6 +69,13 @@ class SettingsAdvancedController : SettingsController() {
|
||||
|
||||
onClick { LibraryUpdateService.start(context, target = Target.TRACKING) }
|
||||
}
|
||||
preference {
|
||||
titleRes = R.string.pref_clean_downloads
|
||||
|
||||
summaryRes = R.string.pref_clean_downloads_summary
|
||||
|
||||
onClick { LibraryUpdateService.start(context, target = Target.CLEANUP) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun clearChapterCache() {
|
||||
|
@ -288,7 +288,7 @@ class SettingsBackupController : SettingsController() {
|
||||
.onPositive { _, _ ->
|
||||
val context = applicationContext
|
||||
if (context != null) {
|
||||
RestoringBackupDialog().showDialog(router, TAG_RESTORING_BACKUP_DIALOG)
|
||||
activity?.toast(R.string.restoring_backup)
|
||||
BackupRestoreService.start(context, args.getParcelable(KEY_URI)!!)
|
||||
}
|
||||
}
|
||||
|
5
app/src/main/res/drawable/ic_error_grey.xml
Normal file
5
app/src/main/res/drawable/ic_error_grey.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#898989"
|
||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#FF000000" android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2L11,7h2v6z"/>
|
||||
</vector>
|
@ -234,7 +234,7 @@
|
||||
<string name="restore_completed">S\'ha completat la restauració</string>
|
||||
<string name="error_opening_log">No s\'ha pogut obrir el registre</string>
|
||||
<string name="restore_completed_content">La restauració ha tardat %1$s.
|
||||
\nS\'han produït $2$s errors.</string>
|
||||
\nS\'han produït %2$s errors.</string>
|
||||
<string name="backup_restore_content">La restauració obté dades de les fonts, poden haver-hi despeses de l\'operador.
|
||||
\nAssegureu-vos que heu iniciat la sessió a les fonts en què cal abans de restaurar.</string>
|
||||
<string name="file_saved">S\'ha desat el fitxer a %1$s</string>
|
||||
|
@ -306,13 +306,18 @@
|
||||
<string name="dialog_restoring_source_not_found">Restoring backup\n%1$s source not found</string>
|
||||
<string name="backup_created">Backup created</string>
|
||||
<string name="restore_completed">Restore completed</string>
|
||||
<string name="restore_error">Restore error</string>
|
||||
<string name="error_opening_log">Could not open log</string>
|
||||
<string name="restore_completed_content">Restore took %1$s.\n%2$s errors found.</string>
|
||||
<string name="backup_restore_content">Restore uses source to fetch data, carrier costs may apply.\n\nMake sure you have installed all necessary extensions and are logged in to sources and tracking services before restoring.</string>
|
||||
<string name="restore_completed_content">Restored %1$s. %2$s errors found</string>
|
||||
<string name="restore_completed_content_2">%1$d skipped</string>
|
||||
<string name="backup_restore_content">Restore uses the network to fetch data, carrier costs may apply.\n\nMake sure you have installed all necessary extensions and are logged in to sources and tracking services before restoring.</string>
|
||||
<string name="file_saved">File saved at %1$s</string>
|
||||
<string name="backup_choice">What do you want to backup?</string>
|
||||
<string name="restoring_backup">Restoring backup</string>
|
||||
<string name="creating_backup">Creating backup</string>
|
||||
<string name="sources_missing">Sources missing: %1$d</string>
|
||||
<string name="x_licensed_manga">%1$d manga are now licensed and could not be restored</string>
|
||||
<string name="not_logged_into">Not logged into %1$s</string>
|
||||
|
||||
<!-- Advanced section -->
|
||||
<string name="pref_clear_chapter_cache">Clear chapter cache</string>
|
||||
@ -330,6 +335,8 @@
|
||||
<string name="pref_refresh_library_metadata_summary">Updates covers, genres, description and manga status information</string>
|
||||
<string name="pref_refresh_library_tracking">Refresh tracking metadata</string>
|
||||
<string name="pref_refresh_library_tracking_summary">Updates status, score and last chapter read from the tracking services</string>
|
||||
<string name="pref_clean_downloads">Clean up downloaded chapters</string>
|
||||
<string name="pref_clean_downloads_summary">Deletes orphaned,tmp, and read chapter folders for entire library for each Manga</string>
|
||||
|
||||
<!-- About section -->
|
||||
<string name="version">Version</string>
|
||||
@ -406,8 +413,6 @@
|
||||
<string name="delete_downloads_for_manga">Delete downloaded chapters?</string>
|
||||
<string name="copied_to_clipboard">%1$s copied to clipboard</string>
|
||||
<string name="source_not_installed">Source not installed: %1$s</string>
|
||||
<string name="sources_missing">Sources missing: %1$d</string>
|
||||
<string name="not_logged_into">Not logged into %1$s</string>
|
||||
|
||||
<!-- Manga chapters fragment -->
|
||||
<string name="manga_chapters_tab">Chapters</string>
|
||||
@ -534,6 +539,8 @@
|
||||
<string name="notification_no_connection_title">Sync canceled</string>
|
||||
<string name="notification_no_connection_body">Connection not available</string>
|
||||
|
||||
<string name="notification_action_error_log">View all errors</string>
|
||||
|
||||
<!-- File Picker Titles -->
|
||||
<string name="file_select_cover">Select cover image</string>
|
||||
<string name="file_select_backup">Select backup file</string>
|
||||
@ -581,6 +588,7 @@
|
||||
<string name="channel_library">Library</string>
|
||||
<string name="channel_downloader">Downloader</string>
|
||||
<string name="channel_ext_updates">Extension Updates</string>
|
||||
<string name="channel_backup_restore">Restore Backup</string>
|
||||
<string name="channel_library_updates">Updating Library</string>
|
||||
<string name="channel_new_chapters">New Chapters</string>
|
||||
|
||||
|
@ -14,7 +14,10 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.*
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.fail
|
||||
import org.junit.Before
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
@ -23,7 +26,6 @@ import org.mockito.Mockito.*
|
||||
import org.robolectric.RuntimeEnvironment
|
||||
import org.robolectric.annotation.Config
|
||||
import rx.Observable
|
||||
import rx.observers.TestSubscriber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.InjektModule
|
||||
import uy.kohesive.injekt.api.InjektRegistrar
|
||||
@ -166,7 +168,7 @@ class BackupTest {
|
||||
assertThat(favoriteManga[0].viewer).isEqualTo(3)
|
||||
|
||||
// Update json with all options enabled
|
||||
mangaEntries.add(backupManager.backupMangaObject(manga,1))
|
||||
mangaEntries.add(backupManager.backupMangaObject(manga, 1))
|
||||
|
||||
// Change manga in database to default values
|
||||
val dbManga = getSingleManga("One Piece")
|
||||
@ -178,7 +180,7 @@ class BackupTest {
|
||||
assertThat(favoriteManga[0].viewer).isEqualTo(0)
|
||||
|
||||
// Restore local manga
|
||||
backupManager.restoreMangaNoFetch(manga,dbManga)
|
||||
backupManager.restoreMangaNoFetch(manga, dbManga)
|
||||
|
||||
// Test if restore successful
|
||||
favoriteManga = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
@ -201,13 +203,16 @@ class BackupTest {
|
||||
// Restore manga with fetch observable
|
||||
val networkManga = getSingleManga("One Piece")
|
||||
networkManga.description = "This is a description"
|
||||
`when`(source.fetchMangaDetails(jsonManga)).thenReturn(Observable.just(networkManga))
|
||||
`when`(source.fetchMangaDetailsObservable(jsonManga)).thenReturn(Observable.just(networkManga))
|
||||
|
||||
val obs = backupManager.restoreMangaFetchObservable(source, jsonManga)
|
||||
val testSubscriber = TestSubscriber<Manga>()
|
||||
obs.subscribe(testSubscriber)
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
backupManager.restoreMangaFetch(source, jsonManga)
|
||||
} catch (e: Exception) {
|
||||
fail("Unexpected onError events")
|
||||
}
|
||||
}
|
||||
|
||||
testSubscriber.assertNoErrors()
|
||||
|
||||
// Check if restore successful
|
||||
val dbCats = backupManager.databaseHelper.getFavoriteMangas().executeAsBlocking()
|
||||
@ -231,7 +236,7 @@ class BackupTest {
|
||||
|
||||
// Create restore list
|
||||
val chapters = ArrayList<Chapter>()
|
||||
for (i in 1..8){
|
||||
for (i in 1..8) {
|
||||
val chapter = getSingleChapter("Chapter $i")
|
||||
chapter.read = true
|
||||
chapters.add(chapter)
|
||||
@ -245,14 +250,16 @@ class BackupTest {
|
||||
// Create list
|
||||
val chaptersRemote = ArrayList<Chapter>()
|
||||
(1..10).mapTo(chaptersRemote) { getSingleChapter("Chapter $it") }
|
||||
`when`(source.fetchChapterList(manga)).thenReturn(Observable.just(chaptersRemote))
|
||||
`when`(source.fetchChapterListObservable(manga)).thenReturn(Observable.just(chaptersRemote))
|
||||
|
||||
// Call restoreChapterFetchObservable
|
||||
val obs = backupManager.restoreChapterFetchObservable(source, manga, restoredChapters)
|
||||
val testSubscriber = TestSubscriber<Pair<List<Chapter>, List<Chapter>>>()
|
||||
obs.subscribe(testSubscriber)
|
||||
|
||||
testSubscriber.assertNoErrors()
|
||||
GlobalScope.launch {
|
||||
try {
|
||||
backupManager.restoreChapterFetch(source, manga, restoredChapters)
|
||||
} catch (e: Exception) {
|
||||
fail("Unexpected onError events")
|
||||
}
|
||||
}
|
||||
|
||||
val dbCats = backupManager.databaseHelper.getChapters(manga).executeAsBlocking()
|
||||
assertThat(dbCats).hasSize(10)
|
||||
@ -263,7 +270,7 @@ class BackupTest {
|
||||
* Test to check if history restore works
|
||||
*/
|
||||
@Test
|
||||
fun restoreHistoryForManga(){
|
||||
fun restoreHistoryForManga() {
|
||||
// Initialize json with version 2
|
||||
initializeJsonTest(2)
|
||||
|
||||
@ -376,7 +383,7 @@ class BackupTest {
|
||||
return category
|
||||
}
|
||||
|
||||
fun clearDatabase(){
|
||||
fun clearDatabase() {
|
||||
db.deleteMangas().executeAsBlocking()
|
||||
db.deleteHistory().executeAsBlocking()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user