Change periodical backup creation
This commit is contained in:
@@ -16,8 +16,8 @@ android {
|
||||
applicationId 'org.koitharu.kotatsu'
|
||||
minSdk = 21
|
||||
targetSdk = 35
|
||||
versionCode = 683
|
||||
versionName = '7.7-a4'
|
||||
versionCode = 685
|
||||
versionName = '7.7-a6'
|
||||
generatedDensities = []
|
||||
testInstrumentationRunner 'org.koitharu.kotatsu.HiltTestRunner'
|
||||
ksp {
|
||||
@@ -82,7 +82,7 @@ afterEvaluate {
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:1.4') {
|
||||
implementation('com.github.KotatsuApp:kotatsu-parsers:79e1d59482') {
|
||||
exclude group: 'org.json', module: 'json'
|
||||
}
|
||||
|
||||
@@ -134,9 +134,10 @@ dependencies {
|
||||
implementation 'androidx.hilt:hilt-work:1.2.0'
|
||||
kapt 'androidx.hilt:hilt-compiler:1.2.0'
|
||||
|
||||
implementation 'io.coil-kt.coil3:coil-core:3.0.0-rc01'
|
||||
implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc01'
|
||||
implementation 'io.coil-kt.coil3:coil-gif:3.0.0-rc01'
|
||||
implementation 'io.coil-kt.coil3:coil-core:3.0.0-rc02'
|
||||
implementation 'io.coil-kt.coil3:coil-network-okhttp:3.0.0-rc02'
|
||||
implementation 'io.coil-kt.coil3:coil-gif:3.0.0-rc02'
|
||||
implementation 'io.coil-kt.coil3:coil-svg:3.0.0-rc02'
|
||||
implementation 'org.aomedia.avif.android:avif:1.1.1.14d8e3c4'
|
||||
implementation 'com.github.KotatsuApp:subsampling-scale-image-view:d1d10a6975'
|
||||
implementation 'com.github.solkin:disk-lru-cache:1.4'
|
||||
|
||||
@@ -2,6 +2,7 @@ package org.koitharu.kotatsu.core.network
|
||||
|
||||
import android.util.Log
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okio.Buffer
|
||||
import org.koitharu.kotatsu.core.network.CommonHeaders.ACCEPT_ENCODING
|
||||
@@ -12,8 +13,11 @@ class CurlLoggingInterceptor(
|
||||
|
||||
private val escapeRegex = Regex("([\\[\\]\"])")
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(chain.request()).also {
|
||||
logRequest(it.networkResponse?.request ?: it.request)
|
||||
}
|
||||
|
||||
private fun logRequest(request: Request) {
|
||||
var isCompressed = false
|
||||
|
||||
val curlCmd = StringBuilder()
|
||||
@@ -46,8 +50,6 @@ class CurlLoggingInterceptor(
|
||||
|
||||
log("---cURL (" + request.url + ")")
|
||||
log(curlCmd.toString())
|
||||
|
||||
return chain.proceed(request)
|
||||
}
|
||||
|
||||
private fun String.escape() = replace(escapeRegex) { match ->
|
||||
|
||||
@@ -266,19 +266,26 @@
|
||||
tools:node="merge" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:label="@string/local_manga_processing" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.ImportService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
android:name="org.koitharu.kotatsu.settings.backup.PeriodicalBackupService"
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:label="@string/periodic_backups" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.alternatives.ui.AutoFixService"
|
||||
android:foregroundServiceType="dataSync" />
|
||||
<service android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService" />
|
||||
android:foregroundServiceType="dataSync"
|
||||
android:label="@string/fixing_manga" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.local.ui.LocalIndexUpdateService"
|
||||
android:label="@string/local_manga_processing" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
|
||||
android:label="@string/manga_shelf"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.widget.recent.RecentWidgetService"
|
||||
android:label="@string/recent_manga"
|
||||
android:permission="android.permission.BIND_REMOTEVIEWS" />
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.sync.ui.SyncAuthenticatorService"
|
||||
@@ -315,7 +322,8 @@
|
||||
</service>
|
||||
<service
|
||||
android:name="org.koitharu.kotatsu.details.service.MangaPrefetchService"
|
||||
android:exported="false" />
|
||||
android:exported="false"
|
||||
android:label="@string/prefetch_content" />
|
||||
|
||||
<provider
|
||||
android:name="org.koitharu.kotatsu.search.ui.MangaSuggestionsProvider"
|
||||
|
||||
@@ -48,25 +48,21 @@ class AutoFixService : CoroutineIntentService() {
|
||||
notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
}
|
||||
|
||||
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||
val ids = requireNotNull(intent.getLongArrayExtra(DATA_IDS))
|
||||
startForeground(startId)
|
||||
try {
|
||||
for (mangaId in ids) {
|
||||
val result = runCatchingCancellable {
|
||||
autoFixUseCase.invoke(mangaId)
|
||||
}
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = buildNotification(result)
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
}
|
||||
startForeground(this)
|
||||
for (mangaId in ids) {
|
||||
val result = runCatchingCancellable {
|
||||
autoFixUseCase.invoke(mangaId)
|
||||
}
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = buildNotification(result)
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
}
|
||||
} finally {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(startId: Int, error: Throwable) {
|
||||
override fun IntentJobContext.onError(error: Throwable) {
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = runBlocking { buildNotification(Result.failure(error)) }
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
@@ -74,7 +70,7 @@ class AutoFixService : CoroutineIntentService() {
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun startForeground(startId: Int) {
|
||||
private fun startForeground(jobContext: IntentJobContext) {
|
||||
val title = applicationContext.getString(R.string.fixing_manga)
|
||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_MIN)
|
||||
.setName(title)
|
||||
@@ -98,12 +94,11 @@ class AutoFixService : CoroutineIntentService() {
|
||||
.addAction(
|
||||
materialR.drawable.material_ic_clear_black_24dp,
|
||||
applicationContext.getString(android.R.string.cancel),
|
||||
getCancelIntent(startId),
|
||||
jobContext.getCancelIntent(),
|
||||
)
|
||||
.build()
|
||||
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
jobContext.setForeground(
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
|
||||
@@ -15,6 +15,7 @@ import coil3.gif.AnimatedImageDecoder
|
||||
import coil3.gif.GifDecoder
|
||||
import coil3.network.okhttp.OkHttpNetworkFetcherFactory
|
||||
import coil3.request.allowRgb565
|
||||
import coil3.svg.SvgDecoder
|
||||
import coil3.util.DebugLogger
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
@@ -126,6 +127,7 @@ interface AppModule {
|
||||
} else {
|
||||
add(GifDecoder.Factory())
|
||||
}
|
||||
add(SvgDecoder.Factory())
|
||||
add(CbzFetcher.Factory())
|
||||
add(AvifImageDecoder.Factory())
|
||||
add(FaviconFetcher.Factory(mangaRepositoryFactory))
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import android.net.Uri
|
||||
import java.time.LocalDateTime
|
||||
|
||||
data class BackupFile(
|
||||
val uri: Uri,
|
||||
val dateTime: LocalDateTime,
|
||||
): Comparable<BackupFile> {
|
||||
|
||||
override fun compareTo(other: BackupFile): Int = compareValues(dateTime, other.dateTime)
|
||||
}
|
||||
@@ -5,10 +5,12 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.Closeable
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import org.koitharu.kotatsu.core.zip.ZipOutput
|
||||
import java.io.File
|
||||
import java.time.LocalDate
|
||||
import java.time.LocalDateTime
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.DateTimeParseException
|
||||
import java.util.Locale
|
||||
import java.util.zip.Deflater
|
||||
|
||||
@@ -27,20 +29,32 @@ class BackupZipOutput(val file: File) : Closeable {
|
||||
override fun close() {
|
||||
output.close()
|
||||
}
|
||||
}
|
||||
|
||||
const val DIR_BACKUPS = "backups"
|
||||
companion object {
|
||||
|
||||
suspend fun BackupZipOutput(context: Context): BackupZipOutput = runInterruptible(Dispatchers.IO) {
|
||||
val dir = context.run {
|
||||
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
|
||||
const val DIR_BACKUPS = "backups"
|
||||
private val dateTimeFormat = DateTimeFormatter.ofPattern("yyyyMMdd-HHmm")
|
||||
|
||||
fun generateFileName(context: Context) = buildString {
|
||||
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
|
||||
append('_')
|
||||
append(LocalDateTime.now().format(dateTimeFormat))
|
||||
append(".bk.zip")
|
||||
}
|
||||
|
||||
fun parseBackupDateTime(fileName: String): LocalDateTime? = try {
|
||||
LocalDateTime.parse(fileName.substringAfterLast('_').substringBefore('.'), dateTimeFormat)
|
||||
} catch (e: DateTimeParseException) {
|
||||
e.printStackTraceDebug()
|
||||
null
|
||||
}
|
||||
|
||||
suspend fun createTemp(context: Context): BackupZipOutput = runInterruptible(Dispatchers.IO) {
|
||||
val dir = context.run {
|
||||
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
|
||||
}
|
||||
dir.mkdirs()
|
||||
BackupZipOutput(File(dir, generateFileName(context)))
|
||||
}
|
||||
}
|
||||
dir.mkdirs()
|
||||
val filename = buildString {
|
||||
append(context.getString(R.string.app_name).replace(' ', '_').lowercase(Locale.ROOT))
|
||||
append('_')
|
||||
append(LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")))
|
||||
append(".bk.zip")
|
||||
}
|
||||
BackupZipOutput(File(dir, filename))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package org.koitharu.kotatsu.core.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.runInterruptible
|
||||
import okio.IOException
|
||||
import okio.buffer
|
||||
import okio.sink
|
||||
import okio.source
|
||||
import org.jetbrains.annotations.Blocking
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class ExternalBackupStorage @Inject constructor(
|
||||
@ApplicationContext private val context: Context,
|
||||
private val settings: AppSettings,
|
||||
) {
|
||||
|
||||
suspend fun list(): List<BackupFile> = runInterruptible(Dispatchers.IO) {
|
||||
getRoot().listFiles().mapNotNull {
|
||||
if (it.isFile && it.canRead()) {
|
||||
BackupFile(
|
||||
uri = it.uri,
|
||||
dateTime = it.name?.let { fileName ->
|
||||
BackupZipOutput.parseBackupDateTime(fileName)
|
||||
} ?: return@mapNotNull null,
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}.sortedDescending()
|
||||
}
|
||||
|
||||
suspend fun put(file: File): Uri = runInterruptible(Dispatchers.IO) {
|
||||
val out = checkNotNull(getRoot().createFile("application/zip", file.nameWithoutExtension)) {
|
||||
"Cannot create target backup file"
|
||||
}
|
||||
checkNotNull(context.contentResolver.openOutputStream(out.uri, "wt")).sink().use { sink ->
|
||||
file.source().buffer().use { src ->
|
||||
src.readAll(sink)
|
||||
}
|
||||
}
|
||||
out.uri
|
||||
}
|
||||
|
||||
suspend fun delete(victim: BackupFile) = runInterruptible(Dispatchers.IO) {
|
||||
val df = checkNotNull(DocumentFile.fromSingleUri(context, victim.uri)) {
|
||||
"${victim.uri} cannot be resolved to the DocumentFile"
|
||||
}
|
||||
if (!df.delete()) {
|
||||
throw IOException("Cannot delete ${df.uri}")
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getLastBackupDate() = list().maxByOrNull { it.dateTime }?.dateTime
|
||||
|
||||
suspend fun trim(maxCount: Int) {
|
||||
list().drop(maxCount).forEach {
|
||||
delete(it)
|
||||
}
|
||||
}
|
||||
|
||||
@Blocking
|
||||
private fun getRoot(): DocumentFile {
|
||||
val uri = checkNotNull(settings.periodicalBackupDirectory) {
|
||||
"Backup directory is not specified"
|
||||
}
|
||||
val root = DocumentFile.fromTreeUri(context, uri)
|
||||
return checkNotNull(root) { "Cannot obtain DocumentFile from $uri" }
|
||||
}
|
||||
}
|
||||
@@ -473,7 +473,10 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
val periodicalBackupFrequency: Long
|
||||
get() = prefs.getString(KEY_BACKUP_PERIODICAL_FREQUENCY, null)?.toLongOrNull() ?: 7L
|
||||
|
||||
var periodicalBackupOutput: Uri?
|
||||
val periodicalBackupMaxCount: Int
|
||||
get() = prefs.getInt(KEY_BACKUP_PERIODICAL_COUNT, 10)
|
||||
|
||||
var periodicalBackupDirectory: Uri?
|
||||
get() = prefs.getString(KEY_BACKUP_PERIODICAL_OUTPUT, null)?.toUriOrNull()
|
||||
set(value) = prefs.edit { putString(KEY_BACKUP_PERIODICAL_OUTPUT, value?.toString()) }
|
||||
|
||||
@@ -621,6 +624,7 @@ class AppSettings @Inject constructor(@ApplicationContext context: Context) {
|
||||
const val KEY_RESTORE = "restore"
|
||||
const val KEY_BACKUP_PERIODICAL_ENABLED = "backup_periodic"
|
||||
const val KEY_BACKUP_PERIODICAL_FREQUENCY = "backup_periodic_freq"
|
||||
const val KEY_BACKUP_PERIODICAL_COUNT = "backup_periodic_count"
|
||||
const val KEY_BACKUP_PERIODICAL_OUTPUT = "backup_periodic_output"
|
||||
const val KEY_BACKUP_PERIODICAL_LAST = "backup_periodic_last"
|
||||
const val KEY_HISTORY_GROUPING = "history_grouping"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package org.koitharu.kotatsu.core.ui
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
@@ -9,11 +10,10 @@ import android.os.PatternMatcher
|
||||
import androidx.annotation.AnyThread
|
||||
import androidx.annotation.WorkerThread
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.CoroutineExceptionHandler
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -21,60 +21,104 @@ import kotlinx.coroutines.sync.Mutex
|
||||
import kotlinx.coroutines.sync.withLock
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class CoroutineIntentService : BaseService() {
|
||||
|
||||
private val mutex = Mutex()
|
||||
protected open val dispatcher: CoroutineDispatcher = Dispatchers.Default
|
||||
|
||||
final override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
super.onStartCommand(intent, flags, startId)
|
||||
val job = launchCoroutine(intent, startId)
|
||||
val receiver = CancelReceiver(job)
|
||||
ContextCompat.registerReceiver(
|
||||
this,
|
||||
receiver,
|
||||
createIntentFilter(this, startId),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||
)
|
||||
job.invokeOnCompletion { unregisterReceiver(receiver) }
|
||||
launchCoroutine(intent, startId)
|
||||
return START_REDELIVER_INTENT
|
||||
}
|
||||
|
||||
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch(errorHandler(startId)) {
|
||||
private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch {
|
||||
val intentJobContext = IntentJobContextImpl(startId, coroutineContext)
|
||||
mutex.withLock {
|
||||
try {
|
||||
if (intent != null) {
|
||||
withContext(dispatcher) {
|
||||
processIntent(startId, intent)
|
||||
withContext(Dispatchers.Default) {
|
||||
intentJobContext.processIntent(intent)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTraceDebug()
|
||||
onError(startId, e)
|
||||
intentJobContext.onError(e)
|
||||
} finally {
|
||||
stopSelf(startId)
|
||||
intentJobContext.stop()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
protected abstract suspend fun processIntent(startId: Int, intent: Intent)
|
||||
protected abstract suspend fun IntentJobContext.processIntent(intent: Intent)
|
||||
|
||||
@AnyThread
|
||||
protected abstract fun onError(startId: Int, error: Throwable)
|
||||
protected abstract fun IntentJobContext.onError(error: Throwable)
|
||||
|
||||
protected fun getCancelIntent(startId: Int) = PendingIntentCompat.getBroadcast(
|
||||
this,
|
||||
0,
|
||||
createCancelIntent(this, startId),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
false,
|
||||
)
|
||||
interface IntentJobContext {
|
||||
|
||||
private fun errorHandler(startId: Int) = CoroutineExceptionHandler { _, throwable ->
|
||||
throwable.printStackTraceDebug()
|
||||
onError(startId, throwable)
|
||||
val startId: Int
|
||||
|
||||
fun getCancelIntent(): PendingIntent?
|
||||
|
||||
fun setForeground(id: Int, notification: Notification, serviceType: Int)
|
||||
}
|
||||
|
||||
protected inner class IntentJobContextImpl(
|
||||
override val startId: Int,
|
||||
private val coroutineContext: CoroutineContext,
|
||||
) : IntentJobContext {
|
||||
|
||||
private var cancelReceiver: CancelReceiver? = null
|
||||
private var isStopped = false
|
||||
private var isForeground = false
|
||||
|
||||
override fun getCancelIntent(): PendingIntent? {
|
||||
ensureHasCancelReceiver()
|
||||
return PendingIntentCompat.getBroadcast(
|
||||
applicationContext,
|
||||
0,
|
||||
createCancelIntent(this@CoroutineIntentService, startId),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT,
|
||||
false,
|
||||
)
|
||||
}
|
||||
|
||||
override fun setForeground(id: Int, notification: Notification, serviceType: Int) {
|
||||
ServiceCompat.startForeground(this@CoroutineIntentService, id, notification, serviceType)
|
||||
isForeground = true
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
synchronized(this) {
|
||||
cancelReceiver?.let { unregisterReceiver(it) }
|
||||
isStopped = true
|
||||
}
|
||||
if (isForeground) {
|
||||
ServiceCompat.stopForeground(this@CoroutineIntentService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
stopSelf(startId)
|
||||
}
|
||||
|
||||
private fun ensureHasCancelReceiver() {
|
||||
if (cancelReceiver == null && !isStopped) {
|
||||
synchronized(this) {
|
||||
if (cancelReceiver == null && !isStopped) {
|
||||
val job = coroutineContext[Job] ?: return
|
||||
cancelReceiver = CancelReceiver(job).also { receiver ->
|
||||
ContextCompat.registerReceiver(
|
||||
applicationContext,
|
||||
receiver,
|
||||
createIntentFilter(this@CoroutineIntentService, startId),
|
||||
ContextCompat.RECEIVER_NOT_EXPORTED,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class CancelReceiver(
|
||||
|
||||
@@ -34,7 +34,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
@Inject
|
||||
lateinit var historyRepository: HistoryRepository
|
||||
|
||||
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||
when (intent.action) {
|
||||
ACTION_PREFETCH_DETAILS -> prefetchDetails(
|
||||
manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga
|
||||
@@ -50,7 +50,7 @@ class MangaPrefetchService : CoroutineIntentService() {
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(startId: Int, error: Throwable) = Unit
|
||||
override fun IntentJobContext.onError(error: Throwable) = Unit
|
||||
|
||||
private suspend fun prefetchDetails(manga: Manga) {
|
||||
val source = mangaRepositoryFactory.create(manga.source)
|
||||
|
||||
@@ -196,6 +196,9 @@ class MangaIndex(source: String?) {
|
||||
@Blocking
|
||||
@WorkerThread
|
||||
fun read(fileSystem: FileSystem, path: Path): MangaIndex? = runCatchingCancellable {
|
||||
if (!fileSystem.exists(path)) {
|
||||
return@runCatchingCancellable null
|
||||
}
|
||||
val text = fileSystem.source(path).use {
|
||||
it.buffer().use { buffer ->
|
||||
buffer.readUtf8()
|
||||
|
||||
@@ -11,7 +11,6 @@ import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.PendingIntentCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import coil3.ImageLoader
|
||||
import coil3.request.ImageRequest
|
||||
@@ -48,23 +47,19 @@ class ImportService : CoroutineIntentService() {
|
||||
notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||
}
|
||||
|
||||
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||
val uri = requireNotNull(intent.getStringExtra(DATA_URI)?.toUriOrNull()) { "No input uri" }
|
||||
startForeground()
|
||||
try {
|
||||
val result = runCatchingCancellable {
|
||||
importer.import(uri).manga
|
||||
}
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = buildNotification(result)
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
}
|
||||
} finally {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
startForeground(this)
|
||||
val result = runCatchingCancellable {
|
||||
importer.import(uri).manga
|
||||
}
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = buildNotification(result)
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onError(startId: Int, error: Throwable) {
|
||||
override fun IntentJobContext.onError(error: Throwable) {
|
||||
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
|
||||
val notification = runBlocking { buildNotification(Result.failure(error)) }
|
||||
notificationManager.notify(TAG, startId, notification)
|
||||
@@ -72,7 +67,7 @@ class ImportService : CoroutineIntentService() {
|
||||
}
|
||||
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun startForeground() {
|
||||
private fun startForeground(jobContext: IntentJobContext) {
|
||||
val title = applicationContext.getString(R.string.importing_manga)
|
||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_DEFAULT)
|
||||
.setName(title)
|
||||
@@ -95,8 +90,7 @@ class ImportService : CoroutineIntentService() {
|
||||
.setCategory(NotificationCompat.CATEGORY_PROGRESS)
|
||||
.build()
|
||||
|
||||
ServiceCompat.startForeground(
|
||||
this,
|
||||
jobContext.setForeground(
|
||||
FOREGROUND_NOTIFICATION_ID,
|
||||
notification,
|
||||
ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
package org.koitharu.kotatsu.local.ui
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.ServiceInfo
|
||||
import androidx.core.app.NotificationChannelCompat
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.app.NotificationManagerCompat
|
||||
import androidx.core.app.ServiceCompat
|
||||
import androidx.core.content.ContextCompat
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
@@ -42,21 +43,17 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||
val manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
|
||||
val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return
|
||||
startForeground()
|
||||
try {
|
||||
val mangaWithChapters = localMangaRepository.getDetails(manga)
|
||||
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
|
||||
localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga)))
|
||||
} finally {
|
||||
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
}
|
||||
startForeground(this)
|
||||
val mangaWithChapters = localMangaRepository.getDetails(manga)
|
||||
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
|
||||
localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga)))
|
||||
}
|
||||
|
||||
override fun onError(startId: Int, error: Throwable) {
|
||||
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
|
||||
override fun IntentJobContext.onError(error: Throwable) {
|
||||
val notification = NotificationCompat.Builder(applicationContext, CHANNEL_ID)
|
||||
.setContentTitle(getString(R.string.error_occurred))
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setDefaults(0)
|
||||
@@ -64,13 +61,14 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
|
||||
.setContentText(error.getDisplayMessage(resources))
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setAutoCancel(true)
|
||||
.setContentIntent(ErrorReporterReceiver.getPendingIntent(this, error))
|
||||
.setContentIntent(ErrorReporterReceiver.getPendingIntent(applicationContext, error))
|
||||
.build()
|
||||
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.notify(NOTIFICATION_ID + startId, notification)
|
||||
}
|
||||
|
||||
private fun startForeground() {
|
||||
@SuppressLint("InlinedApi")
|
||||
private fun startForeground(jobContext: IntentJobContext) {
|
||||
val title = getString(R.string.local_manga_processing)
|
||||
val manager = NotificationManagerCompat.from(this)
|
||||
val channel = NotificationChannelCompat.Builder(CHANNEL_ID, NotificationManagerCompat.IMPORTANCE_LOW)
|
||||
@@ -92,7 +90,7 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
|
||||
.setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED)
|
||||
.setOngoing(false)
|
||||
.build()
|
||||
startForeground(NOTIFICATION_ID, notification)
|
||||
jobContext.setForeground(NOTIFICATION_ID, notification, ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
@@ -12,9 +12,9 @@ class LocalIndexUpdateService : CoroutineIntentService() {
|
||||
@Inject
|
||||
lateinit var localMangaIndex: LocalMangaIndex
|
||||
|
||||
override suspend fun processIntent(startId: Int, intent: Intent) {
|
||||
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||
localMangaIndex.update()
|
||||
}
|
||||
|
||||
override fun onError(startId: Int, error: Throwable) = Unit
|
||||
override fun IntentJobContext.onError(error: Throwable) = Unit
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionListener
|
||||
import org.koitharu.kotatsu.search.ui.suggestion.SearchSuggestionViewModel
|
||||
import org.koitharu.kotatsu.settings.SettingsActivity
|
||||
import org.koitharu.kotatsu.settings.about.AppUpdateActivity
|
||||
import org.koitharu.kotatsu.settings.backup.PeriodicalBackupService
|
||||
import javax.inject.Inject
|
||||
import com.google.android.material.R as materialR
|
||||
|
||||
@@ -353,6 +354,7 @@ class MainActivity : BaseActivity<ActivityMainBinding>(), AppBarOwner, BottomNav
|
||||
requestNotificationsPermission()
|
||||
}
|
||||
startService(Intent(this@MainActivity, LocalIndexUpdateService::class.java))
|
||||
startService(Intent(this@MainActivity, PeriodicalBackupService::class.java))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -68,7 +68,7 @@ class AppBackupAgent : BackupAgent() {
|
||||
|
||||
@VisibleForTesting
|
||||
fun createBackupFile(context: Context, repository: BackupRepository) = runBlocking {
|
||||
BackupZipOutput(context).use { backup ->
|
||||
BackupZipOutput.createTemp(context).use { backup ->
|
||||
backup.put(repository.createIndex())
|
||||
backup.put(repository.dumpHistory())
|
||||
backup.put(repository.dumpCategories())
|
||||
|
||||
@@ -23,7 +23,7 @@ class BackupViewModel @Inject constructor(
|
||||
|
||||
init {
|
||||
launchLoadingJob {
|
||||
val file = BackupZipOutput(context).use { backup ->
|
||||
val file = BackupZipOutput.createTemp(context).use { backup ->
|
||||
val step = 1f / 6f
|
||||
backup.put(repository.createIndex())
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Intent
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
import org.koitharu.kotatsu.core.backup.ExternalBackupStorage
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.CoroutineIntentService
|
||||
import java.time.LocalDateTime
|
||||
import java.time.temporal.ChronoUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class PeriodicalBackupService : CoroutineIntentService() {
|
||||
|
||||
@Inject
|
||||
lateinit var externalBackupStorage: ExternalBackupStorage
|
||||
|
||||
@Inject
|
||||
lateinit var repository: BackupRepository
|
||||
|
||||
@Inject
|
||||
lateinit var settings: AppSettings
|
||||
|
||||
override suspend fun IntentJobContext.processIntent(intent: Intent) {
|
||||
if (!settings.isPeriodicalBackupEnabled || settings.periodicalBackupDirectory == null) {
|
||||
return
|
||||
}
|
||||
val lastBackupDate = externalBackupStorage.getLastBackupDate()
|
||||
if (lastBackupDate != null && lastBackupDate.plus(settings.periodicalBackupFrequency, ChronoUnit.MILLIS)
|
||||
.isAfter(LocalDateTime.now())
|
||||
) {
|
||||
return
|
||||
}
|
||||
val output = BackupZipOutput.createTemp(applicationContext)
|
||||
try {
|
||||
output.use { backup ->
|
||||
backup.put(repository.createIndex())
|
||||
backup.put(repository.dumpHistory())
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSources())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
}
|
||||
externalBackupStorage.put(output.file)
|
||||
externalBackupStorage.trim(settings.periodicalBackupMaxCount)
|
||||
} finally {
|
||||
output.file.delete()
|
||||
}
|
||||
}
|
||||
|
||||
override fun IntentJobContext.onError(error: Throwable) = Unit
|
||||
}
|
||||
@@ -14,14 +14,16 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.koitharu.kotatsu.R
|
||||
import org.koitharu.kotatsu.core.backup.DIR_BACKUPS
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput.Companion.DIR_BACKUPS
|
||||
import org.koitharu.kotatsu.core.backup.ExternalBackupStorage
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.ui.BasePreferenceFragment
|
||||
import org.koitharu.kotatsu.core.util.ext.resolveFile
|
||||
import org.koitharu.kotatsu.core.util.ext.tryLaunch
|
||||
import org.koitharu.kotatsu.core.util.ext.viewLifecycleScope
|
||||
import java.io.File
|
||||
import java.text.SimpleDateFormat
|
||||
import java.time.format.DateTimeFormatter
|
||||
import java.time.format.FormatStyle
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
@@ -29,7 +31,7 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
||||
ActivityResultCallback<Uri?> {
|
||||
|
||||
@Inject
|
||||
lateinit var scheduler: PeriodicalBackupWorker.Scheduler
|
||||
lateinit var backupStorage: ExternalBackupStorage
|
||||
|
||||
private val outputSelectCall = registerForActivityResult(
|
||||
ActivityResultContracts.OpenDocumentTree(),
|
||||
@@ -57,7 +59,7 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
||||
if (result != null) {
|
||||
val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
context?.contentResolver?.takePersistableUriPermission(result, takeFlags)
|
||||
settings.periodicalBackupOutput = result
|
||||
settings.periodicalBackupDirectory = result
|
||||
bindOutputSummary()
|
||||
}
|
||||
}
|
||||
@@ -66,7 +68,7 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_OUTPUT) ?: return
|
||||
viewLifecycleScope.launch {
|
||||
preference.summary = withContext(Dispatchers.Default) {
|
||||
val value = settings.periodicalBackupOutput
|
||||
val value = settings.periodicalBackupDirectory
|
||||
value?.toUserFriendlyString(preference.context) ?: preference.context.run {
|
||||
getExternalFilesDir(DIR_BACKUPS) ?: File(filesDir, DIR_BACKUPS)
|
||||
}.path
|
||||
@@ -78,11 +80,11 @@ class PeriodicalBackupSettingsFragment : BasePreferenceFragment(R.string.periodi
|
||||
val preference = findPreference<Preference>(AppSettings.KEY_BACKUP_PERIODICAL_LAST) ?: return
|
||||
viewLifecycleScope.launch {
|
||||
val lastDate = withContext(Dispatchers.Default) {
|
||||
scheduler.getLastSuccessfulBackup()
|
||||
backupStorage.getLastBackupDate()
|
||||
}
|
||||
preference.summary = lastDate?.let {
|
||||
val format = SimpleDateFormat.getDateTimeInstance(SimpleDateFormat.MEDIUM, SimpleDateFormat.SHORT)
|
||||
preference.context.getString(R.string.last_successful_backup, format.format(it))
|
||||
val formatter = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.LONG)
|
||||
preference.context.getString(R.string.last_successful_backup, it.format(formatter))
|
||||
}
|
||||
preference.isVisible = lastDate != null
|
||||
}
|
||||
|
||||
@@ -1,112 +0,0 @@
|
||||
package org.koitharu.kotatsu.settings.backup
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.hilt.work.HiltWorker
|
||||
import androidx.work.Constraints
|
||||
import androidx.work.CoroutineWorker
|
||||
import androidx.work.ExistingPeriodicWorkPolicy
|
||||
import androidx.work.PeriodicWorkRequestBuilder
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.WorkerParameters
|
||||
import androidx.work.await
|
||||
import androidx.work.workDataOf
|
||||
import dagger.Reusable
|
||||
import dagger.assisted.Assisted
|
||||
import dagger.assisted.AssistedInject
|
||||
import org.koitharu.kotatsu.core.backup.BackupRepository
|
||||
import org.koitharu.kotatsu.core.backup.BackupZipOutput
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.awaitUniqueWorkInfoByName
|
||||
import org.koitharu.kotatsu.core.util.ext.deleteAwait
|
||||
import org.koitharu.kotatsu.settings.work.PeriodicWorkScheduler
|
||||
import java.util.Date
|
||||
import java.util.concurrent.TimeUnit
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltWorker
|
||||
class PeriodicalBackupWorker @AssistedInject constructor(
|
||||
@Assisted appContext: Context,
|
||||
@Assisted params: WorkerParameters,
|
||||
private val repository: BackupRepository,
|
||||
private val settings: AppSettings,
|
||||
) : CoroutineWorker(appContext, params) {
|
||||
|
||||
override suspend fun doWork(): Result {
|
||||
val resultData = workDataOf(DATA_TIMESTAMP to Date().time)
|
||||
val file = BackupZipOutput(applicationContext).use { backup ->
|
||||
backup.put(repository.createIndex())
|
||||
backup.put(repository.dumpHistory())
|
||||
backup.put(repository.dumpCategories())
|
||||
backup.put(repository.dumpFavourites())
|
||||
backup.put(repository.dumpBookmarks())
|
||||
backup.put(repository.dumpSources())
|
||||
backup.put(repository.dumpSettings())
|
||||
backup.finish()
|
||||
backup.file
|
||||
}
|
||||
val dirUri = settings.periodicalBackupOutput ?: return Result.success(resultData)
|
||||
val target = DocumentFile.fromTreeUri(applicationContext, dirUri)
|
||||
?.createFile("application/zip", file.nameWithoutExtension)
|
||||
?.uri ?: return Result.failure()
|
||||
applicationContext.contentResolver.openOutputStream(target, "wt")?.use { output ->
|
||||
file.inputStream().copyTo(output)
|
||||
} ?: return Result.failure()
|
||||
file.deleteAwait()
|
||||
return Result.success(resultData)
|
||||
}
|
||||
|
||||
@Reusable
|
||||
class Scheduler @Inject constructor(
|
||||
private val workManager: WorkManager,
|
||||
private val settings: AppSettings,
|
||||
) : PeriodicWorkScheduler {
|
||||
|
||||
override suspend fun schedule() {
|
||||
val constraints = Constraints.Builder()
|
||||
.setRequiresStorageNotLow(true)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
constraints.setRequiresDeviceIdle(true)
|
||||
}
|
||||
val request = PeriodicWorkRequestBuilder<PeriodicalBackupWorker>(
|
||||
settings.periodicalBackupFrequency,
|
||||
TimeUnit.DAYS,
|
||||
).setConstraints(constraints.build())
|
||||
.keepResultsForAtLeast(20, TimeUnit.DAYS)
|
||||
.addTag(TAG)
|
||||
.build()
|
||||
workManager
|
||||
.enqueueUniquePeriodicWork(TAG, ExistingPeriodicWorkPolicy.UPDATE, request)
|
||||
.await()
|
||||
}
|
||||
|
||||
override suspend fun unschedule() {
|
||||
workManager
|
||||
.cancelUniqueWork(TAG)
|
||||
.await()
|
||||
}
|
||||
|
||||
override suspend fun isScheduled(): Boolean {
|
||||
return workManager
|
||||
.awaitUniqueWorkInfoByName(TAG)
|
||||
.any { !it.state.isFinished }
|
||||
}
|
||||
|
||||
suspend fun getLastSuccessfulBackup(): Date? {
|
||||
return workManager
|
||||
.awaitUniqueWorkInfoByName(TAG)
|
||||
.lastOrNull { x -> x.state == WorkInfo.State.SUCCEEDED }
|
||||
?.outputData
|
||||
?.getLong(DATA_TIMESTAMP, 0)
|
||||
?.let { if (it != 0L) Date(it) else null }
|
||||
}
|
||||
}
|
||||
|
||||
private companion object {
|
||||
|
||||
const val TAG = "backups"
|
||||
const val DATA_TIMESTAMP = "ts"
|
||||
}
|
||||
}
|
||||
@@ -50,6 +50,9 @@ class SliderPreference @JvmOverloads constructor(
|
||||
valueTo = getFloat(R.styleable.SliderPreference_android_valueTo, valueTo.toFloat()).toInt()
|
||||
stepSize = getFloat(R.styleable.SliderPreference_android_stepSize, stepSize.toFloat()).toInt()
|
||||
isTickVisible = getBoolean(R.styleable.SliderPreference_tickVisible, isTickVisible)
|
||||
if (getBoolean(R.styleable.SliderPreference_useSimpleSummaryProvider, false)) {
|
||||
summaryProvider = SimpleSummaryProvider
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,6 +121,11 @@ class SliderPreference @JvmOverloads constructor(
|
||||
}
|
||||
}
|
||||
|
||||
private object SimpleSummaryProvider : SummaryProvider<SliderPreference> {
|
||||
|
||||
override fun provideSummary(preference: SliderPreference) = preference.value.toString()
|
||||
}
|
||||
|
||||
private class SavedState : AbsSavedState {
|
||||
|
||||
val valueFrom: Int
|
||||
|
||||
@@ -5,7 +5,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koitharu.kotatsu.core.prefs.AppSettings
|
||||
import org.koitharu.kotatsu.core.util.ext.processLifecycleScope
|
||||
import org.koitharu.kotatsu.settings.backup.PeriodicalBackupWorker
|
||||
import org.koitharu.kotatsu.suggestions.ui.SuggestionsWorker
|
||||
import org.koitharu.kotatsu.tracker.work.TrackWorker
|
||||
import javax.inject.Inject
|
||||
@@ -16,7 +15,6 @@ class WorkScheduleManager @Inject constructor(
|
||||
private val settings: AppSettings,
|
||||
private val suggestionScheduler: SuggestionsWorker.Scheduler,
|
||||
private val trackerScheduler: TrackWorker.Scheduler,
|
||||
private val periodicalBackupScheduler: PeriodicalBackupWorker.Scheduler,
|
||||
) : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
@@ -35,13 +33,6 @@ class WorkScheduleManager @Inject constructor(
|
||||
isEnabled = settings.isSuggestionsEnabled,
|
||||
force = key != AppSettings.KEY_SUGGESTIONS,
|
||||
)
|
||||
|
||||
AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
|
||||
AppSettings.KEY_BACKUP_PERIODICAL_FREQUENCY -> updateWorker(
|
||||
scheduler = periodicalBackupScheduler,
|
||||
isEnabled = settings.isPeriodicalBackupEnabled,
|
||||
force = key != AppSettings.KEY_BACKUP_PERIODICAL_ENABLED,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +41,6 @@ class WorkScheduleManager @Inject constructor(
|
||||
processLifecycleScope.launch(Dispatchers.Default) {
|
||||
updateWorkerImpl(trackerScheduler, settings.isTrackerEnabled, true) // always force due to adaptive interval
|
||||
updateWorkerImpl(suggestionScheduler, settings.isSuggestionsEnabled, false)
|
||||
updateWorkerImpl(periodicalBackupScheduler, settings.isPeriodicalBackupEnabled, false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
<attr name="android:valueTo" />
|
||||
<attr name="android:stepSize" />
|
||||
<attr name="tickVisible" />
|
||||
<attr name="useSimpleSummaryProvider" />
|
||||
</declare-styleable>
|
||||
|
||||
<declare-styleable name="ListItemTextView">
|
||||
|
||||
@@ -761,4 +761,5 @@
|
||||
<string name="landscape">Landscape</string>
|
||||
<string name="breadcrumbs_separator" translatable="false"><![CDATA[" > "]]></string>
|
||||
<string name="access_denied_403">Access denied (403)</string>
|
||||
<string name="max_backups_count">Max number of backups</string>
|
||||
</resources>
|
||||
|
||||
@@ -10,6 +10,11 @@
|
||||
android:layout="@layout/preference_toggle_header"
|
||||
android:title="@string/periodic_backups_enable" />
|
||||
|
||||
<Preference
|
||||
android:dependency="backup_periodic"
|
||||
android:key="backup_periodic_output"
|
||||
android:title="@string/backups_output_directory" />
|
||||
|
||||
<ListPreference
|
||||
android:defaultValue="7"
|
||||
android:dependency="backup_periodic"
|
||||
@@ -19,10 +24,14 @@
|
||||
android:title="@string/backup_frequency"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<Preference
|
||||
android:dependency="backup_periodic"
|
||||
android:key="backup_periodic_output"
|
||||
android:title="@string/backups_output_directory" />
|
||||
<org.koitharu.kotatsu.settings.utils.SliderPreference
|
||||
android:key="backup_periodic_count"
|
||||
android:stepSize="1"
|
||||
android:title="@string/max_backups_count"
|
||||
android:valueFrom="1"
|
||||
android:valueTo="32"
|
||||
app:defaultValue="10"
|
||||
app:useSimpleSummaryProvider="true" />
|
||||
|
||||
<Preference
|
||||
android:dependency="backup_periodic"
|
||||
|
||||
Reference in New Issue
Block a user