From 110fb9fb591b0b66c70f9111cf86233d84f8c30f Mon Sep 17 00:00:00 2001 From: tarsin Date: Sun, 18 Jun 2023 16:13:54 +0800 Subject: [PATCH] Refine HttpEngineCoroutinizer --- .../hippo/ehviewer/client/EhRequestBuilder.kt | 4 +- .../spider/Android14CoroutineHttpEngine.kt | 35 +++++++--- .../com/hippo/ehviewer/spider/SpiderDen.kt | 69 ++++++++++--------- 3 files changed, 61 insertions(+), 47 deletions(-) diff --git a/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.kt b/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.kt index 9f2534c15..4b14a8479 100644 --- a/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.kt +++ b/app/src/main/java/com/hippo/ehviewer/client/EhRequestBuilder.kt @@ -63,7 +63,7 @@ const val TAG_CALL = "CancellableCallReadingScope" // Since Okio is not suspendable/cancellable, also okhttp, [executeAsync] only make [Request -> Response] cancellable, reading a Response is still not cancellable // Considering interrupting is not safe and performant, See https://github.com/Kotlin/kotlinx.coroutines/issues/3551#issuecomment-1353245978, we have such [usingCancellable] // TODO: Remove logging after this function stable -suspend inline fun Call.usingCancellable(crossinline block: Response.() -> R): R = executeAsync().use { +suspend inline fun Call.usingCancellable(crossinline block: suspend Response.() -> R): R = executeAsync().use { val call = this Log.d(TAG_CALL, "Got response of call$call") coroutineScope { @@ -101,7 +101,7 @@ suspend inline fun Call.usingCancellable(crossinline block: Response.() -> R } suspend inline fun Request.execute(block: Response.() -> R) = okHttpClient.newCall(this).executeAsync().use(block) -suspend inline fun Request.executeNonCache(crossinline block: Response.() -> R) = nonCacheOkHttpClient.newCall(this).usingCancellable(block) +suspend inline fun Request.executeNonCache(crossinline block: suspend Response.() -> R) = nonCacheOkHttpClient.newCall(this).usingCancellable(block) const val CHROME_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36" const val CHROME_ACCEPT = "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9" diff --git a/app/src/main/java/com/hippo/ehviewer/spider/Android14CoroutineHttpEngine.kt b/app/src/main/java/com/hippo/ehviewer/spider/Android14CoroutineHttpEngine.kt index b6bd7f274..ebeede1de 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/Android14CoroutineHttpEngine.kt +++ b/app/src/main/java/com/hippo/ehviewer/spider/Android14CoroutineHttpEngine.kt @@ -9,6 +9,8 @@ import android.net.http.UrlResponseInfo import android.os.Build import androidx.annotation.RequiresExtension import com.hippo.ehviewer.EhApplication +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.launch import kotlinx.coroutines.suspendCancellableCoroutine import splitties.init.appCtx import java.nio.ByteBuffer @@ -29,7 +31,8 @@ class CronetRequest { lateinit var mConsumer: (UrlResponseInfo, ByteBuffer) -> Unit lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit lateinit var request: UrlRequest - lateinit var mCont: Continuation + lateinit var daemonCont: Continuation + lateinit var readerCont: Continuation val callback = object : UrlRequest.Callback { override fun onRedirectReceived(p0: UrlRequest, p1: UrlResponseInfo, p2: String) { // No-op @@ -37,7 +40,6 @@ class CronetRequest { override fun onResponseStarted(p0: UrlRequest, p1: UrlResponseInfo) { onResponse(p1) - request.read(buffer) } override fun onReadCompleted(p0: UrlRequest, p1: UrlResponseInfo, p2: ByteBuffer) { @@ -52,11 +54,12 @@ class CronetRequest { override fun onSucceeded(p0: UrlRequest, p1: UrlResponseInfo) { val length = p1.receivedByteCount // TODO: validate body length - mCont.resume(true) + readerCont.resume(Unit) + daemonCont.resume(true) } override fun onFailed(p0: UrlRequest, p1: UrlResponseInfo?, p2: HttpException) { - mCont.resumeWithException(p2) + daemonCont.resumeWithException(p2) } override fun onCanceled(p0: UrlRequest, p1: UrlResponseInfo?) { @@ -72,15 +75,25 @@ inline fun cronetRequest(url: String, conf: UrlRequest.Builder.() -> Unit) = Cro request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply(conf).build() } -infix fun CronetRequest.consumeBody(callback: (UrlResponseInfo, ByteBuffer) -> Unit) = apply { +suspend infix fun CronetRequest.consumeBody(callback: (UrlResponseInfo, ByteBuffer) -> Unit) = apply { mConsumer = callback + suspendCancellableCoroutine { cont -> + readerCont = cont + request.read(buffer) + } } -suspend inline infix fun CronetRequest.execute(noinline callback: CronetRequest.(UrlResponseInfo) -> Unit): Boolean { - onResponse = callback - return suspendCancellableCoroutine { cont -> - cont.invokeOnCancellation { request.cancel() } - mCont = cont - request.start() +suspend inline infix fun CronetRequest.execute(noinline callback: suspend CronetRequest.(UrlResponseInfo) -> Unit): Boolean { + return coroutineScope { + onResponse = { + launch { + callback(it) + } + } + suspendCancellableCoroutine { cont -> + cont.invokeOnCancellation { request.cancel() } + daemonCont = cont + request.start() + } } } diff --git a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.kt b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.kt index a1b39dd9d..4d8a986d8 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.kt @@ -26,7 +26,6 @@ import com.hippo.ehviewer.client.data.GalleryInfo import com.hippo.ehviewer.client.ehRequest import com.hippo.ehviewer.client.executeNonCache import com.hippo.ehviewer.client.getImageKey -import com.hippo.ehviewer.coil.edit import com.hippo.ehviewer.coil.read import com.hippo.ehviewer.coil.suspendEdit import com.hippo.ehviewer.download.downloadLocation @@ -43,10 +42,7 @@ import okio.Buffer import okio.ForwardingSink import okio.sink import java.io.IOException -import java.nio.channels.FileChannel -import java.nio.file.StandardOpenOption import java.util.Locale -import kotlin.io.path.deleteIfExists import kotlin.io.path.readText import com.hippo.ehviewer.EhApplication.Companion.imageCache as sCache @@ -137,24 +133,21 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) { notifyProgress: (Long, Long, Int) -> Unit, ): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7) { - val key = getImageKey(mGid, index) - sCache.suspendEdit(key) { - val path = data.toNioPath() - path.deleteIfExists() - @Suppress("BlockingMethodInNonBlockingContext") - FileChannel.open(path, StandardOpenOption.APPEND, StandardOpenOption.CREATE_NEW).use { chan -> - cronetRequest(url) { - referer?.let { addHeader("Referer", it) } - } execute { - val headers = it.headers.asMap - val type = headers["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg" - val length = headers["Content-Length"]!!.first().toLong() - metadata.toFile().writeText(type) + cronetRequest(url) { + referer?.let { addHeader("Referer", it) } + } execute { info -> + val headers = info.headers.asMap + val type = headers["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg" + val length = headers["Content-Length"]!!.first().toLong() + saveResponseMeta(index, type, length) { file -> + file.openOutputStream().use { + val chan = it.channel consumeBody { info, buffer -> val read = chan.write(buffer) notifyProgress(length, info.receivedByteCount, read) } } + file.length() } } } else { @@ -168,7 +161,30 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) { } } - private fun saveFromHttpResponse(index: Int, response: Response, notifyProgress: (Long, Long, Int) -> Unit): Boolean { + private suspend fun saveResponseMeta(index: Int, ext: String, length: Long, fops: suspend (UniFile) -> Long): Boolean { + suspend fun realFops(f: UniFile) = fops(f).apply { + if (ext.lowercase() == "gif") { + f.openFileDescriptor("rw").use { rewriteGifSource2(it.fd) } + } + } + findDownloadFileForIndex(index, ext)?.run { + return realFops(this) == length + } + + // Read Mode, allow save to cache + if (mode == SpiderQueen.MODE_READ) { + val key = getImageKey(mGid, index) + var received = 0L + sCache.suspendEdit(key) { + metadata.toFile().writeText(ext) + received = realFops(UniFile.fromFile(data.toFile())!!) + } + return received == length + } + return false + } + + private suspend fun saveFromHttpResponse(index: Int, response: Response, notifyProgress: (Long, Long, Int) -> Unit): Boolean { val contentType = response.body.contentType() val extension = contentType?.subtype ?: "jpg" val length = response.body.contentLength() @@ -188,22 +204,7 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) { } } - findDownloadFileForIndex(index, extension)?.run { - return doSave(this) == length - } - - // Read Mode, allow save to cache - if (mode == SpiderQueen.MODE_READ) { - val key = getImageKey(mGid, index) - var received: Long = 0 - sCache.edit(key) { - metadata.toFile().writeText(extension) - received = doSave(UniFile.fromFile(data.toFile())!!) - } - return received == length - } - - return false + return saveResponseMeta(index, extension, length) { doSave(it) } } fun saveToUniFile(index: Int, file: UniFile): Boolean {