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.
This commit is contained in:
MohitMaliFtechiz 2024-12-23 18:46:32 +05:30
parent 0f16730120
commit 2e63d70e6c
19 changed files with 262 additions and 118 deletions

View File

@ -12,7 +12,7 @@ repositories {
dependencies { dependencies {
implementation("com.android.tools.build:gradle:8.1.3") 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.jacoco:org.jacoco.core:0.8.12")
implementation("org.jlleitschuh.gradle:ktlint-gradle:10.3.0") implementation("org.jlleitschuh.gradle:ktlint-gradle:10.3.0")
implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20230406-2.0.0") { implementation("com.google.apis:google-api-services-androidpublisher:v3-rev20230406-2.0.0") {

View File

@ -101,7 +101,7 @@ object Libs {
/** /**
* https://kotlinlang.org/ * 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 Versions.org_jetbrains_kotlin
/** /**

View File

@ -26,13 +26,13 @@ object Versions {
const val com_squareup_okhttp3: String = "4.12.0" 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 androidx_navigation: String = "2.5.3"
const val navigation_ui_ktx: String = "2.4.1" 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" 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 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" const val io_mockk: String = "1.13.13"
@ -110,7 +110,7 @@ object Versions {
const val keeper = "0.16.1" const val keeper = "0.16.1"
const val fetch: String = "3.3.0" const val fetch: String = "3.4.1"
} }
/** /**

View File

@ -28,6 +28,7 @@ import org.gradle.kotlin.dsl.apply
import org.gradle.kotlin.dsl.dependencies import org.gradle.kotlin.dsl.dependencies
import org.gradle.testing.jacoco.plugins.JacocoPluginExtension import org.gradle.testing.jacoco.plugins.JacocoPluginExtension
import org.gradle.testing.jacoco.plugins.JacocoTaskExtension import org.gradle.testing.jacoco.plugins.JacocoTaskExtension
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
import org.jlleitschuh.gradle.ktlint.KtlintExtension import org.jlleitschuh.gradle.ktlint.KtlintExtension
@ -75,7 +76,10 @@ class AllProjectConfigurer {
targetCompatibility = Config.javaVersion targetCompatibility = Config.javaVersion
} }
target.tasks.withType(KotlinCompile::class.java) { 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 { buildFeatures.apply {
viewBinding = true viewBinding = true
@ -191,7 +195,7 @@ class AllProjectConfigurer {
fun configureDependencies(target: Project) { fun configureDependencies(target: Project) {
target.dependencies { target.dependencies {
implementation(Libs.kotlin_stdlib_jdk7) implementation(Libs.kotlin_stdlib_jdk8)
implementation(Libs.appcompat) implementation(Libs.appcompat)
implementation(Libs.material) implementation(Libs.material)
implementation(Libs.constraintlayout) implementation(Libs.constraintlayout)
@ -227,7 +231,6 @@ class AllProjectConfigurer {
implementation(Libs.roomRxjava2) implementation(Libs.roomRxjava2)
kapt(Libs.roomCompiler) kapt(Libs.roomCompiler)
implementation(Libs.tracing) implementation(Libs.tracing)
implementation(Libs.fetch)
implementation(Libs.fetchOkhttp) implementation(Libs.fetchOkhttp)
} }
} }

View File

@ -63,4 +63,8 @@ dependencies {
implementation(Libs.kotlinx_coroutines_android) implementation(Libs.kotlinx_coroutines_android)
implementation(Libs.kotlinx_coroutines_rx3) implementation(Libs.kotlinx_coroutines_rx3)
implementation(Libs.zxing) implementation(Libs.zxing)
api(Libs.fetch) {
// Todo: Will remove this when we add support for Android 15
exclude("androidx.core", "core-ktx")
}
} }

View File

@ -28,7 +28,6 @@ import androidx.multidex.MultiDex
import com.jakewharton.threetenabp.AndroidThreeTen import com.jakewharton.threetenabp.AndroidThreeTen
import org.kiwix.kiwixmobile.core.di.components.CoreComponent import org.kiwix.kiwixmobile.core.di.components.CoreComponent
import org.kiwix.kiwixmobile.core.di.components.DaggerCoreComponent 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.main.CoreMainActivity
import org.kiwix.kiwixmobile.core.utils.files.FileLogger import org.kiwix.kiwixmobile.core.utils.files.FileLogger
import javax.inject.Inject import javax.inject.Inject
@ -43,9 +42,6 @@ abstract class CoreApp : Application() {
lateinit var coreComponent: CoreComponent lateinit var coreComponent: CoreComponent
} }
@Inject
lateinit var downloadMonitor: DownloadMonitor
@Inject @Inject
lateinit var darkModeConfig: DarkModeConfig lateinit var darkModeConfig: DarkModeConfig
@ -84,7 +80,6 @@ abstract class CoreApp : Application() {
AndroidThreeTen.init(this) AndroidThreeTen.init(this)
coreComponent.inject(this) coreComponent.inject(this)
serviceWorkerInitialiser.init(this) serviceWorkerInitialiser.init(this)
downloadMonitor.init()
darkModeConfig.init() darkModeConfig.init()
fileLogger.writeLogFile(this) fileLogger.writeLogFile(this)
configureStrictMode() configureStrictMode()

View File

@ -35,6 +35,7 @@ import javax.inject.Inject
class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) { class NewBookDao @Inject constructor(private val box: Box<BookOnDiskEntity>) {
@Suppress("NoOp")
fun books() = box.asFlowable() fun books() = box.asFlowable()
.flatMap { books -> .flatMap { books ->
io.reactivex.rxjava3.core.Flowable.fromIterable(books) io.reactivex.rxjava3.core.Flowable.fromIterable(books)

View File

@ -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.MutexModule
import org.kiwix.kiwixmobile.core.di.modules.NetworkModule import org.kiwix.kiwixmobile.core.di.modules.NetworkModule
import org.kiwix.kiwixmobile.core.di.modules.SearchModule 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.downloader.Downloader
import org.kiwix.kiwixmobile.core.error.ErrorActivity import org.kiwix.kiwixmobile.core.error.ErrorActivity
import org.kiwix.kiwixmobile.core.main.KiwixWebView import org.kiwix.kiwixmobile.core.main.KiwixWebView
@ -111,6 +112,7 @@ interface CoreComponent {
fun notificationManager(): NotificationManager fun notificationManager(): NotificationManager
fun searchResultGenerator(): SearchResultGenerator fun searchResultGenerator(): SearchResultGenerator
fun mutex(): Mutex fun mutex(): Mutex
fun provideDownloadMonitor(): DownloadMonitor
fun inject(application: CoreApp) fun inject(application: CoreApp)
fun inject(kiwixWebView: KiwixWebView) fun inject(kiwixWebView: KiwixWebView)

View File

@ -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.DownloadRequester
import org.kiwix.kiwixmobile.core.downloader.Downloader import org.kiwix.kiwixmobile.core.downloader.Downloader
import org.kiwix.kiwixmobile.core.downloader.DownloaderImpl 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.DownloadManagerRequester
import org.kiwix.kiwixmobile.core.downloader.downloadManager.FetchDownloadNotificationManager import org.kiwix.kiwixmobile.core.downloader.downloadManager.FetchDownloadNotificationManager
import org.kiwix.kiwixmobile.core.utils.CONNECT_TIME_OUT import org.kiwix.kiwixmobile.core.utils.CONNECT_TIME_OUT
@ -56,10 +55,9 @@ object DownloaderModule {
@Singleton @Singleton
fun providesDownloadRequester( fun providesDownloadRequester(
fetch: Fetch, fetch: Fetch,
sharedPreferenceUtil: SharedPreferenceUtil, sharedPreferenceUtil: SharedPreferenceUtil
downloadManagerMonitor: DownloadManagerMonitor
): DownloadRequester = ): DownloadRequester =
DownloadManagerRequester(fetch, sharedPreferenceUtil, downloadManagerMonitor) DownloadManagerRequester(fetch, sharedPreferenceUtil)
@Provides @Provides
@Singleton @Singleton

View File

@ -18,5 +18,6 @@
package org.kiwix.kiwixmobile.core.downloader package org.kiwix.kiwixmobile.core.downloader
interface DownloadMonitor { interface DownloadMonitor {
fun init() fun startMonitoringDownload()
fun stopListeningDownloads()
} }

View File

@ -20,10 +20,16 @@ package org.kiwix.kiwixmobile.core.downloader.downloadManager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent import com.tonyodev.fetch2.Download
import android.util.Log 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.downloader.DownloadMonitor
import org.kiwix.kiwixmobile.core.extensions.isServiceRunning
import javax.inject.Inject import javax.inject.Inject
const val ZERO = 0 const val ZERO = 0
@ -33,27 +39,103 @@ const val DEFAULT_INT_VALUE = -1
@SuppressLint("CheckResult") @SuppressLint("CheckResult")
class DownloadManagerMonitor @Inject constructor( class DownloadManagerMonitor @Inject constructor(
val context: Context val fetch: Fetch,
val context: Context,
val downloadRoomDao: DownloadRoomDao
) : DownloadMonitor { ) : DownloadMonitor {
private val updater = PublishSubject.create<() -> Unit>()
private var updaterDisposable: Disposable? = null
init { private fun setupUpdater() {
startMonitoringDownloads() updaterDisposable = updater.subscribeOn(Schedulers.io())
.observeOn(Schedulers.io())
.subscribe(
{ it.invoke() },
Throwable::printStackTrace
)
} }
/** private val fetchListener = object : FetchListener {
* Starts monitoring the downloads by ensuring that the `DownloadMonitorService` is running. override fun onAdded(download: Download) {
* This service keeps the Fetch instance alive when the application is in the background // Do nothing
* or has been killed by the user or system, allowing downloads to continue in the background. }
*/
fun startMonitoringDownloads() { override fun onCancelled(download: Download) {
if (!context.isServiceRunning(DownloadMonitorService::class.java)) { delete(download)
context.startService(Intent(context, DownloadMonitorService::class.java)).also { }
Log.e("DOWNLOAD_MANAGER_MONITOR", "Starting DownloadMonitorService")
} 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<DownloadBlock>,
totalBlocks: Int
) {
update(download)
}
override fun onWaitingNetwork(download: Download) {
update(download)
} }
} }
override fun init() { private fun update(download: Download) {
// empty method to so class does not get reported unused 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()
} }
} }

