Android working downloader notification

This commit is contained in:
Syer10
2022-03-07 13:57:26 -05:00
parent 42ad4ee821
commit cbdb504b19
11 changed files with 128 additions and 70 deletions

View File

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

View File

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

View File

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

View 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
}
}

View File

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

View File

@@ -93,5 +93,11 @@ abstract class WebsocketService(
STOPPED
}
enum class Actions {
STOP,
START,
RESTART
}
private companion object : CKLogger({})
}

View File

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

View File

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

View 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.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()
}

View File

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

View File

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