Refactor backup service

This commit is contained in:
arkon
2020-04-26 16:07:07 -04:00
parent 52474e39d9
commit d19d787f6e
8 changed files with 109 additions and 114 deletions
@@ -4,10 +4,7 @@ import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
object BackupConst {
const val INTENT_FILTER = "SettingsBackupFragment"
const val ACTION_BACKUP_COMPLETED = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED"
const val ACTION_BACKUP_ERROR = "$ID.$INTENT_FILTER.ACTION_BACKUP_ERROR"
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
private const val NAME = "BackupRestoreServices"
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
}
@@ -1,25 +1,22 @@
package eu.kanade.tachiyomi.data.backup
import android.app.IntentService
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
import eu.kanade.tachiyomi.data.database.models.Manga
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.isServiceRunning
/**
* [IntentService] used to backup [Manga] information to [JsonArray]
* Service for backing up library information to a JSON file.
*/
class BackupCreateService : IntentService(NAME) {
class BackupCreateService : Service() {
companion object {
// Name of class
private const val NAME = "BackupCreateService"
// Options for backup
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
// Filter options
internal const val BACKUP_CATEGORY = 0x1
internal const val BACKUP_CATEGORY_MASK = 0x1
@@ -31,6 +28,15 @@ class BackupCreateService : IntentService(NAME) {
internal const val BACKUP_TRACK_MASK = 0x8
internal const val BACKUP_ALL = 0xF
/**
* 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
*
@@ -38,24 +44,78 @@ class BackupCreateService : IntentService(NAME) {
* @param uri path of Uri
* @param flags determines what to backup
*/
fun makeBackup(context: Context, uri: Uri, flags: Int) {
val intent = Intent(context, BackupCreateService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
putExtra(EXTRA_FLAGS, flags)
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)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
}
context.startService(intent)
}
}
private val backupManager by lazy { BackupManager(this) }
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
override fun onHandleIntent(intent: Intent?) {
if (intent == null) return
private lateinit var backupManager: BackupManager
private lateinit var notifier: BackupNotifier
// Get values
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
// Create backup
backupManager.createBackup(uri, flags, false)
override fun onCreate() {
super.onCreate()
notifier = BackupNotifier(this)
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
)
wakeLock.acquire()
}
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)
backupManager = BackupManager(this)
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false))
val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile)
} catch (e: Exception) {
notifier.showBackupError(e.message)
}
stopSelf(startId)
return START_NOT_STICKY
}
}
@@ -20,9 +20,10 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
val backupManager = BackupManager(context)
val uri = Uri.parse(preferences.backupsDirectory().get())
val flags = BackupCreateService.BACKUP_ALL
return if (backupManager.createBackup(uri, flags, true)) {
return try {
backupManager.createBackup(uri, flags, true)
Result.success()
} else {
} catch (e: Exception) {
Result.failure()
}
}
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.github.salomonbrys.kotson.fromJson
import com.github.salomonbrys.kotson.registerTypeAdapter
@@ -49,7 +48,6 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.system.sendLocalBroadcast
import kotlin.math.max
import rx.Observable
import timber.log.Timber
@@ -102,7 +100,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @param uri path of Uri
* @param isJob backup called from job
*/
fun createBackup(uri: Uri, flags: Int, isJob: Boolean): Boolean {
fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
// Create root object
val root = JsonObject()
@@ -155,6 +153,8 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
newFile.openOutputStream().bufferedWriter().use {
parser.toJson(root, it)
}
return newFile.uri.toString()
} else {
val file = UniFile.fromUri(context, uri)
?: throw Exception("Couldn't create backup file")
@@ -162,25 +162,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
parser.toJson(root, it)
}
// Show completed dialog
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED)
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
}
context.sendLocalBroadcast(intent)
return file.uri.toString()
}
return true
} catch (e: Exception) {
Timber.e(e)
if (!isJob) {
// Show error dialog
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_ERROR)
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
}
context.sendLocalBroadcast(intent)
}
return false
throw e
}
}
@@ -0,0 +1,166 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import java.io.File
import java.util.concurrent.TimeUnit
internal class BackupNotifier(private val context: Context) {
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachi)
setAutoCancel(false)
}
private fun NotificationCompat.Builder.show(id: Int) {
context.notificationManager.notify(id, build())
}
fun showBackupProgress(): NotificationCompat.Builder {
val builder = with(notificationBuilder) {
setContentTitle(context.getString(R.string.creating_backup))
setProgress(0, 0, true)
setOngoing(true)
}
builder.show(Notifications.ID_BACKUP_PROGRESS)
return builder
}
fun showBackupError(error: String?) {
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.creating_backup_error))
setContentText(error)
// Remove progress bar
setProgress(0, 0, false)
setOngoing(false)
}
notificationBuilder.show(Notifications.ID_BACKUP_COMPLETE)
}
fun showBackupComplete(unifile: UniFile) {
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.backup_created))
if (unifile.filePath != null) {
setContentText(unifile.filePath)
}
// Remove progress bar
setProgress(0, 0, false)
setOngoing(false)
// Clear old actions if they exist
if (mActions.isNotEmpty()) {
mActions.clear()
}
addAction(
R.drawable.ic_share_24dp,
context.getString(R.string.action_share),
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
)
}
notificationBuilder.show(Notifications.ID_BACKUP_COMPLETE)
}
fun showRestoreProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder {
val builder = with(notificationBuilder) {
setContentTitle(context.getString(R.string.restoring_backup))
setContentText(content)
setProgress(maxAmount, progress, false)
setOngoing(true)
// Clear old actions if they exist
if (mActions.isNotEmpty()) {
mActions.clear()
}
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.action_stop),
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS)
)
}
builder.show(Notifications.ID_RESTORE_PROGRESS)
return builder
}
fun showRestoreError(error: String?) {
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
with(notificationBuilder) {
setContentTitle(context.getString(R.string.restoring_backup_error))
setContentText(error)
// Remove progress bar
setProgress(0, 0, false)
setOngoing(false)
}
notificationBuilder.show(Notifications.ID_RESTORE_COMPLETE)
}
fun showRestoreComplete(time: Long, errorCount: Int, path: String?, file: String?) {
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
val timeString = context.getString(
R.string.restore_duration,
TimeUnit.MILLISECONDS.toMinutes(time),
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(time)
)
)
with(notificationBuilder) {
setSmallIcon(R.drawable.ic_tachi)
setAutoCancel(false)
setContentTitle(context.getString(R.string.restore_completed))
setContentText(context.getString(R.string.restore_completed_content, timeString, errorCount))
// Remove progress bar
setProgress(0, 0, false)
setOngoing(false)
// Clear old actions if they exist
if (mActions.isNotEmpty()) {
mActions.clear()
}
if (errorCount > 0 && !path.isNullOrEmpty() && !file.isNullOrEmpty()) {
val destFile = File(path, file)
val uri = destFile.getUriCompat(context)
addAction(
R.drawable.nnf_ic_file_folder,
context.getString(R.string.action_open_log),
NotificationReceiver.openErrorLogPendingActivity(context, uri)
)
}
}
notificationBuilder.show(Notifications.ID_RESTORE_COMPLETE)
}
}
@@ -32,7 +32,6 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.setting.backup.BackupNotifier
import eu.kanade.tachiyomi.util.system.isServiceRunning
import java.io.File
import java.text.SimpleDateFormat
@@ -47,7 +46,7 @@ import timber.log.Timber
import uy.kohesive.injekt.injectLazy
/**
* Restores backup from json file
* Restores backup from a JSON file.
*/
class BackupRestoreService : Service() {
@@ -116,16 +115,12 @@ class BackupRestoreService : Service() {
private val errors = mutableListOf<Pair<Date, String>>()
private lateinit var backupManager: BackupManager
private lateinit var notifier: BackupNotifier
private val db: DatabaseHelper by injectLazy()
private val trackManager: TrackManager by injectLazy()
private lateinit var notifier: BackupNotifier
/**
* Method called when the service is created. It injects dependencies and acquire the wake lock.
*/
override fun onCreate() {
super.onCreate()
notifier = BackupNotifier(this)
@@ -133,7 +128,7 @@ class BackupRestoreService : Service() {
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock"
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
)
wakeLock.acquire()
}