Use WakeLock for background operations

This commit is contained in:
Koitharu
2025-02-23 19:10:05 +02:00
parent fc5ad9ff90
commit ea13c7dbd8
7 changed files with 93 additions and 50 deletions

View File

@@ -24,8 +24,10 @@ import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject import javax.inject.Inject
@@ -51,12 +53,14 @@ class AutoFixService : CoroutineIntentService() {
val ids = requireNotNull(intent.getLongArrayExtra(DATA_IDS)) val ids = requireNotNull(intent.getLongArrayExtra(DATA_IDS))
startForeground(this) startForeground(this)
for (mangaId in ids) { for (mangaId in ids) {
val result = runCatchingCancellable { powerManager.withPartialWakeLock(TAG) {
autoFixUseCase.invoke(mangaId) val result = runCatchingCancellable {
} autoFixUseCase.invoke(mangaId)
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { }
val notification = buildNotification(result) if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
notificationManager.notify(TAG, startId, notification) val notification = buildNotification(result)
notificationManager.notify(TAG, startId, notification)
}
} }
} }
} }

View File

@@ -56,6 +56,7 @@ import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import org.xmlpull.v1.XmlPullParser import org.xmlpull.v1.XmlPullParser
import org.xmlpull.v1.XmlPullParserException import org.xmlpull.v1.XmlPullParserException
import java.io.File import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.math.roundToLong import kotlin.math.roundToLong
val Context.activityManager: ActivityManager? val Context.activityManager: ActivityManager?
@@ -228,3 +229,21 @@ fun Context.restartApplication() {
startActivity(intent) startActivity(intent)
activity?.finishAndRemoveTask() activity?.finishAndRemoveTask()
} }
internal inline fun <R> PowerManager?.withPartialWakeLock(tag: String, body: (PowerManager.WakeLock?) -> R): R {
val wakeLock = newPartialWakeLock(tag)
return try {
wakeLock?.acquire(TimeUnit.HOURS.toMillis(1))
body(wakeLock)
} finally {
wakeLock?.release()
}
}
private fun PowerManager?.newPartialWakeLock(tag: String): PowerManager.WakeLock? {
return if (this != null && isWakeLockLevelSupported(PowerManager.PARTIAL_WAKE_LOCK)) {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, tag)
} else {
null
}
}

View File

@@ -23,9 +23,11 @@ import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra import org.koitharu.kotatsu.core.util.ext.mangaSourceExtra
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull import org.koitharu.kotatsu.core.util.ext.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.local.data.importer.SingleMangaImporter import org.koitharu.kotatsu.local.data.importer.SingleMangaImporter
import org.koitharu.kotatsu.parsers.model.Manga import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -50,12 +52,14 @@ class ImportService : CoroutineIntentService() {
override suspend fun IntentJobContext.processIntent(intent: Intent) { override suspend fun IntentJobContext.processIntent(intent: Intent) {
val uri = requireNotNull(intent.getStringExtra(DATA_URI)?.toUriOrNull()) { "No input uri" } val uri = requireNotNull(intent.getStringExtra(DATA_URI)?.toUriOrNull()) { "No input uri" }
startForeground(this) startForeground(this)
val result = runCatchingCancellable { powerManager.withPartialWakeLock(TAG) {
importer.import(uri).manga val result = runCatchingCancellable {
} importer.import(uri).manga
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) { }
val notification = buildNotification(result) if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
notificationManager.notify(TAG, startId, notification) val notification = buildNotification(result)
notificationManager.notify(TAG, startId, notification)
}
} }
} }

View File

