From 0d10899417150828ae99815aeef05b1991240ad1 Mon Sep 17 00:00:00 2001 From: tarsin Date: Sun, 18 Jun 2023 15:09:50 +0800 Subject: [PATCH] Initial support Android14 HttpEngine --- .../hippo/ehviewer/coil/DiskCacheScoped.kt | 13 +++ .../spider/Android14CoroutineHttpEngine.kt | 85 +++++++++++++++++++ .../com/hippo/ehviewer/spider/SpiderDen.kt | 40 +++++++-- 3 files changed, 131 insertions(+), 7 deletions(-) create mode 100644 app/src/main/java/com/hippo/ehviewer/spider/Android14CoroutineHttpEngine.kt diff --git a/app/src/main/java/com/hippo/ehviewer/coil/DiskCacheScoped.kt b/app/src/main/java/com/hippo/ehviewer/coil/DiskCacheScoped.kt index 8f08b41f1..b357a3dff 100644 --- a/app/src/main/java/com/hippo/ehviewer/coil/DiskCacheScoped.kt +++ b/app/src/main/java/com/hippo/ehviewer/coil/DiskCacheScoped.kt @@ -15,4 +15,17 @@ inline fun DiskCache.edit(key: String, crossinline block: DiskCache.Editor.() -> return true } +suspend inline fun DiskCache.suspendEdit(key: String, crossinline block: suspend DiskCache.Editor.() -> Unit): Boolean { + val editor = openEditor(key) ?: return false + editor.runCatching { + block(this) + }.onFailure { + editor.abort() + throw it + }.onSuccess { + editor.commit() + } + return true +} + inline fun DiskCache.read(key: String, block: DiskCache.Snapshot.() -> T): T? = openSnapshot(key)?.use(block) diff --git a/app/src/main/java/com/hippo/ehviewer/spider/Android14CoroutineHttpEngine.kt b/app/src/main/java/com/hippo/ehviewer/spider/Android14CoroutineHttpEngine.kt new file mode 100644 index 000000000..7916ec244 --- /dev/null +++ b/app/src/main/java/com/hippo/ehviewer/spider/Android14CoroutineHttpEngine.kt @@ -0,0 +1,85 @@ +@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) + +package com.hippo.ehviewer.spider + +import android.net.http.HttpEngine +import android.net.http.HttpException +import android.net.http.UrlRequest +import android.net.http.UrlResponseInfo +import android.os.Build +import androidx.annotation.RequiresExtension +import com.hippo.ehviewer.EhApplication +import kotlinx.coroutines.suspendCancellableCoroutine +import splitties.init.appCtx +import java.nio.ByteBuffer +import kotlin.coroutines.Continuation +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +val cronetHttpClient = HttpEngine.Builder(appCtx).apply { + setEnableHttp2(true) + setEnableBrotli(true) + setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISABLED, 0) + setEnableQuic(true) +}.build() + +val cronetHttpClientExecutor = EhApplication.nonCacheOkHttpClient.dispatcher.executorService + +class CronetRequest { + lateinit var mConsumer: (UrlResponseInfo, ByteBuffer) -> Unit + lateinit var onResponse: (UrlResponseInfo) -> Unit + lateinit var request: UrlRequest + lateinit var mCont: Continuation + val callback = object : UrlRequest.Callback { + override fun onRedirectReceived(p0: UrlRequest, p1: UrlResponseInfo, p2: String) { + // No-op + } + + override fun onResponseStarted(p0: UrlRequest, p1: UrlResponseInfo) { + request.read(buffer) + } + + override fun onReadCompleted(p0: UrlRequest, p1: UrlResponseInfo, p2: ByteBuffer) { + p2.flip() + mConsumer(p1, p2) + cronetHttpClientExecutor.execute { + buffer.flip() + request.read(buffer) + } + } + + override fun onSucceeded(p0: UrlRequest, p1: UrlResponseInfo) { + val length = p1.receivedByteCount + // TODO: validate body length + mCont.resume(true) + } + + override fun onFailed(p0: UrlRequest, p1: UrlResponseInfo?, p2: HttpException) { + mCont.resumeWithException(p2) + } + + override fun onCanceled(p0: UrlRequest, p1: UrlResponseInfo?) { + // No-op + } + } + + // TODO: Pool it + val buffer: ByteBuffer = ByteBuffer.allocateDirect(4096) +} + +inline fun cronetRequest(url: String, conf: UrlRequest.Builder.() -> Unit) = CronetRequest().apply { + request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply(conf).build() +} + +infix fun CronetRequest.onResponse(callback: (UrlResponseInfo) -> Unit) = apply { + onResponse = callback +} + +suspend inline infix fun CronetRequest.execute(noinline consumer: (UrlResponseInfo, ByteBuffer) -> Unit): Boolean { + mConsumer = consumer + return suspendCancellableCoroutine { cont -> + cont.invokeOnCancellation { request.cancel() } + mCont = 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 3d1866ae6..b74023623 100644 --- a/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.kt +++ b/app/src/main/java/com/hippo/ehviewer/spider/SpiderDen.kt @@ -16,8 +16,10 @@ package com.hippo.ehviewer.spider import android.graphics.ImageDecoder +import android.os.Build import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor.MODE_READ_WRITE +import android.os.ext.SdkExtensions import com.hippo.ehviewer.EhDB import com.hippo.ehviewer.client.EhUtils.getSuitableTitle import com.hippo.ehviewer.client.data.GalleryInfo @@ -26,6 +28,7 @@ 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 import com.hippo.ehviewer.gallery.SUPPORT_IMAGE_EXTENSIONS import com.hippo.ehviewer.image.Image.CloseableSource @@ -34,12 +37,16 @@ import com.hippo.ehviewer.util.sendTo import com.hippo.ehviewer.yorozuya.FileUtils import com.hippo.unifile.UniFile import com.hippo.unifile.openOutputStream +import okhttp3.MediaType.Companion.toMediaType import okhttp3.Response 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 @@ -129,13 +136,32 @@ class SpiderDen(private val mGalleryInfo: GalleryInfo) { referer: String?, notifyProgress: (Long, Long, Int) -> Unit, ): Boolean { - // TODO: Use HttpEngine[https://developer.android.com/reference/android/net/http/HttpEngine] directly here if available - // Since we don't want unnecessary copy between jvm heap & native heap - return ehRequest(url, referer).executeNonCache { - if (code >= 400) { - false - } else { - saveFromHttpResponse(index, this, notifyProgress) + 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 { + cronetRequest(url) { + referer?.let { addHeader("Referer", it) } + } onResponse { + val type = it.headers.asMap["Content-Type"]?.first()?.toMediaType()?.subtype ?: "jpg" + metadata.toFile().writeText(type) + } execute { info, buffer -> + val read = it.write(buffer) + val length = info.headers.asMap["Content-Length"]!!.first().toLong() + notifyProgress(length, info.receivedByteCount, read) + } + } + } + } else { + ehRequest(url, referer).executeNonCache { + if (code >= 400) { + false + } else { + saveFromHttpResponse(index, this, notifyProgress) + } } } }