From d27f431f6f3488e6bbb9891f0ca1674211e53211 Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Sun, 19 Jul 2026 09:23:31 -0700 Subject: [PATCH] Comment cleanup --- .../app/data/LocalLibraryItem.kt | 1 - .../app/device/FolderScanner.kt | 30 ++++++++++++------- .../audiobookshelf/app/managers/DbManager.kt | 6 ++-- .../app/managers/DownloadItemManager.kt | 8 +---- .../app/managers/IncompleteDownloadCleanup.kt | 6 ++-- .../app/managers/InternalDownloadManager.kt | 6 ++-- .../app/models/DownloadItemPart.kt | 4 --- .../app/plugins/AbsDownloader.kt | 10 +------ .../app/services/DownloadService.kt | 3 -- .../app/services/DownloadServiceHost.kt | 3 +- 10 files changed, 32 insertions(+), 45 deletions(-) diff --git a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt index 9251e7d9..8fbe9ac3 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/data/LocalLibraryItem.kt @@ -98,7 +98,6 @@ class LocalLibraryItem( return true } - /** App-private files have paths; SAF files must be validated through their persisted URI. */ @JsonIgnore private fun trackExists(ctx: Context, contentUrl: String?, path: String): Boolean { return LocalFile("", null, contentUrl ?: "", "", path, "", null, 0).exists(ctx) diff --git a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt index c53b9b8b..69d45512 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/device/FolderScanner.kt @@ -10,7 +10,7 @@ import com.audiobookshelf.app.models.DownloadItem import com.audiobookshelf.app.models.DownloadItemPart import java.io.File -/** Creates local-library records from the completed download manifest, not a recursive rescan. */ +/** Creates local-library records from a completed download manifest. */ class FolderScanner(private val ctx: Context) { private val tag = "FolderScanner" @@ -19,9 +19,13 @@ class FolderScanner(private val ctx: Context) { var localMediaProgress: LocalMediaProgress? ) - private fun localLibraryItemId(mediaItemId: String) = "local_${DeviceManager.getBase64Id(mediaItemId)}" + private fun localLibraryItemId(mediaItemId: String) = + "local_${DeviceManager.getBase64Id(mediaItemId)}" - private fun createLocalFile(part: DownloadItemPart, externalFile: DocumentFile? = null): LocalFile? { + private fun createLocalFile( + part: DownloadItemPart, + externalFile: DocumentFile? = null + ): LocalFile? { if (part.isInternalStorage) { val file = File(part.finalDestinationPath) if (!file.exists()) return null @@ -43,7 +47,8 @@ class FolderScanner(private val ctx: Context) { try { ctx.contentResolver.openFileDescriptor(uri, "r")?.use { descriptor -> descriptor.statSize.coerceAtLeast(0L) - } ?: 0L + } + ?: 0L } catch (e: Exception) { Log.e(tag, "Could not open completed SAF file: $contentUrl", e) return null @@ -123,8 +128,7 @@ class FolderScanner(private val ctx: Context) { if (part.isInternalStorage) { null } else { - part.completedDestinationUri - ?.let { DocumentFileCompat.fromUri(ctx, Uri.parse(it)) } + part.completedDestinationUri?.let { DocumentFileCompat.fromUri(ctx, Uri.parse(it)) } ?: resolveExternalFile(externalFolder, part) } Log.d(tag, "Resolve part ${part.filename}: externalFile=${externalFile?.uri}") @@ -203,7 +207,9 @@ class FolderScanner(private val ctx: Context) { val result = DownloadItemScanResult(localItem, null) item.userMediaProgress?.let { progress -> - val progressId = if (item.episodeId.isNullOrEmpty()) localItem.id else "${localItem.id}-$localEpisodeId" + val progressId = + if (item.episodeId.isNullOrEmpty()) localItem.id + else "${localItem.id}-$localEpisodeId" result.localMediaProgress = LocalMediaProgress( progressId, @@ -247,14 +253,18 @@ class FolderScanner(private val ctx: Context) { */ private fun resolveExternalFile(folder: DocumentFile?, part: DownloadItemPart): DocumentFile? { if (folder == null) return null - folder.findFile(part.filename)?.let { return it } + folder.findFile(part.filename)?.let { + return it + } val expectedBaseName = part.filename.substringBeforeLast('.') return folder.listFiles().firstOrNull { document -> document.name == part.filename || document.fullName == part.filename || - (part.audioTrack != null && document.isFile && + (part.audioTrack != null && + document.isFile && (document.name ?: "").substringBeforeLast('.') == expectedBaseName) || - (part.audioTrack != null && document.isFile && + (part.audioTrack != null && + document.isFile && document.fullName.substringBeforeLast('.') == expectedBaseName) } } 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 d02f2d3c..2f4c4572 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 @@ -123,9 +123,9 @@ class DbManager { } /** - * The downloader now persists app-owned staging paths instead of DownloadManager state. Old - * queue entries cannot be resumed safely, but completed local media lives in other books and is - * deliberately left alone. + * Removes DownloadManager queue entries that cannot be resumed by the app-managed downloader. + * + * This migration preserves completed local media, which is stored in separate books. */ fun clearLegacyDownloadQueueOnce() { val metadata = Paper.book("downloadQueueMetadata") 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 4698a936..532c3bd8 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 @@ -24,10 +24,7 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import okhttp3.Call -/** - * Process-owned Android download queue. Every network write goes through app-owned staging and is - * admitted only after reserving enough space for the complete operation. - */ +/** Manages the process-owned queue for app-managed downloads. */ class DownloadItemManager( private val folderScanner: FolderScanner, private val context: Context, @@ -36,7 +33,6 @@ class DownloadItemManager( private val tag = "DownloadItemManager" private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) private val activeCalls = ConcurrentHashMap() - /** DocumentsProvider does not make concurrent createDirectory/findFile calls atomic. */ private val safFolderLocks = ConcurrentHashMap() private val reservations = mutableMapOf() private val lastPersistTime = mutableMapOf() @@ -366,8 +362,6 @@ class DownloadItemManager( } private fun tryReserve(part: DownloadItemPart): Boolean { - // Covers from older servers often omit a size. Keep unknown-length work serial and use the - // runtime low-space guard rather than leaving those queue items permanently deferred. if (part.fileSize <= 0L && currentDownloadItemParts.any { it.fileSize <= 0L }) return false val staging = File(part.destinationPath) staging.parentFile?.mkdirs() diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/IncompleteDownloadCleanup.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/IncompleteDownloadCleanup.kt index 7d517a8b..82e1716a 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/IncompleteDownloadCleanup.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/IncompleteDownloadCleanup.kt @@ -14,7 +14,7 @@ import com.audiobookshelf.app.models.DownloadItem import java.io.File import java.util.concurrent.TimeUnit -/** Removes only terminally failed download items after their retention window has elapsed. */ +/** Removes terminally failed downloads after their retention window elapses. */ object IncompleteDownloadCleanup { private const val tag = "IncompleteDownloadCleanup" private const val RETENTION_MS = 24L * 60L * 60L * 1000L @@ -34,7 +34,7 @@ object IncompleteDownloadCleanup { WorkManager.getInstance(context).cancelUniqueWork(WORK_PREFIX + itemId) } - /** Called on app/service startup as a catch-up for work delayed by Android or force-stop. */ + /** Removes failures retained longer than 24 hours when scheduled work did not run. */ fun cleanupExpired(context: Context): Set { val now = System.currentTimeMillis() return DeviceManager.dbManager.getDownloadItems() @@ -49,7 +49,6 @@ object IncompleteDownloadCleanup { private fun isEligible(item: DownloadItem, now: Long): Boolean { val failedAt = item.terminalFailureAt ?: return false if (now - failedAt < RETENTION_MS) return false - // Do not remove an item while another part is still downloading, waiting, or finalizing. return item.downloadItemParts.all { part -> part.moved || (part.failed && !part.isMoving) } @@ -61,7 +60,6 @@ object IncompleteDownloadCleanup { if (part.isInternalStorage && part.moved) { deleteAppOwnedFile(context, File(part.finalDestinationPath)) } else if (!part.isInternalStorage && part.moved) { - // Never infer an arbitrary SAF path during cleanup. The stored document URI is authoritative. part.completedDestinationUri?.let { uriString -> try { DocumentFile.fromSingleUri(context, Uri.parse(uriString))?.delete() diff --git a/android/app/src/main/java/com/audiobookshelf/app/managers/InternalDownloadManager.kt b/android/app/src/main/java/com/audiobookshelf/app/managers/InternalDownloadManager.kt index 2854fc2a..63e33828 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/managers/InternalDownloadManager.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/managers/InternalDownloadManager.kt @@ -20,8 +20,10 @@ class InternalDownloadManager( ) { private val tag = "InternalDownloadManager" /** - * Returns the active call so the queue can cancel a stalled transfer. A partial staging file is - * retained only when the server proves that it honoured a subsequent range request. + * Starts or resumes a download. + * + * @param url authenticated download URL + * @return active call, used to cancel a stalled transfer */ fun download(url: String): Call { destinationFile.parentFile?.mkdirs() diff --git a/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt index 0a6081ae..31d887c2 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/models/DownloadItemPart.kt @@ -15,7 +15,6 @@ data class DownloadItemPart( val downloadItemId: String, val filename: String, val fileSize: Long, - /** App-owned staging location. This is intentionally a String so it survives process storage. */ @JsonIgnore val destinationPath: String, val finalDestinationPath:String, val serverPath: String, @@ -32,15 +31,12 @@ data class DownloadItemPart( @JsonIgnore val uri: Uri, @JsonIgnore val destinationUri: Uri, @JsonIgnore val finalDestinationUri: Uri, - /** Final SAF document returned by the provider after a successful move. */ - /** Persisted Android-only SAF URI used to reopen a completed document after process recovery. */ @JsonIgnore var completedDestinationUri: String?, val finalDestinationSubfolder: String, var downloadId: Long?, @JsonIgnore var lastUpdateTime: Long?, var progress: Long, var bytesDownloaded: Long, - /** Android queue state; hidden from the shared Capacitor download-part payload. */ @JsonIgnore var retryCount: Int = 0, @JsonIgnore var waitingForSpace: Boolean = false ) { diff --git a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt index 94f65bdd..68cfdd67 100644 --- a/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt +++ b/android/app/src/main/java/com/audiobookshelf/app/plugins/AbsDownloader.kt @@ -5,7 +5,6 @@ import android.util.Log import com.audiobookshelf.app.MainActivity import com.audiobookshelf.app.data.* import com.audiobookshelf.app.device.DeviceManager -import com.audiobookshelf.app.device.FolderScanner import com.audiobookshelf.app.models.DownloadItem import com.audiobookshelf.app.models.DownloadItemPart import com.audiobookshelf.app.server.ApiHandler @@ -27,7 +26,6 @@ class AbsDownloader : Plugin() { lateinit var mainActivity: MainActivity lateinit var apiHandler: ApiHandler - lateinit var folderScanner: FolderScanner lateinit var downloadItemManager: DownloadItemManager private val clientEventEmitter = (object : DownloadItemManager.DownloadEventEmitter { @@ -45,7 +43,6 @@ class AbsDownloader : Plugin() { override fun load() { mainActivity = (activity as MainActivity) - folderScanner = FolderScanner(mainActivity) apiHandler = ApiHandler(mainActivity) downloadItemManager = DownloadServiceHost.ensure(mainActivity) DownloadServiceHost.attachBridge(mainActivity, clientEventEmitter) @@ -56,10 +53,7 @@ class AbsDownloader : Plugin() { super.handleOnDestroy() } - /** - * Queue restoration happens before the WebView mounts. Replay its parent items when Vue registers - * the listener so subsequent part updates always have a matching store entry. - */ + /** Replays restored queue items when the frontend subscribes to download events. */ @PluginMethod(returnType = PluginMethod.RETURN_NONE) override fun addListener(call: PluginCall) { super.addListener(call) @@ -151,8 +145,6 @@ class AbsDownloader : Plugin() { val isInternal = localFolder.id.startsWith("internal-") val finalInternalFolderPath = "${mainActivity.filesDir}/downloads/${libraryItem.id}" - // Keep internal staging on the same filesystem as its final file so finalization is a rename. - // External-folder downloads can use app external storage and are moved through SAF afterwards. val tempFolderPath = if (isInternal) { "${mainActivity.filesDir}/download-staging/${libraryItem.id}" 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 ebd938d0..04363bee 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 @@ -14,8 +14,6 @@ import com.audiobookshelf.app.models.DownloadItemPart /** Android-owned foreground lifecycle for transfers that must outlive the WebView and Activity. */ class DownloadService : Service() { - private var lastPart: DownloadItemPart? = null - override fun onCreate() { super.onCreate() createChannel() @@ -40,7 +38,6 @@ class DownloadService : Service() { override fun onBind(intent: Intent?): IBinder? = null fun onPartUpdate(part: DownloadItemPart) { - lastPart = part val text = if (part.waitingForSpace) "Waiting for available storage" else "Downloading ${part.filename}" val progress = part.progress.coerceIn(0L, 100L).toInt() val notification = notification(text, progress, part.fileSize > 0L) 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 ce4882f7..ef2839dd 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 @@ -28,10 +28,9 @@ object DownloadServiceHost { return manager!! } + /** Attaches the frontend after restored queue items have been emitted. */ @Synchronized fun attachBridge(context: Context, emitter: DownloadItemManager.DownloadEventEmitter) { - // Rehydrate the frontend's parent items before allowing part-progress events through. - // Otherwise a running restored queue can emit a part before Vue knows its DownloadItem. bridgeReady = false bridgeEmitter = emitter val queue = ensure(context)