More updates to backup logic from upstream

Removed BackupCreateService among other things
This commit is contained in:
Jays2Kings 2022-04-26 02:23:32 -04:00
parent 3587feb8fa
commit ce3a774e05
9 changed files with 173 additions and 238 deletions

View file

@ -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"/>

View file

@ -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

View file

@ -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
} }

View file

@ -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
}
}

View file

@ -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

View file

@ -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()
} }
/** /**

View file

@ -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/"

View 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>

View file

@ -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>