From e11b8cb66ce97c4b047104e7849d864035c81a4e Mon Sep 17 00:00:00 2001 From: Nicholas Wallace Date: Mon, 7 Apr 2025 19:04:29 -0700 Subject: [PATCH] Initial internal download continue incomplete download --- .../app/managers/DownloadItemManager.kt | 8 +- .../app/managers/InternalDownloadManager.kt | 119 ++++++++++++------ 2 files changed, 85 insertions(+), 42 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 b57cc9f6..d62578ff 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 @@ -19,7 +19,6 @@ import com.fasterxml.jackson.core.json.JsonReadFeature import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.getcapacitor.JSObject import java.io.File -import java.io.FileOutputStream import java.util.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.GlobalScope @@ -113,7 +112,6 @@ class DownloadItemManager( val file = File(downloadItemPart.finalDestinationPath) file.parentFile?.mkdirs() - val fileOutputStream = FileOutputStream(downloadItemPart.finalDestinationPath) val internalProgressCallback = object : InternalProgressCallback { override fun onProgress(totalBytesWritten: Long, progress: Long) { @@ -131,7 +129,11 @@ class DownloadItemManager( tag, "Start internal download to destination path ${downloadItemPart.finalDestinationPath} from ${downloadItemPart.serverUrl}" ) - InternalDownloadManager(fileOutputStream, internalProgressCallback) + InternalDownloadManager( + mainActivity, + downloadItemPart.finalDestinationUri, + internalProgressCallback + ) .download(downloadItemPart.serverUrl) downloadItemPart.downloadId = 1 currentDownloadItemParts.add(downloadItemPart) 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 bc37e8bd..1ab15a3d 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 @@ -1,5 +1,7 @@ package com.audiobookshelf.app.managers +import android.content.Context +import android.net.Uri import android.util.Log import java.io.* import java.util.concurrent.TimeUnit @@ -12,14 +14,14 @@ import okhttp3.* * @property progressCallback The callback to report download progress. */ class InternalDownloadManager( - private val outputStream: FileOutputStream, + private val context: Context, + private val fileUri: Uri, private val progressCallback: DownloadItemManager.InternalProgressCallback ) : AutoCloseable { private val tag = "InternalDownloadManager" private val client: OkHttpClient = OkHttpClient.Builder().connectTimeout(30, TimeUnit.SECONDS).build() - private val writer = BinaryFileWriter(outputStream, progressCallback) /** * Downloads a file from the given URL. @@ -29,69 +31,108 @@ class InternalDownloadManager( */ @Throws(IOException::class) fun download(url: String) { - val request: Request = Request.Builder().url(url).build() - client.newCall(request) - .enqueue( - object : Callback { - override fun onFailure(call: Call, e: IOException) { - Log.e(tag, "Download URL $url FAILED", e) - progressCallback.onComplete(true) - } + try { + val existingSize = getInternalFileSize(fileUri) + Log.d(tag, "Existing file size: $existingSize bytes") - override fun onResponse(call: Call, response: Response) { - response.body?.let { responseBody -> - val length: Long = response.header("Content-Length")?.toLongOrNull() ?: 0L - writer.write(responseBody.byteStream(), length) + val request = + Request.Builder() + .url(url) + .apply { + if (existingSize > 0) { + addHeader("Range", "bytes=$existingSize-") // Resume download } - ?: run { - Log.e(tag, "Response doesn't contain a file") - progressCallback.onComplete(true) - } } - } - ) + .build() + + client.newCall(request) + .enqueue( + object : Callback { + override fun onFailure(call: Call, e: IOException) { + Log.e(tag, "Download failed: $url", e) + progressCallback.onComplete(true) + } + + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + Log.e(tag, "Failed to download: ${response.code}") + progressCallback.onComplete(true) + return + } + + response.body?.let { responseBody -> + val totalLength = + (response.header("Content-Length")?.toLongOrNull() + ?: 0L) + existingSize + val outputStream = getOutputStream(fileUri, existingSize) + + outputStream?.let { + BinaryFileWriter(it, progressCallback, existingSize).use { writer -> + writer.write(responseBody.byteStream(), totalLength) + } + } + ?: run { + Log.e(tag, "Failed to open output stream") + progressCallback.onComplete(true) + } + } + ?: run { + Log.e(tag, "Response doesn't contain a file") + progressCallback.onComplete(true) + } + } + } + ) + } catch (e: Exception) { + Log.e(tag, "Download error", e) + progressCallback.onComplete(true) + } } - /** - * Closes the download manager and releases resources. - * - * @throws Exception If an error occurs during closing. - */ - @Throws(Exception::class) - override fun close() { - writer.close() + private fun getInternalFileSize(uri: Uri): Long { + val file = File(uri.path!!) + return if (file.exists()) file.length() else 0L } + + private fun getOutputStream(uri: Uri, existingSize: Long): OutputStream? { + val file = File(uri.path!!) + return if (file.exists()) { + FileOutputStream(file, true).apply { + channel.position(existingSize) // Move to the end of the file to append + } + } else { + FileOutputStream(file) // Create a new file if it doesn't exist + } + } + + @Throws(Exception::class) override fun close() {} } -/** - * Writes binary data to an output stream. - * - * @property outputStream The output stream to write the data to. - * @property progressCallback The callback to report write progress. - */ class BinaryFileWriter( private val outputStream: OutputStream, - private val progressCallback: DownloadItemManager.InternalProgressCallback + private val progressCallback: DownloadItemManager.InternalProgressCallback, + private val existingSize: Long ) : AutoCloseable { /** * Writes data from the input stream to the output stream. * * @param inputStream The input stream to read the data from. - * @param length The total length of the data to be written. + * @param totalLength The total length of the data to be written. * @return The total number of bytes written. * @throws IOException If an I/O error occurs. */ @Throws(IOException::class) - fun write(inputStream: InputStream, length: Long): Long { + fun write(inputStream: InputStream, totalLength: Long): Long { BufferedInputStream(inputStream).use { input -> val dataBuffer = ByteArray(CHUNK_SIZE) - var totalBytes: Long = 0 + var totalBytes = existingSize var readBytes: Int + while (input.read(dataBuffer).also { readBytes = it } != -1) { totalBytes += readBytes outputStream.write(dataBuffer, 0, readBytes) - progressCallback.onProgress(totalBytes, (totalBytes * 100L) / length) + progressCallback.onProgress(totalBytes, (totalBytes * 100L) / totalLength) } progressCallback.onComplete(false) return totalBytes