diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 22bad7dd9..444c9619a 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -111,6 +111,7 @@
{
val stateFlow = MutableStateFlow(
- DownloadState.Queued(startId = startId, manga = manga, cover = null)
+ DownloadState.Queued(startId = startId, manga = manga, cover = null),
)
val pausingHandle = PausingHandle()
val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId)
@@ -100,7 +100,7 @@ class DownloadManager(
data.chapters
} else {
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
- }
+ },
) { "Chapters list must not be null" }
check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
check(chaptersIdsSet.isNullOrEmpty()) {
@@ -118,7 +118,7 @@ class DownloadManager(
chapter = chapter,
file = file,
pageNumber = pageIndex,
- ext = MimeTypeMap.getFileExtensionFromUrl(url)
+ ext = MimeTypeMap.getFileExtensionFromUrl(url),
)
}
outState.value = DownloadState.Progress(
@@ -128,7 +128,7 @@ class DownloadManager(
totalChapters = chapters.size,
currentChapter = chapterIndex,
totalPages = pages.size,
- currentPage = pageIndex
+ currentPage = pageIndex,
)
if (settings.isDownloadsSlowdownEnabled) {
@@ -209,7 +209,7 @@ class DownloadManager(
manga = prevValue.manga,
cover = prevValue.cover,
error = throwable,
- canRetry = false
+ canRetry = false,
)
}
@@ -220,7 +220,7 @@ class DownloadManager(
.referer(manga.publicUrl)
.size(coverWidth, coverHeight)
.scale(Scale.FILL)
- .build()
+ .build(),
).drawable
}.getOrNull()
@@ -240,7 +240,7 @@ class DownloadManager(
okHttp = okHttp,
cache = cache,
localMangaRepository = localMangaRepository,
- settings = settings
+ settings = settings,
)
}
}
\ No newline at end of file
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt
index d7f86abfb..343507b94 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/domain/DownloadState.kt
@@ -108,34 +108,6 @@ sealed interface DownloadState {
}
}
- @Deprecated("TODO: remove")
- class WaitingForNetwork(
- override val startId: Int,
- override val manga: Manga,
- override val cover: Drawable?,
- ) : DownloadState {
-
- override fun equals(other: Any?): Boolean {
- if (this === other) return true
- if (javaClass != other?.javaClass) return false
-
- other as WaitingForNetwork
-
- if (startId != other.startId) return false
- if (manga != other.manga) return false
- if (cover != other.cover) return false
-
- return true
- }
-
- override fun hashCode(): Int {
- var result = startId
- result = 31 * result + manga.hashCode()
- result = 31 * result + (cover?.hashCode() ?: 0)
- return result
- }
- }
-
class Done(
override val startId: Int,
override val manga: Manga,
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt
index 476f6efb6..c91517b42 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/DownloadItemAD.kt
@@ -18,9 +18,8 @@ fun downloadItemAD(
scope: CoroutineScope,
coil: ImageLoader,
) = adapterDelegateViewBinding, ProgressJob, ItemDownloadBinding>(
- { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) }
+ { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) },
) {
-
var job: Job? = null
val percentPattern = context.resources.getString(R.string.percent_string_pattern)
@@ -91,13 +90,6 @@ fun downloadItemAD(
binding.textViewPercent.isVisible = false
binding.textViewDetails.isVisible = false
}
- is DownloadState.WaitingForNetwork -> {
- binding.textViewStatus.setText(R.string.waiting_for_network)
- binding.progressBar.isIndeterminate = false
- binding.progressBar.isVisible = false
- binding.textViewPercent.isVisible = false
- binding.textViewDetails.isVisible = false
- }
}
}.launchIn(scope)
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
index 399340aa3..12efd0d34 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadNotification.kt
@@ -7,163 +7,282 @@ import android.app.PendingIntent
import android.content.Context
import android.os.Build
import android.text.format.DateUtils
+import android.util.SparseArray
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap
-import com.google.android.material.R as materialR
+import androidx.core.text.HtmlCompat
+import androidx.core.text.htmlEncode
+import androidx.core.text.parseAsHtml
+import androidx.core.util.forEach
+import androidx.core.util.size
import org.koitharu.kotatsu.R
import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.parsers.model.Manga
+import org.koitharu.kotatsu.parsers.util.ellipsize
import org.koitharu.kotatsu.parsers.util.format
import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.getDisplayMessage
+import com.google.android.material.R as materialR
-class DownloadNotification(private val context: Context, startId: Int) {
+class DownloadNotification(private val context: Context) {
+
+ private val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
+ private val states = SparseArray()
+ private val groupBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
- private val builder = NotificationCompat.Builder(context, CHANNEL_ID)
- private val cancelAction = NotificationCompat.Action(
- materialR.drawable.material_ic_clear_black_24dp,
- context.getString(android.R.string.cancel),
- PendingIntent.getBroadcast(
- context,
- startId * 2,
- DownloadService.getCancelIntent(startId),
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
- )
- )
- private val retryAction = NotificationCompat.Action(
- R.drawable.ic_restart_black,
- context.getString(R.string.try_again),
- PendingIntent.getBroadcast(
- context,
- startId * 2 + 1,
- DownloadService.getResumeIntent(startId),
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
- )
- )
private val listIntent = PendingIntent.getActivity(
context,
REQUEST_LIST,
DownloadsActivity.newIntent(context),
- PendingIntentCompat.FLAG_IMMUTABLE
+ PendingIntentCompat.FLAG_IMMUTABLE,
)
init {
- builder.setOnlyAlertOnce(true)
- builder.setDefaults(0)
- builder.color = ContextCompat.getColor(context, R.color.blue_primary)
- builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
- builder.setSilent(true)
+ groupBuilder.setOnlyAlertOnce(true)
+ groupBuilder.setDefaults(0)
+ groupBuilder.color = ContextCompat.getColor(context, R.color.blue_primary)
+ groupBuilder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
+ groupBuilder.setSilent(true)
+ groupBuilder.setGroup(GROUP_ID)
+ groupBuilder.setContentIntent(listIntent)
+ groupBuilder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
+ groupBuilder.setGroupSummary(true)
+ groupBuilder.setContentTitle(context.getString(R.string.downloading_manga))
}
- fun create(state: DownloadState, timeLeft: Long): Notification {
- builder.setContentTitle(state.manga.title)
- builder.setContentText(context.getString(R.string.manga_downloading_))
- builder.setProgress(1, 0, true)
- builder.setSmallIcon(android.R.drawable.stat_sys_download)
- builder.setContentIntent(listIntent)
- builder.setStyle(null)
- builder.setLargeIcon(state.cover?.toBitmap())
- builder.clearActions()
- builder.setVisibility(
+ fun buildGroupNotification(): Notification {
+ val style = NotificationCompat.InboxStyle(groupBuilder)
+ var progress = 0f
+ var isAllDone = true
+ groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+ states.forEach { _, state ->
if (state.manga.isNsfw) {
- NotificationCompat.VISIBILITY_PRIVATE
- } else {
- NotificationCompat.VISIBILITY_PUBLIC
+ groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
}
- )
- when (state) {
- is DownloadState.Cancelled -> {
- builder.setProgress(1, 0, true)
- builder.setContentText(context.getString(R.string.cancelling_))
- builder.setContentIntent(null)
- builder.setStyle(null)
- builder.setOngoing(true)
- }
- is DownloadState.Done -> {
- builder.setProgress(0, 0, false)
- builder.setContentText(context.getString(R.string.download_complete))
- builder.setContentIntent(createMangaIntent(context, state.localManga))
- builder.setAutoCancel(true)
- builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
- builder.setCategory(null)
- builder.setStyle(null)
- builder.setOngoing(false)
- }
- is DownloadState.Error -> {
- val message = state.error.getDisplayMessage(context.resources)
- builder.setProgress(0, 0, false)
- builder.setSmallIcon(android.R.drawable.stat_notify_error)
- builder.setSubText(context.getString(R.string.error))
- builder.setContentText(message)
- builder.setAutoCancel(!state.canRetry)
- builder.setOngoing(state.canRetry)
- builder.setCategory(NotificationCompat.CATEGORY_ERROR)
- builder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
- if (state.canRetry) {
- builder.addAction(cancelAction)
- builder.addAction(retryAction)
+ val summary = when (state) {
+ is DownloadState.Cancelled -> {
+ progress++
+ context.getString(R.string.cancelling_)
+ }
+ is DownloadState.Done -> {
+ progress++
+ context.getString(R.string.completed)
+ }
+ is DownloadState.Error -> {
+ isAllDone = false
+ context.getString(R.string.error)
+ }
+ is DownloadState.PostProcessing -> {
+ progress++
+ isAllDone = false
+ context.getString(R.string.processing_)
+ }
+ is DownloadState.Preparing -> {
+ isAllDone = false
+ context.getString(R.string.preparing_)
+ }
+ is DownloadState.Progress -> {
+ isAllDone = false
+ progress += state.percent
+ context.getString(R.string.percent_string_pattern, (state.percent * 100).format())
+ }
+ is DownloadState.Queued -> {
+ isAllDone = false
+ context.getString(R.string.queued)
}
}
- is DownloadState.PostProcessing -> {
- builder.setProgress(1, 0, true)
- builder.setContentText(context.getString(R.string.processing_))
- builder.setStyle(null)
- builder.setOngoing(true)
- }
- is DownloadState.Queued -> {
- builder.setProgress(0, 0, false)
- builder.setContentText(context.getString(R.string.queued))
- builder.setStyle(null)
- builder.setOngoing(true)
- builder.addAction(cancelAction)
- }
- is DownloadState.Preparing -> {
- builder.setProgress(1, 0, true)
- builder.setContentText(context.getString(R.string.preparing_))
- builder.setStyle(null)
- builder.setOngoing(true)
- builder.addAction(cancelAction)
- }
- is DownloadState.Progress -> {
- builder.setProgress(state.max, state.progress, false)
- if (timeLeft > 0L) {
- val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS)
- builder.setContentText(eta)
- } else {
- val percent = (state.percent * 100).format()
- builder.setContentText(context.getString(R.string.percent_string_pattern, percent))
- }
- builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
- builder.setStyle(null)
- builder.setOngoing(true)
- builder.addAction(cancelAction)
- }
- is DownloadState.WaitingForNetwork -> {
- builder.setProgress(0, 0, false)
- builder.setContentText(context.getString(R.string.waiting_for_network))
- builder.setStyle(null)
- builder.setOngoing(true)
- builder.addAction(cancelAction)
- }
+ style.addLine(
+ context.getString(
+ R.string.download_summary_pattern,
+ state.manga.title.ellipsize(10).htmlEncode(),
+ summary.htmlEncode(),
+ ).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY),
+ )
}
- return builder.build()
+ progress /= states.size.toFloat()
+ style.setBigContentTitle(context.getString(R.string.downloading_manga))
+ groupBuilder.setContentText(context.resources.getQuantityString(R.plurals.items, states.size, states.size()))
+ groupBuilder.setNumber(states.size)
+ groupBuilder.setSmallIcon(
+ if (isAllDone) android.R.drawable.stat_sys_download_done else android.R.drawable.stat_sys_download,
+ )
+ when (progress) {
+ 1f -> groupBuilder.setProgress(0, 0, false)
+ 0f -> groupBuilder.setProgress(1, 0, true)
+ else -> groupBuilder.setProgress(100, (progress * 100f).toInt(), progress == 0f)
+ }
+ return groupBuilder.build()
+ }
+
+ fun dismiss() {
+ manager.cancel(ID_GROUP)
+ }
+
+ fun newItem(startId: Int) = Item(startId)
+
+ inner class Item(
+ private val startId: Int,
+ ) {
+
+ private val builder = NotificationCompat.Builder(context, CHANNEL_ID)
+ private val cancelAction = NotificationCompat.Action(
+ materialR.drawable.material_ic_clear_black_24dp,
+ context.getString(android.R.string.cancel),
+ PendingIntent.getBroadcast(
+ context,
+ startId * 2,
+ DownloadService.getCancelIntent(startId),
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
+ ),
+ )
+ private val retryAction = NotificationCompat.Action(
+ R.drawable.ic_restart_black,
+ context.getString(R.string.try_again),
+ PendingIntent.getBroadcast(
+ context,
+ startId * 2 + 1,
+ DownloadService.getResumeIntent(startId),
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
+ ),
+ )
+
+ init {
+ builder.setOnlyAlertOnce(true)
+ builder.setDefaults(0)
+ builder.color = ContextCompat.getColor(context, R.color.blue_primary)
+ builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
+ builder.setSilent(true)
+ builder.setGroup(GROUP_ID)
+ builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
+ }
+
+ fun notify(state: DownloadState, timeLeft: Long) {
+ builder.setContentTitle(state.manga.title)
+ builder.setContentText(context.getString(R.string.manga_downloading_))
+ builder.setProgress(1, 0, true)
+ builder.setSmallIcon(android.R.drawable.stat_sys_download)
+ builder.setContentIntent(listIntent)
+ builder.setStyle(null)
+ builder.setLargeIcon(state.cover?.toBitmap())
+ builder.clearActions()
+ builder.setVisibility(
+ if (state.manga.isNsfw) {
+ NotificationCompat.VISIBILITY_PRIVATE
+ } else {
+ NotificationCompat.VISIBILITY_PUBLIC
+ },
+ )
+ when (state) {
+ is DownloadState.Cancelled -> {
+ builder.setProgress(1, 0, true)
+ builder.setContentText(context.getString(R.string.cancelling_))
+ builder.setContentIntent(null)
+ builder.setStyle(null)
+ builder.setOngoing(true)
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
+ }
+ is DownloadState.Done -> {
+ builder.setProgress(0, 0, false)
+ builder.setContentText(context.getString(R.string.download_complete))
+ builder.setContentIntent(createMangaIntent(context, state.localManga))
+ builder.setAutoCancel(true)
+ builder.setSmallIcon(android.R.drawable.stat_sys_download_done)
+ builder.setCategory(null)
+ builder.setStyle(null)
+ builder.setOngoing(false)
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
+ }
+ is DownloadState.Error -> {
+ val message = state.error.getDisplayMessage(context.resources)
+ builder.setProgress(0, 0, false)
+ builder.setSmallIcon(android.R.drawable.stat_notify_error)
+ builder.setSubText(context.getString(R.string.error))
+ builder.setContentText(message)
+ builder.setAutoCancel(!state.canRetry)
+ builder.setOngoing(state.canRetry)
+ builder.setCategory(NotificationCompat.CATEGORY_ERROR)
+ builder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
+ if (state.canRetry) {
+ builder.addAction(cancelAction)
+ builder.addAction(retryAction)
+ }
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
+ }
+ is DownloadState.PostProcessing -> {
+ builder.setProgress(1, 0, true)
+ builder.setContentText(context.getString(R.string.processing_))
+ builder.setStyle(null)
+ builder.setOngoing(true)
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
+ }
+ is DownloadState.Queued -> {
+ builder.setProgress(0, 0, false)
+ builder.setContentText(context.getString(R.string.queued))
+ builder.setStyle(null)
+ builder.setOngoing(true)
+ builder.addAction(cancelAction)
+ builder.priority = NotificationCompat.PRIORITY_LOW
+ }
+ is DownloadState.Preparing -> {
+ builder.setProgress(1, 0, true)
+ builder.setContentText(context.getString(R.string.preparing_))
+ builder.setStyle(null)
+ builder.setOngoing(true)
+ builder.addAction(cancelAction)
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
+ }
+ is DownloadState.Progress -> {
+ builder.setProgress(state.max, state.progress, false)
+ if (timeLeft > 0L) {
+ val eta = DateUtils.getRelativeTimeSpanString(timeLeft, 0L, DateUtils.SECOND_IN_MILLIS)
+ builder.setContentText(eta)
+ } else {
+ val percent = (state.percent * 100).format()
+ builder.setContentText(context.getString(R.string.percent_string_pattern, percent))
+ }
+ builder.setCategory(NotificationCompat.CATEGORY_PROGRESS)
+ builder.setStyle(null)
+ builder.setOngoing(true)
+ builder.addAction(cancelAction)
+ builder.priority = NotificationCompat.PRIORITY_DEFAULT
+ }
+ }
+ val notification = builder.build()
+ states.append(startId, state)
+ updateGroupNotification()
+ manager.notify(TAG, startId, notification)
+ }
+
+ fun dismiss() {
+ manager.cancel(TAG, startId)
+ states.remove(startId)
+ updateGroupNotification()
+ }
+ }
+
+ private fun updateGroupNotification() {
+ val notification = buildGroupNotification()
+ manager.notify(ID_GROUP, notification)
}
private fun createMangaIntent(context: Context, manga: Manga) = PendingIntent.getActivity(
context,
manga.hashCode(),
DetailsActivity.newIntent(context, manga),
- PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE
+ PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
)
companion object {
+ private const val TAG = "download"
private const val CHANNEL_ID = "download"
+ private const val GROUP_ID = "downloads"
private const val REQUEST_LIST = 6
+ const val ID_GROUP = 9999
fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@@ -172,7 +291,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
val channel = NotificationChannel(
CHANNEL_ID,
context.getString(R.string.downloads),
- NotificationManager.IMPORTANCE_LOW
+ NotificationManager.IMPORTANCE_LOW,
)
channel.enableVibration(false)
channel.enableLights(false)
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
index 993eab52e..d75082e2a 100644
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
+++ b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/DownloadService.kt
@@ -38,7 +38,7 @@ import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
class DownloadService : BaseService() {
private lateinit var downloadManager: DownloadManager
- private lateinit var notificationSwitcher: ForegroundNotificationSwitcher
+ private lateinit var downloadNotification: DownloadNotification
private val jobs = LinkedHashMap>()
private val jobCount = MutableStateFlow(0)
@@ -47,13 +47,14 @@ class DownloadService : BaseService() {
override fun onCreate() {
super.onCreate()
isRunning = true
- notificationSwitcher = ForegroundNotificationSwitcher(this)
+ downloadNotification = DownloadNotification(this)
val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
downloadManager = get().create(
coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)),
)
DownloadNotification.createChannel(this)
+ startForeground(DownloadNotification.ID_GROUP, downloadNotification.buildGroupNotification())
val intentFilter = IntentFilter()
intentFilter.addAction(ACTION_DOWNLOAD_CANCEL)
intentFilter.addAction(ACTION_DOWNLOAD_RESUME)
@@ -80,6 +81,7 @@ class DownloadService : BaseService() {
}
override fun onDestroy() {
+ downloadNotification.dismiss()
unregisterReceiver(controlReceiver)
isRunning = false
super.onDestroy()
@@ -98,10 +100,10 @@ class DownloadService : BaseService() {
private fun listenJob(job: ProgressJob) {
lifecycleScope.launch {
val startId = job.progressValue.startId
- val notification = DownloadNotification(this@DownloadService, startId)
+ val notificationItem = downloadNotification.newItem(startId)
try {
val timeLeftEstimator = TimeLeftEstimator()
- notificationSwitcher.notify(startId, notification.create(job.progressValue, -1L))
+ notificationItem.notify(job.progressValue, -1L)
job.progressAsFlow()
.onEach { state ->
if (state is DownloadState.Progress) {
@@ -114,7 +116,7 @@ class DownloadService : BaseService() {
.whileActive()
.collect { state ->
val timeLeft = timeLeftEstimator.getEstimatedTimeLeft()
- notificationSwitcher.notify(startId, notification.create(state, timeLeft))
+ notificationItem.notify(state, timeLeft)
}
job.join()
} finally {
@@ -124,14 +126,11 @@ class DownloadService : BaseService() {
.putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)),
)
}
- notificationSwitcher.detach(
- startId,
- if (job.isCancelled) {
- null
- } else {
- notification.create(job.progressValue, -1L)
- },
- )
+ if (job.isCancelled) {
+ notificationItem.dismiss()
+ } else {
+ notificationItem.notify(job.progressValue, -1L)
+ }
stopSelf(startId)
}
}
diff --git a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt b/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt
deleted file mode 100644
index 679405295..000000000
--- a/app/src/main/java/org/koitharu/kotatsu/download/ui/service/ForegroundNotificationSwitcher.kt
+++ /dev/null
@@ -1,62 +0,0 @@
-package org.koitharu.kotatsu.download.ui.service
-
-import android.app.Notification
-import android.app.NotificationManager
-import android.app.Service
-import android.content.Context
-import android.os.Handler
-import android.os.Looper
-import android.util.SparseArray
-import androidx.core.app.ServiceCompat
-import androidx.core.util.isEmpty
-import androidx.core.util.size
-
-private const val DEFAULT_DELAY = 500L
-
-class ForegroundNotificationSwitcher(
- private val service: Service,
-) {
-
- private val notificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
- private val notifications = SparseArray()
- private val handler = Handler(Looper.getMainLooper())
-
- @Synchronized
- fun notify(startId: Int, notification: Notification) {
- if (notifications.isEmpty()) {
- service.startForeground(startId, notification)
- } else {
- notificationManager.notify(startId, notification)
- }
- notifications[startId] = notification
- }
-
- @Synchronized
- fun detach(startId: Int, notification: Notification?) {
- notifications.remove(startId)
- if (notifications.isEmpty()) {
- ServiceCompat.stopForeground(service, ServiceCompat.STOP_FOREGROUND_DETACH)
- }
- val nextIndex = notifications.size - 1
- if (nextIndex >= 0) {
- val nextStartId = notifications.keyAt(nextIndex)
- val nextNotification = notifications.valueAt(nextIndex)
- service.startForeground(nextStartId, nextNotification)
- }
- handler.postDelayed(NotifyRunnable(startId, notification), DEFAULT_DELAY)
- }
-
- private inner class NotifyRunnable(
- private val startId: Int,
- private val notification: Notification?,
- ) : Runnable {
-
- override fun run() {
- if (notification != null) {
- notificationManager.notify(startId, notification)
- } else {
- notificationManager.cancel(startId)
- }
- }
- }
-}
\ 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 07ab4f6bb..ab2697193 100644
--- a/app/src/main/res/values-ru/strings.xml
+++ b/app/src/main/res/values-ru/strings.xml
@@ -1,320 +1,322 @@
- Закрыть меню
- Открыть меню
- На устройстве
- Избранное
- История
- Произошла ошибка
- Не удалось подключиться к интернету
- Подробности
- Главы
- Список
- Подробный список
- Таблица
- Вид списка
- Настройки
- Онлайн каталоги
- Загрузка…
- Глава %1$d из %2$d
- Закрыть
- Повторить
- Очистить историю
- Ничего не найдено
- Истории пока нет
- Читать
- Избранного пока нет
- В избранное
- Новая категория
- Добавить
- Введите название
- Сохранить
- Поделиться
- Создать ярлык…
- Поделиться %s
- Поиск
- Поиск манги
- Загрузка…
- Обработка…
- Загружено
- Загрузки
- Имя
- Популярная
- Обновлённая
- Новая
- Рейтинг
- Порядок сортировки
- Фильтр
- Тема
- Светлая
- Тёмная
- Как в системе
- Страницы
- Очистить
- Очистить всю историю чтения полностью\?
- Удалить
- «%s» удалено из истории
- «%s» удалено с устройства
- Дождитесь завершения загрузки…
- Сохранить страницу
- Сохранено
- Поделиться изображением
- Импорт
- Удалить
- Операция не поддерживается
- Выберите CBZ-файл или ZIP
- Нет описания
- История и кэш
- Очистить кэш страниц
- Кэш
- Б|кБ|МБ|ГБ|ТБ
- Стандартный
- Манхва
- Режим чтения
- Размер таблицы
- Поиск по %s
- Удалить мангу
- Удалить \"%s\" с устройства навсегда\?
- Настройки чтения
- Листание страниц
- Нажатия по краям
- Кнопки громкости
- Продолжить
- Предупреждение
- Это может привести к расходу большого количества трафика
- Больше не спрашивать
- Отмена…
- Ошибка
- Очистить кэш миниатюр
- Очистить историю поиска
- Очищено
- Только жесты
- Внутренний накопитель
- Внешнее хранилище
- Домен
- Проверять наличие новых версий приложения
- Доступна новая версия приложения
- Показывать уведомление, если доступна новая версия
- Открыть в веб-браузере
- В этой манге %s. Сохранить их все\?
- Сохранить
- Уведомления
- Включено %1$d из %2$d
- Новые главы
- Загрузить
- Читать с начала
- Перезапустить
- Настройки уведомлений
- Звук уведомления
- Светодиодная индикация
- Вибросигнал
- Категории избранного
- Категории…
- Переименовать
- Удалить категорию \"%s\" из избранного\?
+ Закрыть меню
+ Открыть меню
+ На устройстве
+ Избранное
+ История
+ Произошла ошибка
+ Не удалось подключиться к интернету
+ Подробности
+ Главы
+ Список
+ Подробный список
+ Таблица
+ Вид списка
+ Настройки
+ Онлайн каталоги
+ Загрузка…
+ Глава %1$d из %2$d
+ Закрыть
+ Повторить
+ Очистить историю
+ Ничего не найдено
+ Истории пока нет
+ Читать
+ Избранного пока нет
+ В избранное
+ Новая категория
+ Добавить
+ Введите название
+ Сохранить
+ Поделиться
+ Создать ярлык…
+ Поделиться %s
+ Поиск
+ Поиск манги
+ Загрузка…
+ Обработка…
+ Загружено
+ Загрузки
+ Имя
+ Популярная
+ Обновлённая
+ Новая
+ Рейтинг
+ Порядок сортировки
+ Фильтр
+ Тема
+ Светлая
+ Тёмная
+ Как в системе
+ Страницы
+ Очистить
+ Очистить всю историю чтения полностью\?
+ Удалить
+ «%s» удалено из истории
+ «%s» удалено с устройства
+ Дождитесь завершения загрузки…
+ Сохранить страницу
+ Сохранено
+ Поделиться изображением
+ Импорт
+ Удалить
+ Операция не поддерживается
+ Выберите CBZ-файл или ZIP
+ Нет описания
+ История и кэш
+ Очистить кэш страниц
+ Кэш
+ Б|кБ|МБ|ГБ|ТБ
+ Стандартный
+ Манхва
+ Режим чтения
+ Размер таблицы
+ Поиск по %s
+ Удалить мангу
+ Удалить \"%s\" с устройства навсегда\?
+ Настройки чтения
+ Листание страниц
+ Нажатия по краям
+ Кнопки громкости
+ Продолжить
+ Предупреждение
+ Это может привести к расходу большого количества трафика
+ Больше не спрашивать
+ Отмена…
+ Ошибка
+ Очистить кэш миниатюр
+ Очистить историю поиска
+ Очищено
+ Только жесты
+ Внутренний накопитель
+ Внешнее хранилище
+ Домен
+ Проверять наличие новых версий приложения
+ Доступна новая версия приложения
+ Показывать уведомление, если доступна новая версия
+ Открыть в веб-браузере
+ В этой манге %s. Сохранить их все\?
+ Сохранить
+ Уведомления
+ Включено %1$d из %2$d
+ Новые главы
+ Загрузить
+ Читать с начала
+ Перезапустить
+ Настройки уведомлений
+ Звук уведомления
+ Светодиодная индикация
+ Вибросигнал
+ Категории избранного
+ Категории…
+ Переименовать
+ Удалить категорию \"%s\" из избранного\?
\nВся манга в ней будет потеряна.
- Удалить
- Как-то здесь пусто…
- Попробуйте переформулировать запрос.
- Вы можете использовать категории для организации своих избранных. Нажмите «+», чтобы создать категорию
- То, что вы прочитаете, будет отображено здесь
- Найдите, что почитать, в боковом меню.
- Сохраните что-нибудь
- Сохраните что-нибудь из онлайн-каталога или импортируйте из файла.
- Полка
- Недавнее
- Анимация листания
- Папка для загрузок
- Недоступно
- Нет доступного хранилища
- Другое хранилище
- Готово
- Всё избранное
- Категория пуста
- Прочитать позже
- Обновления
- Новые главы из того, что вы читаете, будут показаны здесь
- Результаты поиска
- Похожие
- Новая версия: %s
- Размер: %s
- Ожидание подключения…
- Очистить ленту обновлений
- Очищено
- Повернуть экран
- Обновить
- Обновление скоро начнётся
- Следить за обновлениями
- Не проверять
- Введите пароль
- Неверный пароль
- Защитить приложение
- Запрашивать пароль при запуске Kotatsu
- Повторите пароль
- Пароли не совпадают
- О программе
- Версия %s
- Проверить обновления
- Проверка обновления…
- Не удалось проверить обновления
- Нет доступных обновлений
- Справа налево (←)
- Создать категорию
- Масштабирование
- Вписать в экран
- Подогнать по высоте
- Подогнать по ширине
- Исходный размер
- Чёрная
- Потребляет меньше энергии на экранах AMOLED
- Резервное копирование и восстановление
- Создать резервную копию
- Восстановить данные
- Восстановлено
- Подготовка…
- Файл не найден
- Все данные были восстановлены
- Данные были восстановлены, но возникли некоторые ошибки
- Вы можете создать резервную копию избранного и истории и потом восстановить их
- Только что
- Вчера
- Давно
- Группировать
- Сегодня
- Попробовать ещё раз
- Выбранный режим будет сохранён для текущей манги
- Без звука
- Необходимо пройти CAPTCHA
- Пройти
- Очистить куки
- Все файлы cookie были удалены
- Проверка новых глав: %1$d из %2$d
- Очистить ленту
- Удалить всю историю обновлений навсегда\?
- Проверка новых глав
- В обратном порядке
- Войти
- Авторизуйтесь, чтобы просмотреть этот контент
- По умолчанию: %s
- …и ещё %1$d
- Далее
- Введите пароль для запуска приложения
- Подтвердить
- Пароль должен состоять из 4 символов или более
- Поиск только по %s
- Другие
- Добро пожаловать
- Удалить все последние поисковые запросы навсегда\?
- Резервная копия сохранена
- Некоторые устройства имеют различное поведение системы, что может привести к нарушению фоновых задач.
- Подробнее
- В очереди
- Нет активных загрузок
- Глава отсутствует
- Скачайте или прочитайте эту недостающую главу онлайн.
- Помочь с переводом приложения
- Перевод
- Тема на 4PDA
- Обратная связь
- Авторизация выполнена
- Вход в %s не поддерживается
- Вы выйдете из всех источников
- Жанры
- Завершено
- Онгоинг
- Формат даты
- По умолчанию
- Исключить NSFW мангу из истории
- Вы должны ввести имя
- Показывать номера страницы
- Включенные источники
- Доступные источники
- Динамическая тема
- Применяет тему приложения, основанную на цветовой палитре обоев на устройстве
- Политика скриншотов
- Разрешить
- Запретить для NSFW
- Всегда блокировать
- Рекомендации
- Включить рекомендации
- Предлагать мангу на основе Ваших предпочтений
- Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы
- Начните читать мангу, чтобы получать персональные предложения
- Не предлагать NSFW мангу
- Включено
- Выключено
- Не удалось загрузить список жанров
- Вычисление…
- Создать проблему на GitHub
- Импорт манги: %1$d из %2$d
- Сбросить фильтр
- Поиск по жанрам
- Выберите языки, на которых Вы хоите читать мангу. Это можно будет изменить позже в настройках.
- Никогда
- Только по Wi-Fi
- Всегда
- Предварительная загрузка страниц
- Вы авторизованы как %s
- 18+
- Разные языки
- Найти главу
- В этой манге нет глав
- Оформление
- Контент
- Обновление рекомендаций
- Исключить жанры
- Укажите жанры, которые Вы не хотите видеть в рекомендациях
- Удалить выбранную мангу с накопителя?
- Удаление завершено
- Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе
- Загружать параллельно
- Замедление загрузки
- Помогает избежать блокировки IP-адреса
- Обработка сохранённой манги
- Главы будут удалены в фоновом режиме. Это может занять какое-то время
- Скрыть
- Доступны новые источники манги
- Проверять новые главы и уведомлять о них
- Вы будете получать уведомления об обновлении манги, которую Вы читаете
- Вы не будете получать уведомления, но новые главы будут отображаться в списке
- Включить уведомления
- Название
- Изменить
- Изменить категорию
- Отслеживание
- Нет категорий избранного
- Добавить закладку
- Удалить закладку
- Закладки
- Закладка удалена
- Закладка добавлена
- Отменить
- Удалено из истории
- DNS через HTTPS
- Режим по умолчанию
- Автоопределение режима чтения
- Автоматически определяет, является ли манга веб-комиксом
- Отключить оптимизацию батареи
- Помогает с фоновой проверкой обновлений
- Что-то пошло не так. Пожалуйста, отправьте отчёт разработчикам, чтобы помочь всё исправить
- Отправить
- Отключить все
- Использовать отпечаток пальца, если доступно
- Манга из Вашего избранного
- Манга, которую Вы недавно читали
- Читаю
- Запланировано
- Отложено
- Заброшено
- Завершено
- Показать процент прочитанного в истории и избранном
- Манга, помеченная как NSFW, никогда не будет добавлена в историю и ваш прогресс чтения не будет сохранен
- %1$s%%
- Отчёт
- Выйти
- Перечитываю
- Показать индикаторы прогресса чтения
- Удаление данных
- Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы
- Показать все
+ Удалить
+ Как-то здесь пусто…
+ Попробуйте переформулировать запрос.
+ Вы можете использовать категории для организации своих избранных. Нажмите «+», чтобы создать категорию
+ То, что вы прочитаете, будет отображено здесь
+ Найдите, что почитать, в боковом меню.
+ Сохраните что-нибудь
+ Сохраните что-нибудь из онлайн-каталога или импортируйте из файла.
+ Полка
+ Недавнее
+ Анимация листания
+ Папка для загрузок
+ Недоступно
+ Нет доступного хранилища
+ Другое хранилище
+ Готово
+ Всё избранное
+ Категория пуста
+ Прочитать позже
+ Обновления
+ Новые главы из того, что вы читаете, будут показаны здесь
+ Результаты поиска
+ Похожие
+ Новая версия: %s
+ Размер: %s
+ Ожидание подключения…
+ Очистить ленту обновлений
+ Очищено
+ Повернуть экран
+ Обновить
+ Обновление скоро начнётся
+ Следить за обновлениями
+ Не проверять
+ Введите пароль
+ Неверный пароль
+ Защитить приложение
+ Запрашивать пароль при запуске Kotatsu
+ Повторите пароль
+ Пароли не совпадают
+ О программе
+ Версия %s
+ Проверить обновления
+ Проверка обновления…
+ Не удалось проверить обновления
+ Нет доступных обновлений
+ Справа налево (←)
+ Создать категорию
+ Масштабирование
+ Вписать в экран
+ Подогнать по высоте
+ Подогнать по ширине
+ Исходный размер
+ Чёрная
+ Потребляет меньше энергии на экранах AMOLED
+ Резервное копирование и восстановление
+ Создать резервную копию
+ Восстановить данные
+ Восстановлено
+ Подготовка…
+ Файл не найден
+ Все данные были восстановлены
+ Данные были восстановлены, но возникли некоторые ошибки
+ Вы можете создать резервную копию избранного и истории и потом восстановить их
+ Только что
+ Вчера
+ Давно
+ Группировать
+ Сегодня
+ Попробовать ещё раз
+ Выбранный режим будет сохранён для текущей манги
+ Без звука
+ Необходимо пройти CAPTCHA
+ Пройти
+ Очистить куки
+ Все файлы cookie были удалены
+ Проверка новых глав: %1$d из %2$d
+ Очистить ленту
+ Удалить всю историю обновлений навсегда\?
+ Проверка новых глав
+ В обратном порядке
+ Войти
+ Авторизуйтесь, чтобы просмотреть этот контент
+ По умолчанию: %s
+ …и ещё %1$d
+ Далее
+ Введите пароль для запуска приложения
+ Подтвердить
+ Пароль должен состоять из 4 символов или более
+ Поиск только по %s
+ Другие
+ Добро пожаловать
+ Удалить все последние поисковые запросы навсегда\?
+ Резервная копия сохранена
+ Некоторые устройства имеют различное поведение системы, что может привести к нарушению фоновых задач.
+ Подробнее
+ В очереди
+ Нет активных загрузок
+ Глава отсутствует
+ Скачайте или прочитайте эту недостающую главу онлайн.
+ Помочь с переводом приложения
+ Перевод
+ Тема на 4PDA
+ Обратная связь
+ Авторизация выполнена
+ Вход в %s не поддерживается
+ Вы выйдете из всех источников
+ Жанры
+ Завершено
+ Онгоинг
+ Формат даты
+ По умолчанию
+ Исключить NSFW мангу из истории
+ Вы должны ввести имя
+ Показывать номера страницы
+ Включенные источники
+ Доступные источники
+ Динамическая тема
+ Применяет тему приложения, основанную на цветовой палитре обоев на устройстве
+ Политика скриншотов
+ Разрешить
+ Запретить для NSFW
+ Всегда блокировать
+ Рекомендации
+ Включить рекомендации
+ Предлагать мангу на основе Ваших предпочтений
+ Все данные анализируются локально на устройстве. Ваши персональные данные не передаются в какие-либо сервисы
+ Начните читать мангу, чтобы получать персональные предложения
+ Не предлагать NSFW мангу
+ Включено
+ Выключено
+ Не удалось загрузить список жанров
+ Вычисление…
+ Создать проблему на GitHub
+ Импорт манги: %1$d из %2$d
+ Сбросить фильтр
+ Поиск по жанрам
+ Выберите языки, на которых Вы хоите читать мангу. Это можно будет изменить позже в настройках.
+ Никогда
+ Только по Wi-Fi
+ Всегда
+ Предварительная загрузка страниц
+ Вы авторизованы как %s
+ 18+
+ Разные языки
+ Найти главу
+ В этой манге нет глав
+ Оформление
+ Контент
+ Обновление рекомендаций
+ Исключить жанры
+ Укажите жанры, которые Вы не хотите видеть в рекомендациях
+ Удалить выбранную мангу с накопителя?
+ Удаление завершено
+ Загрузить выбранную мангу со всеми главами? Это может привести к большому расходу трафика и места на накопителе
+ Загружать параллельно
+ Замедление загрузки
+ Помогает избежать блокировки IP-адреса
+ Обработка сохранённой манги
+ Главы будут удалены в фоновом режиме. Это может занять какое-то время
+ Скрыть
+ Доступны новые источники манги
+ Проверять новые главы и уведомлять о них
+ Вы будете получать уведомления об обновлении манги, которую Вы читаете
+ Вы не будете получать уведомления, но новые главы будут отображаться в списке
+ Включить уведомления
+ Название
+ Изменить
+ Изменить категорию
+ Отслеживание
+ Нет категорий избранного
+ Добавить закладку
+ Удалить закладку
+ Закладки
+ Закладка удалена
+ Закладка добавлена
+ Отменить
+ Удалено из истории
+ DNS через HTTPS
+ Режим по умолчанию
+ Автоопределение режима чтения
+ Автоматически определяет, является ли манга веб-комиксом
+ Отключить оптимизацию батареи
+ Помогает с фоновой проверкой обновлений
+ Что-то пошло не так. Пожалуйста, отправьте отчёт разработчикам, чтобы помочь всё исправить
+ Отправить
+ Отключить все
+ Использовать отпечаток пальца, если доступно
+ Манга из Вашего избранного
+ Манга, которую Вы недавно читали
+ Читаю
+ Запланировано
+ Отложено
+ Заброшено
+ Завершено
+ Показать процент прочитанного в истории и избранном
+ Манга, помеченная как NSFW, никогда не будет добавлена в историю и ваш прогресс чтения не будет сохранен
+ %1$s%%
+ Отчёт
+ Выйти
+ Перечитываю
+ Показать индикаторы прогресса чтения
+ Удаление данных
+ Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы
+ Показать все
+ Downloading manga
+ Completed
\ 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 a34f0fc89..d64335ae2 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -323,4 +323,7 @@
Invalid domain
Select range
Content not found or removed
+ Downloading manga
+ <b>%1$s</b> %2$s
+ Completed
\ No newline at end of file