From 3654b50d5495c8dda7791739aad0c5b7ebb876ab Mon Sep 17 00:00:00 2001 From: LooKeR Date: Mon, 24 Jan 2022 01:37:30 +0530 Subject: [PATCH] Improve: Start working on new downloader --- .../looker/droidify/network/DownloadResult.kt | 15 +++ .../looker/droidify/network/DownloaderX.kt | 118 ++++++++++++++++++ .../utility/extension/OkHttpExtension.kt | 42 +++++++ 3 files changed, 175 insertions(+) create mode 100644 src/main/kotlin/com/looker/droidify/network/DownloadResult.kt create mode 100644 src/main/kotlin/com/looker/droidify/network/DownloaderX.kt create mode 100644 src/main/kotlin/com/looker/droidify/utility/extension/OkHttpExtension.kt diff --git a/src/main/kotlin/com/looker/droidify/network/DownloadResult.kt b/src/main/kotlin/com/looker/droidify/network/DownloadResult.kt new file mode 100644 index 00000000..06a02d6f --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/network/DownloadResult.kt @@ -0,0 +1,15 @@ +package com.looker.droidify.network + +sealed class DownloadResult( + val progress: Long? = 0, + val total: Long? = 0, + val data: T? = null, + val message: String? = null +) { + class Loading(progress: Long? = null, total: Long? = null, data: T? = null) : + DownloadResult(progress, total, data) + + class Success(data: T?) : DownloadResult(data = data) + class Error(message: String, data: T? = null) : + DownloadResult(data = data, message = message) +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/network/DownloaderX.kt b/src/main/kotlin/com/looker/droidify/network/DownloaderX.kt new file mode 100644 index 00000000..0410da18 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/network/DownloaderX.kt @@ -0,0 +1,118 @@ +package com.looker.droidify.network + +import com.looker.droidify.utility.ProgressInputStream +import com.looker.droidify.utility.extension.await +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.currentCoroutineContext +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import okhttp3.Cache +import okhttp3.Call +import okhttp3.OkHttpClient +import okhttp3.Request +import java.io.File +import java.io.FileOutputStream +import java.net.InetSocketAddress +import java.net.Proxy +import java.util.concurrent.TimeUnit.SECONDS + +object DownloaderX { + private val client = OkHttpClient() + + private data class ClientConfiguration(val cache: Cache?, val onion: Boolean) + + private val clients = mutableMapOf() + private val onionProxy = Proxy(Proxy.Type.SOCKS, InetSocketAddress("127.0.0.1", 9050)) + + var proxy: Proxy? = null + set(value) { + if (field != value) { + synchronized(clients) { + field = value + clients.keys.removeAll { !it.onion } + } + } + } + + private fun createCall(request: Request.Builder, authentication: String): Call { + val oldRequest = request.build() + val newRequest = if (authentication.isNotEmpty()) { + request.addHeader("Authorization", authentication).build() + } else { + request.build() + } + val onion = oldRequest.url.host.endsWith(".onion") + val client = synchronized(clients) { + val proxy = if (onion) onionProxy else proxy + val clientConfiguration = ClientConfiguration(null, onion) + clients[clientConfiguration] ?: run { + val client = this.client + .newBuilder() + .connectTimeout(30L, SECONDS) + .readTimeout(15L, SECONDS) + .writeTimeout(15L, SECONDS) + .proxy(proxy).build() + clients[clientConfiguration] = client + client + } + } + return client.newCall(newRequest) + } + + suspend fun startDownload( + url: String, + partialFile: File, + authentication: String + ): Flow> = flow { + val scope = currentCoroutineContext() + val start = if (partialFile.exists()) partialFile.length() + .let { if (it > 0L) it else null } else null + val request = Request.Builder().url(url) + .apply { if (start != null) addHeader("Range", "bytes=$start-") } + + val response = createCall(request, authentication).await() + + response.use { it -> + if (it.code == 304) emit(DownloadResult.Loading()) + else { + val body = it.body!! + val append = start != null && it.header("Content-Range") != null + val progressStart = if (append && start != null) start else 0L + val progressTotal = + body.contentLength().let { if (it >= 0L) it else null } + ?.let { progressStart + it } + withContext(Dispatchers.IO + scope) { + val inputStream = ProgressInputStream(body.byteStream()) { + if (Thread.interrupted()) { + launch { emit(DownloadResult.Error("Thread Interrupted")) } + throw InterruptedException() + } + launch { + emit( + DownloadResult.Loading( + progress = progressStart + it, + total = progressTotal + ) + ) + } + } + inputStream.use { input -> + val outputStream = + if (append) FileOutputStream(partialFile, true) + else FileOutputStream(partialFile) + outputStream.use { output -> + input.copyTo(output) + output.fd.runCatching { sync() } + .onSuccess { emit(DownloadResult.Success(Unit)) } + .onFailure { emit(DownloadResult.Error(it.message.toString())) } + } + } + } + } + } + + } + +} \ No newline at end of file diff --git a/src/main/kotlin/com/looker/droidify/utility/extension/OkHttpExtension.kt b/src/main/kotlin/com/looker/droidify/utility/extension/OkHttpExtension.kt new file mode 100644 index 00000000..7d3b2f01 --- /dev/null +++ b/src/main/kotlin/com/looker/droidify/utility/extension/OkHttpExtension.kt @@ -0,0 +1,42 @@ +package com.looker.droidify.utility.extension + +import kotlinx.coroutines.suspendCancellableCoroutine +import okhttp3.Call +import okhttp3.Callback +import okhttp3.Response +import okhttp3.internal.closeQuietly +import okio.IOException +import kotlin.coroutines.resumeWithException + +suspend fun Call.await(): Response { + return suspendCancellableCoroutine { continuation -> + enqueue( + object : Callback { + override fun onResponse(call: Call, response: Response) { + if (!response.isSuccessful) { + continuation.resumeWithException(Exception("HTTP error ${response.code}")) + return + } + + continuation.resume(response) { + response.body?.closeQuietly() + } + } + + override fun onFailure(call: Call, e: IOException) { + // Don't bother with resuming the continuation if it is already cancelled. + if (continuation.isCancelled) return + continuation.resumeWithException(e) + } + } + ) + + continuation.invokeOnCancellation { + try { + cancel() + } catch (ex: Throwable) { + // Ignore cancel exception + } + } + } +} \ No newline at end of file