From 2e63d70e6cc39c7ee5f0f72edbedb97c6294279c Mon Sep 17 00:00:00 2001 From: MohitMaliFtechiz Date: Mon, 23 Dec 2024 18:46:32 +0530 Subject: [PATCH] Fixed: Downloading starts very slowly on Android 14+. * Upgraded Fetch to 3.4.1. * Upgraded Kotlin kotlin-stdlib from JDK7 to JDK8. * Upgraded Kotlin version to 2.0.0 to support the latest Fetch library. * Upgraded Dagger to 2.53.1. * Upgraded ObjectBox to 4.0.3. * Notification now shows "Pause" and "Resume" buttons instead of "pause" and "resume". * Fixed: Notification buttons were not working on Android 13 and above. * Fixed: Downloading was not working in the background. Added a foreground service for Fetch so that downloading will continue in the background. --- buildSrc/build.gradle.kts | 2 +- buildSrc/src/main/kotlin/Libs.kt | 2 +- buildSrc/src/main/kotlin/Versions.kt | 8 +- .../kotlin/plugin/AllProjectConfigurer.kt | 9 +- core/build.gradle.kts | 4 + .../org/kiwix/kiwixmobile/core/CoreApp.kt | 5 - .../kiwix/kiwixmobile/core/dao/NewBookDao.kt | 1 + .../core/di/components/CoreComponent.kt | 2 + .../core/di/modules/DownloaderModule.kt | 6 +- .../core/downloader/DownloadMonitor.kt | 3 +- .../downloadManager/DownloadManagerMonitor.kt | 118 +++++++++++++++--- .../DownloadManagerRequester.kt | 19 +-- .../downloadManager/DownloadMonitorService.kt | 74 +++++++++-- .../FetchDownloadNotificationManager.kt | 70 ++++------- .../kiwixmobile/core/main/CoreMainActivity.kt | 41 ++++++ .../core/main/CoreReaderFragment.kt | 3 +- .../core/settings/CorePrefsFragment.kt | 8 +- core/src/main/res/values/strings.xml | 2 + gradle.properties | 3 +- 19 files changed, 262 insertions(+), 118 deletions(-) diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index e31bc9a7b..b174f55a7 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -12,7 +12,7 @@ repositories { dependencies { implementation("com.android.tools.build:gradle:8.1.3") - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.20") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.0.0") implementation("org.jacoco:org.jacoco.core:0.8.12") implementation("org.jlleitschuh.gradle:ktlint-gradle:10.3.0") implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20230406-2.0.0") { diff --git a/buildSrc/src/main/kotlin/Libs.kt b/buildSrc/src/main/kotlin/Libs.kt index 0ab74edd6..232bb99d5 100644 --- a/buildSrc/src/main/kotlin/Libs.kt +++ b/buildSrc/src/main/kotlin/Libs.kt @@ -101,7 +101,7 @@ object Libs { /** * https://kotlinlang.org/ */ - const val kotlin_stdlib_jdk7: String = "org.jetbrains.kotlin:kotlin-stdlib-jdk7:" + + const val kotlin_stdlib_jdk8: String = "org.jetbrains.kotlin:kotlin-stdlib-jdk8:" + Versions.org_jetbrains_kotlin /** diff --git a/buildSrc/src/main/kotlin/Versions.kt b/buildSrc/src/main/kotlin/Versions.kt index a2a8242ea..c23818e29 100644 --- a/buildSrc/src/main/kotlin/Versions.kt +++ b/buildSrc/src/main/kotlin/Versions.kt @@ -26,13 +26,13 @@ object Versions { const val com_squareup_okhttp3: String = "4.12.0" - const val org_jetbrains_kotlin: String = "1.9.20" + const val org_jetbrains_kotlin: String = "2.0.0" const val androidx_navigation: String = "2.5.3" const val navigation_ui_ktx: String = "2.4.1" - const val com_google_dagger: String = "2.48.1" + const val com_google_dagger: String = "2.53.1" const val androidx_test: String = "1.6.1" @@ -40,7 +40,7 @@ object Versions { const val androidx_test_orchestrator: String = "1.5.0" - const val io_objectbox: String = "3.5.0" + const val io_objectbox: String = "4.0.3" const val io_mockk: String = "1.13.13" @@ -110,7 +110,7 @@ object Versions { const val keeper = "0.16.1" - const val fetch: String = "3.3.0" + const val fetch: String = "3.4.1" } /** diff --git a/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt b/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt index 570800a3d..a87f9f4d3 100644 --- a/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt +++ b/buildSrc/src/main/kotlin/plugin/AllProjectConfigurer.kt @@ -28,6 +28,7 @@ import org.gradle.kotlin.dsl.apply import org.gradle.kotlin.dsl.dependencies import org.gradle.testing.jacoco.plugins.JacocoPluginExtension import org.gradle.testing.jacoco.plugins.JacocoTaskExtension +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jlleitschuh.gradle.ktlint.KtlintExtension @@ -75,7 +76,10 @@ class AllProjectConfigurer { targetCompatibility = Config.javaVersion } target.tasks.withType(KotlinCompile::class.java) { - kotlinOptions.jvmTarget = "1.8" + compilerOptions { + jvmTarget.set(JvmTarget.JVM_1_8) + freeCompilerArgs.add("-Xjvm-default=all-compatibility") + } } buildFeatures.apply { viewBinding = true @@ -191,7 +195,7 @@ class AllProjectConfigurer { fun configureDependencies(target: Project) { target.dependencies { - implementation(Libs.kotlin_stdlib_jdk7) + implementation(Libs.kotlin_stdlib_jdk8) implementation(Libs.appcompat) implementation(Libs.material) implementation(Libs.constraintlayout) @@ -227,7 +231,6 @@ class AllProjectConfigurer { implementation(Libs.roomRxjava2) kapt(Libs.roomCompiler) implementation(Libs.tracing) - implementation(Libs.fetch) implementation(Libs.fetchOkhttp) } } diff --git a/core/build.gradle.kts b/core/build.gradle.kts index 20216805f..1d61d4aee 100644 --- a/core/build.gradle.kts +++ b/core/build.gradle.kts @@ -63,4 +63,8 @@ dependencies { implementation(Libs.kotlinx_coroutines_android) implementation(Libs.kotlinx_coroutines_rx3) implementation(Libs.zxing) + api(Libs.fetch) { + // Todo: Will remove this when we add support for Android 15 + exclude("androidx.core", "core-ktx") + } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/CoreApp.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/CoreApp.kt index 2584172a9..3fc9a0330 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/CoreApp.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/CoreApp.kt @@ -28,7 +28,6 @@ import androidx.multidex.MultiDex import com.jakewharton.threetenabp.AndroidThreeTen import org.kiwix.kiwixmobile.core.di.components.CoreComponent import org.kiwix.kiwixmobile.core.di.components.DaggerCoreComponent -import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.utils.files.FileLogger import javax.inject.Inject @@ -43,9 +42,6 @@ abstract class CoreApp : Application() { lateinit var coreComponent: CoreComponent } - @Inject - lateinit var downloadMonitor: DownloadMonitor - @Inject lateinit var darkModeConfig: DarkModeConfig @@ -84,7 +80,6 @@ abstract class CoreApp : Application() { AndroidThreeTen.init(this) coreComponent.inject(this) serviceWorkerInitialiser.init(this) - downloadMonitor.init() darkModeConfig.init() fileLogger.writeLogFile(this) configureStrictMode() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt index 67542c596..7fc1ad4b8 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/NewBookDao.kt @@ -35,6 +35,7 @@ import javax.inject.Inject class NewBookDao @Inject constructor(private val box: Box) { + @Suppress("NoOp") fun books() = box.asFlowable() .flatMap { books -> io.reactivex.rxjava3.core.Flowable.fromIterable(books) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt index edc4a1a51..54a0405ab 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreComponent.kt @@ -49,6 +49,7 @@ import org.kiwix.kiwixmobile.core.di.modules.JNIModule import org.kiwix.kiwixmobile.core.di.modules.MutexModule import org.kiwix.kiwixmobile.core.di.modules.NetworkModule import org.kiwix.kiwixmobile.core.di.modules.SearchModule +import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor import org.kiwix.kiwixmobile.core.downloader.Downloader import org.kiwix.kiwixmobile.core.error.ErrorActivity import org.kiwix.kiwixmobile.core.main.KiwixWebView @@ -111,6 +112,7 @@ interface CoreComponent { fun notificationManager(): NotificationManager fun searchResultGenerator(): SearchResultGenerator fun mutex(): Mutex + fun provideDownloadMonitor(): DownloadMonitor fun inject(application: CoreApp) fun inject(kiwixWebView: KiwixWebView) diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DownloaderModule.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DownloaderModule.kt index e36905773..50c3f1416 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DownloaderModule.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/modules/DownloaderModule.kt @@ -32,7 +32,6 @@ import org.kiwix.kiwixmobile.core.data.remote.KiwixService import org.kiwix.kiwixmobile.core.downloader.DownloadRequester import org.kiwix.kiwixmobile.core.downloader.Downloader import org.kiwix.kiwixmobile.core.downloader.DownloaderImpl -import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerMonitor import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerRequester import org.kiwix.kiwixmobile.core.downloader.downloadManager.FetchDownloadNotificationManager import org.kiwix.kiwixmobile.core.utils.CONNECT_TIME_OUT @@ -56,10 +55,9 @@ object DownloaderModule { @Singleton fun providesDownloadRequester( fetch: Fetch, - sharedPreferenceUtil: SharedPreferenceUtil, - downloadManagerMonitor: DownloadManagerMonitor + sharedPreferenceUtil: SharedPreferenceUtil ): DownloadRequester = - DownloadManagerRequester(fetch, sharedPreferenceUtil, downloadManagerMonitor) + DownloadManagerRequester(fetch, sharedPreferenceUtil) @Provides @Singleton diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloadMonitor.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloadMonitor.kt index b8e596bf3..0a18abefe 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloadMonitor.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/DownloadMonitor.kt @@ -18,5 +18,6 @@ package org.kiwix.kiwixmobile.core.downloader interface DownloadMonitor { - fun init() + fun startMonitoringDownload() + fun stopListeningDownloads() } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerMonitor.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerMonitor.kt index d219f59b5..9d7a149e6 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerMonitor.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerMonitor.kt @@ -20,10 +20,16 @@ package org.kiwix.kiwixmobile.core.downloader.downloadManager import android.annotation.SuppressLint import android.content.Context -import android.content.Intent -import android.util.Log +import com.tonyodev.fetch2.Download +import com.tonyodev.fetch2.Error +import com.tonyodev.fetch2.Fetch +import com.tonyodev.fetch2.FetchListener +import com.tonyodev.fetch2core.DownloadBlock +import io.reactivex.disposables.Disposable +import io.reactivex.schedulers.Schedulers +import io.reactivex.subjects.PublishSubject +import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor -import org.kiwix.kiwixmobile.core.extensions.isServiceRunning import javax.inject.Inject const val ZERO = 0 @@ -33,27 +39,103 @@ const val DEFAULT_INT_VALUE = -1 @SuppressLint("CheckResult") class DownloadManagerMonitor @Inject constructor( - val context: Context + val fetch: Fetch, + val context: Context, + val downloadRoomDao: DownloadRoomDao ) : DownloadMonitor { + private val updater = PublishSubject.create<() -> Unit>() + private var updaterDisposable: Disposable? = null - init { - startMonitoringDownloads() + private fun setupUpdater() { + updaterDisposable = updater.subscribeOn(Schedulers.io()) + .observeOn(Schedulers.io()) + .subscribe( + { it.invoke() }, + Throwable::printStackTrace + ) } - /** - * Starts monitoring the downloads by ensuring that the `DownloadMonitorService` is running. - * This service keeps the Fetch instance alive when the application is in the background - * or has been killed by the user or system, allowing downloads to continue in the background. - */ - fun startMonitoringDownloads() { - if (!context.isServiceRunning(DownloadMonitorService::class.java)) { - context.startService(Intent(context, DownloadMonitorService::class.java)).also { - Log.e("DOWNLOAD_MANAGER_MONITOR", "Starting DownloadMonitorService") - } + private val fetchListener = object : FetchListener { + override fun onAdded(download: Download) { + // Do nothing + } + + override fun onCancelled(download: Download) { + delete(download) + } + + override fun onCompleted(download: Download) { + update(download) + } + + override fun onDeleted(download: Download) { + delete(download) + } + + override fun onDownloadBlockUpdated( + download: Download, + downloadBlock: DownloadBlock, + totalBlocks: Int + ) { + update(download) + } + + override fun onError(download: Download, error: Error, throwable: Throwable?) { + update(download) + } + + override fun onPaused(download: Download) { + update(download) + } + + override fun onProgress( + download: Download, + etaInMilliSeconds: Long, + downloadedBytesPerSecond: Long + ) { + update(download) + } + + override fun onQueued(download: Download, waitingOnNetwork: Boolean) { + update(download) + } + + override fun onRemoved(download: Download) { + delete(download) + } + + override fun onResumed(download: Download) { + update(download) + } + + override fun onStarted( + download: Download, + downloadBlocks: List, + totalBlocks: Int + ) { + update(download) + } + + override fun onWaitingNetwork(download: Download) { + update(download) } } - override fun init() { - // empty method to so class does not get reported unused + private fun update(download: Download) { + updater.onNext { downloadRoomDao.update(download) } + } + + private fun delete(download: Download) { + updater.onNext { downloadRoomDao.delete(download) } + } + + override fun startMonitoringDownload() { + fetch.addListener(fetchListener, true) + setupUpdater() + } + + override fun stopListeningDownloads() { + fetch.removeListener(fetchListener) + updaterDisposable?.dispose() } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerRequester.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerRequester.kt index f50e45b81..127720f07 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerRequester.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadManagerRequester.kt @@ -19,9 +19,9 @@ package org.kiwix.kiwixmobile.core.downloader.downloadManager import com.tonyodev.fetch2.Fetch -import com.tonyodev.fetch2.Request import com.tonyodev.fetch2.NetworkType.ALL import com.tonyodev.fetch2.NetworkType.WIFI_ONLY +import com.tonyodev.fetch2.Request import org.kiwix.kiwixmobile.core.downloader.DownloadRequester import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest import org.kiwix.kiwixmobile.core.utils.AUTO_RETRY_MAX_ATTEMPTS @@ -30,27 +30,20 @@ import javax.inject.Inject class DownloadManagerRequester @Inject constructor( private val fetch: Fetch, - private val sharedPreferenceUtil: SharedPreferenceUtil, - private val downloadManagerMonitor: DownloadManagerMonitor + private val sharedPreferenceUtil: SharedPreferenceUtil ) : DownloadRequester { override fun enqueue(downloadRequest: DownloadRequest): Long { val request = downloadRequest.toFetchRequest(sharedPreferenceUtil) fetch.enqueue(request) - return request.id.toLong().also { - downloadManagerMonitor.startMonitoringDownloads() - } + return request.id.toLong() } override fun cancel(downloadId: Long) { - fetch.delete(downloadId.toInt()).also { - downloadManagerMonitor.startMonitoringDownloads() - } + fetch.delete(downloadId.toInt()) } override fun retryDownload(downloadId: Long) { - fetch.retry(downloadId.toInt()).also { - downloadManagerMonitor.startMonitoringDownloads() - } + fetch.retry(downloadId.toInt()) } override fun pauseResumeDownload(downloadId: Long, isPause: Boolean) { @@ -58,8 +51,6 @@ class DownloadManagerRequester @Inject constructor( fetch.resume(downloadId.toInt()) } else { fetch.pause(downloadId.toInt()) - }.also { - downloadManagerMonitor.startMonitoringDownloads() } } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt index d1d7286df..2524aac65 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt @@ -25,6 +25,7 @@ import com.tonyodev.fetch2.Download import com.tonyodev.fetch2.Error import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.FetchListener +import com.tonyodev.fetch2.Status import com.tonyodev.fetch2core.DownloadBlock import io.reactivex.Observable import io.reactivex.disposables.Disposable @@ -45,6 +46,9 @@ class DownloadMonitorService : Service() { @Inject lateinit var fetch: Fetch + @Inject + lateinit var fetchDownloadNotificationManager: FetchDownloadNotificationManager + @Inject lateinit var downloadRoomDao: DownloadRoomDao @@ -55,11 +59,16 @@ class DownloadMonitorService : Service() { .build() .inject(this) super.onCreate() - fetch.addListener(fetchListener, true) setupUpdater() startMonitoringDownloads() + fetch.addListener(fetchListener, true) + setForegroundNotification() } + /** + * Periodically checks if there are active downloads. + * If no downloads are active, it stops the foreground service. + */ private fun startMonitoringDownloads() { // Check if monitoring is already active. If it is, do nothing. if (monitoringDisposable?.isDisposed == false) return @@ -96,7 +105,38 @@ class DownloadMonitorService : Service() { override fun onBind(intent: Intent?): IBinder? = null - override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int = START_NOT_STICKY + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + if (intent?.action == STOP_DOWNLOAD_SERVICE) { + stopForegroundServiceForDownloads() + } + return START_NOT_STICKY + } + + /** + * Sets the foreground notification for the service. + * This notification is used to display the current download progress, + * and it is updated dynamically based on the state of the downloads. + * + * The method checks for any active downloads and, if found, updates the notification + * with the latest download progress. If there are no active downloads, + * the service is stopped and removed from the foreground. + */ + private fun setForegroundNotification() { + updater.onNext { + fetch.getDownloads { downloadList -> + downloadList.firstOrNull { + it.status == Status.NONE || + it.status == Status.ADDED || + it.status == Status.QUEUED || + it.status == Status.DOWNLOADING + }?.let { + val notificationBuilder = + fetchDownloadNotificationManager.getNotificationBuilder(it.id, it.id) + startForeground(it.id, notificationBuilder.build()) + } ?: kotlin.run(::stopForegroundServiceForDownloads) + } + } + } private val fetchListener = object : FetchListener { override fun onAdded(download: Download) { @@ -108,7 +148,7 @@ class DownloadMonitorService : Service() { } override fun onCompleted(download: Download) { - update(download) + update(download, true) } override fun onDeleted(download: Download) { @@ -124,11 +164,11 @@ class DownloadMonitorService : Service() { } override fun onError(download: Download, error: Error, throwable: Throwable?) { - update(download) + update(download, true) } override fun onPaused(download: Download) { - update(download) + update(download, true) } override fun onProgress( @@ -163,25 +203,33 @@ class DownloadMonitorService : Service() { update(download) } - private fun update(download: Download) { - updater.onNext { downloadRoomDao.update(download) } + private fun update(download: Download, shouldSetForegroundNotification: Boolean = false) { + updater.onNext { downloadRoomDao.update(download) }.also { + if (shouldSetForegroundNotification) { + setForegroundNotification() + } + } } private fun delete(download: Download) { - updater.onNext { downloadRoomDao.delete(download) } + updater.onNext { downloadRoomDao.delete(download) }.also { + setForegroundNotification() + } } } + /** + * Stops the foreground service, disposes of resources, and removes the Fetch listener. + */ private fun stopForegroundServiceForDownloads() { - // foreGroundServiceInformation = true to DEFAULT_INT_VALUE - Log.e( - "DOWNLOAD_MANAGER_MONITOR", - "Stopping DownloadMonitorService as there is no more download available to track" - ) monitoringDisposable?.dispose() updaterDisposable?.dispose() fetch.removeListener(fetchListener) stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } + + companion object { + const val STOP_DOWNLOAD_SERVICE = "stop_download_service" + } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/FetchDownloadNotificationManager.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/FetchDownloadNotificationManager.kt index 81d26b6a3..af734d1b2 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/FetchDownloadNotificationManager.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/FetchDownloadNotificationManager.kt @@ -21,46 +21,52 @@ package org.kiwix.kiwixmobile.core.downloader.downloadManager import android.annotation.SuppressLint import android.app.NotificationChannel import android.app.NotificationManager -import android.app.PendingIntent import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.getActivity import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.os.Build import androidx.annotation.RequiresApi import androidx.core.app.NotificationCompat -import com.tonyodev.fetch2.ACTION_TYPE_CANCEL -import com.tonyodev.fetch2.ACTION_TYPE_DELETE -import com.tonyodev.fetch2.ACTION_TYPE_INVALID -import com.tonyodev.fetch2.ACTION_TYPE_PAUSE -import com.tonyodev.fetch2.ACTION_TYPE_RESUME -import com.tonyodev.fetch2.ACTION_TYPE_RETRY import com.tonyodev.fetch2.DefaultFetchNotificationManager import com.tonyodev.fetch2.DownloadNotification -import com.tonyodev.fetch2.EXTRA_ACTION_TYPE -import com.tonyodev.fetch2.EXTRA_DOWNLOAD_ID -import com.tonyodev.fetch2.EXTRA_GROUP_ACTION -import com.tonyodev.fetch2.EXTRA_NAMESPACE -import com.tonyodev.fetch2.EXTRA_NOTIFICATION_GROUP_ID -import com.tonyodev.fetch2.EXTRA_NOTIFICATION_ID import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.R.drawable import com.tonyodev.fetch2.R.string import com.tonyodev.fetch2.util.DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET +import org.kiwix.kiwixmobile.core.CoreApp import org.kiwix.kiwixmobile.core.Intents import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.main.CoreMainActivity +import javax.inject.Inject const val DOWNLOAD_NOTIFICATION_TITLE = "OPEN_ZIM_FILE" -class FetchDownloadNotificationManager( - private val context: Context, +class FetchDownloadNotificationManager @Inject constructor( + context: Context, private val downloadRoomDao: DownloadRoomDao ) : DefaultFetchNotificationManager(context) { override fun getFetchInstanceForNamespace(namespace: String): Fetch = Fetch.getDefaultInstance() + override fun registerBroadcastReceiver() { + val context = CoreApp.instance.applicationContext + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + context.registerReceiver( + broadcastReceiver, + IntentFilter(notificationManagerAction), + Context.RECEIVER_EXPORTED + ) + } else { + context.registerReceiver( + broadcastReceiver, + IntentFilter(notificationManagerAction) + ) + } + } + override fun createNotificationChannels( context: Context, notificationManager: NotificationManager @@ -90,7 +96,6 @@ class FetchDownloadNotificationManager( downloadNotification: DownloadNotification, context: Context ) { - // super method but with pause button removed val smallIcon = if (downloadNotification.isDownloading) { android.R.drawable.stat_sys_download } else { @@ -124,7 +129,7 @@ class FetchDownloadNotificationManager( getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.DELETE) ).addAction( drawable.fetch_notification_pause, - context.getString(R.string.tts_pause), + context.getString(R.string.notification_pause_button_text), getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.PAUSE) ) @@ -132,7 +137,7 @@ class FetchDownloadNotificationManager( notificationBuilder.setTimeoutAfter(getNotificationTimeOutMillis()) .addAction( drawable.fetch_notification_resume, - context.getString(R.string.tts_resume), + context.getString(R.string.notification_resume_button_text), getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.RESUME) ) .addAction( @@ -149,35 +154,6 @@ class FetchDownloadNotificationManager( notificationCustomisation(downloadNotification, notificationBuilder, context) } - override fun getActionPendingIntent( - downloadNotification: DownloadNotification, - actionType: DownloadNotification.ActionType - ): PendingIntent { - val intent = Intent(notificationManagerAction).apply { - putExtra(EXTRA_NAMESPACE, downloadNotification.namespace) - putExtra(EXTRA_DOWNLOAD_ID, downloadNotification.notificationId) - putExtra(EXTRA_NOTIFICATION_ID, downloadNotification.notificationId) - putExtra(EXTRA_GROUP_ACTION, false) - putExtra(EXTRA_NOTIFICATION_GROUP_ID, downloadNotification.groupId) - } - val action = when (actionType) { - DownloadNotification.ActionType.CANCEL -> ACTION_TYPE_CANCEL - DownloadNotification.ActionType.DELETE -> ACTION_TYPE_DELETE - DownloadNotification.ActionType.RESUME -> ACTION_TYPE_RESUME - DownloadNotification.ActionType.PAUSE -> ACTION_TYPE_PAUSE - DownloadNotification.ActionType.RETRY -> ACTION_TYPE_RETRY - else -> ACTION_TYPE_INVALID - } - intent.putExtra(EXTRA_ACTION_TYPE, action) - val flags = PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT - return PendingIntent.getBroadcast( - context, - downloadNotification.notificationId + action, - intent, - flags - ) - } - @SuppressLint("UnspecifiedImmutableFlag") private fun notificationCustomisation( downloadNotification: DownloadNotification, diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.kt index e79cb20aa..858f6e06a 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreMainActivity.kt @@ -52,9 +52,13 @@ import org.kiwix.kiwixmobile.core.base.FragmentActivityExtensions.Super.ShouldCa import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToLibkiwixMigrator import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToRoomMigrator import org.kiwix.kiwixmobile.core.di.components.CoreActivityComponent +import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor +import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadMonitorService +import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadMonitorService.Companion.STOP_DOWNLOAD_SERVICE import org.kiwix.kiwixmobile.core.error.ErrorActivity import org.kiwix.kiwixmobile.core.extensions.browserIntent import org.kiwix.kiwixmobile.core.extensions.getToolbarNavigationIcon +import org.kiwix.kiwixmobile.core.extensions.isServiceRunning import org.kiwix.kiwixmobile.core.extensions.setToolTipWithContentDescription import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderSource @@ -98,6 +102,9 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider { @Inject lateinit var objectBoxToLibkiwixMigrator: ObjectBoxToLibkiwixMigrator @Inject lateinit var objectBoxToRoomMigrator: ObjectBoxToRoomMigrator + @Inject + lateinit var downloadMonitor: DownloadMonitor + override fun onCreate(savedInstanceState: Bundle?) { setTheme(R.style.KiwixTheme) super.onCreate(savedInstanceState) @@ -140,17 +147,51 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider { override fun onStart() { super.onStart() + downloadMonitor.startMonitoringDownload() + stopDownloadServiceIfRunning() rateDialogHandler.checkForRateDialog(getIconResId()) navController.addOnDestinationChangedListener { _, destination, _ -> configureActivityBasedOn(destination) } } + /** + * Stops the DownloadService if it is currently running, + * as the application is now in the foreground and can handle downloads directly. + */ + private fun stopDownloadServiceIfRunning() { + if (isServiceRunning(DownloadMonitorService::class.java)) { + startService( + Intent( + this, + DownloadMonitorService::class.java + ).setAction(STOP_DOWNLOAD_SERVICE) + ) + } + } + override fun onDestroy() { onBackPressedCallBack.remove() super.onDestroy() } + override fun onStop() { + startMonitoringDownloads() + downloadMonitor.stopListeningDownloads() + super.onStop() + } + + /** + * Starts monitoring the downloads by ensuring that the `DownloadMonitorService` is running. + * This service keeps the Fetch instance alive when the application is in the background + * or has been killed by the user or system, allowing downloads to continue in the background. + */ + private fun startMonitoringDownloads() { + if (!isServiceRunning(DownloadMonitorService::class.java)) { + startService(Intent(this, DownloadMonitorService::class.java)) + } + } + open fun configureActivityBasedOn(destination: NavDestination) { if (destination.id !in topLevelDestinations) { handleDrawerOnNavigation() diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt index 9a1142926..055f1d909 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/main/CoreReaderFragment.kt @@ -1851,8 +1851,7 @@ abstract class CoreReaderFragment : setIsCloseAllTabButtonClickable(true) restoreDeletedTabs() } - show() - } + }.show() } } diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt index 4aba32305..ac8638fd1 100644 --- a/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt +++ b/core/src/main/java/org/kiwix/kiwixmobile/core/settings/CorePrefsFragment.kt @@ -78,19 +78,19 @@ abstract class CorePrefsFragment : @JvmField @Inject - protected var sharedPreferenceUtil: SharedPreferenceUtil? = null + var sharedPreferenceUtil: SharedPreferenceUtil? = null @JvmField @Inject - protected var storageCalculator: StorageCalculator? = null + var storageCalculator: StorageCalculator? = null @JvmField @Inject - protected var darkModeConfig: DarkModeConfig? = null + var darkModeConfig: DarkModeConfig? = null @JvmField @Inject - protected var alertDialogShower: DialogShower? = null + var alertDialogShower: DialogShower? = null @JvmField @Inject diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index bc9dd344b..0ca3e5b84 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -147,6 +147,8 @@ pause resume stop + Pause + Resume Internal External Yes diff --git a/gradle.properties b/gradle.properties index 85a362203..e88e39983 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,5 @@ android.enableJetifier=true android.useAndroidX=true org.gradle.jvmargs=-Xmx6096m -kotlin.code.style=1.7 +kotlin.code.style=1.8 +kapt.use.k2=true