From e63ae12c8cca79823141b42119166314b315c125 Mon Sep 17 00:00:00 2001 From: Koitharu Date: Tue, 19 Apr 2022 16:04:24 +0300 Subject: [PATCH] Delete local chapters in a service --- app/src/main/AndroidManifest.xml | 1 + .../kotatsu/base/ui/CoroutineIntentService.kt | 37 +++++++++ .../kotatsu/details/ui/ChaptersFragment.kt | 16 +++- .../kotatsu/details/ui/DetailsActivity.kt | 24 +++--- .../kotatsu/details/ui/DetailsViewModel.kt | 19 +---- .../local/ui/LocalChaptersRemoveService.kt | 80 +++++++++++++++++++ app/src/main/res/values-ru/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + 8 files changed, 149 insertions(+), 32 deletions(-) create mode 100644 app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt create mode 100644 app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6c6c112b8..bba1d89b0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -102,6 +102,7 @@ + diff --git a/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt b/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt new file mode 100644 index 000000000..241d13f94 --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/base/ui/CoroutineIntentService.kt @@ -0,0 +1,37 @@ +package org.koitharu.kotatsu.base.ui + +import android.app.Service +import android.content.Intent +import androidx.lifecycle.lifecycleScope +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext + +abstract class CoroutineIntentService : BaseService() { + + private val mutex = Mutex() + protected open val dispatcher: CoroutineDispatcher = Dispatchers.Default + + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + super.onStartCommand(intent, flags, startId) + launchCoroutine(intent, startId) + return Service.START_REDELIVER_INTENT + } + + private fun launchCoroutine(intent: Intent?, startId: Int) = lifecycleScope.launch { + mutex.withLock { + try { + withContext(dispatcher) { + processIntent(intent) + } + } finally { + stopSelf(startId) + } + } + } + + protected abstract suspend fun processIntent(intent: Intent?) +} \ No newline at end of file diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt index 143a87713..91698a76b 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/ChaptersFragment.kt @@ -11,6 +11,7 @@ import androidx.appcompat.widget.SearchView import androidx.core.graphics.Insets import androidx.core.view.isVisible import androidx.core.view.updatePadding +import com.google.android.material.snackbar.Snackbar import org.koin.androidx.viewmodel.ext.android.sharedViewModel import org.koitharu.kotatsu.R import org.koitharu.kotatsu.base.ui.BaseFragment @@ -21,6 +22,7 @@ import org.koitharu.kotatsu.details.ui.adapter.ChaptersAdapter import org.koitharu.kotatsu.details.ui.adapter.ChaptersSelectionDecoration import org.koitharu.kotatsu.details.ui.model.ChapterListItem import org.koitharu.kotatsu.download.ui.service.DownloadService +import org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService import org.koitharu.kotatsu.parsers.model.MangaSource import org.koitharu.kotatsu.reader.ui.ReaderActivity import org.koitharu.kotatsu.reader.ui.ReaderState @@ -160,8 +162,18 @@ class ChaptersFragment : } R.id.action_delete -> { val ids = selectionDecoration?.checkedItemsIds - if (!ids.isNullOrEmpty()) { - viewModel.deleteChapters(ids.toSet()) + val manga = viewModel.manga.value + when { + ids.isNullOrEmpty() || manga == null -> Unit + ids.size == manga.chapters?.size -> viewModel.deleteLocal() + else -> { + LocalChaptersRemoveService.start(requireContext(), manga, ids) + Snackbar.make( + binding.recyclerViewChapters, + R.string.chapters_will_removed_background, + Snackbar.LENGTH_LONG + ).show() + } } mode.finish() true diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt index b29596141..a1920bf80 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsActivity.kt @@ -82,7 +82,6 @@ class DetailsActivity : viewModel.manga.observe(this, ::onMangaUpdated) viewModel.newChaptersCount.observe(this, ::onNewChaptersChanged) viewModel.onMangaRemoved.observe(this, ::onMangaRemoved) - viewModel.onChaptersRemoved.observe(this, ::onChaptersRemoved) viewModel.onError.observe(this, ::onError) registerReceiver(downloadReceiver, IntentFilter(DownloadService.ACTION_DOWNLOAD_COMPLETE)) @@ -106,10 +105,6 @@ class DetailsActivity : finishAfterTransition() } - private fun onChaptersRemoved(count: Int) { - binding.snackbar.show(getString(R.string.removal_completed)) - } - private fun onError(e: Throwable) { when { ExceptionResolver.canResolve(e) -> { @@ -179,16 +174,15 @@ class DetailsActivity : true } R.id.action_delete -> { - viewModel.manga.value?.let { m -> - MaterialAlertDialogBuilder(this) - .setTitle(R.string.delete_manga) - .setMessage(getString(R.string.text_delete_local_manga, m.title)) - .setPositiveButton(R.string.delete) { _, _ -> - viewModel.deleteLocal(m) - } - .setNegativeButton(android.R.string.cancel, null) - .show() - } + val title = viewModel.manga.value?.title.orEmpty() + MaterialAlertDialogBuilder(this) + .setTitle(R.string.delete_manga) + .setMessage(getString(R.string.text_delete_local_manga, title)) + .setPositiveButton(R.string.delete) { _, _ -> + viewModel.deleteLocal() + } + .setNegativeButton(android.R.string.cancel, null) + .show() true } R.id.action_save -> { diff --git a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt index cba0e11b6..0ee363efd 100644 --- a/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt +++ b/app/src/main/java/org/koitharu/kotatsu/details/ui/DetailsViewModel.kt @@ -86,7 +86,6 @@ class DetailsViewModel( .asLiveData(viewModelScope.coroutineContext) val onMangaRemoved = SingleLiveEvent() - val onChaptersRemoved = SingleLiveEvent() val branches = mangaData.map { it?.chapters?.mapToSet { x -> x.branch }?.sortedBy { x -> x }.orEmpty() @@ -136,8 +135,11 @@ class DetailsViewModel( loadingJob = doLoad() } - fun deleteLocal(manga: Manga) { + fun deleteLocal() { + val m = mangaData.value ?: return launchLoadingJob(Dispatchers.Default) { + val manga = if (m.source == MangaSource.LOCAL) m else localMangaRepository.findSavedManga(m) + checkNotNull(manga) { "Cannot find saved manga for ${m.title}" } val original = localMangaRepository.getRemoteManga(manga) localMangaRepository.delete(manga) || throw IOException("Unable to delete file") runCatching { @@ -185,19 +187,6 @@ class DetailsViewModel( } } - fun deleteChapters(ids: Set) { - val m = mangaData.value ?: return - if (m.chapters?.size == ids.size) { - deleteLocal(m) - return - } - launchLoadingJob { - localMangaRepository.deleteChapters(m, ids) - reload() - onChaptersRemoved.call(ids.size) - } - } - private fun doLoad() = launchLoadingJob(Dispatchers.Default) { var manga = mangaDataRepository.resolveIntent(intent) ?: throw MangaNotFoundException("Cannot find manga") diff --git a/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt new file mode 100644 index 000000000..3bc53726c --- /dev/null +++ b/app/src/main/java/org/koitharu/kotatsu/local/ui/LocalChaptersRemoveService.kt @@ -0,0 +1,80 @@ +package org.koitharu.kotatsu.local.ui + +import android.app.NotificationChannel +import android.app.NotificationManager +import android.content.Context +import android.content.Intent +import android.os.Build +import androidx.core.app.NotificationCompat +import androidx.core.app.ServiceCompat +import androidx.core.content.ContextCompat +import org.koin.android.ext.android.inject +import org.koitharu.kotatsu.R +import org.koitharu.kotatsu.base.ui.CoroutineIntentService +import org.koitharu.kotatsu.core.model.parcelable.ParcelableManga +import org.koitharu.kotatsu.download.ui.service.DownloadService +import org.koitharu.kotatsu.local.domain.LocalMangaRepository +import org.koitharu.kotatsu.parsers.model.Manga + +class LocalChaptersRemoveService : CoroutineIntentService() { + + private val localMangaRepository by inject() + + override suspend fun processIntent(intent: Intent?) { + val manga = intent?.getParcelableExtra(EXTRA_MANGA)?.manga ?: return + val chaptersIds = intent.getLongArrayExtra(EXTRA_CHAPTERS_IDS)?.toSet() ?: return + startForeground() + val mangaWithChapters = localMangaRepository.getDetails(manga) + localMangaRepository.deleteChapters(mangaWithChapters, chaptersIds) + sendBroadcast( + Intent(DownloadService.ACTION_DOWNLOAD_COMPLETE) + .putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)) + ) + ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE) + } + + private fun startForeground() { + val title = getString(R.string.local_manga_processing) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + val channel = NotificationChannel(CHANNEL_ID, title, NotificationManager.IMPORTANCE_LOW) + channel.setShowBadge(false) + channel.enableVibration(false) + channel.setSound(null, null) + channel.enableLights(false) + manager.createNotificationChannel(channel) + } + + val notification = NotificationCompat.Builder(this, CHANNEL_ID) + .setContentTitle(title) + .setPriority(NotificationCompat.PRIORITY_MIN) + .setDefaults(0) + .setColor(ContextCompat.getColor(this, R.color.blue_primary_dark)) + .setSilent(true) + .setProgress(0, 0, true) + .setSmallIcon(android.R.drawable.stat_notify_sync) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_DEFERRED) + .setOngoing(true) + .build() + startForeground(NOTIFICATION_ID, notification) + } + + companion object { + + private const val CHANNEL_ID = "local_processing" + private const val NOTIFICATION_ID = 21 + + private const val EXTRA_MANGA = "manga" + private const val EXTRA_CHAPTERS_IDS = "chapters_ids" + + fun start(context: Context, manga: Manga, chaptersIds: Collection) { + if (chaptersIds.isEmpty()) { + return + } + val intent = Intent(context, LocalChaptersRemoveService::class.java) + intent.putExtra(EXTRA_MANGA, ParcelableManga(manga, withChapters = false)) + intent.putExtra(EXTRA_CHAPTERS_IDS, chaptersIds.toLongArray()) + ContextCompat.startForegroundService(context, intent) + } + } +} \ No newline at end of file diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index a4c27b6a0..3b28783b5 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -274,4 +274,6 @@ Загружать параллельно Замедление загрузки Помогает избежать блокировки IP-адреса + Обработка сохранённой манги + Главы будут удалены в фоновом режиме. Это может занять какое-то время \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index e249d13f7..828ad9fe6 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -277,4 +277,6 @@ Parallel downloads Download slowdown Helps avoid blocking your IP address + Saved manga processing + Chapters will be removed in the background. It can take some time \ No newline at end of file