@@ -17,6 +17,8 @@ import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga
import org.koitharu.kotatsu.core.ui.CoroutineIntentService import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat import org.koitharu.kotatsu.core.util.ext.getParcelableExtraCompat
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.local.data.LocalMangaRepository import org.koitharu.kotatsu.local.data.LocalMangaRepository
import org.koitharu.kotatsu.local.data.LocalStorageChanges import org.koitharu.kotatsu.local.data.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga import org.koitharu.kotatsu.local.domain.model.LocalManga
@@ -44,12 +46,14 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
} }
override suspend fun IntentJobContext.processIntent(intent: Intent) { override suspend fun IntentJobContext.processIntent(intent: Intent) {
startForeground(this)
val manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return val manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return
startForeground(this) powerManager.withPartialWakeLock(TAG) {
val mangaWithChapters = localMangaRepository.getDetails(manga) val mangaWithChapters = localMangaRepository.getDetails(manga)
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds) localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga))) localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga)))
}
} }
override fun IntentJobContext.onError(error: Throwable) { override fun IntentJobContext.onError(error: Throwable) {
@@ -104,6 +108,8 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
private const val EXTRA_MANGA = "manga" private const val EXTRA_MANGA = "manga"
private const val EXTRA_CHAPTERS_IDS = "chapters_ids" private const val EXTRA_CHAPTERS_IDS = "chapters_ids"
private const val TAG = CHANNEL_ID
fun start(context: Context, manga: Manga, chaptersIds: Collection<Long>) { fun start(context: Context, manga: Manga, chaptersIds: Collection<Long>) {
if (chaptersIds.isEmpty()) { if (chaptersIds.isEmpty()) {
return return

View File

@@ -1,6 +1,5 @@
package org.koitharu.kotatsu.settings.backup package org.koitharu.kotatsu.settings.backup
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.ViewGroup import android.view.ViewGroup
@@ -19,20 +18,17 @@ import org.koitharu.kotatsu.core.util.ext.tryLaunch
import org.koitharu.kotatsu.core.util.progress.Progress import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.databinding.DialogProgressBinding import org.koitharu.kotatsu.databinding.DialogProgressBinding
import java.io.File import java.io.File
import java.io.FileOutputStream
@AndroidEntryPoint @AndroidEntryPoint
class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() { class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
private val viewModel by viewModels<BackupViewModel>() private val viewModel by viewModels<BackupViewModel>()
private var backup: File? = null
private val saveFileContract = registerForActivityResult( private val saveFileContract = registerForActivityResult(
ActivityResultContracts.CreateDocument("application/zip"), ActivityResultContracts.CreateDocument("application/zip"),
) { uri -> ) { uri ->
val file = backup if (uri != null) {
if (uri != null && file != null) { viewModel.saveBackup(uri)
saveBackup(file, uri)
} else { } else {
dismiss() dismiss()
} }
@@ -51,6 +47,7 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged) viewModel.progress.observe(viewLifecycleOwner, this::onProgressChanged)
viewModel.onBackupDone.observeEvent(viewLifecycleOwner, this::onBackupDone) viewModel.onBackupDone.observeEvent(viewLifecycleOwner, this::onBackupDone)
viewModel.onError.observeEvent(viewLifecycleOwner, this::onError) viewModel.onError.observeEvent(viewLifecycleOwner, this::onError)
viewModel.onBackupSaved.observeEvent(viewLifecycleOwner) { onBackupSaved() }
} }
override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder { override fun onBuildDialog(builder: MaterialAlertDialogBuilder): MaterialAlertDialogBuilder {
@@ -81,26 +78,14 @@ class BackupDialogFragment : AlertDialogFragment<DialogProgressBinding>() {
} }
private fun onBackupDone(file: File) { private fun onBackupDone(file: File) {
this.backup = file
if (!saveFileContract.tryLaunch(file.name)) { if (!saveFileContract.tryLaunch(file.name)) {
Toast.makeText(requireContext(), R.string.operation_not_supported, Toast.LENGTH_SHORT).show() Toast.makeText(requireContext(), R.string.operation_not_supported, Toast.LENGTH_SHORT).show()
dismiss() dismiss()
} }
} }
private fun saveBackup(file: File, output: Uri) { private fun onBackupSaved() {
try { Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_SHORT).show()
requireContext().contentResolver.openFileDescriptor(output, "w")?.use { fd -> dismiss()
FileOutputStream(fd.fileDescriptor).use {
it.write(file.readBytes())
}
}
Toast.makeText(requireContext(), R.string.backup_saved, Toast.LENGTH_SHORT).show()
dismiss()
} catch (e: InterruptedException) {
throw e
} catch (e: Exception) {
onError(e)
}
} }
} }

View File

@@ -1,8 +1,11 @@
package org.koitharu.kotatsu.settings.backup package org.koitharu.kotatsu.settings.backup
import android.content.ContentResolver
import android.content.Context import android.content.Context
import android.net.Uri
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.backup.BackupRepository import org.koitharu.kotatsu.core.backup.BackupRepository
import org.koitharu.kotatsu.core.backup.BackupZipOutput import org.koitharu.kotatsu.core.backup.BackupZipOutput
@@ -11,6 +14,7 @@ import org.koitharu.kotatsu.core.util.ext.MutableEventFlow
import org.koitharu.kotatsu.core.util.ext.call import org.koitharu.kotatsu.core.util.ext.call
import org.koitharu.kotatsu.core.util.progress.Progress import org.koitharu.kotatsu.core.util.progress.Progress
import java.io.File import java.io.File
import java.io.FileOutputStream
import javax.inject.Inject import javax.inject.Inject
@HiltViewModel @HiltViewModel
@@ -21,9 +25,13 @@ class BackupViewModel @Inject constructor(
val progress = MutableStateFlow(Progress.INDETERMINATE) val progress = MutableStateFlow(Progress.INDETERMINATE)
val onBackupDone = MutableEventFlow<File>() val onBackupDone = MutableEventFlow<File>()
val onBackupSaved = MutableEventFlow<Unit>()
private val contentResolver: ContentResolver = context.contentResolver
private var backupFile: File? = null
init { init {
launchLoadingJob { launchLoadingJob(Dispatchers.Default) {
val file = BackupZipOutput.createTemp(context).use { backup -> val file = BackupZipOutput.createTemp(context).use { backup ->
progress.value = Progress(0, 7) progress.value = Progress(0, 7)
backup.put(repository.createIndex()) backup.put(repository.createIndex())
@@ -52,7 +60,20 @@ class BackupViewModel @Inject constructor(
backup.finish() backup.finish()
backup.file backup.file
} }
backupFile = file
onBackupDone.call(file) onBackupDone.call(file)
} }
} }
fun saveBackup(output: Uri) {
launchLoadingJob(Dispatchers.Default) {
val file = checkNotNull(backupFile)
contentResolver.openFileDescriptor(output, "w")?.use { fd ->
FileOutputStream(fd.fileDescriptor).use {
it.write(file.readBytes())
}
}
onBackupSaved.call(Unit)
}
}
} }

View File

@@ -26,8 +26,10 @@ import org.koitharu.kotatsu.core.ui.CoroutineIntentService
import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission import org.koitharu.kotatsu.core.util.ext.checkNotificationPermission
import org.koitharu.kotatsu.core.util.ext.getDisplayMessage import org.koitharu.kotatsu.core.util.ext.getDisplayMessage
import org.koitharu.kotatsu.core.util.ext.getFileDisplayName import org.koitharu.kotatsu.core.util.ext.getFileDisplayName
import org.koitharu.kotatsu.core.util.ext.powerManager
import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug import org.koitharu.kotatsu.core.util.ext.printStackTraceDebug
import org.koitharu.kotatsu.core.util.ext.toUriOrNull import org.koitharu.kotatsu.core.util.ext.toUriOrNull
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.core.util.progress.Progress import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.parsers.util.mapToArray import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.parsers.util.nullIfEmpty import org.koitharu.kotatsu.parsers.util.nullIfEmpty
@@ -59,20 +61,22 @@ class RestoreService : CoroutineIntentService() {
if (entries.isNullOrEmpty()) { if (entries.isNullOrEmpty()) {
throw IllegalArgumentException("No entries specified") throw IllegalArgumentException("No entries specified")
} }
val result = runInterruptible(Dispatchers.IO) { powerManager.withPartialWakeLock(TAG) {
val tempFile = File.createTempFile("backup_", ".tmp") val result = runInterruptible(Dispatchers.IO) {
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input -> val tempFile = File.createTempFile("backup_", ".tmp")
tempFile.outputStream().use { output -> (contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
input.copyTo(output) tempFile.outputStream().use { output ->
input.copyTo(output)
}
} }
BackupZipInput.from(tempFile)
}.use { backupInput ->
restoreImpl(displayName, backupInput, entries)
}
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(displayName, result)
notificationManager.notify(TAG, startId, notification)
} }
BackupZipInput.from(tempFile)
}.use { backupInput ->
restoreImpl(displayName, backupInput, entries)
}
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(displayName, result)
notificationManager.notify(TAG, startId, notification)
} }
} }