diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 07fe617eaa..34244d904e 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -12,6 +12,9 @@
android:name="android.permission.NEARBY_WIFI_DEVICES"
android:usesPermissionFlags="neverForLocation"
tools:targetApi="s" />
+
+
+
+ if (isNotConnected) {
+ noInternetSnackbar()
+ return@let
+ }
downloader.pauseResumeDownload(
it.downloadId,
it.downloadState.toReadableState(context).contains(getString(string.paused_state))
diff --git a/core/src/main/AndroidManifest.xml b/core/src/main/AndroidManifest.xml
index 1a5bdc0a3c..f6729f3899 100644
--- a/core/src/main/AndroidManifest.xml
+++ b/core/src/main/AndroidManifest.xml
@@ -13,12 +13,8 @@
-
-
-
-
-
+
+
@@ -52,8 +48,8 @@
+ tools:overrideLibrary="com.squareup.picasso.picasso">
@@ -92,8 +88,5 @@
android:name=".error.DiagnosticReportActivity"
android:exported="false" />
-
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt
index 6f21298c13..0d4aafcbda 100644
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt
+++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/DownloadRoomDao.kt
@@ -113,7 +113,7 @@ abstract class DownloadRoomDao {
sharedPreferenceUtil: SharedPreferenceUtil
) {
if (doesNotAlreadyExist(book)) {
- val downloadRequest = DownloadRequest(url)
+ val downloadRequest = DownloadRequest(url, book.title)
saveDownload(
DownloadRoomEntity(
url,
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/DownloadRoomEntity.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/DownloadRoomEntity.kt
index 1a049bae46..dff17d8740 100644
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/DownloadRoomEntity.kt
+++ b/core/src/main/java/org/kiwix/kiwixmobile/core/dao/entities/DownloadRoomEntity.kt
@@ -54,7 +54,8 @@ data class DownloadRoomEntity(
val size: String,
val name: String?,
val favIcon: String,
- val tags: String? = null
+ val tags: String? = null,
+ var pausedByUser: Boolean = false
) {
constructor(downloadUrl: String, downloadId: Long, book: Book, file: String?) : this(
file = file,
@@ -99,7 +100,8 @@ data class DownloadRoomEntity(
totalSizeOfDownload = download.totalSizeOfDownload,
status = download.state,
error = download.error,
- progress = download.progress
+ progress = download.progress,
+ pausedByUser = download.pausedByUser
)
}
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt
index 58a89a6c70..a5561b5786 100644
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt
+++ b/core/src/main/java/org/kiwix/kiwixmobile/core/data/KiwixRoomDatabase.kt
@@ -44,7 +44,7 @@ import org.kiwix.kiwixmobile.core.dao.entities.ZimSourceRoomConverter
NotesRoomEntity::class,
DownloadRoomEntity::class
],
- version = 5,
+ version = 6,
exportSchema = false
)
@TypeConverters(HistoryRoomDaoCoverts::class, ZimSourceRoomConverter::class)
@@ -62,7 +62,13 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
?: Room.databaseBuilder(context, KiwixRoomDatabase::class.java, "KiwixRoom.db")
// We have already database name called kiwix.db in order to avoid complexity we named
// as kiwixRoom.db
- .addMigrations(MIGRATION_1_2, MIGRATION_2_3, MIGRATION_3_4, MIGRATION_4_5)
+ .addMigrations(
+ MIGRATION_1_2,
+ MIGRATION_2_3,
+ MIGRATION_3_4,
+ MIGRATION_4_5,
+ MIGRATION_5_6
+ )
.build().also { db = it }
}
}
@@ -202,6 +208,15 @@ abstract class KiwixRoomDatabase : RoomDatabase() {
}
}
+ @Suppress("MagicNumber")
+ private val MIGRATION_5_6 = object : Migration(5, 6) {
+ override fun migrate(database: SupportSQLiteDatabase) {
+ database.execSQL(
+ "ALTER TABLE DownloadRoomEntity ADD COLUMN pausedByUser INTEGER NOT NULL DEFAULT 0"
+ )
+ }
+ }
+
fun destroyInstance() {
db = null
}
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreServiceComponent.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreServiceComponent.kt
index 27e559a844..bec1c4c337 100644
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreServiceComponent.kt
+++ b/core/src/main/java/org/kiwix/kiwixmobile/core/di/components/CoreServiceComponent.kt
@@ -23,14 +23,12 @@ import dagger.BindsInstance
import dagger.Subcomponent
import org.kiwix.kiwixmobile.core.di.CoreServiceScope
import org.kiwix.kiwixmobile.core.di.modules.CoreServiceModule
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadMonitorService
import org.kiwix.kiwixmobile.core.read_aloud.ReadAloudService
@Subcomponent(modules = [CoreServiceModule::class])
@CoreServiceScope
interface CoreServiceComponent {
fun inject(readAloudService: ReadAloudService)
- fun inject(downloadMonitorService: DownloadMonitorService)
@Subcomponent.Builder
interface Builder {
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 f85bf4e09e..a599aa4363 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
@@ -18,8 +18,6 @@
package org.kiwix.kiwixmobile.core.di.modules
import android.app.DownloadManager
-import android.app.NotificationManager
-import android.content.Context
import dagger.Module
import dagger.Provides
import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
@@ -30,7 +28,6 @@ import org.kiwix.kiwixmobile.core.downloader.DownloaderImpl
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerBroadcastReceiver
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerMonitor
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadManagerRequester
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager
import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import javax.inject.Singleton
@@ -69,11 +66,4 @@ object DownloaderModule {
fun providesDownloadManagerBroadcastReceiver(
callback: DownloadManagerBroadcastReceiver.Callback
): DownloadManagerBroadcastReceiver = DownloadManagerBroadcastReceiver(callback)
-
- @Provides
- @Singleton
- fun providesDownloadNotificationManager(
- context: Context,
- notificationManager: NotificationManager
- ): DownloadNotificationManager = DownloadNotificationManager(context, notificationManager)
}
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 4d41202155..4496f37e86 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
@@ -18,37 +18,90 @@
package org.kiwix.kiwixmobile.core.downloader.downloadManager
+import android.annotation.SuppressLint
import android.app.DownloadManager
+import android.content.ContentUris
+import android.content.ContentValues
import android.content.Context
import android.content.Intent
+import android.database.Cursor
+import android.net.Uri
import io.reactivex.Observable
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.dao.entities.DownloadRoomEntity
import org.kiwix.kiwixmobile.core.downloader.DownloadMonitor
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_CANCEL
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_PAUSE
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_QUERY_DOWNLOAD_STATUS
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_RESUME
-import org.kiwix.kiwixmobile.core.extensions.isServiceRunning
+import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
+import org.kiwix.kiwixmobile.core.utils.NetworkUtils
import org.kiwix.kiwixmobile.core.utils.files.Log
import java.util.concurrent.TimeUnit
import javax.inject.Inject
+const val ZERO = 0
+const val HUNDERED = 100
+const val THOUSAND = 1000
+const val DEFAULT_INT_VALUE = -1
+
+/*
+ These below values of android.provider.Downloads.Impl class,
+ there is no direct way to access them so we defining the values
+ from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/provider/Downloads.java
+ */
+const val CONTROL_PAUSE = 1
+const val CONTROL_RUN = 0
+const val STATUS_RUNNING = 192
+const val STATUS_PAUSED_BY_APP = 193
+const val COLUMN_CONTROL = "control"
+val downloadBaseUri: Uri = Uri.parse("content://downloads/my_downloads")
+const val DOWNLOAD_NOTIFICATION_TITLE = "OPEN_ZIM_FILE"
+
class DownloadManagerMonitor @Inject constructor(
+ private var downloadManager: DownloadManager,
val downloadRoomDao: DownloadRoomDao,
private val context: Context
) : DownloadMonitor, DownloadManagerBroadcastReceiver.Callback {
private val lock = Any()
private var monitoringDisposable: Disposable? = null
+ private val downloadInfoMap = mutableMapOf()
+ private val updater = PublishSubject.create<() -> Unit>()
init {
+ setupUpdater()
startMonitoringDownloads()
}
+ override fun downloadCompleteOrCancelled(intent: Intent) {
+ synchronized(lock) {
+ intent.extras?.let {
+ val downloadId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID, DEFAULT_INT_VALUE.toLong())
+ if (downloadId != DEFAULT_INT_VALUE.toLong()) {
+ queryDownloadStatus(downloadId)
+ }
+ }
+ }
+ }
+
+ @Suppress("CheckResult")
+ private fun setupUpdater() {
+ updater.subscribeOn(Schedulers.io()).observeOn(Schedulers.io()).subscribe(
+ {
+ synchronized(lock) { it.invoke() }
+ },
+ Throwable::printStackTrace
+ )
+ }
+
+ /**
+ * Starts monitoring ongoing downloads using a periodic observable.
+ * This method sets up an observable that runs every 5 seconds to check the status of downloads.
+ * It only starts the monitoring process if it's not already running and disposes of the observable
+ * when there are no ongoing downloads to avoid unnecessary resource usage.
+ */
@Suppress("MagicNumber")
fun startMonitoringDownloads() {
+ // Check if monitoring is already active. If it is, do nothing.
if (monitoringDisposable?.isDisposed == false) return
monitoringDisposable = Observable.interval(ZERO.toLong(), 5, TimeUnit.SECONDS)
.subscribeOn(Schedulers.io())
@@ -57,19 +110,9 @@ class DownloadManagerMonitor @Inject constructor(
{
try {
synchronized(lock) {
- // Observe downloads when the application is in the foreground.
- // This is especially useful when downloads are resumed but the
- // Download Manager takes some time to update the download status.
- // In such cases, the foreground service may stop prematurely due to
- // a lack of active downloads during this update delay.
- if (downloadRoomDao.downloads().blockingFirst().isNotEmpty()) {
- // Check if there are active downloads and the service is not running.
- // If so, start the DownloadMonitorService to properly track download progress.
- if (shouldStartService()) {
- startService()
- } else {
- // Do nothing; it is for fixing the error when "if" is used as an expression.
- }
+ val downloadingList = downloadRoomDao.downloads().blockingFirst()
+ if (downloadingList.isNotEmpty()) {
+ checkDownloads(downloadingList)
} else {
monitoringDisposable?.dispose()
}
@@ -85,61 +128,459 @@ class DownloadManagerMonitor @Inject constructor(
)
}
- /**
- * Determines if the DownloadMonitorService should be started.
- * Checks if there are active downloads and if the service is not already running.
- */
- private fun shouldStartService(): Boolean =
- getActiveDownloads().isNotEmpty() &&
- !context.isServiceRunning(DownloadMonitorService::class.java)
+ @SuppressLint("Range")
+ private fun checkDownloads(downloadingList: List) {
+ synchronized(lock) {
+ downloadingList.forEach {
+ queryDownloadStatus(it.downloadId)
+ }
+ }
+ }
- private fun getActiveDownloads(): List =
- downloadRoomDao.downloadRoomEntity().blockingFirst().filter {
- it.status != Status.PAUSED && it.status != Status.CANCELLED
+ @SuppressLint("Range")
+ fun queryDownloadStatus(downloadId: Long) {
+ synchronized(lock) {
+ updater.onNext {
+ downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
+ if (cursor.moveToFirst()) {
+ handleDownloadStatus(cursor, downloadId)
+ } else {
+ handleCancelledDownload(downloadId)
+ }
+ }
+ }
}
+ }
- override fun downloadCompleteOrCancelled(intent: Intent) {
+ @SuppressLint("Range")
+ private fun handleDownloadStatus(cursor: Cursor, downloadId: Long) {
+ val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
+ val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
+ val bytesDownloaded =
+ cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
+ val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
+ val progress = calculateProgress(bytesDownloaded, totalBytes)
+
+ val etaInMilliSeconds = calculateETA(downloadId, bytesDownloaded, totalBytes)
+
+ when (status) {
+ DownloadManager.STATUS_FAILED -> handleFailedDownload(
+ downloadId,
+ reason,
+ progress,
+ etaInMilliSeconds,
+ bytesDownloaded,
+ totalBytes
+ )
+
+ DownloadManager.STATUS_PAUSED -> handlePausedDownload(
+ downloadId,
+ progress,
+ bytesDownloaded,
+ totalBytes,
+ reason
+ )
+
+ DownloadManager.STATUS_PENDING -> handlePendingDownload(downloadId)
+ DownloadManager.STATUS_RUNNING -> handleRunningDownload(
+ downloadId,
+ progress,
+ etaInMilliSeconds,
+ bytesDownloaded,
+ totalBytes
+ )
+
+ DownloadManager.STATUS_SUCCESSFUL -> handleSuccessfulDownload(
+ downloadId,
+ progress,
+ etaInMilliSeconds
+ )
+ }
+ }
+
+ private fun handleCancelledDownload(downloadId: Long) {
+ updater.onNext {
+ updateDownloadStatus(downloadId, Status.CANCELLED, Error.CANCELLED)
+ downloadRoomDao.delete(downloadId)
+ downloadInfoMap.remove(downloadId)
+ }
+ }
+
+ @Suppress("LongParameterList")
+ private fun handleFailedDownload(
+ downloadId: Long,
+ reason: Int,
+ progress: Int,
+ etaInMilliSeconds: Long,
+ bytesDownloaded: Long,
+ totalBytes: Long
+ ) {
+ val error = mapDownloadError(reason)
+ updateDownloadStatus(
+ downloadId,
+ Status.FAILED,
+ error,
+ progress,
+ etaInMilliSeconds,
+ bytesDownloaded,
+ totalBytes
+ )
+ }
+
+ private fun handlePausedDownload(
+ downloadId: Long,
+ progress: Int,
+ bytesDownloaded: Long,
+ totalSizeOfDownload: Long,
+ reason: Int
+ ) {
+ val pauseReason = mapDownloadPauseReason(reason)
+ updateDownloadStatus(
+ downloadId = downloadId,
+ status = Status.PAUSED,
+ error = pauseReason,
+ progress = progress,
+ bytesDownloaded = bytesDownloaded,
+ totalSizeOfDownload = totalSizeOfDownload
+ )
+ }
+
+ private fun handlePendingDownload(downloadId: Long) {
+ updateDownloadStatus(
+ downloadId,
+ Status.QUEUED,
+ Error.NONE
+ )
+ }
+
+ private fun handleRunningDownload(
+ downloadId: Long,
+ progress: Int,
+ etaInMilliSeconds: Long,
+ bytesDownloaded: Long,
+ totalSizeOfDownload: Long
+ ) {
+ updateDownloadStatus(
+ downloadId,
+ Status.DOWNLOADING,
+ Error.NONE,
+ progress,
+ etaInMilliSeconds,
+ bytesDownloaded,
+ totalSizeOfDownload
+ )
+ }
+
+ private fun handleSuccessfulDownload(
+ downloadId: Long,
+ progress: Int,
+ etaInMilliSeconds: Long
+ ) {
+ updateDownloadStatus(
+ downloadId,
+ Status.COMPLETED,
+ Error.NONE,
+ progress,
+ etaInMilliSeconds
+ )
+ downloadInfoMap.remove(downloadId)
+ }
+
+ private fun calculateProgress(bytesDownloaded: Long, totalBytes: Long): Int =
+ if (totalBytes > ZERO) {
+ (bytesDownloaded / totalBytes.toDouble()).times(HUNDERED).toInt()
+ } else {
+ ZERO
+ }
+
+ private fun calculateETA(downloadedFileId: Long, bytesDownloaded: Long, totalBytes: Long): Long {
+ val currentTime = System.currentTimeMillis()
+ val downloadInfo = downloadInfoMap.getOrPut(downloadedFileId) {
+ DownloadInfo(startTime = currentTime, initialBytesDownloaded = bytesDownloaded)
+ }
+
+ val elapsedTime = currentTime - downloadInfo.startTime
+ val downloadSpeed = if (elapsedTime > ZERO) {
+ (bytesDownloaded - downloadInfo.initialBytesDownloaded) / (elapsedTime / THOUSAND.toFloat())
+ } else {
+ ZERO.toFloat()
+ }
+
+ return if (downloadSpeed > ZERO) {
+ ((totalBytes - bytesDownloaded) / downloadSpeed).toLong() * THOUSAND
+ } else {
+ ZERO.toLong()
+ }
+ }
+
+ private fun mapDownloadError(reason: Int): Error {
+ return when (reason) {
+ DownloadManager.ERROR_CANNOT_RESUME -> Error.ERROR_CANNOT_RESUME
+ DownloadManager.ERROR_DEVICE_NOT_FOUND -> Error.ERROR_DEVICE_NOT_FOUND
+ DownloadManager.ERROR_FILE_ALREADY_EXISTS -> Error.ERROR_FILE_ALREADY_EXISTS
+ DownloadManager.ERROR_FILE_ERROR -> Error.ERROR_FILE_ERROR
+ DownloadManager.ERROR_HTTP_DATA_ERROR -> Error.ERROR_HTTP_DATA_ERROR
+ DownloadManager.ERROR_INSUFFICIENT_SPACE -> Error.ERROR_INSUFFICIENT_SPACE
+ DownloadManager.ERROR_TOO_MANY_REDIRECTS -> Error.ERROR_TOO_MANY_REDIRECTS
+ DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> Error.ERROR_UNHANDLED_HTTP_CODE
+ DownloadManager.ERROR_UNKNOWN -> Error.UNKNOWN
+ else -> Error.UNKNOWN
+ }
+ }
+
+ private fun mapDownloadPauseReason(reason: Int): Error {
+ return when (reason) {
+ DownloadManager.PAUSED_QUEUED_FOR_WIFI -> Error.QUEUED_FOR_WIFI
+ DownloadManager.PAUSED_WAITING_TO_RETRY -> Error.WAITING_TO_RETRY
+ DownloadManager.PAUSED_WAITING_FOR_NETWORK -> Error.WAITING_FOR_NETWORK
+ DownloadManager.PAUSED_UNKNOWN -> Error.PAUSED_UNKNOWN
+ else -> Error.PAUSED_UNKNOWN
+ }
+ }
+
+ @Suppress("LongParameterList")
+ private fun updateDownloadStatus(
+ downloadId: Long,
+ status: Status,
+ error: Error,
+ progress: Int = DEFAULT_INT_VALUE,
+ etaInMilliSeconds: Long = DEFAULT_INT_VALUE.toLong(),
+ bytesDownloaded: Long = DEFAULT_INT_VALUE.toLong(),
+ totalSizeOfDownload: Long = DEFAULT_INT_VALUE.toLong(),
+ pausedByUser: Boolean? = null
+ ) {
synchronized(lock) {
- intent.extras?.let {
- val downloadId = it.getLong(DownloadManager.EXTRA_DOWNLOAD_ID, -1L)
- if (downloadId != -1L) {
- context.startService(
- getDownloadMonitorIntent(
- ACTION_QUERY_DOWNLOAD_STATUS,
- downloadId.toInt()
- )
- )
+ updater.onNext {
+ Log.e(
+ "DOWNLOAD_MONITOR",
+ "updateDownloadStatus: " +
+ "\n Status = $status" +
+ "\n Error = $error" +
+ "\n Progress = $progress" +
+ "\n DownloadId = $downloadId"
+ )
+ downloadRoomDao.getEntityForDownloadId(downloadId)?.let { downloadEntity ->
+ if (shouldUpdateDownloadStatus(downloadEntity)) {
+ val downloadModel = DownloadModel(downloadEntity).apply {
+ pausedByUser?.let {
+ this.pausedByUser = it
+ downloadEntity.pausedByUser = it
+ }
+ if (shouldUpdateDownloadStatus(status, error, downloadEntity)) {
+ state = status
+ }
+ this.error = error
+ if (progress > ZERO) {
+ this.progress = progress
+ }
+ this.etaInMilliSeconds = etaInMilliSeconds
+ if (bytesDownloaded != DEFAULT_INT_VALUE.toLong()) {
+ this.bytesDownloaded = bytesDownloaded
+ }
+ if (totalSizeOfDownload != DEFAULT_INT_VALUE.toLong()) {
+ this.totalSizeOfDownload = totalSizeOfDownload
+ }
+ }
+ downloadRoomDao.update(downloadModel)
+ return@let
+ }
}
}
}
}
- private fun startService() {
- context.startService(Intent(context, DownloadMonitorService::class.java))
+ /**
+ * Determines whether the download status should be updated based on the current status and error.
+ *
+ * This method evaluates the current download status and error conditions, ensuring proper handling
+ * for paused downloads, queued downloads, and network-related retries. It coordinates with the
+ * Download Manager to resume downloads when necessary and prevents premature status updates.
+ *
+ * @param status The current status of the download.
+ * @param error The current error state of the download.
+ * @param downloadRoomEntity The download entity containing the current status and download ID.
+ * @return `true` if the status should be updated, `false` otherwise.
+ */
+ private fun shouldUpdateDownloadStatus(
+ status: Status,
+ error: Error,
+ downloadRoomEntity: DownloadRoomEntity
+ ): Boolean {
+ synchronized(lock) {
+ return@shouldUpdateDownloadStatus when {
+ // Check if the download is paused and was previously queued.
+ isPausedAndQueued(status, downloadRoomEntity) ->
+ handlePausedAndQueuedDownload(error, downloadRoomEntity)
+
+ // Check if the download is paused and retryable due to network availability.
+ isPausedAndRetryable(
+ status,
+ error,
+ downloadRoomEntity.pausedByUser
+ ) -> {
+ handleRetryablePausedDownload(downloadRoomEntity)
+ }
+
+ // Default case: update the status.
+ else -> true
+ }
+ }
+ }
+
+ /**
+ * Checks if the download is paused and was previously queued.
+ *
+ * Specifically, it evaluates whether the current status is "Paused" while the previous status
+ * was "Queued", indicating that the user might have initiated a resume action.
+ *
+ * @param status The current status of the download.
+ * @param downloadRoomEntity The download entity to evaluate.
+ * @return `true` if the download is paused and queued, `false` otherwise.
+ */
+ private fun isPausedAndQueued(status: Status, downloadRoomEntity: DownloadRoomEntity): Boolean =
+ status == Status.PAUSED && downloadRoomEntity.status == Status.QUEUED
+
+ /**
+ * Checks if the download is paused and retryable based on the error and network conditions.
+ *
+ * This evaluates whether the download can be resumed, considering its paused state,
+ * error condition (e.g., waiting for retry), and the availability of a network connection.
+ *
+ * @param status The current status of the download.
+ * @param error The current error state of the download.
+ * @param pausedByUser To identify if the download paused by user or downloadManager.
+ * @return `true` if the download is paused and retryable, `false` otherwise.
+ */
+ private fun isPausedAndRetryable(status: Status, error: Error, pausedByUser: Boolean): Boolean {
+ return status == Status.PAUSED &&
+ (error == Error.WAITING_TO_RETRY || error == Error.PAUSED_UNKNOWN) &&
+ NetworkUtils.isNetworkAvailable(context) &&
+ !pausedByUser
+ }
+
+ /**
+ * Handles the case where a paused download was previously queued.
+ *
+ * This ensures that the download manager is instructed to resume the download and prevents
+ * the status from being prematurely updated to "Paused". Instead, the user will see the "Pending"
+ * state, indicating that the download is in the process of resuming.
+ *
+ * @param error The current error state of the download.
+ * @param downloadRoomEntity The download entity to evaluate.
+ * @return `true` if the status should be updated, `false` otherwise.
+ */
+ private fun handlePausedAndQueuedDownload(
+ error: Error,
+ downloadRoomEntity: DownloadRoomEntity
+ ): Boolean {
+ return when (error) {
+ // When the pause reason is unknown or waiting to retry, and the user
+ // resumes the download, attempt to resume the download if it was not resumed
+ // due to some reason.
+ Error.PAUSED_UNKNOWN,
+ Error.WAITING_TO_RETRY -> {
+ resumeDownload(downloadRoomEntity.downloadId, shouldUpdateStatus = false)
+ false
+ }
+
+ // For any other error state, update the status to reflect the current state
+ // and provide feedback to the user.
+ else -> true
+ }
+ }
+
+ /**
+ * Handles the case where a paused download is retryable due to network availability.
+ *
+ * If the download manager is waiting to retry due to a network error caused by fluctuations,
+ * this method resumes the download and ensures the status reflects the resumption process.
+ *
+ * @param downloadRoomEntity The download entity to evaluate.
+ * @return `true` to update the status and attempt to resume the download.
+ */
+ private fun handleRetryablePausedDownload(downloadRoomEntity: DownloadRoomEntity): Boolean {
+ resumeDownload(downloadRoomEntity.downloadId, shouldUpdateStatus = false)
+ return true
}
fun pauseDownload(downloadId: Long) {
- context.startService(getDownloadMonitorIntent(ACTION_PAUSE, downloadId.toInt()))
- startMonitoringDownloads()
+ synchronized(lock) {
+ updater.onNext {
+ if (pauseResumeDownloadInDownloadManagerContentResolver(
+ downloadId,
+ CONTROL_PAUSE,
+ STATUS_PAUSED_BY_APP
+ )
+ ) {
+ // pass true when user paused the download to not retry the download automatically.
+ updateDownloadStatus(downloadId, Status.PAUSED, Error.NONE, pausedByUser = true)
+ }
+ }
+ }
}
- fun resumeDownload(downloadId: Long) {
- context.startService(getDownloadMonitorIntent(ACTION_RESUME, downloadId.toInt()))
- startMonitoringDownloads()
+ fun resumeDownload(
+ downloadId: Long,
+ shouldUpdateStatus: Boolean = true
+ ) {
+ synchronized(lock) {
+ updater.onNext {
+ if (pauseResumeDownloadInDownloadManagerContentResolver(
+ downloadId,
+ CONTROL_RUN,
+ STATUS_RUNNING
+ )
+ ) {
+ if (shouldUpdateStatus) {
+ // pass false when user resumed the download to proceed with further checks.
+ updateDownloadStatus(downloadId, Status.QUEUED, Error.NONE, pausedByUser = false)
+ }
+ }
+ }
+ }
}
fun cancelDownload(downloadId: Long) {
- context.startService(getDownloadMonitorIntent(ACTION_CANCEL, downloadId.toInt()))
- startMonitoringDownloads()
+ synchronized(lock) {
+ updater.onNext {
+ // Remove the download from DownloadManager on IO thread.
+ downloadManager.remove(downloadId)
+ handleCancelledDownload(downloadId)
+ }
+ }
}
- private fun getDownloadMonitorIntent(action: String, downloadId: Int): Intent =
- Intent(context, DownloadMonitorService::class.java).apply {
- putExtra(DownloadNotificationManager.NOTIFICATION_ACTION, action)
- putExtra(DownloadNotificationManager.EXTRA_DOWNLOAD_ID, downloadId)
+ @SuppressLint("Range")
+ private fun pauseResumeDownloadInDownloadManagerContentResolver(
+ downloadId: Long,
+ control: Int,
+ status: Int
+ ): Boolean {
+ return try {
+ // Update the status to paused/resumed in the database
+ val contentValues = ContentValues().apply {
+ put(COLUMN_CONTROL, control)
+ put(DownloadManager.COLUMN_STATUS, status)
+ }
+ val uri = ContentUris.withAppendedId(downloadBaseUri, downloadId)
+ context.contentResolver
+ .update(uri, contentValues, null, null)
+ true
+ } catch (ignore: Exception) {
+ Log.e("DOWNLOAD_MONITOR", "Couldn't pause/resume the download. Original exception = $ignore")
+ false
}
+ }
+
+ private fun shouldUpdateDownloadStatus(downloadRoomEntity: DownloadRoomEntity) =
+ downloadRoomEntity.status != Status.COMPLETED
override fun init() {
// empty method to so class does not get reported unused
}
}
+
+data class DownloadInfo(
+ var startTime: Long,
+ var initialBytesDownloaded: Long
+)
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 25f9e6685e..616560f16a 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
@@ -20,7 +20,7 @@ package org.kiwix.kiwixmobile.core.downloader.downloadManager
import android.app.DownloadManager
import android.app.DownloadManager.Request
-import android.app.DownloadManager.Request.VISIBILITY_HIDDEN
+import android.app.DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED
import android.net.Uri
import androidx.core.net.toUri
import kotlinx.coroutines.CoroutineScope
@@ -60,7 +60,7 @@ class DownloadManagerRequester @Inject constructor(
.downloadRoomDao
.getEntityForDownloadId(downloadId)?.let { downloadRoomEntity ->
downloadRoomEntity.url?.let {
- val downloadRequest = DownloadRequest(urlString = it)
+ val downloadRequest = DownloadRequest(urlString = it, downloadRoomEntity.title)
val newDownloadEntity =
downloadRoomEntity.copy(downloadId = enqueue(downloadRequest), id = 0)
// cancel the previous download and its data from database and fileSystem.
@@ -97,6 +97,7 @@ fun DownloadRequest.toDownloadManagerRequest(
return if (urlString.isAuthenticationUrl) {
// return the request with "Authorization" header if the url is a Authentication url.
DownloadManager.Request(urlString.removeAuthenticationFromUrl.toUri()).apply {
+ setTitle(bookTitle)
setDestinationUri(Uri.fromFile(getDestinationFile(sharedPreferenceUtil)))
setAllowedNetworkTypes(
if (sharedPreferenceUtil.prefWifiOnly)
@@ -105,7 +106,7 @@ fun DownloadRequest.toDownloadManagerRequest(
Request.NETWORK_MOBILE or Request.NETWORK_WIFI
)
setAllowedOverMetered(!sharedPreferenceUtil.prefWifiOnly)
- setNotificationVisibility(VISIBILITY_HIDDEN) // hide the default notification.
+ setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
val userNameAndPassword = System.getenv(urlString.secretKey) ?: ""
val userName = userNameAndPassword.substringBefore(":", "")
val password = userNameAndPassword.substringAfter(":", "")
@@ -115,6 +116,7 @@ fun DownloadRequest.toDownloadManagerRequest(
} else {
// return the request for normal urls.
DownloadManager.Request(uri).apply {
+ setTitle(bookTitle)
setDestinationUri(Uri.fromFile(getDestinationFile(sharedPreferenceUtil)))
setAllowedNetworkTypes(
if (sharedPreferenceUtil.prefWifiOnly)
@@ -123,7 +125,7 @@ fun DownloadRequest.toDownloadManagerRequest(
Request.NETWORK_MOBILE or Request.NETWORK_WIFI
)
setAllowedOverMetered(!sharedPreferenceUtil.prefWifiOnly)
- setNotificationVisibility(VISIBILITY_HIDDEN) // hide the default notification.
+ setNotificationVisibility(VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
}
}
}
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
deleted file mode 100644
index 403db07dfb..0000000000
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadMonitorService.kt
+++ /dev/null
@@ -1,629 +0,0 @@
-/*
- * Kiwix Android
- * Copyright (c) 2024 Kiwix
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package org.kiwix.kiwixmobile.core.downloader.downloadManager
-
-import android.annotation.SuppressLint
-import android.app.DownloadManager
-import android.app.Service
-import android.content.ContentUris
-import android.content.ContentValues
-import android.content.Intent
-import android.database.Cursor
-import android.net.Uri
-import android.os.IBinder
-import io.reactivex.Observable
-import io.reactivex.disposables.Disposable
-import io.reactivex.schedulers.Schedulers
-import io.reactivex.subjects.PublishSubject
-import org.kiwix.kiwixmobile.core.CoreApp
-import org.kiwix.kiwixmobile.core.dao.DownloadRoomDao
-import org.kiwix.kiwixmobile.core.dao.entities.DownloadRoomEntity
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_CANCEL
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_PAUSE
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_QUERY_DOWNLOAD_STATUS
-import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_RESUME
-import org.kiwix.kiwixmobile.core.downloader.model.DownloadModel
-import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
-import org.kiwix.kiwixmobile.core.utils.files.Log
-import java.util.concurrent.TimeUnit
-import javax.inject.Inject
-
-const val ZERO = 0
-const val HUNDERED = 100
-const val THOUSAND = 1000
-const val DEFAULT_INT_VALUE = -1
-
-/*
- These below values of android.provider.Downloads.Impl class,
- there is no direct way to access them so we defining the values
- from https://android.googlesource.com/platform/frameworks/base/+/refs/heads/master/core/java/android/provider/Downloads.java
- */
-const val CONTROL_PAUSE = 1
-const val CONTROL_RUN = 0
-const val STATUS_RUNNING = 192
-const val STATUS_PAUSED_BY_APP = 193
-const val COLUMN_CONTROL = "control"
-val downloadBaseUri: Uri = Uri.parse("content://downloads/my_downloads")
-
-class DownloadMonitorService : Service() {
-
- @Inject
- lateinit var downloadManager: DownloadManager
-
- @Inject
- lateinit var downloadRoomDao: DownloadRoomDao
-
- @Inject
- lateinit var downloadNotificationManager: DownloadNotificationManager
- private val lock = Any()
- private var monitoringDisposable: Disposable? = null
- private val downloadInfoMap = mutableMapOf()
- private val updater = PublishSubject.create<() -> Unit>()
- private var foreGroundServiceInformation: Pair = true to DEFAULT_INT_VALUE
-
- override fun onBind(intent: Intent?): IBinder? = null
-
- override fun onCreate() {
- CoreApp.coreComponent
- .coreServiceComponent()
- .service(this)
- .build()
- .inject(this)
- super.onCreate()
- setupUpdater()
- startMonitoringDownloads()
- }
-
- override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
- val downloadId =
- intent?.getIntExtra(DownloadNotificationManager.EXTRA_DOWNLOAD_ID, DEFAULT_INT_VALUE)
- ?: DEFAULT_INT_VALUE
- val notificationAction = intent?.getStringExtra(DownloadNotificationManager.NOTIFICATION_ACTION)
- if (downloadId != DEFAULT_INT_VALUE) {
- when (notificationAction) {
- ACTION_PAUSE -> pauseDownload(downloadId.toLong())
- ACTION_RESUME -> resumeDownload(downloadId.toLong())
- ACTION_CANCEL -> cancelDownload(downloadId.toLong())
- ACTION_QUERY_DOWNLOAD_STATUS -> {
- updater.onNext {
- queryDownloadStatus(downloadId.toLong())
- }
- }
- }
- }
- return START_NOT_STICKY
- }
-
- @Suppress("CheckResult")
- private fun setupUpdater() {
- updater.subscribeOn(Schedulers.io()).observeOn(Schedulers.io()).subscribe(
- {
- synchronized(lock) { it.invoke() }
- },
- Throwable::printStackTrace
- )
- }
-
- /**
- * Starts monitoring ongoing downloads using a periodic observable.
- * This method sets up an observable that runs every 5 seconds to check the status of downloads.
- * It only starts the monitoring process if it's not already running and disposes of the observable
- * when there are no ongoing downloads to avoid unnecessary resource usage.
- */
- @Suppress("MagicNumber")
- private fun startMonitoringDownloads() {
- // Check if monitoring is already active. If it is, do nothing.
- if (monitoringDisposable?.isDisposed == false) return
- monitoringDisposable = Observable.interval(ZERO.toLong(), 5, TimeUnit.SECONDS)
- .subscribeOn(Schedulers.io())
- .observeOn(Schedulers.io())
- .subscribe(
- {
- try {
- synchronized(lock) {
- if (downloadRoomDao.downloads().blockingFirst().isNotEmpty()) {
- checkDownloads()
- } else {
- stopForegroundServiceForDownloads()
- }
- }
- } catch (ignore: Exception) {
- Log.e(
- "DOWNLOAD_MONITOR",
- "Couldn't get the downloads update. Original exception = $ignore"
- )
- }
- },
- Throwable::printStackTrace
- )
- }
-
- @SuppressLint("Range")
- private fun checkDownloads() {
- synchronized(lock) {
- val query = DownloadManager.Query().setFilterByStatus(
- DownloadManager.STATUS_RUNNING or
- DownloadManager.STATUS_PAUSED or
- DownloadManager.STATUS_PENDING or
- DownloadManager.STATUS_SUCCESSFUL
- )
- downloadManager.query(query).use { cursor ->
- if (cursor.moveToFirst()) {
- do {
- val downloadId = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_ID))
- queryDownloadStatus(downloadId)
- } while (cursor.moveToNext())
- }
- }
- }
- }
-
- @SuppressLint("Range")
- fun queryDownloadStatus(downloadId: Long) {
- synchronized(lock) {
- downloadManager.query(DownloadManager.Query().setFilterById(downloadId)).use { cursor ->
- if (cursor.moveToFirst()) {
- handleDownloadStatus(cursor, downloadId)
- } else {
- handleCancelledDownload(downloadId)
- }
- }
- }
- }
-
- @SuppressLint("Range")
- private fun handleDownloadStatus(cursor: Cursor, downloadId: Long) {
- val status = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
- val reason = cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_REASON))
- val bytesDownloaded =
- cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_BYTES_DOWNLOADED_SO_FAR))
- val totalBytes = cursor.getLong(cursor.getColumnIndex(DownloadManager.COLUMN_TOTAL_SIZE_BYTES))
- val progress = calculateProgress(bytesDownloaded, totalBytes)
-
- val etaInMilliSeconds = calculateETA(downloadId, bytesDownloaded, totalBytes)
-
- when (status) {
- DownloadManager.STATUS_FAILED -> handleFailedDownload(
- downloadId,
- reason,
- progress,
- etaInMilliSeconds,
- bytesDownloaded,
- totalBytes
- )
-
- DownloadManager.STATUS_PAUSED -> handlePausedDownload(
- downloadId,
- progress,
- bytesDownloaded,
- totalBytes,
- reason
- )
-
- DownloadManager.STATUS_PENDING -> handlePendingDownload(downloadId)
- DownloadManager.STATUS_RUNNING -> handleRunningDownload(
- downloadId,
- progress,
- etaInMilliSeconds,
- bytesDownloaded,
- totalBytes
- )
-
- DownloadManager.STATUS_SUCCESSFUL -> handleSuccessfulDownload(
- downloadId,
- progress,
- etaInMilliSeconds
- )
- }
- }
-
- private fun handleCancelledDownload(downloadId: Long) {
- updater.onNext {
- updateDownloadStatus(downloadId, Status.CANCELLED, Error.CANCELLED)
- downloadRoomDao.delete(downloadId)
- downloadInfoMap.remove(downloadId)
- }
- }
-
- @Suppress("LongParameterList")
- private fun handleFailedDownload(
- downloadId: Long,
- reason: Int,
- progress: Int,
- etaInMilliSeconds: Long,
- bytesDownloaded: Long,
- totalBytes: Long
- ) {
- val error = mapDownloadError(reason)
- updateDownloadStatus(
- downloadId,
- Status.FAILED,
- error,
- progress,
- etaInMilliSeconds,
- bytesDownloaded,
- totalBytes
- )
- }
-
- private fun handlePausedDownload(
- downloadId: Long,
- progress: Int,
- bytesDownloaded: Long,
- totalSizeOfDownload: Long,
- reason: Int
- ) {
- val pauseReason = mapDownloadPauseReason(reason)
- updateDownloadStatus(
- downloadId = downloadId,
- status = Status.PAUSED,
- error = pauseReason,
- progress = progress,
- bytesDownloaded = bytesDownloaded,
- totalSizeOfDownload = totalSizeOfDownload
- )
- }
-
- private fun handlePendingDownload(downloadId: Long) {
- updateDownloadStatus(
- downloadId,
- Status.QUEUED,
- Error.NONE
- )
- }
-
- private fun handleRunningDownload(
- downloadId: Long,
- progress: Int,
- etaInMilliSeconds: Long,
- bytesDownloaded: Long,
- totalSizeOfDownload: Long
- ) {
- updateDownloadStatus(
- downloadId,
- Status.DOWNLOADING,
- Error.NONE,
- progress,
- etaInMilliSeconds,
- bytesDownloaded,
- totalSizeOfDownload
- )
- }
-
- private fun handleSuccessfulDownload(
- downloadId: Long,
- progress: Int,
- etaInMilliSeconds: Long
- ) {
- updateDownloadStatus(
- downloadId,
- Status.COMPLETED,
- Error.NONE,
- progress,
- etaInMilliSeconds
- )
- downloadInfoMap.remove(downloadId)
- }
-
- private fun calculateProgress(bytesDownloaded: Long, totalBytes: Long): Int =
- if (totalBytes > ZERO) {
- (bytesDownloaded / totalBytes.toDouble()).times(HUNDERED).toInt()
- } else {
- ZERO
- }
-
- private fun calculateETA(downloadedFileId: Long, bytesDownloaded: Long, totalBytes: Long): Long {
- val currentTime = System.currentTimeMillis()
- val downloadInfo = downloadInfoMap.getOrPut(downloadedFileId) {
- DownloadInfo(startTime = currentTime, initialBytesDownloaded = bytesDownloaded)
- }
-
- val elapsedTime = currentTime - downloadInfo.startTime
- val downloadSpeed = if (elapsedTime > ZERO) {
- (bytesDownloaded - downloadInfo.initialBytesDownloaded) / (elapsedTime / THOUSAND.toFloat())
- } else {
- ZERO.toFloat()
- }
-
- return if (downloadSpeed > ZERO) {
- ((totalBytes - bytesDownloaded) / downloadSpeed).toLong() * THOUSAND
- } else {
- ZERO.toLong()
- }
- }
-
- private fun mapDownloadError(reason: Int): Error {
- return when (reason) {
- DownloadManager.ERROR_CANNOT_RESUME -> Error.ERROR_CANNOT_RESUME
- DownloadManager.ERROR_DEVICE_NOT_FOUND -> Error.ERROR_DEVICE_NOT_FOUND
- DownloadManager.ERROR_FILE_ALREADY_EXISTS -> Error.ERROR_FILE_ALREADY_EXISTS
- DownloadManager.ERROR_FILE_ERROR -> Error.ERROR_FILE_ERROR
- DownloadManager.ERROR_HTTP_DATA_ERROR -> Error.ERROR_HTTP_DATA_ERROR
- DownloadManager.ERROR_INSUFFICIENT_SPACE -> Error.ERROR_INSUFFICIENT_SPACE
- DownloadManager.ERROR_TOO_MANY_REDIRECTS -> Error.ERROR_TOO_MANY_REDIRECTS
- DownloadManager.ERROR_UNHANDLED_HTTP_CODE -> Error.ERROR_UNHANDLED_HTTP_CODE
- DownloadManager.ERROR_UNKNOWN -> Error.UNKNOWN
- else -> Error.UNKNOWN
- }
- }
-
- private fun mapDownloadPauseReason(reason: Int): Error {
- return when (reason) {
- DownloadManager.PAUSED_QUEUED_FOR_WIFI -> Error.QUEUED_FOR_WIFI
- DownloadManager.PAUSED_WAITING_TO_RETRY -> Error.WAITING_TO_RETRY
- DownloadManager.PAUSED_WAITING_FOR_NETWORK -> Error.WAITING_FOR_NETWORK
- DownloadManager.PAUSED_UNKNOWN -> Error.PAUSED_UNKNOWN
- else -> Error.PAUSED_UNKNOWN
- }
- }
-
- @Suppress("LongParameterList")
- private fun updateDownloadStatus(
- downloadId: Long,
- status: Status,
- error: Error,
- progress: Int = DEFAULT_INT_VALUE,
- etaInMilliSeconds: Long = DEFAULT_INT_VALUE.toLong(),
- bytesDownloaded: Long = DEFAULT_INT_VALUE.toLong(),
- totalSizeOfDownload: Long = DEFAULT_INT_VALUE.toLong()
- ) {
- synchronized(lock) {
- updater.onNext {
- downloadRoomDao.getEntityForDownloadId(downloadId)?.let { downloadEntity ->
- if (shouldUpdateDownloadStatus(downloadEntity)) {
- val downloadModel = DownloadModel(downloadEntity).apply {
- if (shouldUpdateDownloadStatus(status, error, downloadEntity)) {
- state = status
- }
- this.error = error
- if (progress > ZERO) {
- this.progress = progress
- }
- this.etaInMilliSeconds = etaInMilliSeconds
- if (bytesDownloaded != DEFAULT_INT_VALUE.toLong()) {
- this.bytesDownloaded = bytesDownloaded
- }
- if (totalSizeOfDownload != DEFAULT_INT_VALUE.toLong()) {
- this.totalSizeOfDownload = totalSizeOfDownload
- }
- }
- downloadRoomDao.update(downloadModel)
- updateNotification(downloadModel, downloadEntity.title, downloadEntity.description)
- return@let
- }
- cancelNotificationAndAssignNewNotificationToForegroundService(downloadId)
- } ?: run {
- // already downloaded/cancelled so cancel the notification if any running, and
- // assign new notification to foreground service.
- cancelNotificationAndAssignNewNotificationToForegroundService(downloadId)
- }
- }
- }
- }
-
- /**
- * Determines whether the download status should be updated based on the current status and error.
- *
- * This method checks the current download status and error, and decides whether to update the status
- * of the download entity. Specifically, it handles the case where a download is paused but has been
- * queued for resumption. In such cases, it ensures that the download manager is instructed to resume
- * the download, and prevents the status from being prematurely updated to "Paused".
- *
- * @param status The current status of the download.
- * @param error The current error state of the download.
- * @param downloadRoomEntity The download entity containing the current status and download ID.
- * @return `true` if the status should be updated, `false` otherwise.
- */
- private fun shouldUpdateDownloadStatus(
- status: Status,
- error: Error,
- downloadRoomEntity: DownloadRoomEntity
- ): Boolean {
- synchronized(lock) {
- return@shouldUpdateDownloadStatus if (
- status == Status.PAUSED &&
- downloadRoomEntity.status == Status.QUEUED
- ) {
- // Check if the user has resumed the download.
- // Do not update the download status immediately since the download manager
- // takes some time to actually resume the download. During this time,
- // it will still return the paused state.
- // By not updating the status right away, we ensure that the user
- // sees the "Pending" state, indicating that the download is in the process
- // of resuming.
- when (error) {
- // When the pause reason is unknown or waiting to retry, and the user
- // resumes the download, attempt to resume the download if it was not resumed
- // due to some reason.
- Error.PAUSED_UNKNOWN,
- Error.WAITING_TO_RETRY -> {
- resumeDownload(downloadRoomEntity.downloadId)
- false
- }
-
- // Return true to update the status of the download if there is any other status,
- // e.g., WAITING_FOR_WIFI, WAITING_FOR_NETWORK, or any other pause reason
- // to inform the user.
- else -> true
- }
- } else {
- true
- }
- }
- }
-
- private fun cancelNotificationAndAssignNewNotificationToForegroundService(downloadId: Long) {
- downloadNotificationManager.cancelNotification(downloadId.toInt())
- updateForegroundNotificationOrStopService()
- }
-
- private fun updateForegroundNotificationOrStopService() {
- val activeDownloads = getActiveDownloads()
- if (activeDownloads.isNotEmpty()) {
- // Promote the first active download to foreground
- val downloadRoomEntity = activeDownloads.first()
- foreGroundServiceInformation =
- foreGroundServiceInformation.first to downloadRoomEntity.downloadId.toInt()
- val downloadNotificationModel =
- getDownloadNotificationModel(
- DownloadModel(downloadRoomEntity),
- downloadRoomEntity.title,
- downloadRoomEntity.description
- )
- val notification = downloadNotificationManager.createNotification(downloadNotificationModel)
- startForeground(foreGroundServiceInformation.second, notification)
- } else {
- // Stop the service if no active downloads remain
- stopForegroundServiceForDownloads()
- }
- }
-
- private fun getActiveDownloads(): List =
- downloadRoomDao.downloadRoomEntity().blockingFirst().filter {
- it.status != Status.PAUSED && it.status != Status.CANCELLED
- }
-
- private fun updateNotification(
- downloadModel: DownloadModel,
- title: String,
- description: String?
- ) {
- val downloadNotificationModel = getDownloadNotificationModel(downloadModel, title, description)
- val notification = downloadNotificationManager.createNotification(downloadNotificationModel)
- if (foreGroundServiceInformation.first) {
- startForeground(downloadModel.downloadId.toInt(), notification)
- foreGroundServiceInformation = false to downloadModel.downloadId.toInt()
- } else {
- downloadNotificationManager.updateNotification(
- downloadNotificationModel,
- object : AssignNewForegroundServiceNotification {
- override fun assignNewForegroundServiceNotification(downloadId: Long) {
- cancelNotificationAndAssignNewNotificationToForegroundService(downloadId)
- }
- }
- )
- }
- }
-
- private fun getDownloadNotificationModel(
- downloadModel: DownloadModel,
- title: String,
- description: String?
- ): DownloadNotificationModel =
- DownloadNotificationModel(
- downloadId = downloadModel.downloadId.toInt(),
- status = downloadModel.state,
- progress = downloadModel.progress,
- etaInMilliSeconds = downloadModel.etaInMilliSeconds,
- title = title,
- description = description,
- filePath = downloadModel.file,
- error = DownloadState.from(
- downloadModel.state,
- downloadModel.error,
- downloadModel.book.url
- ).toReadableState(this).toString()
- )
-
- private fun pauseDownload(downloadId: Long) {
- synchronized(lock) {
- updater.onNext {
- if (pauseResumeDownloadInDownloadManagerContentResolver(
- downloadId,
- CONTROL_PAUSE,
- STATUS_PAUSED_BY_APP
- )
- ) {
- updateDownloadStatus(downloadId, Status.PAUSED, Error.NONE)
- }
- }
- }
- }
-
- private fun resumeDownload(downloadId: Long) {
- synchronized(lock) {
- updater.onNext {
- if (pauseResumeDownloadInDownloadManagerContentResolver(
- downloadId,
- CONTROL_RUN,
- STATUS_RUNNING
- )
- ) {
- updateDownloadStatus(downloadId, Status.QUEUED, Error.NONE)
- }
- }
- }
- }
-
- private fun cancelDownload(downloadId: Long) {
- synchronized(lock) {
- updater.onNext {
- // Remove the download from DownloadManager on IO thread.
- downloadManager.remove(downloadId)
- handleCancelledDownload(downloadId)
- }
- }
- }
-
- @SuppressLint("Range")
- private fun pauseResumeDownloadInDownloadManagerContentResolver(
- downloadId: Long,
- control: Int,
- status: Int
- ): Boolean {
- return try {
- // Update the status to paused/resumed in the database
- val contentValues = ContentValues().apply {
- put(COLUMN_CONTROL, control)
- put(DownloadManager.COLUMN_STATUS, status)
- }
- val uri = ContentUris.withAppendedId(downloadBaseUri, downloadId)
- contentResolver
- .update(uri, contentValues, null, null)
- true
- } catch (ignore: Exception) {
- Log.e("DOWNLOAD_MONITOR", "Couldn't pause/resume the download. Original exception = $ignore")
- false
- }
- }
-
- private fun shouldUpdateDownloadStatus(downloadRoomEntity: DownloadRoomEntity) =
- downloadRoomEntity.status != Status.COMPLETED
-
- override fun onDestroy() {
- monitoringDisposable?.dispose()
- super.onDestroy()
- }
-
- private fun stopForegroundServiceForDownloads() {
- foreGroundServiceInformation = true to DEFAULT_INT_VALUE
- monitoringDisposable?.dispose()
- stopForeground(STOP_FOREGROUND_REMOVE)
- stopSelf()
- }
-}
-
-data class DownloadInfo(
- var startTime: Long,
- var initialBytesDownloaded: Long
-)
-
-interface AssignNewForegroundServiceNotification {
- fun assignNewForegroundServiceNotification(downloadId: Long)
-}
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadNotificationManager.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadNotificationManager.kt
deleted file mode 100644
index e7c8600efd..0000000000
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadNotificationManager.kt
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Kiwix Android
- * Copyright (c) 2024 Kiwix
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package org.kiwix.kiwixmobile.core.downloader.downloadManager
-
-import android.annotation.SuppressLint
-import android.app.Notification
-import android.app.NotificationChannel
-import android.app.NotificationManager
-import android.app.PendingIntent
-import android.content.Context
-import android.content.Intent
-import android.os.Build
-import androidx.annotation.RequiresApi
-import androidx.core.app.NotificationCompat
-import org.kiwix.kiwixmobile.core.Intents
-import org.kiwix.kiwixmobile.core.R
-import org.kiwix.kiwixmobile.core.downloader.model.Seconds
-import org.kiwix.kiwixmobile.core.main.CoreMainActivity
-import org.kiwix.kiwixmobile.core.utils.DEFAULT_NOTIFICATION_TIMEOUT_AFTER
-import org.kiwix.kiwixmobile.core.utils.DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET
-import org.kiwix.kiwixmobile.core.utils.DOWNLOAD_NOTIFICATION_CHANNEL_ID
-import java.util.Locale
-import javax.inject.Inject
-
-const val DOWNLOAD_NOTIFICATION_TITLE = "OPEN_ZIM_FILE"
-
-class DownloadNotificationManager @Inject constructor(
- private val context: Context,
- private val notificationManager: NotificationManager
-) {
- private val downloadNotificationsBuilderMap = mutableMapOf()
- private fun createNotificationChannel() {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
- if (notificationManager.getNotificationChannel(DOWNLOAD_NOTIFICATION_CHANNEL_ID) == null) {
- notificationManager.createNotificationChannel(createChannel(context))
- }
- }
- }
-
- fun updateNotification(
- downloadNotificationModel: DownloadNotificationModel,
- assignNewForegroundServiceNotification: AssignNewForegroundServiceNotification
- ) {
- synchronized(downloadNotificationsBuilderMap) {
- if (shouldUpdateNotification(downloadNotificationModel)) {
- notificationManager.notify(
- downloadNotificationModel.downloadId,
- createNotification(downloadNotificationModel)
- )
- } else {
- // the download is cancelled/paused so remove the notification.
- assignNewForegroundServiceNotification.assignNewForegroundServiceNotification(
- downloadNotificationModel.downloadId.toLong()
- )
- }
- }
- }
-
- fun createNotification(downloadNotificationModel: DownloadNotificationModel): Notification {
- synchronized(downloadNotificationsBuilderMap) {
- createNotificationChannel()
- val notificationBuilder = getNotificationBuilder(downloadNotificationModel.downloadId)
- val smallIcon = if (downloadNotificationModel.progress != HUNDERED) {
- android.R.drawable.stat_sys_download
- } else {
- android.R.drawable.stat_sys_download_done
- }
-
- notificationBuilder.setPriority(NotificationCompat.PRIORITY_DEFAULT)
- .setSmallIcon(smallIcon)
- .setContentTitle(downloadNotificationModel.title)
- .setContentText(getSubtitleText(context, downloadNotificationModel))
- .setOngoing(downloadNotificationModel.isOnGoingNotification)
- .setGroupSummary(false)
- if (downloadNotificationModel.isFailed || downloadNotificationModel.isCompleted) {
- notificationBuilder.setProgress(ZERO, ZERO, false)
- } else {
- notificationBuilder.setProgress(HUNDERED, downloadNotificationModel.progress, false)
- }
- when {
- downloadNotificationModel.isDownloading ->
- notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER)
- .addAction(
- R.drawable.ic_baseline_stop,
- context.getString(R.string.cancel),
- getActionPendingIntent(ACTION_CANCEL, downloadNotificationModel.downloadId)
- ).addAction(
- R.drawable.ic_baseline_pause,
- getPauseOrResumeTitle(true),
- getActionPendingIntent(ACTION_PAUSE, downloadNotificationModel.downloadId)
- )
-
- downloadNotificationModel.isPaused ->
- notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER)
- .addAction(
- R.drawable.ic_baseline_stop,
- context.getString(R.string.cancel),
- getActionPendingIntent(ACTION_CANCEL, downloadNotificationModel.downloadId)
- ).addAction(
- R.drawable.ic_baseline_play,
- getPauseOrResumeTitle(false),
- getActionPendingIntent(ACTION_RESUME, downloadNotificationModel.downloadId)
- )
-
- downloadNotificationModel.isQueued ->
- notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER)
-
- else -> notificationBuilder.setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET)
- }
- notificationCustomisation(downloadNotificationModel, notificationBuilder, context)
- return@createNotification notificationBuilder.build()
- }
- }
-
- private fun getPauseOrResumeTitle(isPause: Boolean): String {
- val pauseOrResumeTitle = if (isPause) {
- context.getString(R.string.tts_pause)
- } else {
- context.getString(R.string.tts_resume)
- }
- return pauseOrResumeTitle.replaceFirstChar {
- if (it.isLowerCase()) {
- it.titlecase(Locale.ROOT)
- } else {
- "$it"
- }
- }
- }
-
- private fun shouldUpdateNotification(
- downloadNotificationModel: DownloadNotificationModel
- ): Boolean = !downloadNotificationModel.isCancelled && !downloadNotificationModel.isPaused
-
- @SuppressLint("UnspecifiedImmutableFlag")
- private fun notificationCustomisation(
- downloadNotificationModel: DownloadNotificationModel,
- notificationBuilder: NotificationCompat.Builder,
- context: Context
- ) {
- if (downloadNotificationModel.isCompleted) {
- val internal = Intents.internal(CoreMainActivity::class.java).apply {
- addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- putExtra(DOWNLOAD_NOTIFICATION_TITLE, downloadNotificationModel.filePath)
- }
- val pendingIntent =
- PendingIntent.getActivity(
- context,
- ZERO,
- internal,
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- )
- notificationBuilder.setContentIntent(pendingIntent)
- notificationBuilder.setAutoCancel(true)
- }
- }
-
- @SuppressLint("RestrictedApi")
- private fun getNotificationBuilder(notificationId: Int): NotificationCompat.Builder {
- synchronized(downloadNotificationsBuilderMap) {
- val notificationBuilder = downloadNotificationsBuilderMap[notificationId]
- ?: NotificationCompat.Builder(context, DOWNLOAD_NOTIFICATION_CHANNEL_ID)
- downloadNotificationsBuilderMap[notificationId] = notificationBuilder
- notificationBuilder
- .setGroup("$notificationId")
- .setStyle(null)
- .setProgress(ZERO, ZERO, false)
- .setContentTitle(null)
- .setContentText(null)
- .setContentIntent(null)
- .setGroupSummary(false)
- .setTimeoutAfter(DEFAULT_NOTIFICATION_TIMEOUT_AFTER_RESET)
- .setOngoing(false)
- .setOnlyAlertOnce(true)
- .setSmallIcon(android.R.drawable.stat_sys_download_done)
- .mActions.clear()
- return@getNotificationBuilder notificationBuilder
- }
- }
-
- private fun getActionPendingIntent(action: String, downloadId: Int): PendingIntent {
- val pendingIntent =
- Intent(context, DownloadMonitorService::class.java).apply {
- putExtra(NOTIFICATION_ACTION, action)
- putExtra(EXTRA_DOWNLOAD_ID, downloadId)
- }
- val requestCode = downloadId + action.hashCode()
- return PendingIntent.getService(
- context,
- requestCode,
- pendingIntent,
- PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
- )
- }
-
- @RequiresApi(Build.VERSION_CODES.O)
- private fun createChannel(context: Context) =
- NotificationChannel(
- DOWNLOAD_NOTIFICATION_CHANNEL_ID,
- context.getString(R.string.download_notification_channel_name),
- NotificationManager.IMPORTANCE_HIGH
- ).apply {
- setSound(null, null)
- enableVibration(false)
- }
-
- private fun getSubtitleText(
- context: Context,
- downloadNotificationModel: DownloadNotificationModel
- ): String {
- return when {
- downloadNotificationModel.isCompleted -> context.getString(R.string.complete)
- downloadNotificationModel.isFailed -> context.getString(
- R.string.failed_state,
- downloadNotificationModel.error
- )
-
- downloadNotificationModel.isPaused -> context.getString(R.string.paused_state)
- downloadNotificationModel.isQueued -> context.getString(R.string.pending_state)
- downloadNotificationModel.etaInMilliSeconds <= ZERO ->
- context.getString(R.string.running_state)
-
- else -> Seconds(
- downloadNotificationModel.etaInMilliSeconds / THOUSAND.toLong()
- ).toHumanReadableTime()
- }
- }
-
- fun cancelNotification(notificationId: Int) {
- synchronized(downloadNotificationsBuilderMap) {
- notificationManager.cancel(notificationId)
- downloadNotificationsBuilderMap.remove(notificationId)
- }
- }
-
- companion object {
- const val NOTIFICATION_ACTION = "notification_action"
- const val ACTION_PAUSE = "action_pause"
- const val ACTION_RESUME = "action_resume"
- const val ACTION_CANCEL = "action_cancel"
- const val ACTION_QUERY_DOWNLOAD_STATUS = "action_query_download_status"
- const val EXTRA_DOWNLOAD_ID = "extra_download_id"
- }
-}
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadNotificationModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadNotificationModel.kt
deleted file mode 100644
index 17f2de2c9e..0000000000
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/downloadManager/DownloadNotificationModel.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-/*
- * Kiwix Android
- * Copyright (c) 2024 Kiwix
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package org.kiwix.kiwixmobile.core.downloader.downloadManager
-
-data class DownloadNotificationModel(
- val downloadId: Int,
- val status: Status = Status.NONE,
- val progress: Int,
- val etaInMilliSeconds: Long,
- val title: String,
- val description: String?,
- val filePath: String?,
- val error: String
-) {
- val isPaused get() = status == Status.PAUSED
- val isCompleted get() = status == Status.COMPLETED
- val isFailed get() = status == Status.FAILED
- val isQueued get() = status == Status.QUEUED
- val isDownloading get() = status == Status.DOWNLOADING
- val isCancelled get() = status == Status.CANCELLED
- val isOnGoingNotification: Boolean
- get() {
- return when (status) {
- Status.QUEUED,
- Status.DOWNLOADING -> true
-
- else -> false
- }
- }
-}
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadModel.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadModel.kt
index e76caf8847..d9db90c596 100644
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadModel.kt
+++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadModel.kt
@@ -33,7 +33,8 @@ data class DownloadModel(
var state: Status,
var error: Error,
var progress: Int,
- val book: Book
+ val book: Book,
+ var pausedByUser: Boolean
) {
val bytesRemaining: Long by lazy { totalSizeOfDownload - bytesDownloaded }
val fileNameFromUrl: String by lazy { StorageUtils.getFileNameFromUrl(book.url) }
@@ -48,6 +49,7 @@ data class DownloadModel(
downloadEntity.status,
downloadEntity.error,
downloadEntity.progress,
- downloadEntity.toBook()
+ downloadEntity.toBook(),
+ downloadEntity.pausedByUser
)
}
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadRequest.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadRequest.kt
index 24ec973856..5d28096058 100644
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadRequest.kt
+++ b/core/src/main/java/org/kiwix/kiwixmobile/core/downloader/model/DownloadRequest.kt
@@ -22,7 +22,7 @@ import org.kiwix.kiwixmobile.core.utils.SharedPreferenceUtil
import org.kiwix.kiwixmobile.core.utils.StorageUtils
import java.io.File
-data class DownloadRequest(val urlString: String) {
+data class DownloadRequest(val urlString: String, val bookTitle: String) {
val uri: Uri get() = Uri.parse(urlString)
diff --git a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt
index d3bc7d308c..a028247d2b 100644
--- a/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt
+++ b/core/src/main/java/org/kiwix/kiwixmobile/core/search/SearchFragment.kt
@@ -192,11 +192,12 @@ class SearchFragment : BaseFragment() {
)
}
+ @Suppress("UnnecessarySafeCall")
private fun setupToolbar(view: View) {
view.post {
- with(requireActivity() as CoreMainActivity) {
- setSupportActionBar(view.findViewById(R.id.toolbar))
- supportActionBar?.apply {
+ with(activity as? CoreMainActivity) {
+ this?.setSupportActionBar(view.findViewById(R.id.toolbar))
+ this?.supportActionBar?.apply {
setHomeButtonEnabled(true)
title = getString(R.string.menu_search_in_text)
}
diff --git a/core/src/sharedTestFunctions/java/org/kiwix/sharedFunctions/TestModelFunctions.kt b/core/src/sharedTestFunctions/java/org/kiwix/sharedFunctions/TestModelFunctions.kt
index a8fae21a4b..e6955b2757 100644
--- a/core/src/sharedTestFunctions/java/org/kiwix/sharedFunctions/TestModelFunctions.kt
+++ b/core/src/sharedTestFunctions/java/org/kiwix/sharedFunctions/TestModelFunctions.kt
@@ -58,7 +58,7 @@ fun downloadModel(
book: Book = book()
) = DownloadModel(
databaseId, downloadId, file, etaInMilliSeconds, bytesDownloaded, totalSizeOfDownload,
- status, error, progress, book
+ status, error, progress, book, false
)
fun downloadItem(