From 034a134319bbe12ee40152971adab1bfb7bb0827 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sun, 19 Jul 2026 17:54:46 -0700 Subject: [PATCH] Remove retry stubs --- .../app/managers/DownloadItemManager.kt | 157 ++++++++++-------- .../app/services/DownloadService.kt | 5 - .../app/services/DownloadServiceHost.kt | 6 - 3 files changed, 90 insertions(+), 78 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt index 258b9d62..56035611 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/DownloadItemManager.kt @@ -38,7 +38,8 @@ class DownloadItemManager( private val lastPersistTime = mutableMapOf() private var watcherRunning = false private val jacksonMapper = - jacksonObjectMapper().enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) + jacksonObjectMapper() + .enable(JsonReadFeature.ALLOW_UNESCAPED_CONTROL_CHARS.mappedFeature()) var downloadItemQueue: MutableList = mutableListOf() private set @@ -114,13 +115,6 @@ class DownloadItemManager( notifyQueueChanged() } - @Synchronized - fun retryAll() { - downloadItemQueue.forEach(::retryDownloadItem) - checkUpdateDownloadQueue() - notifyQueueChanged() - } - private fun retryDownloadItem(item: DownloadItem) { item.terminalFailureAt = null IncompleteDownloadCleanup.cancel(context, item.id) @@ -182,34 +176,41 @@ class DownloadItemManager( part.lastUpdateTime = System.currentTimeMillis() currentDownloadItemParts.add(part) persist(item, force = true) + val activeConfig = DeviceManager.serverConnectionConfig val token = - DeviceManager.deviceData.serverConnectionConfigs - .find { it.id == item.serverConnectionConfigId }?.token ?: DeviceManager.token + if (activeConfig?.id == item.serverConnectionConfigId) activeConfig.token + else + DeviceManager.getServerConnectionConfig(item.serverConnectionConfigId)?.token + ?: DeviceManager.token activeCalls[part.id] = - InternalDownloadManager(stagingFile, part.fileSize, object : InternalProgressCallback { - override fun onProgress(totalBytesWritten: Long, progress: Long) { - synchronized(this@DownloadItemManager) { - if (part !in currentDownloadItemParts) return - part.bytesDownloaded = totalBytesWritten - part.progress = progress - part.lastUpdateTime = System.currentTimeMillis() - persist(item) - } - } + InternalDownloadManager( + stagingFile, + part.fileSize, + object : InternalProgressCallback { + override fun onProgress(totalBytesWritten: Long, progress: Long) { + synchronized(this@DownloadItemManager) { + if (part !in currentDownloadItemParts) return + part.bytesDownloaded = totalBytesWritten + part.progress = progress + part.lastUpdateTime = System.currentTimeMillis() + persist(item) + } + } - override fun onComplete(failed: Boolean) { - synchronized(this@DownloadItemManager) { - if (part !in currentDownloadItemParts) return - part.failed = failed - part.completed = !failed - part.lastUpdateTime = System.currentTimeMillis() - activeCalls.remove(part.id) - persist(item, force = true) - } - } - }, { hasAvailableSpace(part) }).download( - serverUrl(item, part), - token) + override fun onComplete(failed: Boolean) { + synchronized(this@DownloadItemManager) { + if (part !in currentDownloadItemParts) return + part.failed = failed + part.completed = !failed + part.lastUpdateTime = System.currentTimeMillis() + activeCalls.remove(part.id) + persist(item, force = true) + } + } + }, + { hasAvailableSpace(part) } + ) + .download(serverUrl(item, part), token) } @Synchronized @@ -218,7 +219,8 @@ class DownloadItemManager( watcherRunning = true scope.launch { while (true) { - val activeParts = synchronized(this@DownloadItemManager) { currentDownloadItemParts.toList() } + val activeParts = + synchronized(this@DownloadItemManager) { currentDownloadItemParts.toList() } activeParts.forEach(::handlePartUpdate) synchronized(this@DownloadItemManager) { checkUpdateDownloadQueue() @@ -235,10 +237,12 @@ class DownloadItemManager( private fun handlePartUpdate(part: DownloadItemPart) { clientEventEmitter.onDownloadItemPartUpdate(part) - val item = synchronized(this) { downloadItemQueue.find { it.id == part.downloadItemId } } ?: run { - removeActivePart(part) - return - } + val item = + synchronized(this) { downloadItemQueue.find { it.id == part.downloadItemId } } + ?: run { + removeActivePart(part) + return + } if (!part.completed && !part.failed) { val lastUpdate = part.lastUpdateTime ?: return if (System.currentTimeMillis() - lastUpdate > STALL_TIMEOUT_MS) { @@ -287,7 +291,8 @@ class DownloadItemManager( val backup = File(finalFile.parentFile, ".${finalFile.name}.abs-backup") try { if (backup.exists() && !backup.delete()) throw IllegalStateException("Could not clear backup") - if (finalFile.exists() && !finalFile.renameTo(backup)) throw IllegalStateException("Could not protect existing file") + if (finalFile.exists() && !finalFile.renameTo(backup)) + throw IllegalStateException("Could not protect existing file") if (!stagingFile.renameTo(finalFile)) { if (backup.exists()) backup.renameTo(finalFile) throw IllegalStateException("Could not finalize internal staging file") @@ -303,34 +308,42 @@ class DownloadItemManager( private fun moveDownloadedFile(item: DownloadItem, part: DownloadItemPart) { if (part.moved || part.isMoving) return - val root = DocumentFile.fromTreeUri(context, Uri.parse(part.localFolderUrl)) - ?: return failFinalization(item, part, "Could not resolve SAF destination") + val root = + DocumentFile.fromTreeUri(context, Uri.parse(part.localFolderUrl)) + ?: return failFinalization(item, part, "Could not resolve SAF destination") part.isMoving = true persist(item, force = true) scope.launch { try { - if (!hasAvailableSpace(part)) throw IllegalStateException("Insufficient storage for SAF copy") + if (!hasAvailableSpace(part)) + throw IllegalStateException("Insufficient storage for SAF copy") val folderKey = "${root.uri}/${part.finalDestinationSubfolder}" val folderLock = safFolderLocks.computeIfAbsent(folderKey) { Any() } - val folder = synchronized(folderLock) { - getOrCreateFolder(root, part.finalDestinationSubfolder) - } ?: throw IllegalStateException("Could not create SAF destination folder") + val folder = + synchronized(folderLock) { getOrCreateFolder(root, part.finalDestinationSubfolder) } + ?: throw IllegalStateException("Could not create SAF destination folder") val temporaryName = ".${part.filename}.${part.id.hashCode()}.part" folder.findFile(temporaryName)?.delete() - val temporary = folder.createFile(mimeTypeFor(part), temporaryName) - ?: throw IllegalStateException("Could not create SAF temporary file") + val temporary = + folder.createFile(mimeTypeFor(part), temporaryName) + ?: throw IllegalStateException("Could not create SAF temporary file") val staging = File(part.destinationPath) FileInputStream(staging).use { input -> context.contentResolver.openOutputStream(temporary.uri, "w")?.use { input.copyTo(it) } ?: throw IllegalStateException("Could not open SAF output stream") } - if (temporary.length() != staging.length()) throw IllegalStateException("SAF copy size mismatch") + if (temporary.length() != staging.length()) + throw IllegalStateException("SAF copy size mismatch") val existing = folder.findFile(part.filename) - if (existing != null && !existing.delete()) throw IllegalStateException("Could not replace existing file") - if (!temporary.renameTo(part.filename)) throw IllegalStateException("Could not finalize SAF temporary file") - val destination = folder.findFile(part.filename) - ?: throw IllegalStateException("Could not reopen finalized SAF file") - if (destination.length() != staging.length()) throw IllegalStateException("SAF final size mismatch") + if (existing != null && !existing.delete()) + throw IllegalStateException("Could not replace existing file") + if (!temporary.renameTo(part.filename)) + throw IllegalStateException("Could not finalize SAF temporary file") + val destination = + folder.findFile(part.filename) + ?: throw IllegalStateException("Could not reopen finalized SAF file") + if (destination.length() != staging.length()) + throw IllegalStateException("SAF final size mismatch") if (!staging.delete()) Log.w(tag, "Could not remove staging file ${staging.name}") part.completedDestinationUri = destination.uri.toString() completePart(item, part) @@ -364,12 +377,17 @@ class DownloadItemManager( if (!item.isDownloadFinished) return scope.launch { folderScanner.scanDownloadItem(item) { scanResult -> - val event = JSObject().apply { - put("libraryItemId", item.id) - put("localFolderId", item.localFolder.id) - scanResult?.localLibraryItem?.let { put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(it))) } - scanResult?.localMediaProgress?.let { put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(it))) } - } + val event = + JSObject().apply { + put("libraryItemId", item.id) + put("localFolderId", item.localFolder.id) + scanResult?.localLibraryItem?.let { + put("localLibraryItem", JSObject(jacksonMapper.writeValueAsString(it))) + } + scanResult?.localMediaProgress?.let { + put("localMediaProgress", JSObject(jacksonMapper.writeValueAsString(it))) + } + } clientEventEmitter.onDownloadItemComplete(event) synchronized(this@DownloadItemManager) { downloadItemQueue.remove(item) @@ -385,7 +403,8 @@ class DownloadItemManager( val staging = File(part.destinationPath) staging.parentFile?.mkdirs() val expectedSize = if (part.fileSize > 0L) part.fileSize else UNKNOWN_PART_RESERVATION_BYTES - val remaining = (expectedSize - (staging.takeIf(File::exists)?.length() ?: 0L)).coerceAtLeast(0L) + val remaining = + (expectedSize - (staging.takeIf(File::exists)?.length() ?: 0L)).coerceAtLeast(0L) val required = if (part.isInternalStorage) remaining else remaining + expectedSize val key = storageKey(staging) val fs = statFsFor(staging) @@ -410,7 +429,8 @@ class DownloadItemManager( } private fun storageKey(file: File): String = - if (file.absolutePath.startsWith(context.filesDir.absolutePath)) "internal" else "external" + if (file.absolutePath.startsWith(context.filesDir.absolutePath)) "internal" + else "external" @Synchronized private fun removeActivePart(part: DownloadItemPart) { @@ -425,7 +445,9 @@ class DownloadItemManager( DeviceManager.dbManager.saveDownloadItem(item) } - private fun notifyQueueChanged() { clientEventEmitter.onQueueChanged(hasWork()) } + private fun notifyQueueChanged() { + clientEventEmitter.onQueueChanged(hasWork()) + } fun destroy() { activeCalls.values.forEach(Call::cancel) @@ -443,11 +465,12 @@ class DownloadItemManager( } private fun mimeTypeFor(part: DownloadItemPart): String = - part.audioTrack?.mimeType ?: when (part.ebookFile?.ebookFormat?.lowercase()) { - "epub" -> "application/epub+zip" - "pdf" -> "application/pdf" - else -> "image/jpeg" - } + part.audioTrack?.mimeType + ?: when (part.ebookFile?.ebookFormat?.lowercase()) { + "epub" -> "application/epub+zip" + "pdf" -> "application/pdf" + else -> "image/jpeg" + } private fun serverUrl(item: DownloadItem, part: DownloadItemPart): String { val rawCover = if (part.serverPath.endsWith("/cover")) "?raw=1" else "" diff --git a/android/app/src/main/java/com/audiobookshelf/app/services/DownloadService.kt b/android/app/src/main/java/com/audiobookshelf/app/services/DownloadService.kt index 04363bee..734041ce 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/services/DownloadService.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/services/DownloadService.kt @@ -24,7 +24,6 @@ class DownloadService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { ACTION_CANCEL -> DownloadServiceHost.cancelAll(this) - ACTION_RETRY -> DownloadServiceHost.retryAll(this) else -> DownloadServiceHost.ensure(this) } return START_STICKY @@ -54,8 +53,6 @@ class DownloadService : Service() { private fun notification(text: String, progress: Int = 0, determinate: Boolean = false): Notification { val cancelIntent = PendingIntent.getService( this, 1, Intent(this, DownloadService::class.java).setAction(ACTION_CANCEL), pendingIntentFlags()) - val retryIntent = PendingIntent.getService( - this, 2, Intent(this, DownloadService::class.java).setAction(ACTION_RETRY), pendingIntentFlags()) return NotificationCompat.Builder(this, CHANNEL_ID) .setSmallIcon(R.drawable.icon) .setContentTitle("Audiobookshelf downloads") @@ -64,7 +61,6 @@ class DownloadService : Service() { .setOngoing(true) .setProgress(100, progress, !determinate) .addAction(0, "Cancel", cancelIntent) - .addAction(0, "Retry", retryIntent) .build() } @@ -79,7 +75,6 @@ class DownloadService : Service() { private const val CHANNEL_ID = "downloads" private const val NOTIFICATION_ID = 4102 private const val ACTION_CANCEL = "com.audiobookshelf.app.download.CANCEL" - private const val ACTION_RETRY = "com.audiobookshelf.app.download.RETRY" fun intent(context: Context) = Intent(context, DownloadService::class.java) } } 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 ef2839dd..d37400fe 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 @@ -55,12 +55,6 @@ object DownloadServiceHost { startService(context) } - @Synchronized - fun retryAll(context: Context) { - startService(context) - ensure(context).retryAll() - } - @Synchronized fun cancelAll(context: Context) { ensure(context).cancelAll() }