Skip to content

Commit c3b60f1

Browse files
FooIbarDude so hot
andauthored
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 <[email protected]>
1 parent 2bb387e commit c3b60f1

File tree

9 files changed

+163
-51
lines changed

9 files changed

+163
-51
lines changed

app/build.gradle.kts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,9 @@ android {
136136
dex {
137137
useLegacyPackaging = false
138138
}
139+
resources {
140+
excludes += "META-INF/DEPENDENCIES"
141+
}
139142
}
140143

141144
dependenciesInfo.includeInApk = false
@@ -245,7 +248,7 @@ dependencies {
245248

246249
implementation(libs.telephoto.zoomable)
247250

248-
implementation(libs.ktor.client.core)
251+
implementation(libs.ktor.client.apache5)
249252

250253
implementation(libs.bundles.kotlinx.serialization)
251254

@@ -255,8 +258,6 @@ dependencies {
255258

256259
coreLibraryDesugaring(libs.desugar)
257260

258-
implementation(libs.cronet.embedded)
259-
260261
implementation(libs.androidx.profileinstaller)
261262
"baselineProfile"(project(":benchmark"))
262263

app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,14 +43,14 @@ import com.hippo.ehviewer.coil.HardwareBitmapInterceptor
4343
import com.hippo.ehviewer.coil.MapExtraInfoInterceptor
4444
import com.hippo.ehviewer.coil.MergeInterceptor
4545
import com.hippo.ehviewer.coil.QrCodeInterceptor
46-
import com.hippo.ehviewer.cronet.cronetHttpClient
4746
import com.hippo.ehviewer.dailycheck.checkDawn
4847
import com.hippo.ehviewer.dao.SearchDatabase
4948
import com.hippo.ehviewer.download.DownloadManager
5049
import com.hippo.ehviewer.download.DownloadsFilterMode
5150
import com.hippo.ehviewer.ktbuilder.diskCache
5251
import com.hippo.ehviewer.ktbuilder.imageLoader
5352
import com.hippo.ehviewer.ktor.Cronet
53+
import com.hippo.ehviewer.ktor.configureClient
5454
import com.hippo.ehviewer.ui.keepNoMediaFileStatus
5555
import com.hippo.ehviewer.ui.lockObserver
5656
import com.hippo.ehviewer.ui.screen.detailCache
@@ -64,11 +64,13 @@ import com.hippo.ehviewer.util.OSUtils
6464
import com.hippo.ehviewer.util.isAtLeastO
6565
import com.hippo.ehviewer.util.isAtLeastP
6666
import com.hippo.ehviewer.util.isAtLeastS
67+
import com.hippo.ehviewer.util.isAtLeastSExtension7
6768
import eu.kanade.tachiyomi.util.lang.launchIO
6869
import eu.kanade.tachiyomi.util.lang.launchUI
6970
import eu.kanade.tachiyomi.util.lang.withUIContext
7071
import eu.kanade.tachiyomi.util.system.logcat
7172
import io.ktor.client.HttpClient
73+
import io.ktor.client.engine.apache5.Apache5
7274
import io.ktor.client.plugins.cookies.HttpCookies
7375
import kotlinx.coroutines.Dispatchers
7476
import kotlinx.coroutines.launch
@@ -193,12 +195,23 @@ class EhApplication :
193195

194196
companion object {
195197
val ktorClient by lazy {
196-
HttpClient(Cronet) {
197-
engine {
198-
client = cronetHttpClient
198+
if (isAtLeastSExtension7) {
199+
HttpClient(Cronet) {
200+
engine {
201+
configureClient()
202+
}
203+
install(HttpCookies) {
204+
storage = EhCookieStore
205+
}
199206
}
200-
install(HttpCookies) {
201-
storage = EhCookieStore
207+
} else {
208+
HttpClient(Apache5) {
209+
engine {
210+
configureClient()
211+
}
212+
install(HttpCookies) {
213+
storage = EhCookieStore
214+
}
202215
}
203216
}
204217
}

app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt

Lines changed: 0 additions & 20 deletions
This file was deleted.
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
package com.hippo.ehviewer.ktor
2+
3+
import com.hippo.ehviewer.util.isAtLeastQ
4+
import io.ktor.client.engine.apache5.Apache5EngineConfig
5+
import java.net.SocketAddress
6+
import moe.tarsin.kt.unreachable
7+
import org.apache.hc.client5.http.config.ConnectionConfig
8+
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder
9+
import org.apache.hc.core5.concurrent.FutureCallback
10+
import org.apache.hc.core5.http.HttpHost
11+
import org.apache.hc.core5.http.nio.ssl.TlsStrategy
12+
import org.apache.hc.core5.http.ssl.TLS
13+
import org.apache.hc.core5.http2.ssl.ApplicationProtocol
14+
import org.apache.hc.core5.http2.ssl.H2TlsSupport
15+
import org.apache.hc.core5.net.NamedEndpoint
16+
import org.apache.hc.core5.reactor.IOReactorConfig
17+
import org.apache.hc.core5.reactor.ssl.SSLBufferMode
18+
import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer
19+
import org.apache.hc.core5.ssl.SSLContexts
20+
import org.apache.hc.core5.util.Timeout
21+
22+
fun Apache5EngineConfig.configureClient() {
23+
customizeClient {
24+
setConnectionManager(
25+
PoolingAsyncClientConnectionManagerBuilder.create().apply {
26+
setMaxConnPerRoute(2)
27+
setDefaultConnectionConfig(
28+
ConnectionConfig.custom().apply {
29+
setConnectTimeout(Timeout.ofMilliseconds(connectTimeout))
30+
setSocketTimeout(Timeout.ofMilliseconds(socketTimeout.toLong()))
31+
}.build(),
32+
)
33+
setIOReactorConfig(
34+
IOReactorConfig.custom().apply {
35+
setIoThreadCount(1)
36+
}.build(),
37+
)
38+
if (!isAtLeastQ) {
39+
// TLS v1.3 support started from Android 10
40+
val tlsVersions = arrayOf(TLS.V_1_2.id)
41+
val protocols = arrayOf(ApplicationProtocol.HTTP_2.id, ApplicationProtocol.HTTP_1_1.id)
42+
val context = SSLContexts.createSystemDefault()
43+
setTlsStrategy(
44+
object : TlsStrategy {
45+
override fun upgrade(s: TransportSecurityLayer, h: HttpHost?, l: SocketAddress?, r: SocketAddress?, a: Any?, t: Timeout?) = unreachable()
46+
47+
override fun upgrade(session: TransportSecurityLayer, endpoint: NamedEndpoint?, attachment: Any?, timeout: Timeout?, callback: FutureCallback<TransportSecurityLayer?>) {
48+
session.startTls(
49+
context,
50+
endpoint,
51+
SSLBufferMode.STATIC,
52+
{ _, engine ->
53+
val params = engine.sslParameters
54+
params.protocols = tlsVersions
55+
H2TlsSupport.setApplicationProtocols(params, protocols)
56+
H2TlsSupport.setEnableRetransmissions(params, false)
57+
engine.sslParameters = params
58+
},
59+
{ _, _ -> null },
60+
timeout,
61+
callback,
62+
)
63+
}
64+
},
65+
)
66+
}
67+
}.build(),
68+
)
69+
}
70+
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
1+
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
2+
13
package com.hippo.ehviewer.ktor
24

3-
import io.ktor.client.engine.HttpClientEngine
5+
import android.os.Build
6+
import androidx.annotation.RequiresExtension
47
import io.ktor.client.engine.HttpClientEngineFactory
8+
import splitties.init.appCtx
59

610
object Cronet : HttpClientEngineFactory<CronetConfig> {
7-
override fun create(block: CronetConfig.() -> Unit): HttpClientEngine = CronetEngine(CronetConfig().apply(block))
11+
override fun create(block: CronetConfig.() -> Unit) = CronetEngine(CronetConfig(appCtx).apply(block))
812
}
Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1+
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
2+
13
package com.hippo.ehviewer.ktor
24

35
import android.content.Context
6+
import android.net.http.HttpEngine
7+
import android.os.Build
8+
import androidx.annotation.RequiresExtension
49
import io.ktor.client.engine.HttpClientEngineConfig
5-
import org.chromium.net.CronetEngine
6-
7-
class CronetConfig : HttpClientEngineConfig() {
8-
var client: CronetEngine? = null
910

10-
fun config(context: Context, block: CronetEngine.Builder.() -> Unit) {
11-
client = CronetEngine.Builder(context).apply(block).build()
12-
}
11+
class CronetConfig(val context: Context) : HttpClientEngineConfig() {
12+
var config: HttpEngine.Builder.() -> Unit = {}
1313
}

app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,15 @@
1+
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
2+
13
package com.hippo.ehviewer.ktor
24

5+
import android.net.http.HttpEngine
6+
import android.net.http.HttpException
7+
import android.net.http.UploadDataProvider
8+
import android.net.http.UploadDataSink
9+
import android.net.http.UrlRequest
10+
import android.net.http.UrlResponseInfo
11+
import android.os.Build
12+
import androidx.annotation.RequiresExtension
313
import io.ktor.client.engine.HttpClientEngineBase
414
import io.ktor.client.engine.callContext
515
import io.ktor.client.request.HttpRequestData
@@ -27,17 +37,15 @@ import kotlinx.coroutines.channels.Channel
2737
import kotlinx.coroutines.channels.consumeEach
2838
import kotlinx.coroutines.job
2939
import kotlinx.coroutines.suspendCancellableCoroutine
30-
import org.chromium.net.CronetException
31-
import org.chromium.net.UrlRequest
32-
import org.chromium.net.UrlResponseInfo
33-
import org.chromium.net.apihelpers.UploadDataProviders
3440

3541
class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cronet") {
3642
// Limit thread to 1 since we are async & non-blocking
3743
override val dispatcher = Dispatchers.Default.limitedParallelism(1)
3844
private val executor = dispatcher.asExecutor()
3945
private val pool = DirectByteBufferPool(32)
40-
private val client = config.client ?: error("Cronet client is not configured")
46+
private val client by lazy {
47+
with(config) { HttpEngine.Builder(context).apply(config).build() }
48+
}
4149

4250
@InternalAPI
4351
override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data)
@@ -48,7 +56,7 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
4856
) = suspendCancellableCoroutine { continuation ->
4957
val requestTime = GMTDate()
5058

51-
val callback = object : UrlRequest.Callback() {
59+
val callback = object : UrlRequest.Callback {
5260
val chunkChan = Channel<ByteBuffer>()
5361
override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) {
5462
continuation.resume(
@@ -87,16 +95,18 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr
8795
chunkChan.close()
8896
}
8997

90-
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) {
98+
override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) {
9199
if (continuation.isActive) {
92100
continuation.resumeWithException(error)
93101
} else {
94102
chunkChan.close(error)
95103
}
96104
}
105+
106+
override fun onCanceled(request: UrlRequest, info: UrlResponseInfo?) = Unit
97107
}
98108

99-
client.newUrlRequestBuilder(data.url.toString(), callback, executor).apply {
109+
client.newUrlRequestBuilder(data.url.toString(), executor, callback).apply {
100110
setHttpMethod(data.method.value)
101111
data.headers.flattenForEach { key, value -> addHeader(key, value) }
102112
data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) }
@@ -117,7 +127,7 @@ private fun UrlResponseInfo.toHttpResponseData(
117127
statusCode = HttpStatusCode.fromValue(httpStatusCode),
118128
requestTime = requestTime,
119129
headers = headers {
120-
allHeaders.forEach { (key, value) ->
130+
headers.asMap.forEach { (key, value) ->
121131
appendAll(key, value)
122132
}
123133
},
@@ -133,6 +143,17 @@ private fun UrlResponseInfo.toHttpResponseData(
133143

134144
private fun OutgoingContent.toUploadDataProvider() = when (this) {
135145
is OutgoingContent.NoContent -> null
136-
is OutgoingContent.ByteArrayContent -> UploadDataProviders.create(bytes())
146+
is OutgoingContent.ByteArrayContent -> object : UploadDataProvider() {
147+
val buffer = ByteBuffer.wrap(bytes()).slice()
148+
override fun getLength() = buffer.limit().toLong()
149+
override fun read(uploadDataSink: UploadDataSink, byteBuffer: ByteBuffer) {
150+
byteBuffer.put(buffer)
151+
uploadDataSink.onReadSucceeded(false)
152+
}
153+
override fun rewind(uploadDataSink: UploadDataSink) {
154+
buffer.position(0)
155+
uploadDataSink.onRewindSucceeded()
156+
}
157+
}
137158
else -> error("UnsupportedContentType $this")
138159
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7)
2+
3+
package com.hippo.ehviewer.ktor
4+
5+
import android.net.http.HttpEngine
6+
import android.os.Build
7+
import androidx.annotation.RequiresExtension
8+
import com.hippo.ehviewer.Settings
9+
import java.io.File
10+
11+
fun CronetConfig.configureClient() {
12+
config = {
13+
setEnableBrotli(true)
14+
setUserAgent(Settings.userAgent)
15+
16+
// Cache Quic hint only since the real cache mechanism should on Ktor layer
17+
val cache = File(context.cacheDir, "http_cache").apply { mkdirs() }
18+
setStoragePath(cache.path)
19+
setEnableHttpCache(HttpEngine.Builder.HTTP_CACHE_DISK_NO_HTTP, 4096)
20+
21+
addQuicHint("e-hentai.org", 443, 443)
22+
addQuicHint("forums.e-hentai.org", 443, 443)
23+
addQuicHint("exhentai.org", 443, 443)
24+
}
25+
}

gradle/libs.versions.toml

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -81,8 +81,6 @@ compose-destinations-compiler = { module = "io.github.raamcosta.compose-destinat
8181

8282
material-motion-core = "io.github.fornewid:material-motion-compose-core:2.0.1"
8383

84-
cronet-embedded = "org.chromium.net:cronet-embedded:119.6045.31"
85-
8684
desugar = "com.android.tools:desugar_jdk_libs_nio:2.1.2"
8785

8886
diff = "io.github.petertrr:kotlin-multiplatform-diff:0.7.0"
@@ -93,7 +91,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.
9391

9492
ktlint = "com.pinterest:ktlint:1.4.0-kotlin-dev-SNAPSHOT"
9593

96-
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
94+
ktor-client-apache5 = { module = "io.ktor:ktor-client-apache5", version.ref = "ktor" }
9795

9896
logcat = "com.squareup.logcat:logcat:0.1"
9997

0 commit comments

Comments
 (0)