diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt index e286d641e..84ba7717b 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/alternatives/ui/AutoFixService.kt @@ -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) + } } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt index 44a2475bd..963ef8dd0 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/core/util/ext/Android.kt @@ -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 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 + } +} diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt index b19c9386d..8214ddd57 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/ImportService.kt @@ -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) + } } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt index 7355465b4..a121d7b33 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt @@ -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(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) { if (chaptersIds.isEmpty()) { return diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt index 0cdaac346..c26c02f98 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupDialogFragment.kt @@ -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() { private val viewModel by viewModels() - 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() { 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() { } 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() } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt index f31e8dbc2..e6341c26d 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/BackupViewModel.kt @@ -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() + val onBackupSaved = MutableEventFlow() + + 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) + } + } } diff --git a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt index ca194b6d3..d5a6733e8 100644 --- a/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt +++ b/app/src/main/kotlin/org/koitharu/kotatsu/settings/backup/RestoreService.kt @@ -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) } }