View File

@ -19,9 +19,9 @@
package org.kiwix.kiwixmobile.core.downloader.downloadManager package org.kiwix.kiwixmobile.core.downloader.downloadManager
import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.Request
import com.tonyodev.fetch2.NetworkType.ALL import com.tonyodev.fetch2.NetworkType.ALL
import com.tonyodev.fetch2.NetworkType.WIFI_ONLY 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.DownloadRequester
import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest import org.kiwix.kiwixmobile.core.downloader.model.DownloadRequest
import org.kiwix.kiwixmobile.core.utils.AUTO_RETRY_MAX_ATTEMPTS import org.kiwix.kiwixmobile.core.utils.AUTO_RETRY_MAX_ATTEMPTS
@ -30,27 +30,20 @@ import javax.inject.Inject
class DownloadManagerRequester @Inject constructor( class DownloadManagerRequester @Inject constructor(
private val fetch: Fetch, private val fetch: Fetch,
private val sharedPreferenceUtil: SharedPreferenceUtil, private val sharedPreferenceUtil: SharedPreferenceUtil
private val downloadManagerMonitor: DownloadManagerMonitor
) : DownloadRequester { ) : DownloadRequester {
override fun enqueue(downloadRequest: DownloadRequest): Long { override fun enqueue(downloadRequest: DownloadRequest): Long {
val request = downloadRequest.toFetchRequest(sharedPreferenceUtil) val request = downloadRequest.toFetchRequest(sharedPreferenceUtil)
fetch.enqueue(request) fetch.enqueue(request)
return request.id.toLong().also { return request.id.toLong()
downloadManagerMonitor.startMonitoringDownloads()
}
} }
override fun cancel(downloadId: Long) { override fun cancel(downloadId: Long) {
fetch.delete(downloadId.toInt()).also { fetch.delete(downloadId.toInt())
downloadManagerMonitor.startMonitoringDownloads()
}
} }
override fun retryDownload(downloadId: Long) { override fun retryDownload(downloadId: Long) {
fetch.retry(downloadId.toInt()).also { fetch.retry(downloadId.toInt())
downloadManagerMonitor.startMonitoringDownloads()
}
} }
override fun pauseResumeDownload(downloadId: Long, isPause: Boolean) { override fun pauseResumeDownload(downloadId: Long, isPause: Boolean) {
@ -58,8 +51,6 @@ class DownloadManagerRequester @Inject constructor(
fetch.resume(downloadId.toInt()) fetch.resume(downloadId.toInt())
} else { } else {
fetch.pause(downloadId.toInt()) fetch.pause(downloadId.toInt())
}.also {
downloadManagerMonitor.startMonitoringDownloads()
} }
} }
} }

