Group download notification

pull/211/head
Koitharu 4 years ago
parent d5bea0ca53
commit 43ef130052
No known key found for this signature in database
GPG Key ID: 8E861F8CE6E7CE27

@ -111,6 +111,7 @@
<service <service
android:name="org.koitharu.kotatsu.download.ui.service.DownloadService" android:name="org.koitharu.kotatsu.download.ui.service.DownloadService"
android:stopWithTask="false"
android:foregroundServiceType="dataSync" /> android:foregroundServiceType="dataSync" />
<service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" /> <service android:name="org.koitharu.kotatsu.local.ui.LocalChaptersRemoveService" />
<service <service

@ -43,10 +43,10 @@ class DownloadManager(
) { ) {
private val coverWidth = context.resources.getDimensionPixelSize( private val coverWidth = context.resources.getDimensionPixelSize(
androidx.core.R.dimen.compat_notification_large_icon_max_width androidx.core.R.dimen.compat_notification_large_icon_max_width,
) )
private val coverHeight = context.resources.getDimensionPixelSize( private val coverHeight = context.resources.getDimensionPixelSize(
androidx.core.R.dimen.compat_notification_large_icon_max_height androidx.core.R.dimen.compat_notification_large_icon_max_height,
) )
private val semaphore = Semaphore(settings.downloadsParallelism) private val semaphore = Semaphore(settings.downloadsParallelism)
@ -56,7 +56,7 @@ class DownloadManager(
startId: Int, startId: Int,
): PausingProgressJob<DownloadState> { ): PausingProgressJob<DownloadState> {
val stateFlow = MutableStateFlow<DownloadState>( val stateFlow = MutableStateFlow<DownloadState>(
DownloadState.Queued(startId = startId, manga = manga, cover = null) DownloadState.Queued(startId = startId, manga = manga, cover = null),
) )
val pausingHandle = PausingHandle() val pausingHandle = PausingHandle()
val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId) val job = downloadMangaImpl(manga, chaptersIds?.takeUnless { it.isEmpty() }, stateFlow, pausingHandle, startId)
@ -100,7 +100,7 @@ class DownloadManager(
data.chapters data.chapters
} else { } else {
data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) } data.chapters?.filter { x -> chaptersIdsSet.remove(x.id) }
} },
) { "Chapters list must not be null" } ) { "Chapters list must not be null" }
check(chapters.isNotEmpty()) { "Chapters list must not be empty" } check(chapters.isNotEmpty()) { "Chapters list must not be empty" }
check(chaptersIdsSet.isNullOrEmpty()) { check(chaptersIdsSet.isNullOrEmpty()) {
@ -118,7 +118,7 @@ class DownloadManager(
chapter = chapter, chapter = chapter,
file = file, file = file,
pageNumber = pageIndex, pageNumber = pageIndex,
ext = MimeTypeMap.getFileExtensionFromUrl(url) ext = MimeTypeMap.getFileExtensionFromUrl(url),
) )
} }
outState.value = DownloadState.Progress( outState.value = DownloadState.Progress(
@ -128,7 +128,7 @@ class DownloadManager(
totalChapters = chapters.size, totalChapters = chapters.size,
currentChapter = chapterIndex, currentChapter = chapterIndex,
totalPages = pages.size, totalPages = pages.size,
currentPage = pageIndex currentPage = pageIndex,
) )
if (settings.isDownloadsSlowdownEnabled) { if (settings.isDownloadsSlowdownEnabled) {
@ -209,7 +209,7 @@ class DownloadManager(
manga = prevValue.manga, manga = prevValue.manga,
cover = prevValue.cover, cover = prevValue.cover,
error = throwable, error = throwable,
canRetry = false canRetry = false,
) )
} }
@ -220,7 +220,7 @@ class DownloadManager(
.referer(manga.publicUrl) .referer(manga.publicUrl)
.size(coverWidth, coverHeight) .size(coverWidth, coverHeight)
.scale(Scale.FILL) .scale(Scale.FILL)
.build() .build(),
).drawable ).drawable
}.getOrNull() }.getOrNull()
@ -240,7 +240,7 @@ class DownloadManager(
okHttp = okHttp, okHttp = okHttp,
cache = cache, cache = cache,
localMangaRepository = localMangaRepository, localMangaRepository = localMangaRepository,
settings = settings settings = settings,
) )
} }
} }

