Improve: Start working on new downloader

This commit is contained in:
LooKeR 2022-01-24 01:37:30 +05:30
parent f465f5da2a
commit 3654b50d54
3 changed files with 175 additions and 0 deletions

View File

@ -0,0 +1,15 @@
package com.looker.droidify.network
sealed class DownloadResult<T>(
val progress: Long? = 0,
val total: Long? = 0,
val data: T? = null,
val message: String? = null
) {
class Loading<T>(progress: Long? = null, total: Long? = null, data: T? = null) :
DownloadResult<T>(progress, total, data)
class Success<T>(data: T?) : DownloadResult<T>(data = data)
class Error<T>(message: String, data: T? = null) :
DownloadResult<T>(data = data, message = message)
}

View File

@ -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<ClientConfiguration, OkHttpClient>()
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<DownloadResult<Unit>> = 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())) }
}
}
}
}
}
}
}

View File

@ -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
}
}
}
}