View File

@ -25,6 +25,7 @@ import com.tonyodev.fetch2.Download
import com.tonyodev.fetch2.Error import com.tonyodev.fetch2.Error
import com.tonyodev.fetch2.Fetch import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchListener import com.tonyodev.fetch2.FetchListener
import com.tonyodev.fetch2.Status
import com.tonyodev.fetch2core.DownloadBlock import com.tonyodev.fetch2core.DownloadBlock
import io.reactivex.Observable import io.reactivex.Observable
import io.reactivex.disposables.Disposable import io.reactivex.disposables.Disposable
@ -45,6 +46,9 @@ class DownloadMonitorService : Service() {
@Inject @Inject
lateinit var fetch: Fetch lateinit var fetch: Fetch
@Inject
lateinit var fetchDownloadNotificationManager: FetchDownloadNotificationManager
@Inject @Inject
lateinit var downloadRoomDao: DownloadRoomDao lateinit var downloadRoomDao: DownloadRoomDao
@ -55,11 +59,16 @@ class DownloadMonitorService : Service() {
.build() .build()
.inject(this) .inject(this)
super.onCreate() super.onCreate()
fetch.addListener(fetchListener, true)
setupUpdater() setupUpdater()
startMonitoringDownloads() 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() { private fun startMonitoringDownloads() {
// Check if monitoring is already active. If it is, do nothing. // Check if monitoring is already active. If it is, do nothing.
if (monitoringDisposable?.isDisposed == false) return if (monitoringDisposable?.isDisposed == false) return
@ -96,7 +105,38 @@ class DownloadMonitorService : Service() {
override fun onBind(intent: Intent?): IBinder? = null 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 { private val fetchListener = object : FetchListener {
override fun onAdded(download: Download) { override fun onAdded(download: Download) {
@ -108,7 +148,7 @@ class DownloadMonitorService : Service() {
} }
override fun onCompleted(download: Download) { override fun onCompleted(download: Download) {
update(download) update(download, true)
} }
override fun onDeleted(download: Download) { override fun onDeleted(download: Download) {
@ -124,11 +164,11 @@ class DownloadMonitorService : Service() {
} }
override fun onError(download: Download, error: Error, throwable: Throwable?) { override fun onError(download: Download, error: Error, throwable: Throwable?) {
update(download) update(download, true)
} }
override fun onPaused(download: Download) { override fun onPaused(download: Download) {
update(download) update(download, true)
} }
override fun onProgress( override fun onProgress(
@ -163,25 +203,33 @@ class DownloadMonitorService : Service() {
update(download) update(download)
} }
private fun update(download: Download) { private fun update(download: Download, shouldSetForegroundNotification: Boolean = false) {
updater.onNext { downloadRoomDao.update(download) } updater.onNext { downloadRoomDao.update(download) }.also {
if (shouldSetForegroundNotification) {
setForegroundNotification()
}
}
} }
private fun delete(download: Download) { 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() { 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() monitoringDisposable?.dispose()
updaterDisposable?.dispose() updaterDisposable?.dispose()
fetch.removeListener(fetchListener) fetch.removeListener(fetchListener)
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf() stopSelf()
} }
companion object {
const val STOP_DOWNLOAD_SERVICE = "stop_download_service"
}
} }

View File

@ -21,46 +21,52 @@ package org.kiwix.kiwixmobile.core.downloader.downloadManager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.NotificationChannel import android.app.NotificationChannel
import android.app.NotificationManager import android.app.NotificationManager
import android.app.PendingIntent
import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.PendingIntent.getActivity import android.app.PendingIntent.getActivity
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat 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.DefaultFetchNotificationManager
import com.tonyodev.fetch2.DownloadNotification 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.Fetch
import com.tonyodev.fetch2.R.drawable import com.tonyodev.fetch2.R.drawable
import com.tonyodev.fetch2.R.string import com.tonyodev.fetch2.R.string
import com.tonyodev.fetch2.util.DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET 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.Intents
import org.kiwix.kiwixmobile.core.R import org.kiwix.kiwixmobile.core.R
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
import org.kiwix.kiwixmobile.core.main.CoreMainActivity import org.kiwix.kiwixmobile.core.main.CoreMainActivity
import javax.inject.Inject
const val DOWNLOAD_NOTIFICATION_TITLE = "OPEN_ZIM_FILE" const val DOWNLOAD_NOTIFICATION_TITLE = "OPEN_ZIM_FILE"
class FetchDownloadNotificationManager( class FetchDownloadNotificationManager @Inject constructor(
private val context: Context, context: Context,
private val downloadRoomDao: DownloadRoomDao private val downloadRoomDao: DownloadRoomDao
) : DefaultFetchNotificationManager(context) { ) : DefaultFetchNotificationManager(context) {
override fun getFetchInstanceForNamespace(namespace: String): Fetch = Fetch.getDefaultInstance() 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( override fun createNotificationChannels(
context: Context, context: Context,
notificationManager: NotificationManager notificationManager: NotificationManager
@ -90,7 +96,6 @@ class FetchDownloadNotificationManager(
downloadNotification: DownloadNotification, downloadNotification: DownloadNotification,
context: Context context: Context
) { ) {
// super method but with pause button removed
val smallIcon = if (downloadNotification.isDownloading) { val smallIcon = if (downloadNotification.isDownloading) {
android.R.drawable.stat_sys_download android.R.drawable.stat_sys_download
} else { } else {
@ -124,7 +129,7 @@ class FetchDownloadNotificationManager(
getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.DELETE) getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.DELETE)
).addAction( ).addAction(
drawable.fetch_notification_pause, drawable.fetch_notification_pause,
context.getString(R.string.tts_pause), context.getString(R.string.notification_pause_button_text),
getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.PAUSE) getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.PAUSE)
) )
@ -132,7 +137,7 @@ class FetchDownloadNotificationManager(
notificationBuilder.setTimeoutAfter(getNotificationTimeOutMillis()) notificationBuilder.setTimeoutAfter(getNotificationTimeOutMillis())
.addAction( .addAction(
drawable.fetch_notification_resume, drawable.fetch_notification_resume,
context.getString(R.string.tts_resume), context.getString(R.string.notification_resume_button_text),
getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.RESUME) getActionPendingIntent(downloadNotification, DownloadNotification.ActionType.RESUME)
) )
.addAction( .addAction(
@ -149,35 +154,6 @@ class FetchDownloadNotificationManager(
notificationCustomisation(downloadNotification, notificationBuilder, context) 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") @SuppressLint("UnspecifiedImmutableFlag")
private fun notificationCustomisation( private fun notificationCustomisation(
downloadNotification: DownloadNotification, downloadNotification: DownloadNotification,

View File

@ -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.ObjectBoxToLibkiwixMigrator
import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToRoomMigrator import org.kiwix.kiwixmobile.core.data.remote.ObjectBoxToRoomMigrator
import org.kiwix.kiwixmobile.core.di.components.CoreActivityComponent 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.error.ErrorActivity
import org.kiwix.kiwixmobile.core.extensions.browserIntent import org.kiwix.kiwixmobile.core.extensions.browserIntent
import org.kiwix.kiwixmobile.core.extensions.getToolbarNavigationIcon 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.extensions.setToolTipWithContentDescription
import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer import org.kiwix.kiwixmobile.core.reader.ZimReaderContainer
import org.kiwix.kiwixmobile.core.reader.ZimReaderSource import org.kiwix.kiwixmobile.core.reader.ZimReaderSource
@ -98,6 +102,9 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
@Inject lateinit var objectBoxToLibkiwixMigrator: ObjectBoxToLibkiwixMigrator @Inject lateinit var objectBoxToLibkiwixMigrator: ObjectBoxToLibkiwixMigrator
@Inject lateinit var objectBoxToRoomMigrator: ObjectBoxToRoomMigrator @Inject lateinit var objectBoxToRoomMigrator: ObjectBoxToRoomMigrator
@Inject
lateinit var downloadMonitor: DownloadMonitor
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTheme(R.style.KiwixTheme) setTheme(R.style.KiwixTheme)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -140,17 +147,51 @@ abstract class CoreMainActivity : BaseActivity(), WebViewProvider {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
downloadMonitor.startMonitoringDownload()
stopDownloadServiceIfRunning()
rateDialogHandler.checkForRateDialog(getIconResId()) rateDialogHandler.checkForRateDialog(getIconResId())
navController.addOnDestinationChangedListener { _, destination, _ -> navController.addOnDestinationChangedListener { _, destination, _ ->
configureActivityBasedOn(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() { override fun onDestroy() {
onBackPressedCallBack.remove() onBackPressedCallBack.remove()
super.onDestroy() 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) { open fun configureActivityBasedOn(destination: NavDestination) {
if (destination.id !in topLevelDestinations) { if (destination.id !in topLevelDestinations) {
handleDrawerOnNavigation() handleDrawerOnNavigation()

View File

@ -1851,8 +1851,7 @@ abstract class CoreReaderFragment :
setIsCloseAllTabButtonClickable(true) setIsCloseAllTabButtonClickable(true)
restoreDeletedTabs() restoreDeletedTabs()
} }
show() }.show()
}
} }
} }

View File

@ -78,19 +78,19 @@ abstract class CorePrefsFragment :
@JvmField @JvmField
@Inject @Inject
protected var sharedPreferenceUtil: SharedPreferenceUtil? = null var sharedPreferenceUtil: SharedPreferenceUtil? = null
@JvmField @JvmField
@Inject @Inject
protected var storageCalculator: StorageCalculator? = null var storageCalculator: StorageCalculator? = null
@JvmField @JvmField
@Inject @Inject
protected var darkModeConfig: DarkModeConfig? = null var darkModeConfig: DarkModeConfig? = null
@JvmField @JvmField
@Inject @Inject
protected var alertDialogShower: DialogShower? = null var alertDialogShower: DialogShower? = null
@JvmField @JvmField
@Inject @Inject

View File

@ -147,6 +147,8 @@
<string name="tts_pause">pause</string> <string name="tts_pause">pause</string>
<string name="tts_resume">resume</string> <string name="tts_resume">resume</string>
<string name="stop">stop</string> <string name="stop">stop</string>
<string name="notification_pause_button_text">Pause</string>
<string name="notification_resume_button_text">Resume</string>
<string name="internal_storage">Internal</string> <string name="internal_storage">Internal</string>
<string name="external_storage">External</string> <string name="external_storage">External</string>
<string name="yes">Yes</string> <string name="yes">Yes</string>

View File

@ -1,4 +1,5 @@
android.enableJetifier=true android.enableJetifier=true
android.useAndroidX=true android.useAndroidX=true
org.gradle.jvmargs=-Xmx6096m org.gradle.jvmargs=-Xmx6096m
kotlin.code.style=1.7 kotlin.code.style=1.8
kapt.use.k2=true