@ -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( class Done(
override val startId: Int, override val startId: Int,
override val manga: Manga, override val manga: Manga,

@ -18,9 +18,8 @@ fun downloadItemAD(
scope: CoroutineScope, scope: CoroutineScope,
coil: ImageLoader, coil: ImageLoader,
) = adapterDelegateViewBinding<ProgressJob<DownloadState>, ProgressJob<DownloadState>, ItemDownloadBinding>( ) = adapterDelegateViewBinding<ProgressJob<DownloadState>, ProgressJob<DownloadState>, ItemDownloadBinding>(
{ inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) } { inflater, parent -> ItemDownloadBinding.inflate(inflater, parent, false) },
) { ) {
var job: Job? = null var job: Job? = null
val percentPattern = context.resources.getString(R.string.percent_string_pattern) val percentPattern = context.resources.getString(R.string.percent_string_pattern)
@ -91,13 +90,6 @@ fun downloadItemAD(
binding.textViewPercent.isVisible = false binding.textViewPercent.isVisible = false
binding.textViewDetails.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) }.launchIn(scope)
} }

@ -7,21 +7,126 @@ import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.os.Build import android.os.Build
import android.text.format.DateUtils import android.text.format.DateUtils
import android.util.SparseArray
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.toBitmap 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.R
import org.koitharu.kotatsu.details.ui.DetailsActivity import org.koitharu.kotatsu.details.ui.DetailsActivity
import org.koitharu.kotatsu.download.domain.DownloadState import org.koitharu.kotatsu.download.domain.DownloadState
import org.koitharu.kotatsu.download.ui.DownloadsActivity import org.koitharu.kotatsu.download.ui.DownloadsActivity
import org.koitharu.kotatsu.parsers.model.Manga 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.parsers.util.format
import org.koitharu.kotatsu.utils.PendingIntentCompat import org.koitharu.kotatsu.utils.PendingIntentCompat
import org.koitharu.kotatsu.utils.ext.getDisplayMessage import org.koitharu.kotatsu.utils.ext.getDisplayMessage
import com.google.android.material.R as materialR
class DownloadNotification(private val context: Context) {
private val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
private val states = SparseArray<DownloadState>()
private val groupBuilder = NotificationCompat.Builder(context, CHANNEL_ID)
private val listIntent = PendingIntent.getActivity(
context,
REQUEST_LIST,
DownloadsActivity.newIntent(context),
PendingIntentCompat.FLAG_IMMUTABLE,
)
class DownloadNotification(private val context: Context, startId: Int) { init {
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 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) {
groupBuilder.setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
}
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)
}
}
style.addLine(
context.getString(
R.string.download_summary_pattern,
state.manga.title.ellipsize(10).htmlEncode(),
summary.htmlEncode(),
).parseAsHtml(HtmlCompat.FROM_HTML_MODE_LEGACY),
)
}
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 builder = NotificationCompat.Builder(context, CHANNEL_ID)
private val cancelAction = NotificationCompat.Action( private val cancelAction = NotificationCompat.Action(
@ -31,8 +136,8 @@ class DownloadNotification(private val context: Context, startId: Int) {
context, context,
startId * 2, startId * 2,
DownloadService.getCancelIntent(startId), DownloadService.getCancelIntent(startId),
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
) ),
) )
private val retryAction = NotificationCompat.Action( private val retryAction = NotificationCompat.Action(
R.drawable.ic_restart_black, R.drawable.ic_restart_black,
@ -41,14 +146,8 @@ class DownloadNotification(private val context: Context, startId: Int) {
context, context,
startId * 2 + 1, startId * 2 + 1,
DownloadService.getResumeIntent(startId), DownloadService.getResumeIntent(startId),
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
) ),
)
private val listIntent = PendingIntent.getActivity(
context,
REQUEST_LIST,
DownloadsActivity.newIntent(context),
PendingIntentCompat.FLAG_IMMUTABLE
) )
init { init {
@ -57,9 +156,11 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.color = ContextCompat.getColor(context, R.color.blue_primary) builder.color = ContextCompat.getColor(context, R.color.blue_primary)
builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE builder.foregroundServiceBehavior = NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE
builder.setSilent(true) builder.setSilent(true)
builder.setGroup(GROUP_ID)
builder.setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
} }
fun create(state: DownloadState, timeLeft: Long): Notification { fun notify(state: DownloadState, timeLeft: Long) {
builder.setContentTitle(state.manga.title) builder.setContentTitle(state.manga.title)
builder.setContentText(context.getString(R.string.manga_downloading_)) builder.setContentText(context.getString(R.string.manga_downloading_))
builder.setProgress(1, 0, true) builder.setProgress(1, 0, true)
@ -73,7 +174,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
NotificationCompat.VISIBILITY_PRIVATE NotificationCompat.VISIBILITY_PRIVATE
} else { } else {
NotificationCompat.VISIBILITY_PUBLIC NotificationCompat.VISIBILITY_PUBLIC
} },
) )
when (state) { when (state) {
is DownloadState.Cancelled -> { is DownloadState.Cancelled -> {
@ -82,6 +183,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.setContentIntent(null) builder.setContentIntent(null)
builder.setStyle(null) builder.setStyle(null)
builder.setOngoing(true) builder.setOngoing(true)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
} }
is DownloadState.Done -> { is DownloadState.Done -> {
builder.setProgress(0, 0, false) builder.setProgress(0, 0, false)
@ -92,6 +194,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.setCategory(null) builder.setCategory(null)
builder.setStyle(null) builder.setStyle(null)
builder.setOngoing(false) builder.setOngoing(false)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
} }
is DownloadState.Error -> { is DownloadState.Error -> {
val message = state.error.getDisplayMessage(context.resources) val message = state.error.getDisplayMessage(context.resources)
@ -107,12 +210,14 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.addAction(cancelAction) builder.addAction(cancelAction)
builder.addAction(retryAction) builder.addAction(retryAction)
} }
builder.priority = NotificationCompat.PRIORITY_DEFAULT
} }
is DownloadState.PostProcessing -> { is DownloadState.PostProcessing -> {
builder.setProgress(1, 0, true) builder.setProgress(1, 0, true)
builder.setContentText(context.getString(R.string.processing_)) builder.setContentText(context.getString(R.string.processing_))
builder.setStyle(null) builder.setStyle(null)
builder.setOngoing(true) builder.setOngoing(true)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
} }
is DownloadState.Queued -> { is DownloadState.Queued -> {
builder.setProgress(0, 0, false) builder.setProgress(0, 0, false)
@ -120,6 +225,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.setStyle(null) builder.setStyle(null)
builder.setOngoing(true) builder.setOngoing(true)
builder.addAction(cancelAction) builder.addAction(cancelAction)
builder.priority = NotificationCompat.PRIORITY_LOW
} }
is DownloadState.Preparing -> { is DownloadState.Preparing -> {
builder.setProgress(1, 0, true) builder.setProgress(1, 0, true)
@ -127,6 +233,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.setStyle(null) builder.setStyle(null)
builder.setOngoing(true) builder.setOngoing(true)
builder.addAction(cancelAction) builder.addAction(cancelAction)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
} }
is DownloadState.Progress -> { is DownloadState.Progress -> {
builder.setProgress(state.max, state.progress, false) builder.setProgress(state.max, state.progress, false)
@ -141,29 +248,41 @@ class DownloadNotification(private val context: Context, startId: Int) {
builder.setStyle(null) builder.setStyle(null)
builder.setOngoing(true) builder.setOngoing(true)
builder.addAction(cancelAction) builder.addAction(cancelAction)
builder.priority = NotificationCompat.PRIORITY_DEFAULT
} }
is DownloadState.WaitingForNetwork -> { }
builder.setProgress(0, 0, false) val notification = builder.build()
builder.setContentText(context.getString(R.string.waiting_for_network)) states.append(startId, state)
builder.setStyle(null) updateGroupNotification()
builder.setOngoing(true) manager.notify(TAG, startId, notification)
builder.addAction(cancelAction) }
fun dismiss() {
manager.cancel(TAG, startId)
states.remove(startId)
updateGroupNotification()
} }
} }
return builder.build()
private fun updateGroupNotification() {
val notification = buildGroupNotification()
manager.notify(ID_GROUP, notification)
} }
private fun createMangaIntent(context: Context, manga: Manga) = PendingIntent.getActivity( private fun createMangaIntent(context: Context, manga: Manga) = PendingIntent.getActivity(
context, context,
manga.hashCode(), manga.hashCode(),
DetailsActivity.newIntent(context, manga), DetailsActivity.newIntent(context, manga),
PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE PendingIntent.FLAG_CANCEL_CURRENT or PendingIntentCompat.FLAG_IMMUTABLE,
) )
companion object { companion object {
private const val TAG = "download"
private const val CHANNEL_ID = "download" private const val CHANNEL_ID = "download"
private const val GROUP_ID = "downloads"
private const val REQUEST_LIST = 6 private const val REQUEST_LIST = 6
const val ID_GROUP = 9999
fun createChannel(context: Context) { fun createChannel(context: Context) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
@ -172,7 +291,7 @@ class DownloadNotification(private val context: Context, startId: Int) {
val channel = NotificationChannel( val channel = NotificationChannel(
CHANNEL_ID, CHANNEL_ID,
context.getString(R.string.downloads), context.getString(R.string.downloads),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW,
) )
channel.enableVibration(false) channel.enableVibration(false)
channel.enableLights(false) channel.enableLights(false)

@ -38,7 +38,7 @@ import org.koitharu.kotatsu.utils.progress.TimeLeftEstimator
class DownloadService : BaseService() { class DownloadService : BaseService() {
private lateinit var downloadManager: DownloadManager private lateinit var downloadManager: DownloadManager
private lateinit var notificationSwitcher: ForegroundNotificationSwitcher private lateinit var downloadNotification: DownloadNotification
private val jobs = LinkedHashMap<Int, PausingProgressJob<DownloadState>>() private val jobs = LinkedHashMap<Int, PausingProgressJob<DownloadState>>()
private val jobCount = MutableStateFlow(0) private val jobCount = MutableStateFlow(0)
@ -47,13 +47,14 @@ class DownloadService : BaseService() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
isRunning = true isRunning = true
notificationSwitcher = ForegroundNotificationSwitcher(this) downloadNotification = DownloadNotification(this)
val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager) val wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager)
.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading") .newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "kotatsu:downloading")
downloadManager = get<DownloadManager.Factory>().create( downloadManager = get<DownloadManager.Factory>().create(
coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)), coroutineScope = lifecycleScope + WakeLockNode(wakeLock, TimeUnit.HOURS.toMillis(1)),
) )
DownloadNotification.createChannel(this) DownloadNotification.createChannel(this)
startForeground(DownloadNotification.ID_GROUP, downloadNotification.buildGroupNotification())
val intentFilter = IntentFilter() val intentFilter = IntentFilter()
intentFilter.addAction(ACTION_DOWNLOAD_CANCEL) intentFilter.addAction(ACTION_DOWNLOAD_CANCEL)
intentFilter.addAction(ACTION_DOWNLOAD_RESUME) intentFilter.addAction(ACTION_DOWNLOAD_RESUME)
@ -80,6 +81,7 @@ class DownloadService : BaseService() {
} }
override fun onDestroy() { override fun onDestroy() {
downloadNotification.dismiss()
unregisterReceiver(controlReceiver) unregisterReceiver(controlReceiver)
isRunning = false isRunning = false
super.onDestroy() super.onDestroy()
@ -98,10 +100,10 @@ class DownloadService : BaseService() {
private fun listenJob(job: ProgressJob<DownloadState>) { private fun listenJob(job: ProgressJob<DownloadState>) {
lifecycleScope.launch { lifecycleScope.launch {
val startId = job.progressValue.startId val startId = job.progressValue.startId
val notification = DownloadNotification(this@DownloadService, startId) val notificationItem = downloadNotification.newItem(startId)
try { try {
val timeLeftEstimator = TimeLeftEstimator() val timeLeftEstimator = TimeLeftEstimator()
notificationSwitcher.notify(startId, notification.create(job.progressValue, -1L)) notificationItem.notify(job.progressValue, -1L)
job.progressAsFlow() job.progressAsFlow()
.onEach { state -> .onEach { state ->
if (state is DownloadState.Progress) { if (state is DownloadState.Progress) {
@ -114,7 +116,7 @@ class DownloadService : BaseService() {
.whileActive() .whileActive()
.collect { state -> .collect { state ->
val timeLeft = timeLeftEstimator.getEstimatedTimeLeft() val timeLeft = timeLeftEstimator.getEstimatedTimeLeft()
notificationSwitcher.notify(startId, notification.create(state, timeLeft)) notificationItem.notify(state, timeLeft)
} }
job.join() job.join()
} finally { } finally {
@ -124,14 +126,11 @@ class DownloadService : BaseService() {
.putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)), .putExtra(EXTRA_MANGA, ParcelableManga(it.localManga, withChapters = false)),
) )
} }
notificationSwitcher.detach(
startId,
if (job.isCancelled) { if (job.isCancelled) {
null notificationItem.dismiss()
} else { } else {
notification.create(job.progressValue, -1L) notificationItem.notify(job.progressValue, -1L)
}, }
)
stopSelf(startId) stopSelf(startId)
} }
} }

@ -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<Notification>()
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)
}
}
}
}

@ -317,4 +317,6 @@
<string name="data_deletion">Удаление данных</string> <string name="data_deletion">Удаление данных</string>
<string name="clear_cookies_summary">Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы</string> <string name="clear_cookies_summary">Может помочь в случае каких-либо проблем. Все авторизации будут аннулированы</string>
<string name="show_all">Показать все</string> <string name="show_all">Показать все</string>
<string name="downloading_manga">Downloading manga</string>
<string name="completed">Completed</string>
</resources> </resources>

@ -323,4 +323,7 @@
<string name="invalid_domain_message">Invalid domain</string> <string name="invalid_domain_message">Invalid domain</string>
<string name="select_range">Select range</string> <string name="select_range">Select range</string>
<string name="not_found_404">Content not found or removed</string> <string name="not_found_404">Content not found or removed</string>
<string name="downloading_manga">Downloading manga</string>
<string name="download_summary_pattern" translatable="false">&lt;b>%1$s&lt;/b> %2$s</string>
<string name="completed">Completed</string>
</resources> </resources>
Loading…
Cancel
Save