Use Apache5 as fallback HTTP engine (#1752)

* Use Apache5 as fallback HTTP engine

* Fix R8

* Depolymorphism CronetEngine context

* Drop Conscrypt, don't enforce anything

* Limit to 1 io thread count

* Fix H2 support below A10

---------

Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
FooIbarandDude so hot authored and GitHub committed 2024-10-18 15:57:42 +08:00
1 parent 2bb387eeaf
commit c3b60f1eac
9 files changed
+163 -51

No files matched your search

+4 -3
View File
@@ -136,6 +136,9 @@ android {
dex {
useLegacyPackaging = false
}
resources {
excludes += "META-INF/DEPENDENCIES"
}
}
dependenciesInfo.includeInApk = false
@@ -245,7 +248,7 @@ dependencies {
implementation(libs.telephoto.zoomable)
implementation(libs.ktor.client.core)
implementation(libs.ktor.client.apache5)
implementation(libs.bundles.kotlinx.serialization)
@@ -255,8 +258,6 @@ dependencies {
coreLibraryDesugaring(libs.desugar)
implementation(libs.cronet.embedded)
implementation(libs.androidx.profileinstaller)
"baselineProfile"(project(":benchmark"))
@@ -43,7 +43,6 @@ import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
import com.hippo.ehviewer.coil.MapExtraInfoInterceptor
import com.hippo.ehviewer.coil.MergeInterceptor
import com.hippo.ehviewer.coil.QrCodeInterceptor
import com.hippo.ehviewer.cronet.cronetHttpClient
import com.hippo.ehviewer.dailycheck.checkDawn
import com.hippo.ehviewer.dao.SearchDatabase
import com.hippo.ehviewer.download.DownloadManager
@@ -51,6 +50,7 @@ import com.hippo.ehviewer.download.DownloadsFilterMode
import com.hippo.ehviewer.ktbuilder.diskCache
import com.hippo.ehviewer.ktbuilder.imageLoader
import com.hippo.ehviewer.ktor.Cronet
import com.hippo.ehviewer.ktor.configureClient
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
import com.hippo.ehviewer.ui.lockObserver
import com.hippo.ehviewer.ui.screen.detailCache
@@ -64,11 +64,13 @@ import com.hippo.ehviewer.util.OSUtils
import com.hippo.ehviewer.util.isAtLeastO
import com.hippo.ehviewer.util.isAtLeastP
import com.hippo.ehviewer.util.isAtLeastS
import com.hippo.ehviewer.util.isAtLeastSExtension7
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import io.ktor.client.HttpClient
import io.ktor.client.engine.apache5.Apache5
import io.ktor.client.plugins.cookies.HttpCookies
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
@@ -193,12 +195,23 @@ class EhApplication :
companion object {
val ktorClient by lazy {
HttpClient(Cronet) {
engine {
client = cronetHttpClient
if (isAtLeastSExtension7) {
HttpClient(Cronet) {
engine {
configureClient()
}
install(HttpCookies) {
storage = EhCookieStore
}
}
install(HttpCookies) {
storage = EhCookieStore
} else {
HttpClient(Apache5) {
engine {
configureClient()
}
install(HttpCookies) {
storage = EhCookieStore
}
}
}
}
@@ -1,20 +0,0 @@
package com.hippo.ehviewer.cronet
import com.hippo.ehviewer.Settings
import java.io.File
import org.chromium.net.CronetEngine
import splitties.init.appCtx
val cronetHttpClient: CronetEngine = CronetEngine.Builder(appCtx).apply {
enableBrotli(true)
setUserAgent(Settings.userAgent)
// Cache Quic hint only since the real cache mechanism should on Ktor layer
val cache = File(appCtx.cacheDir, "http_cache").apply { mkdirs() }
setStoragePath(cache.path)
enableHttpCache(CronetEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
addQuicHint("e-hentai.org", 443, 443)
addQuicHint("forums.e-hentai.org", 443, 443)
addQuicHint("exhentai.org", 443, 443)
}.build()
@@ -0,0 +1,70 @@
package com.hippo.ehviewer.ktor
import com.hippo.ehviewer.util.isAtLeastQ
import io.ktor.client.engine.apache5.Apache5EngineConfig
import java.net.SocketAddress
import moe.tarsin.kt.unreachable
import org.apache.hc.client5.http.config.ConnectionConfig
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder
import org.apache.hc.core5.concurrent.FutureCallback
import org.apache.hc.core5.http.HttpHost
import org.apache.hc.core5.http.nio.ssl.TlsStrategy
import org.apache.hc.core5.http.ssl.TLS
import org.apache.hc.core5.http2.ssl.ApplicationProtocol
import org.apache.hc.core5.http2.ssl.H2TlsSupport
import org.apache.hc.core5.net.NamedEndpoint
import org.apache.hc.core5.reactor.IOReactorConfig
import org.apache.hc.core5.reactor.ssl.SSLBufferMode
import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer
import org.apache.hc.core5.ssl.SSLContexts
import org.apache.hc.core5.util.Timeout
fun Apache5EngineConfig.configureClient() {
customizeClient {
setConnectionManager(
PoolingAsyncClientConnectionManagerBuilder.create().apply {
setMaxConnPerRoute(2)
setDefaultConnectionConfig(
ConnectionConfig.custom().apply {
setConnectTimeout(Timeout.ofMilliseconds(connectTimeout))
setSocketTimeout(Timeout.ofMilliseconds(socketTimeout.toLong()))
}.build(),
)
setIOReactorConfig(
IOReactorConfig.custom().apply {
setIoThreadCount(1)
}.build(),
)
if (!isAtLeastQ) {
// TLS v1.3 support started from Android 10
val tlsVersions = arrayOf(TLS.V_1_2.id)
val protocols = arrayOf(ApplicationProtocol.HTTP_2.id, ApplicationProtocol.HTTP_1_1.id)
val context = SSLContexts.createSystemDefault()
setTlsStrategy(
object : TlsStrategy {
override fun upgrade(s: TransportSecurityLayer, h: HttpHost?, l: SocketAddress?, r: SocketAddress?, a: Any?, t: Timeout?) = unreachable()
override fun upgrade(session: TransportSecurityLayer, endpoint: NamedEndpoint?, attachment: Any?, timeout: Timeout?, callback: FutureCallback<TransportSecurityLayer?>) {
session.startTls(
context,
endpoint,
SSLBufferMode.STATIC,
{ _, engine ->
val params = engine.sslParameters
params.protocols = tlsVersions
H2TlsSupport.setApplicationProtocols(params, protocols)
H2TlsSupport.setEnableRetransmissions(params, false)
engine.sslParameters = params
},
{ _, _ -> null },
timeout,
callback,
)
}
},
)
}
}.build(),
)
}
}
@@ -1,8 +1,12 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.ktor
import io.ktor.client.engine.HttpClientEngine
import android.os.Build
import androidx.annotation.RequiresExtension
import io.ktor.client.engine.HttpClientEngineFactory
import splitties.init.appCtx
object Cronet : HttpClientEngineFactory<CronetConfig> {
override fun create(block: CronetConfig.() -> Unit): HttpClientEngine = CronetEngine(CronetConfig().apply(block))
override fun create(block: CronetConfig.() -> Unit) = CronetEngine(CronetConfig(appCtx).apply(block))
}
@@ -1,13 +1,13 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.ktor
import android.content.Context
import android.net.http.HttpEngine
import android.os.Build
import androidx.annotation.RequiresExtension
import io.ktor.client.engine.HttpClientEngineConfig
import org.chromium.net.CronetEngine
class CronetConfig : HttpClientEngineConfig() {
var client: CronetEngine? = null
fun config(context: Context, block: CronetEngine.Builder.() -> Unit) {
client = CronetEngine.Builder(context).apply(block).build()
}
class CronetConfig(val context: Context) : HttpClientEngineConfig() {
var config: HttpEngine.Builder.() -> Unit = {}
}
@@ -1,5 +1,15 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.ktor
import android.net.http.HttpEngine
import android.net.http.HttpException
import android.net.http.UploadDataProvider
import android.net.http.UploadDataSink
import android.net.http.UrlRequest
import android.net.http.UrlResponseInfo
import android.os.Build
import androidx.annotation.RequiresExtension
import io.ktor.client.engine.HttpClientEngineBase
import io.ktor.client.engine.callContext
import io.ktor.client.request.HttpRequestData
@@ -27,17 +37,15 @@ import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.consumeEach
import kotlinx.coroutines.job
import kotlinx.coroutines.suspendCancellableCoroutine
import org.chromium.net.CronetException
import org.chromium.net.UrlRequest
import org.chromium.net.UrlResponseInfo
import org.chromium.net.apihelpers.UploadDataProviders
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
// Limit thread to 1 since we are async & non-blocking
override val dispatcher = Dispatchers.Default.limitedParallelism(1)
private val executor = dispatcher.asExecutor()
private val pool = DirectByteBufferPool(32)
private val client = config.client ?: error("Cronet client is not configured")
private val client by lazy {
with(config) { HttpEngine.Builder(context).apply(config).build() }
}
@InternalAPI
override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data)
@@ -48,7 +56,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
) = suspendCancellableCoroutine { continuation ->
val requestTime = GMTDate()
val callback = object : UrlRequest.Callback() {
val callback = object : UrlRequest.Callback {
val chunkChan = Channel<ByteBuffer>()
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
continuation.resume(
@@ -87,16 +95,18 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
chunkChan.close()
}
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) {
if (continuation.isActive) {
continuation.resumeWithException(error)
} else {
chunkChan.close(error)
}
}
override fun onCanceled(request: UrlRequest, info: UrlResponseInfo?) = Unit
}
client.newUrlRequestBuilder(data.url.toString(), callback, executor).apply {
client.newUrlRequestBuilder(data.url.toString(), executor, callback).apply {
setHttpMethod(data.method.value)
data.headers.flattenForEach { key, value -> addHeader(key, value) }
data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) }
@@ -117,7 +127,7 @@ private fun UrlResponseInfo.toHttpResponseData(
statusCode = HttpStatusCode.fromValue(httpStatusCode),
requestTime = requestTime,
headers = headers {
allHeaders.forEach { (key, value) ->
headers.asMap.forEach { (key, value) ->
appendAll(key, value)
}
},
@@ -133,6 +143,17 @@ private fun UrlResponseInfo.toHttpResponseData(
private fun OutgoingContent.toUploadDataProvider() = when (this) {
is OutgoingContent.NoContent -> null
is OutgoingContent.ByteArrayContent -> UploadDataProviders.create(bytes())
is OutgoingContent.ByteArrayContent -> object : UploadDataProvider() {
val buffer = ByteBuffer.wrap(bytes()).slice()
override fun getLength() = buffer.limit().toLong()
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
byteBuffer.put(buffer)
uploadDataSink.onReadSucceeded(false)
}
override fun rewind(uploadDataSink: UploadDataSink) {
buffer.position(0)
uploadDataSink.onRewindSucceeded()
}
}
else -> error("UnsupportedContentType $this")
}
@@ -0,0 +1,25 @@
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
package com.hippo.ehviewer.ktor
import android.net.http.HttpEngine
import android.os.Build
import androidx.annotation.RequiresExtension
import com.hippo.ehviewer.Settings
import java.io.File
fun CronetConfig.configureClient() {
config = {
setEnableBrotli(true)
setUserAgent(Settings.userAgent)
// Cache Quic hint only since the real cache mechanism should on Ktor layer
val cache = File(context.cacheDir, "http_cache").apply { mkdirs() }
setStoragePath(cache.path)
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
addQuicHint("e-hentai.org", 443, 443)
addQuicHint("forums.e-hentai.org", 443, 443)
addQuicHint("exhentai.org", 443, 443)
}
}
+1 -3
View File
@@ -81,8 +81,6 @@ compose-destinations-compiler = { module = "io.github.raamcosta.compose-destinat
material-motion-core = "io.github.fornewid:material-motion-compose-core:2.0.1"
cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31"
desugar = "com.android.tools:desugar_jdk_libs_nio:2.1.2"
diff = "io.github.petertrr:kotlin-multiplatform-diff:0.7.0"
@@ -93,7 +91,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
ktlint = "com.pinterest:ktlint:1.4.0-kotlin-dev-SNAPSHOT"
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-apache5 = { module = "io.ktor:ktor-client-apache5", version.ref = "ktor" }
logcat = "com.squareup.logcat:logcat:0.1"