mirror of
https://github.com/null2264/yokai.git
synced 2025-06-21 10:44:42 +00:00
More updates to backup logic from upstream
Removed BackupCreateService among other things
This commit is contained in:
parent
3587feb8fa
commit
ce3a774e05
9 changed files with 173 additions and 238 deletions
|
@ -235,10 +235,6 @@
|
||||||
android:name=".data.updater.AppUpdateService"
|
android:name=".data.updater.AppUpdateService"
|
||||||
android:exported="false" />
|
android:exported="false" />
|
||||||
|
|
||||||
<service
|
|
||||||
android:name=".data.backup.BackupCreateService"
|
|
||||||
android:exported="false"/>
|
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".data.backup.BackupRestoreService"
|
android:name=".data.backup.BackupRestoreService"
|
||||||
android:exported="false"/>
|
android:exported="false"/>
|
||||||
|
|
|
@ -23,7 +23,7 @@ abstract class AbstractBackupManager(protected val context: Context) {
|
||||||
protected val preferences: PreferencesHelper by injectLazy()
|
protected val preferences: PreferencesHelper by injectLazy()
|
||||||
protected val customMangaManager: CustomMangaManager by injectLazy()
|
protected val customMangaManager: CustomMangaManager by injectLazy()
|
||||||
|
|
||||||
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
|
abstract fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String?
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns manga
|
* Returns manga
|
||||||
|
|
|
@ -11,4 +11,17 @@ object BackupConst {
|
||||||
|
|
||||||
const val BACKUP_TYPE_LEGACY = 0
|
const val BACKUP_TYPE_LEGACY = 0
|
||||||
const val BACKUP_TYPE_FULL = 1
|
const val BACKUP_TYPE_FULL = 1
|
||||||
|
|
||||||
|
// 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_CUSTOM_INFO = 0x10
|
||||||
|
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
||||||
|
internal const val BACKUP_ALL = 0x1F
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,116 +0,0 @@
|
||||||
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 androidx.core.content.ContextCompat
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import com.hippo.unifile.UniFile
|
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
|
||||||
import eu.kanade.tachiyomi.data.notification.Notifications
|
|
||||||
import eu.kanade.tachiyomi.util.system.acquireWakeLock
|
|
||||||
import eu.kanade.tachiyomi.util.system.isServiceRunning
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Service for backing up library information to a JSON file.
|
|
||||||
*/
|
|
||||||
class BackupCreateService : Service() {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
// 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_CUSTOM_INFO = 0x10
|
|
||||||
internal const val BACKUP_CUSTOM_INFO_MASK = 0x10
|
|
||||||
internal const val BACKUP_ALL = 0x1F
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 =
|
|
||||||
context.isServiceRunning(BackupCreateService::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Make a backup from library
|
|
||||||
*
|
|
||||||
* @param context context of application
|
|
||||||
* @param uri path of Uri
|
|
||||||
* @param flags determines what to backup
|
|
||||||
*/
|
|
||||||
fun start(context: Context, uri: Uri, flags: Int) {
|
|
||||||
if (!isRunning(context)) {
|
|
||||||
val intent = Intent(context, BackupCreateService::class.java).apply {
|
|
||||||
putExtra(BackupConst.EXTRA_URI, uri)
|
|
||||||
putExtra(BackupConst.EXTRA_FLAGS, flags)
|
|
||||||
}
|
|
||||||
ContextCompat.startForegroundService(context, intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Wake lock that will be held until the service is destroyed.
|
|
||||||
*/
|
|
||||||
private lateinit var wakeLock: PowerManager.WakeLock
|
|
||||||
|
|
||||||
private lateinit var notifier: BackupNotifier
|
|
||||||
|
|
||||||
override fun onCreate() {
|
|
||||||
super.onCreate()
|
|
||||||
|
|
||||||
notifier = BackupNotifier(this)
|
|
||||||
wakeLock = acquireWakeLock()
|
|
||||||
|
|
||||||
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun stopService(name: Intent?): Boolean {
|
|
||||||
destroyJob()
|
|
||||||
return super.stopService(name)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
destroyJob()
|
|
||||||
super.onDestroy()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun destroyJob() {
|
|
||||||
if (wakeLock.isHeld) {
|
|
||||||
wakeLock.release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method needs to be implemented, but it's not used/needed.
|
|
||||||
*/
|
|
||||||
override fun onBind(intent: Intent): IBinder? = null
|
|
||||||
|
|
||||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
||||||
if (intent == null) return START_NOT_STICKY
|
|
||||||
|
|
||||||
try {
|
|
||||||
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
|
|
||||||
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
|
|
||||||
val backupFileUri = FullBackupManager(this).createBackup(uri!!, backupFlags, false)?.toUri()
|
|
||||||
val unifile = UniFile.fromUri(this, backupFileUri)
|
|
||||||
notifier.showBackupComplete(unifile)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
notifier.showBackupError(e.message)
|
|
||||||
}
|
|
||||||
|
|
||||||
stopSelf(startId)
|
|
||||||
return START_NOT_STICKY
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,14 +1,23 @@
|
||||||
package eu.kanade.tachiyomi.data.backup
|
package eu.kanade.tachiyomi.data.backup
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.work.ExistingPeriodicWorkPolicy
|
import androidx.work.ExistingPeriodicWorkPolicy
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
|
import androidx.work.OneTimeWorkRequestBuilder
|
||||||
import androidx.work.PeriodicWorkRequestBuilder
|
import androidx.work.PeriodicWorkRequestBuilder
|
||||||
|
import androidx.work.WorkInfo
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
|
import androidx.work.workDataOf
|
||||||
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
|
||||||
|
import eu.kanade.tachiyomi.data.notification.Notifications
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import eu.kanade.tachiyomi.util.system.notificationManager
|
||||||
|
import timber.log.Timber
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
|
@ -18,36 +27,71 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
|
||||||
|
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val uri = preferences.backupsDirectory().get().toUri()
|
val notifier = BackupNotifier(context)
|
||||||
val flags = BackupCreateService.BACKUP_ALL
|
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
|
||||||
|
?: preferences.backupsDirectory().get().toUri()
|
||||||
|
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
|
||||||
|
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
|
||||||
|
|
||||||
|
context.notificationManager.notify(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
|
||||||
return try {
|
return try {
|
||||||
FullBackupManager(context).createBackup(uri, flags, true)
|
val location = FullBackupManager(context).createBackup(uri, flags, isAutoBackup)
|
||||||
|
if (!isAutoBackup) notifier.showBackupComplete(UniFile.fromUri(context, location.toUri()))
|
||||||
Result.success()
|
Result.success()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
Timber.e(e)
|
||||||
|
if (!isAutoBackup) notifier.showBackupError(e.message)
|
||||||
Result.failure()
|
Result.failure()
|
||||||
|
} finally {
|
||||||
|
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "BackupCreator"
|
fun isManualJobRunning(context: Context): Boolean {
|
||||||
|
val list = WorkManager.getInstance(context).getWorkInfosByTag(TAG_MANUAL).get()
|
||||||
|
return list.find { it.state == WorkInfo.State.RUNNING } != null
|
||||||
|
}
|
||||||
|
|
||||||
fun setupTask(context: Context, prefInterval: Int? = null) {
|
fun setupTask(context: Context, prefInterval: Int? = null) {
|
||||||
val preferences = Injekt.get<PreferencesHelper>()
|
val preferences = Injekt.get<PreferencesHelper>()
|
||||||
val interval = prefInterval ?: preferences.backupInterval().get()
|
val interval = prefInterval ?: preferences.backupInterval().get()
|
||||||
|
val workManager = WorkManager.getInstance(context)
|
||||||
if (interval > 0) {
|
if (interval > 0) {
|
||||||
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
|
||||||
interval.toLong(),
|
interval.toLong(),
|
||||||
TimeUnit.HOURS,
|
TimeUnit.HOURS,
|
||||||
10,
|
10,
|
||||||
TimeUnit.MINUTES
|
TimeUnit.MINUTES,
|
||||||
)
|
)
|
||||||
.addTag(TAG)
|
.addTag(TAG_AUTO)
|
||||||
|
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.REPLACE, request)
|
workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.REPLACE, request)
|
||||||
} else {
|
} else {
|
||||||
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
|
workManager.cancelUniqueWork(TAG_AUTO)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startNow(context: Context, uri: Uri, flags: Int) {
|
||||||
|
val inputData = workDataOf(
|
||||||
|
IS_AUTO_BACKUP_KEY to false,
|
||||||
|
LOCATION_URI_KEY to uri.toString(),
|
||||||
|
BACKUP_FLAGS_KEY to flags,
|
||||||
|
)
|
||||||
|
val request = OneTimeWorkRequestBuilder<BackupCreatorJob>()
|
||||||
|
.addTag(TAG_MANUAL)
|
||||||
|
.setInputData(inputData)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance(context).enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val TAG_AUTO = "BackupCreator"
|
||||||
|
private const val TAG_MANUAL = "$TAG_AUTO:manual"
|
||||||
|
|
||||||
|
private const val IS_AUTO_BACKUP_KEY = "is_auto_backup" // Boolean
|
||||||
|
private const val LOCATION_URI_KEY = "location_uri" // String
|
||||||
|
private const val BACKUP_FLAGS_KEY = "backup_flags" // Int
|
||||||
|
|
|
@ -5,16 +5,16 @@ import android.net.Uri
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CHAPTER_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CUSTOM_INFO
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CUSTOM_INFO_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CUSTOM_INFO_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_HISTORY_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
|
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_TRACK_MASK
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
import eu.kanade.tachiyomi.data.backup.full.models.Backup
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
|
||||||
|
@ -34,6 +34,7 @@ import okio.buffer
|
||||||
import okio.gzip
|
import okio.gzip
|
||||||
import okio.sink
|
import okio.sink
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.io.FileOutputStream
|
||||||
import kotlin.math.max
|
import kotlin.math.max
|
||||||
|
|
||||||
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
|
@ -44,9 +45,9 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
* Create backup Json file from database
|
* Create backup Json file from database
|
||||||
*
|
*
|
||||||
* @param uri path of Uri
|
* @param uri path of Uri
|
||||||
* @param isJob backup called from job
|
* @param isAutoBackup backup called from scheduled backup job
|
||||||
*/
|
*/
|
||||||
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
|
override fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
|
||||||
// Create root object
|
// Create root object
|
||||||
var backup: Backup? = null
|
var backup: Backup? = null
|
||||||
|
|
||||||
|
@ -57,13 +58,14 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
backupManga(databaseManga, flags),
|
backupManga(databaseManga, flags),
|
||||||
backupCategories(),
|
backupCategories(),
|
||||||
emptyList(),
|
emptyList(),
|
||||||
backupExtensionInfo(databaseManga)
|
backupExtensionInfo(databaseManga),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var file: UniFile? = null
|
||||||
try {
|
try {
|
||||||
val file: UniFile = (
|
file = (
|
||||||
if (isJob) {
|
if (isAutoBackup) {
|
||||||
// Get dir of file and create
|
// Get dir of file and create
|
||||||
var dir = UniFile.fromUri(context, uri)
|
var dir = UniFile.fromUri(context, uri)
|
||||||
dir = dir.createDirectory("automatic")
|
dir = dir.createDirectory("automatic")
|
||||||
|
@ -85,15 +87,28 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
)
|
)
|
||||||
?: throw Exception("Couldn't create backup file")
|
?: throw Exception("Couldn't create backup file")
|
||||||
|
|
||||||
|
if (!file.isFile) {
|
||||||
|
throw IllegalStateException("Failed to get handle on file")
|
||||||
|
}
|
||||||
|
|
||||||
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
|
||||||
if (byteArray.isEmpty()) {
|
if (byteArray.isEmpty()) {
|
||||||
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
throw IllegalStateException(context.getString(R.string.empty_backup_error))
|
||||||
}
|
}
|
||||||
|
|
||||||
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
|
file.openOutputStream().also {
|
||||||
return file.uri.toString()
|
// Force overwrite old file
|
||||||
|
(it as? FileOutputStream)?.channel?.truncate(0)
|
||||||
|
}.sink().gzip().buffer().use { it.write(byteArray) }
|
||||||
|
val fileUri = file.uri
|
||||||
|
|
||||||
|
// Make sure it's a valid backup file
|
||||||
|
FullBackupRestoreValidator().validate(context, fileUri)
|
||||||
|
|
||||||
|
return fileUri.toString()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
|
file?.delete()
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -285,7 +300,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -6,34 +6,34 @@ import android.content.ActivityNotFoundException
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.MenuInflater
|
||||||
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.core.os.bundleOf
|
import androidx.core.os.bundleOf
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.preference.PreferenceScreen
|
import androidx.preference.PreferenceScreen
|
||||||
import com.hippo.unifile.UniFile
|
import com.hippo.unifile.UniFile
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupConst
|
import eu.kanade.tachiyomi.data.backup.BackupConst
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreateService
|
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
|
||||||
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
|
||||||
|
import eu.kanade.tachiyomi.data.backup.ValidatorParseException
|
||||||
import eu.kanade.tachiyomi.data.backup.full.FullBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.full.FullBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
|
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
|
||||||
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestoreValidator
|
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestoreValidator
|
||||||
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||||
import eu.kanade.tachiyomi.ui.main.MainActivity
|
import eu.kanade.tachiyomi.ui.main.MainActivity
|
||||||
import eu.kanade.tachiyomi.util.system.MiuiUtil
|
import eu.kanade.tachiyomi.util.system.MiuiUtil
|
||||||
import eu.kanade.tachiyomi.util.system.disableItems
|
import eu.kanade.tachiyomi.util.system.disableItems
|
||||||
import eu.kanade.tachiyomi.util.system.getFilePicker
|
|
||||||
import eu.kanade.tachiyomi.util.system.materialAlertDialog
|
import eu.kanade.tachiyomi.util.system.materialAlertDialog
|
||||||
|
import eu.kanade.tachiyomi.util.system.openInBrowser
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
|
import eu.kanade.tachiyomi.util.view.requestFilePermissionsSafe
|
||||||
import kotlinx.coroutines.flow.launchIn
|
import kotlinx.coroutines.flow.launchIn
|
||||||
import kotlinx.coroutines.flow.onEach
|
import kotlinx.coroutines.flow.onEach
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
|
|
||||||
|
|
||||||
class SettingsBackupController : SettingsController() {
|
class SettingsBackupController : SettingsController() {
|
||||||
|
|
||||||
|
@ -60,7 +60,7 @@ class SettingsBackupController : SettingsController() {
|
||||||
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
|
context.toast(R.string.restore_miui_warning, Toast.LENGTH_LONG)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!BackupCreateService.isRunning(context)) {
|
if (!BackupCreatorJob.isManualJobRunning(context)) {
|
||||||
val ctrl = CreateBackupDialog()
|
val ctrl = CreateBackupDialog()
|
||||||
ctrl.targetController = this@SettingsBackupController
|
ctrl.targetController = this@SettingsBackupController
|
||||||
ctrl.showDialog(router)
|
ctrl.showDialog(router)
|
||||||
|
@ -83,7 +83,7 @@ class SettingsBackupController : SettingsController() {
|
||||||
(activity as? MainActivity)?.getExtensionUpdates(true)
|
(activity as? MainActivity)?.getExtensionUpdates(true)
|
||||||
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
val intent = Intent(Intent.ACTION_GET_CONTENT)
|
||||||
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
intent.addCategory(Intent.CATEGORY_OPENABLE)
|
||||||
intent.type = "application/*"
|
intent.type = "*/*"
|
||||||
val title = resources?.getString(R.string.select_backup_file)
|
val title = resources?.getString(R.string.select_backup_file)
|
||||||
val chooser = Intent.createChooser(intent, title)
|
val chooser = Intent.createChooser(intent, title)
|
||||||
startActivityForResult(chooser, CODE_BACKUP_RESTORE)
|
startActivityForResult(chooser, CODE_BACKUP_RESTORE)
|
||||||
|
@ -97,7 +97,7 @@ class SettingsBackupController : SettingsController() {
|
||||||
titleRes = R.string.automatic_backups
|
titleRes = R.string.automatic_backups
|
||||||
|
|
||||||
intListPreference(activity) {
|
intListPreference(activity) {
|
||||||
key = Keys.backupInterval
|
bindTo(preferences.backupInterval())
|
||||||
titleRes = R.string.backup_frequency
|
titleRes = R.string.backup_frequency
|
||||||
entriesRes = arrayOf(
|
entriesRes = arrayOf(
|
||||||
R.string.manual,
|
R.string.manual,
|
||||||
|
@ -108,109 +108,88 @@ class SettingsBackupController : SettingsController() {
|
||||||
R.string.weekly
|
R.string.weekly
|
||||||
)
|
)
|
||||||
entryValues = listOf(0, 6, 12, 24, 48, 168)
|
entryValues = listOf(0, 6, 12, 24, 48, 168)
|
||||||
defaultValue = 0
|
|
||||||
|
|
||||||
onChange { newValue ->
|
onChange { newValue ->
|
||||||
// Always cancel the previous task, it seems that sometimes they are not updated
|
|
||||||
|
|
||||||
val interval = newValue as Int
|
val interval = newValue as Int
|
||||||
BackupCreatorJob.setupTask(context, interval)
|
BackupCreatorJob.setupTask(context, interval)
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
preference {
|
preference {
|
||||||
key = Keys.backupDirectory
|
bindTo(preferences.backupsDirectory())
|
||||||
titleRes = R.string.backup_location
|
titleRes = R.string.backup_location
|
||||||
|
|
||||||
onClick {
|
onClick {
|
||||||
val currentDir = preferences.backupsDirectory().get()
|
|
||||||
try {
|
try {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||||
startActivityForResult(intent, CODE_BACKUP_DIR)
|
startActivityForResult(intent, CODE_BACKUP_DIR)
|
||||||
} catch (e: ActivityNotFoundException) {
|
} catch (e: ActivityNotFoundException) {
|
||||||
// Fall back to custom picker on error
|
activity?.toast(R.string.file_picker_error)
|
||||||
startActivityForResult(preferences.context.getFilePicker(currentDir), CODE_BACKUP_DIR)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
visibleIf(preferences.backupInterval()) { it > 0 }
|
||||||
|
|
||||||
preferences.backupsDirectory().asFlow()
|
preferences.backupsDirectory().asFlow()
|
||||||
.onEach { path ->
|
.onEach { path ->
|
||||||
val dir = UniFile.fromUri(context, path.toUri())
|
val dir = UniFile.fromUri(context, path.toUri())
|
||||||
summary = dir.filePath + "/automatic"
|
summary = dir.filePath + "/automatic"
|
||||||
}
|
}
|
||||||
.launchIn(viewScope)
|
.launchIn(viewScope)
|
||||||
preferences.backupInterval().asImmediateFlow { isVisible = it > 0 }
|
|
||||||
.launchIn(viewScope)
|
|
||||||
}
|
}
|
||||||
intListPreference(activity) {
|
intListPreference(activity) {
|
||||||
key = Keys.numberOfBackups
|
bindTo(preferences.numberOfBackups())
|
||||||
titleRes = R.string.max_auto_backups
|
titleRes = R.string.max_auto_backups
|
||||||
entries = listOf("1", "2", "3", "4", "5")
|
entries = (1..5).map(Int::toString)
|
||||||
entryRange = 1..5
|
entryRange = 1..5
|
||||||
defaultValue = 1
|
|
||||||
|
|
||||||
preferences.backupInterval().asImmediateFlow { isVisible = it > 0 }
|
visibleIf(preferences.backupInterval()) { it > 0 }
|
||||||
.launchIn(viewScope)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
infoPreference(R.string.backup_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.settings_backup, menu)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_backup_help -> activity?.openInBrowser(HELP_URL)
|
||||||
|
}
|
||||||
|
return super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (data != null && resultCode == Activity.RESULT_OK) {
|
if (data != null && resultCode == Activity.RESULT_OK) {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
val uri = data.data
|
val uri = data.data
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
activity.toast(R.string.backup_restore_invalid_uri)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
when (requestCode) {
|
when (requestCode) {
|
||||||
CODE_BACKUP_DIR -> {
|
CODE_BACKUP_DIR -> {
|
||||||
// Get UriPermission so it's possible to write files
|
// Get UriPermission so it's possible to write files
|
||||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
if (uri != null) {
|
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set backup Uri
|
|
||||||
preferences.backupsDirectory().set(uri.toString())
|
preferences.backupsDirectory().set(uri.toString())
|
||||||
}
|
}
|
||||||
CODE_BACKUP_CREATE -> {
|
CODE_BACKUP_CREATE -> {
|
||||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
|
||||||
if (uri != null) {
|
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
||||||
activity.contentResolver.takePersistableUriPermission(uri, flags)
|
|
||||||
}
|
|
||||||
|
|
||||||
val file = UniFile.fromUri(activity, uri)
|
|
||||||
|
|
||||||
activity.toast(R.string.creating_backup)
|
activity.toast(R.string.creating_backup)
|
||||||
|
BackupCreatorJob.startNow(activity, uri, backupFlags)
|
||||||
BackupCreateService.start(
|
|
||||||
activity,
|
|
||||||
file.uri,
|
|
||||||
backupFlags,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
CODE_BACKUP_RESTORE -> {
|
CODE_BACKUP_RESTORE -> {
|
||||||
uri?.path?.let {
|
RestoreBackupDialog(uri).showDialog(router)
|
||||||
val fileName = DocumentFile.fromSingleUri(activity, uri)?.name ?: uri.toString()
|
|
||||||
when {
|
|
||||||
fileName.endsWith(".proto.gz") -> {
|
|
||||||
RestoreBackupDialog(
|
|
||||||
uri,
|
|
||||||
BackupConst.BACKUP_TYPE_FULL
|
|
||||||
).showDialog(router)
|
|
||||||
}
|
|
||||||
fileName.endsWith(".json") -> {
|
|
||||||
RestoreBackupDialog(
|
|
||||||
uri,
|
|
||||||
BackupConst.BACKUP_TYPE_LEGACY
|
|
||||||
).showDialog(router)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
activity.toast(activity.getString(R.string.invalid_backup_file_type, fileName))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -218,7 +197,6 @@ class SettingsBackupController : SettingsController() {
|
||||||
|
|
||||||
fun createBackup(flags: Int) {
|
fun createBackup(flags: Int) {
|
||||||
backupFlags = flags
|
backupFlags = flags
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Use Android's built-in file creator
|
// Use Android's built-in file creator
|
||||||
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT)
|
||||||
|
@ -233,7 +211,6 @@ class SettingsBackupController : SettingsController() {
|
||||||
}
|
}
|
||||||
|
|
||||||
class CreateBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
class CreateBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val activity = activity!!
|
val activity = activity!!
|
||||||
val options = arrayOf(
|
val options = arrayOf(
|
||||||
|
@ -263,11 +240,11 @@ class SettingsBackupController : SettingsController() {
|
||||||
for (i in 1 until listView.count) {
|
for (i in 1 until listView.count) {
|
||||||
if (listView.isItemChecked(i)) {
|
if (listView.isItemChecked(i)) {
|
||||||
when (i) {
|
when (i) {
|
||||||
1 -> flags = flags or BackupCreateService.BACKUP_CATEGORY
|
1 -> flags = flags or BackupConst.BACKUP_CATEGORY
|
||||||
2 -> flags = flags or BackupCreateService.BACKUP_CHAPTER
|
2 -> flags = flags or BackupConst.BACKUP_CHAPTER
|
||||||
3 -> flags = flags or BackupCreateService.BACKUP_TRACK
|
3 -> flags = flags or BackupConst.BACKUP_TRACK
|
||||||
4 -> flags = flags or BackupCreateService.BACKUP_HISTORY
|
4 -> flags = flags or BackupConst.BACKUP_HISTORY
|
||||||
5 -> flags = flags or BackupCreateService.BACKUP_CUSTOM_INFO
|
5 -> flags = flags or BackupConst.BACKUP_CUSTOM_INFO
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -281,32 +258,28 @@ class SettingsBackupController : SettingsController() {
|
||||||
}
|
}
|
||||||
|
|
||||||
class RestoreBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
class RestoreBackupDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||||
constructor(uri: Uri, type: Int) : this(
|
constructor(uri: Uri) : this(
|
||||||
bundleOf(
|
bundleOf(KEY_URI to uri),
|
||||||
KEY_URI to uri,
|
|
||||||
KEY_TYPE to type
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||||
val activity = activity!!
|
val activity = activity!!
|
||||||
val uri: Uri = args.getParcelable(KEY_URI)!!
|
val uri: Uri = args.getParcelable(KEY_URI)!!
|
||||||
val type: Int = args.getInt(KEY_TYPE)
|
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
|
var type = BackupConst.BACKUP_TYPE_FULL
|
||||||
|
val results = try {
|
||||||
|
FullBackupRestoreValidator().validate(activity, uri)
|
||||||
|
} catch (_: ValidatorParseException) {
|
||||||
|
type = BackupConst.BACKUP_TYPE_LEGACY
|
||||||
|
LegacyBackupRestoreValidator().validate(activity, uri)
|
||||||
|
}
|
||||||
|
|
||||||
var message = if (type == BackupConst.BACKUP_TYPE_FULL) {
|
var message = if (type == BackupConst.BACKUP_TYPE_FULL) {
|
||||||
activity.getString(R.string.restore_content_full)
|
activity.getString(R.string.restore_content_full)
|
||||||
} else {
|
} else {
|
||||||
activity.getString(R.string.restore_content)
|
activity.getString(R.string.restore_content)
|
||||||
}
|
}
|
||||||
|
|
||||||
val validator = if (type == BackupConst.BACKUP_TYPE_FULL) {
|
|
||||||
FullBackupRestoreValidator()
|
|
||||||
} else {
|
|
||||||
LegacyBackupRestoreValidator()
|
|
||||||
}
|
|
||||||
|
|
||||||
val results = validator.validate(activity, uri)
|
|
||||||
if (results.missingSources.isNotEmpty()) {
|
if (results.missingSources.isNotEmpty()) {
|
||||||
message += "\n\n${activity.getString(R.string.restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
|
message += "\n\n${activity.getString(R.string.restore_missing_sources)}\n${results.missingSources.joinToString("\n") { "- $it" }}"
|
||||||
}
|
}
|
||||||
|
@ -332,16 +305,13 @@ class SettingsBackupController : SettingsController() {
|
||||||
.create()
|
.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val KEY_URI = "RestoreBackupDialog.uri"
|
|
||||||
const val KEY_TYPE = "RestoreBackupDialog.type"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private companion object {
|
|
||||||
const val CODE_BACKUP_DIR = 503
|
|
||||||
const val CODE_BACKUP_CREATE = 504
|
|
||||||
const val CODE_BACKUP_RESTORE = 505
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private const val KEY_URI = "RestoreBackupDialog.uri"
|
||||||
|
|
||||||
|
private const val CODE_BACKUP_DIR = 503
|
||||||
|
private const val CODE_BACKUP_CREATE = 504
|
||||||
|
private const val CODE_BACKUP_RESTORE = 505
|
||||||
|
|
||||||
|
private const val HELP_URL = "https://tachiyomi.org/help/guides/backups/"
|
||||||
|
|
11
app/src/main/res/menu/settings_backup.xml
Normal file
11
app/src/main/res/menu/settings_backup.xml
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_backup_help"
|
||||||
|
android:icon="@drawable/ic_help_24dp"
|
||||||
|
android:title="@string/help"
|
||||||
|
app:iconTint="?attr/colorOnSurface"
|
||||||
|
app:showAsAction="ifRoom" />
|
||||||
|
|
||||||
|
</menu>
|
|
@ -758,7 +758,9 @@
|
||||||
<string name="restore_content">Restore uses sources 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_content">Restore uses sources 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_content_full">Data from the backup file will be restored.\n\nYou will need to install any missing extensions and log in to tracking services afterwards to use them.</string>
|
<string name="restore_content_full">Data from the backup file will be restored.\n\nYou will need to install any missing extensions and log in to tracking services afterwards to use them.</string>
|
||||||
<string name="restoring_backup_canceled">Canceled restore</string>
|
<string name="restoring_backup_canceled">Canceled restore</string>
|
||||||
|
<string name="backup_restore_invalid_uri">Error: empty URI</string>
|
||||||
<string name="creating_backup">Creating backup</string>
|
<string name="creating_backup">Creating backup</string>
|
||||||
|
<string name="backup_info">Automatic backups are highly recommended. You should keep copies in other places as well.</string>
|
||||||
<string name="what_should_backup">What do you want to backup?</string>
|
<string name="what_should_backup">What do you want to backup?</string>
|
||||||
<string name="restoring_backup">Restoring backup</string>
|
<string name="restoring_backup">Restoring backup</string>
|
||||||
<string name="restore_duration">%02d min, %02d sec</string>
|
<string name="restore_duration">%02d min, %02d sec</string>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue