diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt index 63c97a7c..26b3edee 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/DbManager.kt @@ -121,6 +121,86 @@ class DbManager { return downloadItems } + /** + * Validate persisted download queue entries before restoring them. + * + * Queue entries can become incompatible or malformed, for example after an app update. If any + * entry cannot be read or fails the checks required for queue restoration, the entire persisted + * queue is cleared so startup can continue without restoring invalid data. + * + * @return true if the queue passes validation or was successfully cleared; false if an invalid + * queue could not be cleared + */ + fun ensureValidDownloadQueue(): Boolean { + val downloadBook = Paper.book("downloadItems") + val queueIsValid = + try { + downloadBook.allKeys + .map { downloadItemId -> + val item = + downloadBook.read(downloadItemId) + ?: throw IllegalStateException( + "Download queue entry $downloadItemId could not be read" + ) + if (item.id != downloadItemId) { + throw IllegalStateException( + "Download queue entry $downloadItemId has a mismatched item ID" + ) + } + item + } + .all { item -> + listOf(item.id, item.libraryItemId, item.localFolder.id).all { + !it.isNullOrBlank() + } && + item.downloadItemParts.isNotEmpty() && + item.downloadItemParts.all { part -> + listOf( + part.id, + part.downloadItemId, + part.filename, + part.destinationPath, + part.finalDestinationPath, + part.serverPath, + part.localFolderId + ) + .all { !it.isNullOrBlank() } && + part.downloadItemId == item.id + } + } + } catch (readError: Exception) { + AbsLogger.error( + tag, + "Could not read persisted download queue; clearing queue (${readError.message})" + ) + false + } + + if (queueIsValid) return true + + AbsLogger.error(tag, "Persisted download queue contains invalid data; clearing queue") + return try { + downloadBook.destroy() + val remainingKeys = downloadBook.allKeys + if (remainingKeys.isEmpty()) { + AbsLogger.info(tag, "Cleared invalid persisted download queue") + true + } else { + AbsLogger.error( + tag, + "Invalid persisted download queue still contains ${remainingKeys.size} entries after clearing" + ) + false + } + } catch (clearError: Exception) { + AbsLogger.error( + tag, + "Could not clear invalid persisted download queue (${clearError.message})" + ) + false + } + } + fun saveLocalMediaProgress(mediaProgress: LocalMediaProgress) { Paper.book("localMediaProgress").write(mediaProgress.id, mediaProgress) } diff --git a/android/app/src/main/java/com/audiobookshelf/app/services/DownloadServiceHost.kt b/android/app/src/main/java/com/audiobookshelf/app/services/DownloadServiceHost.kt index 1bf1f8c3..aaa1eb7a 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/services/DownloadServiceHost.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/services/DownloadServiceHost.kt @@ -2,6 +2,7 @@ package com.audiobookshelf.app.services import android.content.Context import androidx.core.content.ContextCompat +import com.audiobookshelf.app.device.DeviceManager import com.audiobookshelf.app.device.FolderScanner import com.audiobookshelf.app.managers.DbManager import com.audiobookshelf.app.managers.DownloadItemManager @@ -43,9 +44,20 @@ object DownloadServiceHost { DbManager.initialize(appContext) manager = DownloadItemManager(FolderScanner(appContext), appContext, ForwardingEmitter) restoreJob = scope.launch { - IncompleteDownloadCleanup.cleanupExpired(appContext) - manager!!.restoreQueue() - onRestoreComplete(appContext) + try { + if (DeviceManager.dbManager.ensureValidDownloadQueue()) { + IncompleteDownloadCleanup.cleanupExpired(appContext) + manager!!.restoreQueue() + } else { + AbsLogger.error(TAG, "Skipping invalid download queue because it could not be cleared") + } + onRestoreComplete(appContext) + } catch (restoreError: Exception) { + AbsLogger.error( + TAG, + "Could not restore download queue (${restoreError.javaClass.simpleName}: ${restoreError.message})" + ) + } } } return manager!!