forked from geode-sdk/android-launcher
239 lines
7.5 KiB
Kotlin
239 lines
7.5 KiB
Kotlin
package com.geode.launcher.utils
|
|
|
|
import android.os.Build
|
|
import android.os.FileUtils
|
|
import kotlinx.coroutines.Dispatchers
|
|
import kotlinx.coroutines.runInterruptible
|
|
import kotlinx.coroutines.withContext
|
|
import okhttp3.MediaType
|
|
import okhttp3.OkHttpClient
|
|
import okhttp3.Request
|
|
import okhttp3.RequestBody
|
|
import okhttp3.ResponseBody
|
|
import okhttp3.coroutines.executeAsync
|
|
import okio.Buffer
|
|
import okio.BufferedSink
|
|
import okio.BufferedSource
|
|
import okio.FileSystem
|
|
import okio.ForwardingSink
|
|
import okio.ForwardingSource
|
|
import okio.Path.Companion.toOkioPath
|
|
import okio.Sink
|
|
import okio.Source
|
|
import okio.buffer
|
|
import java.io.File
|
|
import java.io.IOException
|
|
import java.io.InputStream
|
|
import java.io.OutputStream
|
|
import java.util.concurrent.TimeUnit
|
|
import java.util.zip.ZipInputStream
|
|
|
|
|
|
typealias ProgressCallback = (progress: Long, outOf: Long) -> Unit
|
|
|
|
object DownloadUtils {
|
|
suspend fun downloadStream(
|
|
httpClient: OkHttpClient,
|
|
url: String,
|
|
onProgress: ProgressCallback? = null,
|
|
onResponse: suspend (ResponseBody) -> Unit,
|
|
) {
|
|
val request = Request.Builder()
|
|
.url(url)
|
|
.build()
|
|
|
|
// build a new client using the same pool as the old client
|
|
// (more efficient)
|
|
val progressClientBuilder = httpClient.newBuilder()
|
|
.cache(null)
|
|
// disable timeout
|
|
.readTimeout(0, TimeUnit.SECONDS)
|
|
|
|
// add progress listener
|
|
if (onProgress != null) {
|
|
progressClientBuilder.addNetworkInterceptor { chain ->
|
|
val originalResponse = chain.proceed(chain.request())
|
|
originalResponse.newBuilder()
|
|
.body(ProgressResponseBody(
|
|
originalResponse.body, onProgress
|
|
))
|
|
.build()
|
|
}
|
|
}
|
|
|
|
val progressClient = progressClientBuilder.build()
|
|
|
|
val call = progressClient.newCall(request)
|
|
call.executeAsync().use { response ->
|
|
when (response.code) {
|
|
200 -> onResponse(response.body)
|
|
else -> throw IOException("unexpected response ${response.code}")
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun copyZipStreamToDirectory(inputStream: InputStream, output: File) = runInterruptible {
|
|
ZipInputStream(inputStream).use { zipStream ->
|
|
if (output.exists()) output.deleteRecursively()
|
|
output.mkdirs()
|
|
|
|
val canonicalOutput = output.canonicalPath
|
|
|
|
var entry = zipStream.nextEntry
|
|
while (entry != null) {
|
|
val destination = File(output, entry.name)
|
|
if (!destination.canonicalPath.startsWith(canonicalOutput)) {
|
|
throw IOException("attempted copy to outside of output directory: ${entry.name}")
|
|
}
|
|
|
|
if (destination.isDirectory) {
|
|
destination.mkdirs()
|
|
} else {
|
|
destination.parentFile?.mkdirs()
|
|
|
|
destination.outputStream().use { destinationStream ->
|
|
zipStream.copyTo(destinationStream)
|
|
}
|
|
}
|
|
|
|
entry = zipStream.nextEntry
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Extracts a file named by zipPath from inputStream and copies it to outputStream
|
|
*
|
|
* inputStream and outputStream are both closed after this function executes
|
|
*/
|
|
suspend fun extractFileFromZipStream(inputStream: InputStream, output: File, zipPath: String) {
|
|
// note to self: ZipInputStreams are a little silly
|
|
// (runInterruptible allows it to cancel, otherwise it waits for the stream to finish)
|
|
runInterruptible {
|
|
inputStream.use { input ->
|
|
output.outputStream().use { out ->
|
|
// nice indentation
|
|
ZipInputStream(input).use { zip ->
|
|
var entry = zip.nextEntry
|
|
while (entry != null) {
|
|
if (entry.name == zipPath) {
|
|
zip.copyTo(out)
|
|
return@runInterruptible
|
|
}
|
|
|
|
entry = zip.nextEntry
|
|
}
|
|
|
|
// no matching entries found, throw exception
|
|
throw IOException("no file found for $zipPath")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
suspend fun copyFile(from: File, to: File) {
|
|
val fs = FileSystem.SYSTEM
|
|
|
|
val fromPath = from.toOkioPath()
|
|
val toPath = to.toOkioPath()
|
|
|
|
withContext(Dispatchers.IO) {
|
|
fs.copy(fromPath, toPath)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Copies data from inputStream to outputStream
|
|
*
|
|
* Both streams are closed upon the conclusion of this function.
|
|
*/
|
|
fun copyFile(inputStream: InputStream, outputStream: OutputStream) {
|
|
// gotta love copying
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
inputStream.use { input -> outputStream.use { output ->
|
|
FileUtils.copy(input, output)
|
|
}}
|
|
} else {
|
|
inputStream.use { input -> outputStream.use { output ->
|
|
input.copyTo(output)
|
|
}}
|
|
}
|
|
}
|
|
}
|
|
|
|
// based on <https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/Progress.java>
|
|
// lazily ported to kotlin
|
|
private class ProgressResponseBody (
|
|
private val responseBody: ResponseBody,
|
|
private val progressCallback: ProgressCallback
|
|
) : ResponseBody() {
|
|
private val bufferedSource: BufferedSource by lazy {
|
|
source(responseBody.source()).buffer()
|
|
}
|
|
|
|
override fun contentType(): MediaType? {
|
|
return responseBody.contentType()
|
|
}
|
|
|
|
override fun contentLength(): Long {
|
|
return responseBody.contentLength()
|
|
}
|
|
|
|
override fun source(): BufferedSource {
|
|
return bufferedSource
|
|
}
|
|
|
|
private fun source(source: Source): Source {
|
|
return object : ForwardingSource(source) {
|
|
var totalBytesRead = 0L
|
|
|
|
override fun read(sink: Buffer, byteCount: Long): Long {
|
|
val bytesRead = super.read(sink, byteCount)
|
|
totalBytesRead += if (bytesRead != -1L) bytesRead else 0
|
|
|
|
progressCallback(
|
|
totalBytesRead,
|
|
responseBody.contentLength()
|
|
)
|
|
|
|
return bytesRead
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// now based on <https://github.com/square/okhttp/blob/master/samples/guide/src/main/java/okhttp3/recipes/UploadProgress.java#L84>
|
|
class ProgressRequestBody(
|
|
private val requestBody: RequestBody,
|
|
private val progressCallback: ProgressCallback
|
|
) : RequestBody() {
|
|
override fun contentType(): MediaType? {
|
|
return requestBody.contentType()
|
|
}
|
|
|
|
override fun contentLength(): Long {
|
|
return requestBody.contentLength()
|
|
}
|
|
|
|
override fun writeTo(sink: BufferedSink) {
|
|
val bufferedSink = this.sink(sink).buffer()
|
|
requestBody.writeTo(bufferedSink)
|
|
bufferedSink.flush()
|
|
}
|
|
|
|
private fun sink(sink: Sink): Sink {
|
|
return object : ForwardingSink(sink) {
|
|
var totalBytesWritten = 0L
|
|
|
|
override fun write(source: Buffer, byteCount: Long) {
|
|
super.write(source, byteCount)
|
|
totalBytesWritten += byteCount
|
|
progressCallback(
|
|
totalBytesWritten,
|
|
contentLength()
|
|
)
|
|
}
|
|
}
|
|
}
|
|
} |