mirror of
https://github.com/Suwayomi/TachideskJUI.git
synced 2025-12-10 14:52:03 +01:00
Android working downloader notification
This commit is contained in:
@@ -4,6 +4,7 @@ import android.os.Bundle
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import ca.gosyer.data.base.WebsocketService.Actions
|
||||
import ca.gosyer.jui.android.data.download.AndroidDownloadService
|
||||
import ca.gosyer.ui.base.theme.AppTheme
|
||||
import ca.gosyer.ui.main.MainMenu
|
||||
@@ -18,7 +19,7 @@ class MainActivity : AppCompatActivity() {
|
||||
appComponent.appMigrations.runMigrations()
|
||||
}
|
||||
|
||||
AndroidDownloadService.start(this, AndroidDownloadService.Actions.START)
|
||||
AndroidDownloadService.start(this, Actions.START)
|
||||
|
||||
val uiHooks = appComponent.uiComponent.getHooks()
|
||||
setContent {
|
||||
|
||||
@@ -6,19 +6,19 @@
|
||||
|
||||
package ca.gosyer.jui.android.data.download
|
||||
|
||||
import android.app.Notification
|
||||
import android.app.Service
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.os.IBinder
|
||||
import android.os.PowerManager
|
||||
import androidx.core.content.ContextCompat
|
||||
import ca.gosyer.core.lang.chop
|
||||
import ca.gosyer.core.lang.throwIfCancellation
|
||||
import ca.gosyer.core.logging.CKLogger
|
||||
import ca.gosyer.core.prefs.getAsFlow
|
||||
import ca.gosyer.data.base.WebsocketService
|
||||
import ca.gosyer.data.base.WebsocketService.Actions
|
||||
import ca.gosyer.data.base.WebsocketService.Status
|
||||
import ca.gosyer.data.download.DownloadService
|
||||
import ca.gosyer.data.download.DownloadService.Companion.status
|
||||
import ca.gosyer.data.download.model.DownloadState
|
||||
import ca.gosyer.data.download.model.DownloadStatus
|
||||
import ca.gosyer.data.server.requests.downloadsQuery
|
||||
@@ -26,7 +26,6 @@ import ca.gosyer.i18n.MR
|
||||
import ca.gosyer.jui.android.AppComponent
|
||||
import ca.gosyer.jui.android.R
|
||||
import ca.gosyer.jui.android.data.notification.Notifications
|
||||
import ca.gosyer.jui.android.util.acquireWakeLock
|
||||
import ca.gosyer.jui.android.util.notification
|
||||
import ca.gosyer.jui.android.util.notificationBuilder
|
||||
import ca.gosyer.jui.android.util.notificationManager
|
||||
@@ -41,7 +40,6 @@ import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.SupervisorJob
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlinx.coroutines.cancelChildren
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.filterIsInstance
|
||||
@@ -51,18 +49,11 @@ import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.job
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class AndroidDownloadService : Service() {
|
||||
|
||||
enum class Actions {
|
||||
STOP,
|
||||
START,
|
||||
RESTART
|
||||
}
|
||||
|
||||
companion object : CKLogger({}) {
|
||||
val running = MutableStateFlow(true)
|
||||
|
||||
private var instance: AndroidDownloadService? = null
|
||||
|
||||
fun start(context: Context, actions: Actions) {
|
||||
@@ -83,11 +74,6 @@ class AndroidDownloadService : Service() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wake lock to prevent the device to enter sleep mode.
|
||||
*/
|
||||
private lateinit var wakeLock: PowerManager.WakeLock
|
||||
|
||||
private lateinit var ioScope: CoroutineScope
|
||||
|
||||
override fun onBind(intent: Intent): IBinder? {
|
||||
@@ -97,16 +83,14 @@ class AndroidDownloadService : Service() {
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_RUNNING, getPlaceholderNotification())
|
||||
wakeLock = acquireWakeLock(javaClass.name)
|
||||
running.value = true
|
||||
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, placeholderNotification)
|
||||
status.value = Status.STARTING
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
ioScope.cancel()
|
||||
running.value = false
|
||||
wakeLock.releaseIfNeeded()
|
||||
notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
status.value = Status.STOPPED
|
||||
notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
|
||||
if (instance == this) {
|
||||
instance = null
|
||||
}
|
||||
@@ -121,7 +105,8 @@ class AndroidDownloadService : Service() {
|
||||
val action = intent.action
|
||||
info("using an intent with action $action")
|
||||
when (action) {
|
||||
Actions.START.name -> startWebsocket()
|
||||
Actions.START.name,
|
||||
Actions.RESTART.name -> startWebsocket()
|
||||
Actions.STOP.name -> stopSelf()
|
||||
else -> info("This should never happen. No action in the received intent")
|
||||
}
|
||||
@@ -152,10 +137,10 @@ class AndroidDownloadService : Service() {
|
||||
.serverUrl()
|
||||
.getAsFlow()
|
||||
.mapLatest { serverUrl ->
|
||||
DownloadService.status.value = WebsocketService.Status.STARTING
|
||||
status.value = Status.STARTING
|
||||
while (true) {
|
||||
if (errorConnectionCount > 3) {
|
||||
DownloadService.status.value = WebsocketService.Status.STOPPED
|
||||
status.value = Status.STOPPED
|
||||
throw CancellationException()
|
||||
}
|
||||
runCatching {
|
||||
@@ -164,7 +149,7 @@ class AndroidDownloadService : Service() {
|
||||
path = downloadsQuery()
|
||||
) {
|
||||
errorConnectionCount = 0
|
||||
DownloadService.status.value = WebsocketService.Status.RUNNING
|
||||
status.value = Status.RUNNING
|
||||
send(Frame.Text("STATUS"))
|
||||
|
||||
incoming.receiveAsFlow()
|
||||
@@ -176,13 +161,13 @@ class AndroidDownloadService : Service() {
|
||||
.collect()
|
||||
}
|
||||
}.throwIfCancellation().isFailure.let {
|
||||
DownloadService.status.value = WebsocketService.Status.STARTING
|
||||
status.value = Status.STARTING
|
||||
if (it) errorConnectionCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
.catch {
|
||||
DownloadService.status.value = WebsocketService.Status.STOPPED
|
||||
status.value = Status.STOPPED
|
||||
error(it) { "Error while running websocket service" }
|
||||
stopSelf()
|
||||
}
|
||||
@@ -197,8 +182,14 @@ class AndroidDownloadService : Service() {
|
||||
if (downloadingChapter != null) {
|
||||
val notification = with(progressNotificationBuilder) {
|
||||
val max = downloadingChapter.chapter.pageCount ?: 0
|
||||
val current = downloadingChapter.progress.toInt().coerceAtMost(max)
|
||||
val current = (max * downloadingChapter.progress).toInt().coerceIn(0, max)
|
||||
setProgress(max, current, false)
|
||||
|
||||
val title = downloadingChapter.manga.title.chop(15)
|
||||
val quotedTitle = Pattern.quote(title)
|
||||
val chapter = downloadingChapter.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
|
||||
setContentTitle("$title - $chapter".chop(30))
|
||||
|
||||
setContentText(
|
||||
MR.strings.chapter_downloading_progress
|
||||
.format(
|
||||
@@ -209,36 +200,22 @@ class AndroidDownloadService : Service() {
|
||||
)
|
||||
}.build()
|
||||
notificationManager.notify(
|
||||
Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS,
|
||||
Notifications.ID_DOWNLOAD_CHAPTER,
|
||||
notification
|
||||
)
|
||||
} else {
|
||||
notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
|
||||
notificationManager.notify(Notifications.ID_DOWNLOAD_CHAPTER, placeholderNotification)
|
||||
}
|
||||
}
|
||||
|
||||
private fun PowerManager.WakeLock.releaseIfNeeded() {
|
||||
if (isHeld) release()
|
||||
}
|
||||
|
||||
private fun PowerManager.WakeLock.acquireIfNeeded() {
|
||||
if (!isHeld) acquire()
|
||||
}
|
||||
|
||||
private val icon by lazy {
|
||||
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
|
||||
}
|
||||
|
||||
private fun getPlaceholderNotification(): Notification {
|
||||
return notification(Notifications.CHANNEL_DOWNLOADER_RUNNING) {
|
||||
private val placeholderNotification by lazy {
|
||||
notification(Notifications.CHANNEL_DOWNLOADER) {
|
||||
setContentTitle(MR.strings.downloader_running.desc().toString(this@AndroidDownloadService))
|
||||
setSmallIcon(R.drawable.ic_round_get_app_24)
|
||||
}
|
||||
}
|
||||
|
||||
private val progressNotificationBuilder by lazy {
|
||||
notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
|
||||
setLargeIcon(icon)
|
||||
notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
|
||||
setSmallIcon(R.drawable.ic_round_get_app_24)
|
||||
setAutoCancel(false)
|
||||
setOnlyAlertOnce(true)
|
||||
|
||||
@@ -19,10 +19,8 @@ object Notifications {
|
||||
* Notification channel and ids used by the downloader.
|
||||
*/
|
||||
private const val GROUP_DOWNLOADER = "group_downloader"
|
||||
const val CHANNEL_DOWNLOADER_RUNNING = "downloader_running_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER_RUNNING = -101
|
||||
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER_PROGRESS = -102
|
||||
const val CHANNEL_DOWNLOADER = "downloader_channel"
|
||||
const val ID_DOWNLOAD_CHAPTER = -101
|
||||
|
||||
/**
|
||||
* Notification channel and ids used for app updates.
|
||||
@@ -50,18 +48,10 @@ object Notifications {
|
||||
notificationService.createNotificationChannelsCompat(
|
||||
listOf(
|
||||
buildNotificationChannel(
|
||||
CHANNEL_DOWNLOADER_RUNNING,
|
||||
NotificationManagerCompat.IMPORTANCE_MIN
|
||||
) {
|
||||
setName(MR.strings.group_downloader_channel_running.desc().toString(context))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
buildNotificationChannel(
|
||||
CHANNEL_DOWNLOADER_PROGRESS,
|
||||
CHANNEL_DOWNLOADER,
|
||||
NotificationManagerCompat.IMPORTANCE_LOW
|
||||
) {
|
||||
setName(MR.strings.group_downloader_channel_progress.desc().toString(context))
|
||||
setName(MR.strings.group_downloader_channel.desc().toString(context))
|
||||
setGroup(GROUP_DOWNLOADER)
|
||||
setShowBadge(false)
|
||||
},
|
||||
|
||||
19
core/src/commonMain/kotlin/ca/gosyer/core/lang/String.kt
Normal file
19
core/src/commonMain/kotlin/ca/gosyer/core/lang/String.kt
Normal file
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.core.lang
|
||||
|
||||
/**
|
||||
* Replaces the given string to have at most [count] characters using [replacement] at its end.
|
||||
* If [replacement] is longer than [count] an exception will be thrown when `length > count`.
|
||||
*/
|
||||
fun String.chop(count: Int, replacement: String = "…"): String {
|
||||
return if (length > count) {
|
||||
take(count - replacement.length) + replacement
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
@file:JvmName("JvmStringsKt")
|
||||
|
||||
package ca.gosyer.core.lang
|
||||
|
||||
|
||||
@@ -93,5 +93,11 @@ abstract class WebsocketService(
|
||||
STOPPED
|
||||
}
|
||||
|
||||
enum class Actions {
|
||||
STOP,
|
||||
START,
|
||||
RESTART
|
||||
}
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
|
||||
@@ -251,8 +251,7 @@
|
||||
|
||||
<!-- Android notifications -->
|
||||
<string name="group_downloader">Downloader</string>
|
||||
<string name="group_downloader_channel_running">Running</string>
|
||||
<string name="group_downloader_channel_progress">Progress</string>
|
||||
<string name="group_downloader_channel">Progress</string>
|
||||
<string name="group_updates">Updates</string>
|
||||
<string name="group_updates_channel_app">App updates</string>
|
||||
<string name="group_updates_channel_ext">Extension Updates</string>
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.downloads
|
||||
|
||||
import android.content.Intent
|
||||
import androidx.core.content.ContextCompat
|
||||
import ca.gosyer.data.base.WebsocketService
|
||||
import ca.gosyer.data.download.DownloadService
|
||||
import ca.gosyer.uicore.vm.ContextWrapper
|
||||
|
||||
internal actual fun startDownloadService(
|
||||
contextWrapper: ContextWrapper,
|
||||
downloadService: DownloadService,
|
||||
actions: WebsocketService.Actions
|
||||
) {
|
||||
val context = contextWrapper.context
|
||||
val intent = Intent(
|
||||
context,
|
||||
Class.forName("ca.gosyer.jui.android.data.download.AndroidDownloadService")
|
||||
).apply {
|
||||
action = actions.name
|
||||
}
|
||||
ContextCompat.startForegroundService(context, intent)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.downloads
|
||||
|
||||
import ca.gosyer.data.base.WebsocketService
|
||||
import ca.gosyer.data.download.DownloadService
|
||||
import ca.gosyer.uicore.vm.ContextWrapper
|
||||
|
||||
internal actual fun startDownloadService(
|
||||
contextWrapper: ContextWrapper,
|
||||
downloadService: DownloadService,
|
||||
actions: WebsocketService.Actions
|
||||
) {
|
||||
downloadService.init()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
package ca.gosyer.ui.downloads
|
||||
|
||||
import ca.gosyer.data.base.WebsocketService
|
||||
import ca.gosyer.data.download.DownloadService
|
||||
import ca.gosyer.uicore.vm.ContextWrapper
|
||||
|
||||
internal expect fun startDownloadService(
|
||||
contextWrapper: ContextWrapper,
|
||||
downloadService: DownloadService,
|
||||
actions: WebsocketService.Actions
|
||||
)
|
||||
@@ -7,6 +7,7 @@
|
||||
package ca.gosyer.ui.downloads
|
||||
|
||||
import ca.gosyer.core.logging.CKLogger
|
||||
import ca.gosyer.data.base.WebsocketService.Actions
|
||||
import ca.gosyer.data.download.DownloadService
|
||||
import ca.gosyer.data.models.Chapter
|
||||
import ca.gosyer.data.server.interactions.ChapterInteractionHandler
|
||||
@@ -26,7 +27,7 @@ class DownloadsScreenViewModel @Inject constructor(
|
||||
private val downloadService: DownloadService,
|
||||
private val downloadsHandler: DownloadInteractionHandler,
|
||||
private val chapterHandler: ChapterInteractionHandler,
|
||||
contextWrapper: ContextWrapper,
|
||||
private val contextWrapper: ContextWrapper,
|
||||
standalone: Boolean
|
||||
) : ViewModel(contextWrapper) {
|
||||
private val uiScope = if (standalone) {
|
||||
@@ -87,7 +88,7 @@ class DownloadsScreenViewModel @Inject constructor(
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
||||
fun restartDownloader() = downloadService.init()
|
||||
fun restartDownloader() = startDownloadService(contextWrapper, downloadService, Actions.RESTART)
|
||||
|
||||
private companion object : CKLogger({})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user