Improved handling of scenarios where download progress was interrupted due to network errors (e.g., network fluctuations). The application now correctly retrieves download progress from the DownloadManager and, if necessary, automatically resumes paused downloads without requiring user intervention.

* Downloads paused due to network errors like "Waiting to Retry" are now resumed automatically when the network becomes available.
* For downloads configured to only proceed on Wi-Fi, the application will resume progress when a Wi-Fi connection is re-established. Similarly, downloads queued for mobile networks will resume when the mobile network reconnects.
This commit is contained in:
MohitMaliFtechiz 2024-12-10 18:58:42 +05:30 committed by Kelson
parent 6330a78df2
commit 7c7dc2fcbb
2 changed files with 178 additions and 50 deletions

View File

@ -32,6 +32,7 @@ import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificatio
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_QUERY_DOWNLOAD_STATUS 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.downloadManager.DownloadNotificationManager.Companion.ACTION_RESUME
import org.kiwix.kiwixmobile.core.extensions.isServiceRunning import org.kiwix.kiwixmobile.core.extensions.isServiceRunning
import org.kiwix.kiwixmobile.core.utils.NetworkUtils
import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.core.utils.files.Log
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -62,10 +63,6 @@ class DownloadManagerMonitor @Inject constructor(
// Download Manager takes some time to update the download status. // Download Manager takes some time to update the download status.
// In such cases, the foreground service may stop prematurely due to // In such cases, the foreground service may stop prematurely due to
// a lack of active downloads during this update delay. // a lack of active downloads during this update delay.
Log.e(
"DOWNLOAD_MONITOR",
"startMonitoringDownloads: monitor ${shouldStartDownloadMonitorService()}"
)
if (downloadRoomDao.downloads().blockingFirst().isNotEmpty()) { if (downloadRoomDao.downloads().blockingFirst().isNotEmpty()) {
// Check if there are active downloads and the service is not running. // Check if there are active downloads and the service is not running.
// If so, start the DownloadMonitorService to properly track download progress. // If so, start the DownloadMonitorService to properly track download progress.
@ -98,11 +95,52 @@ class DownloadManagerMonitor @Inject constructor(
!context.isServiceRunning(DownloadMonitorService::class.java) !context.isServiceRunning(DownloadMonitorService::class.java)
private fun getActiveDownloads(): List<DownloadRoomEntity> = private fun getActiveDownloads(): List<DownloadRoomEntity> =
downloadRoomDao.downloadRoomEntity().blockingFirst().filter { downloadRoomDao.downloadRoomEntity().blockingFirst().filter(::isActiveDownload)
(it.status != Status.PAUSED || it.error == Error.WAITING_TO_RETRY) &&
it.status != Status.CANCELLED /**
* Determines if a given download is considered active.
*
* @param download The DownloadRoomEntity to evaluate.
* @return True if the download is active, false otherwise.
*/
private fun isActiveDownload(download: DownloadRoomEntity): Boolean =
(download.status != Status.PAUSED || isPausedAndRetryable(download)) &&
download.status != Status.CANCELLED
/**
* Checks if a paused download is eligible for retry based on its error status and network conditions.
*
* @param download The DownloadRoomEntity to evaluate.
* @return True if the paused download is retryable, false otherwise.
*/
private fun isPausedAndRetryable(download: DownloadRoomEntity): Boolean {
return download.status == Status.PAUSED &&
(
isQueuedForWiFiAndConnected(download) ||
isQueuedForNetwork(download) ||
download.error == Error.WAITING_TO_RETRY
) &&
NetworkUtils.isNetworkAvailable(context)
} }
/**
* Checks if the download is queued for Wi-Fi and the device is connected to Wi-Fi.
*
* @param download The DownloadRoomEntity to evaluate.
* @return True if the download is queued for Wi-Fi and connected, false otherwise.
*/
private fun isQueuedForWiFiAndConnected(download: DownloadRoomEntity): Boolean =
download.error == Error.QUEUED_FOR_WIFI && NetworkUtils.isWiFi(context)
/**
* Checks if the download is waiting for a network connection and the network is now available.
*
* @param download The DownloadRoomEntity to evaluate.
* @return True if the download is waiting for a network and connected, false otherwise.
*/
private fun isQueuedForNetwork(download: DownloadRoomEntity): Boolean =
download.error == Error.WAITING_FOR_NETWORK && NetworkUtils.isNetworkAvailable(context)
override fun downloadCompleteOrCancelled(intent: Intent) { override fun downloadCompleteOrCancelled(intent: Intent) {
synchronized(lock) { synchronized(lock) {
intent.extras?.let { intent.extras?.let {

View File

@ -40,6 +40,7 @@ import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificatio
import org.kiwix.kiwixmobile.core.downloader.downloadManager.DownloadNotificationManager.Companion.ACTION_RESUME 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.DownloadModel
import org.kiwix.kiwixmobile.core.downloader.model.DownloadState import org.kiwix.kiwixmobile.core.downloader.model.DownloadState
import org.kiwix.kiwixmobile.core.utils.NetworkUtils
import org.kiwix.kiwixmobile.core.utils.files.Log import org.kiwix.kiwixmobile.core.utils.files.Log
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Inject import javax.inject.Inject
@ -137,7 +138,6 @@ class DownloadMonitorService : Service() {
{ {
try { try {
synchronized(lock) { synchronized(lock) {
Log.e("DOWNLOAD_MONITOR", "startMonitoringDownloads: service")
if (downloadRoomDao.downloads().blockingFirst().isNotEmpty()) { if (downloadRoomDao.downloads().blockingFirst().isNotEmpty()) {
checkDownloads() checkDownloads()
} else { } else {
@ -386,11 +386,6 @@ class DownloadMonitorService : Service() {
) { ) {
synchronized(lock) { synchronized(lock) {
updater.onNext { updater.onNext {
Log.e(
"DOWNLOAD_MONITOR",
"updateDownloadStatus: status = $status\n" +
"error = $error\n bytesDownloaded = $bytesDownloaded"
)
downloadRoomDao.getEntityForDownloadId(downloadId)?.let { downloadEntity -> downloadRoomDao.getEntityForDownloadId(downloadId)?.let { downloadEntity ->
if (shouldUpdateDownloadStatus(downloadEntity)) { if (shouldUpdateDownloadStatus(downloadEntity)) {
val downloadModel = DownloadModel(downloadEntity).apply { val downloadModel = DownloadModel(downloadEntity).apply {
@ -426,10 +421,9 @@ class DownloadMonitorService : Service() {
/** /**
* Determines whether the download status should be updated based on the current status and error. * 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 * This method evaluates the current download status and error conditions, ensuring proper handling
* of the download entity. Specifically, it handles the case where a download is paused but has been * for paused downloads, queued downloads, and network-related retries. It coordinates with the
* queued for resumption. In such cases, it ensures that the download manager is instructed to resume * Download Manager to resume downloads when necessary and prevents premature status updates.
* the download, and prevents the status from being prematurely updated to "Paused".
* *
* @param status The current status of the download. * @param status The current status of the download.
* @param error The current error state of the download. * @param error The current error state of the download.
@ -442,18 +436,65 @@ class DownloadMonitorService : Service() {
downloadRoomEntity: DownloadRoomEntity downloadRoomEntity: DownloadRoomEntity
): Boolean { ): Boolean {
synchronized(lock) { synchronized(lock) {
return@shouldUpdateDownloadStatus if ( return@shouldUpdateDownloadStatus when {
status == Status.PAUSED && // Check if the download is paused and was previously queued.
downloadRoomEntity.status == Status.QUEUED isPausedAndQueued(status, downloadRoomEntity) ->
) { handlePausedAndQueuedDownload(error, downloadRoomEntity)
// Check if the user has resumed the download.
// Do not update the download status immediately since the download manager // Check if the download is paused and retryable due to network availability.
// takes some time to actually resume the download. During this time, isPausedAndRetryable(status, error) -> handleRetryablePausedDownload(downloadRoomEntity)
// it will still return the paused state.
// By not updating the status right away, we ensure that the user // Default case: update the status.
// sees the "Pending" state, indicating that the download is in the process else -> true
// of resuming. }
when (error) { }
}
/**
* 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.
* @return `true` if the download is paused and retryable, `false` otherwise.
*/
private fun isPausedAndRetryable(status: Status, error: Error): Boolean {
return status == Status.PAUSED &&
error == Error.WAITING_TO_RETRY &&
NetworkUtils.isNetworkAvailable(this)
}
/**
* 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 // 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 // resumes the download, attempt to resume the download if it was not resumed
// due to some reason. // due to some reason.
@ -463,15 +504,24 @@ class DownloadMonitorService : Service() {
false false
} }
// Return true to update the status of the download if there is any other status, // For any other error state, update the status to reflect the current state
// e.g., WAITING_FOR_WIFI, WAITING_FOR_NETWORK, or any other pause reason // and provide feedback to the user.
// to inform the user.
else -> true else -> true
} }
} 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)
return true
} }
private fun cancelNotificationAndAssignNewNotificationToForegroundService(downloadId: Long) { private fun cancelNotificationAndAssignNewNotificationToForegroundService(downloadId: Long) {
@ -501,11 +551,52 @@ class DownloadMonitorService : Service() {
} }
private fun getActiveDownloads(): List<DownloadRoomEntity> = private fun getActiveDownloads(): List<DownloadRoomEntity> =
downloadRoomDao.downloadRoomEntity().blockingFirst().filter { downloadRoomDao.downloadRoomEntity().blockingFirst().filter(::isActiveDownload)
(it.status != Status.PAUSED || it.error == Error.WAITING_TO_RETRY) &&
it.status != Status.CANCELLED /**
* Determines if a given download is considered active.
*
* @param download The DownloadRoomEntity to evaluate.
* @return True if the download is active, false otherwise.
*/
private fun isActiveDownload(download: DownloadRoomEntity): Boolean =
(download.status != Status.PAUSED || isPausedAndRetryable(download)) &&
download.status != Status.CANCELLED
/**
* Checks if a paused download is eligible for retry based on its error status and network conditions.
*
* @param download The DownloadRoomEntity to evaluate.
* @return True if the paused download is retryable, false otherwise.
*/
private fun isPausedAndRetryable(download: DownloadRoomEntity): Boolean {
return download.status == Status.PAUSED &&
(
isQueuedForWiFiAndConnected(download) ||
isQueuedForNetwork(download) ||
download.error == Error.WAITING_TO_RETRY
) &&
NetworkUtils.isNetworkAvailable(this)
} }
/**
* Checks if the download is queued for Wi-Fi and the device is connected to Wi-Fi.
*
* @param download The DownloadRoomEntity to evaluate.
* @return True if the download is queued for Wi-Fi and connected, false otherwise.
*/
private fun isQueuedForWiFiAndConnected(download: DownloadRoomEntity): Boolean =
download.error == Error.QUEUED_FOR_WIFI && NetworkUtils.isWiFi(this)
/**
* Checks if the download is waiting for a network connection and the network is now available.
*
* @param download The DownloadRoomEntity to evaluate.
* @return True if the download is waiting for a network and connected, false otherwise.
*/
private fun isQueuedForNetwork(download: DownloadRoomEntity): Boolean =
download.error == Error.WAITING_FOR_NETWORK && NetworkUtils.isNetworkAvailable(this)
private fun updateNotification( private fun updateNotification(
downloadModel: DownloadModel, downloadModel: DownloadModel,
title: String, title: String,
@ -619,7 +710,6 @@ class DownloadMonitorService : Service() {
} }
private fun stopForegroundServiceForDownloads() { private fun stopForegroundServiceForDownloads() {
Log.e("DOWNLOAD_MONITOR", "stopForegroundServiceForDownloads: service")
foreGroundServiceInformation = true to DEFAULT_INT_VALUE foreGroundServiceInformation = true to DEFAULT_INT_VALUE
monitoringDisposable?.dispose() monitoringDisposable?.dispose()
stopForeground(STOP_FOREGROUND_REMOVE) stopForeground(STOP_FOREGROUND_REMOVE)