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.getDisplayMessage
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.toBitmapOrNull
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
import javax.inject.Inject
@@ -51,12 +53,14 @@ class AutoFixService : CoroutineIntentService() {
val ids = requireNotNull(intent.getLongArrayExtra(DATA_IDS))
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)
powerManager.withPartialWakeLock(TAG) {
val result = runCatchingCancellable {
autoFixUseCase.invoke(mangaId)
}
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
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.XmlPullParserException
import java.io.File
import java.util.concurrent.TimeUnit
import kotlin.math.roundToLong
val Context.activityManager: ActivityManager?
@@ -228,3 +229,21 @@ fun Context.restartApplication() {
startActivity(intent)
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.getDisplayMessage
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.toBitmapOrNull
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.parsers.model.Manga
import org.koitharu.kotatsu.parsers.util.runCatchingCancellable
@@ -50,12 +52,14 @@ class ImportService : CoroutineIntentService() {
override suspend fun IntentJobContext.processIntent(intent: Intent) {
val uri = requireNotNull(intent.getStringExtra(DATA_URI)?.toUriOrNull()) { "No input uri" }
startForeground(this)
val result = runCatchingCancellable {
importer.import(uri).manga
}
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
val notification = buildNotification(result)
notificationManager.notify(TAG, startId, notification)
powerManager.withPartialWakeLock(TAG) {
val result = runCatchingCancellable {
importer.import(uri).manga
}
if (applicationContext.checkNotificationPermission(CHANNEL_ID)) {
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.util.ext.getDisplayMessage
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.LocalStorageChanges
import org.koitharu.kotatsu.local.domain.model.LocalManga
@@ -44,12 +46,14 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
}
override suspend fun IntentJobContext.processIntent(intent: Intent) {
startForeground(this)
val manga = intent.getParcelableExtraCompat<ParcelableManga>(EXTRA_MANGA)?.manga ?: return
val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return
startForeground(this)
val mangaWithChapters = localMangaRepository.getDetails(manga)
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga)))
powerManager.withPartialWakeLock(TAG) {
val mangaWithChapters = localMangaRepository.getDetails(manga)
localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds)
localStorageChanges.emit(LocalManga(localMangaRepository.getDetails(manga)))
}
}
override fun IntentJobContext.onError(error: Throwable) {
@@ -104,6 +108,8 @@ class LocalChaptersRemoveService : CoroutineIntentService() {
private const val EXTRA_MANGA = "manga"
private const val EXTRA_CHAPTERS_IDS = "chapters_ids"
private const val TAG = CHANNEL_ID
fun start(context: Context, manga: Manga, chaptersIds: Collection<Long>) {
if (chaptersIds.isEmpty()) {
return

View File

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

View File

@@ -1,8 +1,11 @@
package org.koitharu.kotatsu.settings.backup
import android.content.ContentResolver
import android.content.Context
import android.net.Uri
import dagger.hilt.android.lifecycle.HiltViewModel
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import org.koitharu.kotatsu.core.backup.BackupRepository
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.progress.Progress
import java.io.File
import java.io.FileOutputStream
import javax.inject.Inject
@HiltViewModel
@@ -21,9 +25,13 @@ class BackupViewModel @Inject constructor(
val progress = MutableStateFlow(Progress.INDETERMINATE)
val onBackupDone = MutableEventFlow<File>()
val onBackupSaved = MutableEventFlow<Unit>()
private val contentResolver: ContentResolver = context.contentResolver
private var backupFile: File? = null
init {
launchLoadingJob {
launchLoadingJob(Dispatchers.Default) {
val file = BackupZipOutput.createTemp(context).use { backup ->
progress.value = Progress(0, 7)
backup.put(repository.createIndex())
@@ -52,7 +60,20 @@ class BackupViewModel @Inject constructor(
backup.finish()
backup.file
}
backupFile = 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.getDisplayMessage
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.toUriOrNull
import org.koitharu.kotatsu.core.util.ext.withPartialWakeLock
import org.koitharu.kotatsu.core.util.progress.Progress
import org.koitharu.kotatsu.parsers.util.mapToArray
import org.koitharu.kotatsu.parsers.util.nullIfEmpty
@@ -59,20 +61,22 @@ class RestoreService : CoroutineIntentService() {
if (entries.isNullOrEmpty()) {
throw IllegalArgumentException("No entries specified")
}
val result = runInterruptible(Dispatchers.IO) {
val tempFile = File.createTempFile("backup_", ".tmp")
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
tempFile.outputStream().use { output ->
input.copyTo(output)
powerManager.withPartialWakeLock(TAG) {
val result = runInterruptible(Dispatchers.IO) {
val tempFile = File.createTempFile("backup_", ".tmp")
(contentResolver.openInputStream(uri) ?: throw FileNotFoundException()).use { input ->
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)
}
}