From cbdb504b19e8c82781a3751a96793aa50913f298 Mon Sep 17 00:00:00 2001 From: Syer10 Date: Mon, 7 Mar 2022 13:57:26 -0500 Subject: [PATCH] Android working downloader notification --- .../ca/gosyer/jui/android/MainActivity.kt | 3 +- .../data/download/AndroidDownloadService.kt | 79 +++++++------------ .../data/notification/Notifications.kt | 18 +---- .../kotlin/ca/gosyer/core/lang/String.kt | 19 +++++ .../kotlin/ca/gosyer/core/lang/String.kt | 1 + .../ca/gosyer/data/base/WebsocketService.kt | 6 ++ .../resources/MR/values/base/strings.xml | 3 +- .../ui/downloads/AndroidDownloadService.kt | 28 +++++++ .../ui/downloads/DesktopDownloadService.kt | 19 +++++ .../ca/gosyer/ui/downloads/DownloadService.kt | 17 ++++ .../ui/downloads/DownloadsScreenViewModel.kt | 5 +- 11 files changed, 128 insertions(+), 70 deletions(-) create mode 100644 core/src/commonMain/kotlin/ca/gosyer/core/lang/String.kt create mode 100644 presentation/src/androidMain/kotlin/ca/gosyer/ui/downloads/AndroidDownloadService.kt create mode 100644 presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DesktopDownloadService.kt create mode 100644 presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadService.kt diff --git a/android/src/main/java/ca/gosyer/jui/android/MainActivity.kt b/android/src/main/java/ca/gosyer/jui/android/MainActivity.kt index c148214e..081de59d 100644 --- a/android/src/main/java/ca/gosyer/jui/android/MainActivity.kt +++ b/android/src/main/java/ca/gosyer/jui/android/MainActivity.kt @@ -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 { diff --git a/android/src/main/java/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt b/android/src/main/java/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt index f09be393..f8c9c0d8 100644 --- a/android/src/main/java/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt +++ b/android/src/main/java/ca/gosyer/jui/android/data/download/AndroidDownloadService.kt @@ -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) diff --git a/android/src/main/java/ca/gosyer/jui/android/data/notification/Notifications.kt b/android/src/main/java/ca/gosyer/jui/android/data/notification/Notifications.kt index 4d7376d1..c9468cd5 100644 --- a/android/src/main/java/ca/gosyer/jui/android/data/notification/Notifications.kt +++ b/android/src/main/java/ca/gosyer/jui/android/data/notification/Notifications.kt @@ -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) }, diff --git a/core/src/commonMain/kotlin/ca/gosyer/core/lang/String.kt b/core/src/commonMain/kotlin/ca/gosyer/core/lang/String.kt new file mode 100644 index 00000000..942a55a4 --- /dev/null +++ b/core/src/commonMain/kotlin/ca/gosyer/core/lang/String.kt @@ -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 + } +} \ No newline at end of file diff --git a/core/src/jvmMain/kotlin/ca/gosyer/core/lang/String.kt b/core/src/jvmMain/kotlin/ca/gosyer/core/lang/String.kt index 861fd481..7d29c5bf 100644 --- a/core/src/jvmMain/kotlin/ca/gosyer/core/lang/String.kt +++ b/core/src/jvmMain/kotlin/ca/gosyer/core/lang/String.kt @@ -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 diff --git a/data/src/jvmMain/kotlin/ca/gosyer/data/base/WebsocketService.kt b/data/src/jvmMain/kotlin/ca/gosyer/data/base/WebsocketService.kt index 7920ebc2..0b131551 100644 --- a/data/src/jvmMain/kotlin/ca/gosyer/data/base/WebsocketService.kt +++ b/data/src/jvmMain/kotlin/ca/gosyer/data/base/WebsocketService.kt @@ -93,5 +93,11 @@ abstract class WebsocketService( STOPPED } + enum class Actions { + STOP, + START, + RESTART + } + private companion object : CKLogger({}) } diff --git a/i18n/src/commonMain/resources/MR/values/base/strings.xml b/i18n/src/commonMain/resources/MR/values/base/strings.xml index af6de20e..17badfad 100644 --- a/i18n/src/commonMain/resources/MR/values/base/strings.xml +++ b/i18n/src/commonMain/resources/MR/values/base/strings.xml @@ -251,8 +251,7 @@ Downloader - Running - Progress + Progress Updates App updates Extension Updates diff --git a/presentation/src/androidMain/kotlin/ca/gosyer/ui/downloads/AndroidDownloadService.kt b/presentation/src/androidMain/kotlin/ca/gosyer/ui/downloads/AndroidDownloadService.kt new file mode 100644 index 00000000..7e895351 --- /dev/null +++ b/presentation/src/androidMain/kotlin/ca/gosyer/ui/downloads/AndroidDownloadService.kt @@ -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) +} \ No newline at end of file diff --git a/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DesktopDownloadService.kt b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DesktopDownloadService.kt new file mode 100644 index 00000000..8b98ebf5 --- /dev/null +++ b/presentation/src/desktopMain/kotlin/ca/gosyer/ui/downloads/DesktopDownloadService.kt @@ -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() +} \ No newline at end of file diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadService.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadService.kt new file mode 100644 index 00000000..4ed40da1 --- /dev/null +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadService.kt @@ -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 +) \ No newline at end of file diff --git a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt index 9f8ad891..e23a5190 100644 --- a/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt +++ b/presentation/src/jvmMain/kotlin/ca/gosyer/ui/downloads/DownloadsScreenViewModel.kt @@ -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({}) }