Delete local chapters in a service

This commit is contained in:
Koitharu
2022-04-19 16:04:24 +03:00
parent cbd3d439cd
commit e63ae12c8c
8 changed files with 149 additions and 32 deletions

View File

@@ -102,6 +102,7 @@
<service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
android:foregroundServiceType="dataSync" />
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
<service
android:name="org.koitharu.kotatsu.widget.shelf.ShelfWidgetService"
android:permission="android.permission.BIND_REMOTEVIEWS" />

View File

@@ -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?)
}

View File

@@ -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

View File

@@ -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 -> {

View File

@@ -86,7 +86,6 @@ class DetailsViewModel(
.asLiveData(viewModelScope.coroutineContext)
val onMangaRemoved = SingleLiveEvent<Manga>()
val onChaptersRemoved = SingleLiveEvent<Int>()
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<Long>) {
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")

View File

@@ -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<LocalMangaRepository>()
override suspend fun processIntent(intent: Intent?) {
val manga = intent?.getParcelableExtra<ParcelableManga>(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<Long>) {
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)
}
}
}

View File

@@ -274,4 +274,6 @@
<string name="parallel_downloads">Загружать параллельно</string>
<string name="download_slowdown">Замедление загрузки</string>
<string name="download_slowdown_summary">Помогает избежать блокировки IP-адреса</string>
<string name="local_manga_processing">Обработка сохранённой манги</string>
<string name="chapters_will_removed_background">Главы будут удалены в фоновом режиме. Это может занять какое-то время</string>
</resources>

View File

@@ -277,4 +277,6 @@
<string name="parallel_downloads">Parallel downloads</string>
<string name="download_slowdown">Download slowdown</string>
<string name="download_slowdown_summary">Helps avoid blocking your IP address</string>
<string name="local_manga_processing">Saved manga processing</string>
<string name="chapters_will_removed_background">Chapters will be removed in the background. It can take some time</string>
</resources>