From da15ba8b9ab75cdff76f643d410c9da670b8b438 Mon Sep 17 00:00:00 2001 From: GG Bond Date: Mon, 4 Dec 2023 20:01:42 +0800 Subject: [PATCH] Always use KtorHttpUriFetcher --- .../com/hippo/ehviewer/cronet/CronetKt.kt | 68 ----------------- .../com/hippo/ehviewer/EhApplication.kt | 24 +----- ...ttpUriFetcher.kt => KtorHttpUriFetcher.kt} | 21 +++-- .../hippo/ehviewer/cronet/CronetExtensions.kt | 28 ------- .../com/hippo/ehviewer/cronet/CronetKt.kt | 76 +------------------ 5 files changed, 14 insertions(+), 203 deletions(-) rename app/src/main/kotlin/com/hippo/ehviewer/coil/{CronetHttpUriFetcher.kt => KtorHttpUriFetcher.kt} (71%) diff --git a/app/src/gms/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt b/app/src/gms/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt index c8cd78d01..cc45103c1 100644 --- a/app/src/gms/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt +++ b/app/src/gms/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt @@ -1,23 +1,8 @@ package com.hippo.ehviewer.cronet import com.hippo.ehviewer.Settings -import com.hippo.ehviewer.client.CHROME_ACCEPT -import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE -import com.hippo.ehviewer.client.EhCookieStore -import java.nio.ByteBuffer -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine import okio.Path.Companion.toOkioPath import org.chromium.net.CronetEngine -import org.chromium.net.CronetException -import org.chromium.net.UrlRequest -import org.chromium.net.UrlResponseInfo import splitties.init.appCtx val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply { @@ -33,56 +18,3 @@ val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply { addQuicHint("forums.e-hentai.org", 443, 443) addQuicHint("exhentai.org", 443, 443) }.build() - -class CronetRequest { - lateinit var consumer: (ByteBuffer) -> Unit - lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit - lateinit var request: UrlRequest - lateinit var onError: (Throwable) -> Unit - lateinit var readerCont: Continuation - val callback = object : UrlRequest.Callback() { - override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) { - req.followRedirect() - } - - override fun onResponseStarted(req: UrlRequest, info: UrlResponseInfo) { - onResponse(info) - } - - override fun onReadCompleted(req: UrlRequest, info: UrlResponseInfo, data: ByteBuffer) { - consumer(data) - } - - override fun onSucceeded(req: UrlRequest, info: UrlResponseInfo) { - readerCont.resume(Unit) - } - - override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: CronetException) { - onError(e) - } - } -} - -inline fun cronetRequest(url: String, referer: String? = null, origin: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply { - request = cronetHttpClient.newUrlRequestBuilder(url, callback, cronetHttpClientExecutor).apply { - addHeader("Cookie", EhCookieStore.getCookieHeader(url)) - addHeader("Accept", CHROME_ACCEPT) - addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE) - referer?.let { addHeader("Referer", it) } - origin?.let { addHeader("Origin", it) } - }.apply(conf).build() -} - -suspend inline fun CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R { - contract { - callsInPlace(callback, InvocationKind.EXACTLY_ONCE) - } - return coroutineScope { - suspendCancellableCoroutine { cont -> - onResponse = { launch { cont.resume(callback(it)) } } - cont.invokeOnCancellation { request.cancel() } - onError = { cont.resumeWithException(it) } - request.start() - } - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt index 33a14c3af..d8362889c 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt @@ -32,7 +32,7 @@ import com.hippo.ehviewer.client.EhCookieStore import com.hippo.ehviewer.client.EhTagDatabase import com.hippo.ehviewer.client.data.GalleryDetail import com.hippo.ehviewer.coil.MergeInterceptor -import com.hippo.ehviewer.coil.installCronetHttpUriFetcher +import com.hippo.ehviewer.coil.installKtorHttpUriFetcher import com.hippo.ehviewer.dailycheck.checkDawn import com.hippo.ehviewer.dao.SearchDatabase import com.hippo.ehviewer.download.DownloadManager @@ -60,7 +60,6 @@ import eu.kanade.tachiyomi.util.system.logcat import io.ktor.client.HttpClient import io.ktor.client.engine.okhttp.OkHttp import io.ktor.client.plugins.cookies.HttpCookies -import io.ktor.http.HttpHeaders import kotlinx.coroutines.launch import moe.tarsin.kt.unreachable import okhttp3.AsyncDns @@ -161,12 +160,8 @@ class EhApplication : Application(), ImageLoaderFactory { override fun newImageLoader() = imageLoader { components { - if (isCronetSupported) { - callFactory { unreachable() } - installCronetHttpUriFetcher() - } else { - callFactory(coilClient) - } + callFactory { unreachable() } + installKtorHttpUriFetcher() if (isAtLeastP) { add { result, options, _ -> ImageDecoderDecoder(result.source, options, false) } } @@ -219,19 +214,6 @@ class EhApplication : Application(), ImageLoaderFactory { } } - // Use KtorClient directly when coil 3.0 released - private val coilClient by lazy { - httpClient(baseOkHttpClient) { - addInterceptor { - val req = it.request() - val newReq = req.newBuilder().apply { - addHeader(HttpHeaders.Cookie, EhCookieStore.getCookieHeader(req.url.toString())) - }.build() - it.proceed(newReq) - } - } - } - val galleryDetailCache by lazy { LruCache(25).also { lifecycleScope.launch { diff --git a/app/src/main/kotlin/com/hippo/ehviewer/coil/CronetHttpUriFetcher.kt b/app/src/main/kotlin/com/hippo/ehviewer/coil/KtorHttpUriFetcher.kt similarity index 71% rename from app/src/main/kotlin/com/hippo/ehviewer/coil/CronetHttpUriFetcher.kt rename to app/src/main/kotlin/com/hippo/ehviewer/coil/KtorHttpUriFetcher.kt index 47679d659..3c27726eb 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/coil/CronetHttpUriFetcher.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/coil/KtorHttpUriFetcher.kt @@ -12,25 +12,22 @@ import coil.fetch.FetchResult import coil.fetch.Fetcher import coil.fetch.SourceResult import coil.request.Options -import com.hippo.ehviewer.cronet.copyToChannel -import com.hippo.ehviewer.cronet.cronetRequest -import com.hippo.ehviewer.cronet.execute -import java.io.RandomAccessFile +import com.hippo.ehviewer.client.statement +import com.hippo.ehviewer.util.copyTo +import io.ktor.client.statement.bodyAsChannel /** - * A HttpUriFetcher impl use Cronet for jvmheapless IO + * A HttpUriFetcher impl use Ktor (Will do jvmheapless IO when backing engine is Cronet) * cacheHeader is never respected | recorded since thumb is immutable */ -class CronetHttpUriFetcher(private val data: String, private val options: Options, private val imageLoader: ImageLoader) : Fetcher { +class KtorHttpUriFetcher(private val data: String, private val options: Options, private val imageLoader: ImageLoader) : Fetcher { override suspend fun fetch(): FetchResult { val diskCacheKey = options.diskCacheKey ?: data val diskCache = requireNotNull(imageLoader.diskCache) val snapshot = diskCache.openSnapshot(diskCacheKey) ?: run { - val success = cronetRequest(data).execute { + val success = statement(data).execute { diskCache.suspendEdit(diskCacheKey) { - RandomAccessFile(data.toFile(), "rw").use { - copyToChannel(it.channel) - } + it.bodyAsChannel().copyTo(data.toFile()) } } check(success) @@ -45,9 +42,9 @@ class CronetHttpUriFetcher(private val data: String, private val options: Option } } -fun ComponentRegistry.Builder.installCronetHttpUriFetcher() = add { data: Uri, options, loader -> +fun ComponentRegistry.Builder.installKtorHttpUriFetcher() = add { data: Uri, options, loader -> if (data.scheme != "http" && data.scheme != "https") return@add null - CronetHttpUriFetcher(data.toString(), options, loader) + KtorHttpUriFetcher(data.toString(), options, loader) } private fun getMimeType(url: String) = MimeTypeMap.getSingleton().getMimeTypeFromUrl(url) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetExtensions.kt b/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetExtensions.kt index 3702cb5c0..9a0866939 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetExtensions.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetExtensions.kt @@ -1,39 +1,11 @@ package com.hippo.ehviewer.cronet import io.ktor.utils.io.pool.DirectByteBufferPool -import io.ktor.utils.io.pool.useInstance -import java.nio.ByteBuffer -import java.nio.channels.FileChannel -import kotlin.coroutines.resumeWithException import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor -import kotlinx.coroutines.suspendCancellableCoroutine val pool = DirectByteBufferPool(32) // Limit thread to 1 since we are async & non-blocking val cronetDispatcher = Dispatchers.Default.limitedParallelism(1) val cronetHttpClientExecutor = cronetDispatcher.asExecutor() - -@Suppress("NewApi") -suspend inline fun CronetRequest.awaitBodyFully(crossinline callback: (ByteBuffer) -> Unit) { - return pool.useInstance { buffer -> - suspendCancellableCoroutine { cont -> - consumer = { - check(it === buffer) - buffer.flip() - callback(it) - buffer.clear() - request.read(buffer) - } - onError = { readerCont.resumeWithException(it) } - readerCont = cont - request.read(buffer) - } - } -} - -suspend inline fun CronetRequest.copyToChannel(chan: FileChannel, crossinline listener: ((Int) -> Unit) = {}) = awaitBodyFully { - val bytes = chan.write(it) - listener(bytes) -} diff --git a/app/src/oss/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt b/app/src/oss/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt index 55c53a4f1..8f9b18faf 100644 --- a/app/src/oss/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt +++ b/app/src/oss/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt @@ -3,28 +3,13 @@ package com.hippo.ehviewer.cronet import android.net.http.HttpEngine -import android.net.http.HttpException -import android.net.http.UrlRequest -import android.net.http.UrlResponseInfo -import com.hippo.ehviewer.client.CHROME_ACCEPT -import com.hippo.ehviewer.client.CHROME_ACCEPT_LANGUAGE -import com.hippo.ehviewer.client.CHROME_USER_AGENT -import com.hippo.ehviewer.client.EhCookieStore -import java.nio.ByteBuffer -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.coroutines.Continuation -import kotlin.coroutines.resume -import kotlin.coroutines.resumeWithException -import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.suspendCancellableCoroutine +import com.hippo.ehviewer.Settings import okio.Path.Companion.toOkioPath import splitties.init.appCtx val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply { setEnableBrotli(true) - setUserAgent(CHROME_USER_AGENT) + setUserAgent(Settings.userAgent) // Cache Quic hint only since the real cache mechanism should on Ktor layer val cache = (appCtx.cacheDir.toOkioPath() / "http_cache").toFile().apply { mkdirs() } @@ -35,60 +20,3 @@ val cronetHttpClient: HttpEngine = HttpEngine.Builder(appCtx).apply { addQuicHint("forums.e-hentai.org", 443, 443) addQuicHint("exhentai.org", 443, 443) }.build() - -class CronetRequest { - lateinit var consumer: (ByteBuffer) -> Unit - lateinit var onResponse: CronetRequest.(UrlResponseInfo) -> Unit - lateinit var request: UrlRequest - lateinit var onError: (Throwable) -> Unit - lateinit var readerCont: Continuation - val callback = object : UrlRequest.Callback { - override fun onRedirectReceived(req: UrlRequest, info: UrlResponseInfo, url: String) { - req.followRedirect() - } - - override fun onResponseStarted(req: UrlRequest, info: UrlResponseInfo) { - onResponse(info) - } - - override fun onReadCompleted(req: UrlRequest, info: UrlResponseInfo, data: ByteBuffer) { - consumer(data) - } - - override fun onSucceeded(req: UrlRequest, info: UrlResponseInfo) { - readerCont.resume(Unit) - } - - override fun onFailed(req: UrlRequest, info: UrlResponseInfo?, e: HttpException) { - onError(e) - } - - override fun onCanceled(req: UrlRequest, info: UrlResponseInfo?) { - // No-op - } - } -} - -inline fun cronetRequest(url: String, referer: String? = null, origin: String? = null, conf: UrlRequest.Builder.() -> Unit = {}) = CronetRequest().apply { - request = cronetHttpClient.newUrlRequestBuilder(url, cronetHttpClientExecutor, callback).apply { - addHeader("Cookie", EhCookieStore.getCookieHeader(url)) - addHeader("Accept", CHROME_ACCEPT) - addHeader("Accept-Language", CHROME_ACCEPT_LANGUAGE) - referer?.let { addHeader("Referer", it) } - origin?.let { addHeader("Origin", it) } - }.apply(conf).build() -} - -suspend inline fun CronetRequest.execute(crossinline callback: suspend CronetRequest.(UrlResponseInfo) -> R): R { - contract { - callsInPlace(callback, InvocationKind.EXACTLY_ONCE) - } - return coroutineScope { - suspendCancellableCoroutine { cont -> - onResponse = { launch { cont.resume(callback(it)) } } - cont.invokeOnCancellation { request.cancel() } - onError = { cont.resumeWithException(it) } - request.start() - } - } -}