Partially Reland "Use Apache5 as fallback HTTP engine" (#1755)

* Use Apache5 as fallback HTTP engine

* Fix R8

* Depolymorphism CronetEngine context

* Conditionally insert conscrypt

* Limit IO thread cnt to 1

* Don't package compose destination as well

* Refine

* Add an option for debugging HTTP engines

---------

Co-authored-by: FooIbar <118464521+FooIbar@users.noreply.github.com>
Co-authored-by: Dude so hot <djohn@fbi.gov>
This commit is contained in:
authored and GitHub committed 2024-10-18 10:18:46 +00:00
1 parent ba2c69690f
commit b796dbcde3
12 files changed
+143 -51

No files matched your search

+8 -3
View File
@@ -136,6 +136,13 @@ android {
dex {
useLegacyPackaging = false
}
resources {
excludes += listOf(
"META-INF/DEPENDENCIES",
"com", // Compose Destination
"org", // Apache 5 HC version info
)
}
}
dependenciesInfo.includeInApk = false
@@ -245,7 +252,7 @@ dependencies {
implementation(libs.telephoto.zoomable)
implementation(libs.ktor.client.core)
implementation(libs.bundles.http.client)
implementation(libs.bundles.kotlinx.serialization)
@@ -255,8 +262,6 @@ dependencies {
coreLibraryDesugaring(libs.desugar)
implementation(libs.cronet.embedded)
implementation(libs.androidx.profileinstaller)
"baselineProfile"(project(":benchmark"))
+3 -1
View File
@@ -14,7 +14,9 @@
# Ktor logger
-dontwarn org.slf4j.impl.StaticLoggerBinder
-dontwarn org.conscrypt.Conscrypt
# Conscrypt
-dontwarn com.android.org.conscrypt.SSLParametersImpl
-dontwarn org.apache.harmony.xnet.provider.jsse.SSLParametersImpl
# https://issuetracker.google.com/222232895
-dontwarn androidx.window.extensions.**
@@ -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 && Settings.enableCronet) {
HttpClient(Cronet) {
engine {
configureClient()
}
install(HttpCookies) {
storage = EhCookieStore
}
}
install(HttpCookies) {
storage = EhCookieStore
} else {
HttpClient(Apache5) {
engine {
configureClient()
}
install(HttpCookies) {
storage = EhCookieStore
}
}
}
}
@@ -164,6 +164,7 @@ object Settings : DataStorePreferences(null) {
var downloadOriginImage by boolPref("download_origin_image", false)
var saveAsCbz by boolPref("save_as_cbz", false)
var archiveMetadata by boolPref("archive_metadata", true)
var enableCronet by boolPref("enable_cronet", true)
var desktopSite by boolPref("desktop_site", false)
var recentFavCat by intPref("recent_fav_cat", FavListUrlBuilder.FAV_CAT_LOCAL)
var defaultFavSlot by intPref("default_favorite_slot", -2)
@@ -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,34 @@
package com.hippo.ehviewer.ktor
import com.hippo.ehviewer.util.isAtLeastQ
import io.ktor.client.engine.apache5.Apache5EngineConfig
import java.security.Security
import org.apache.hc.client5.http.config.ConnectionConfig
import org.apache.hc.client5.http.impl.nio.PoolingAsyncClientConnectionManagerBuilder
import org.apache.hc.core5.reactor.IOReactorConfig
import org.apache.hc.core5.util.Timeout
import org.conscrypt.Conscrypt
fun Apache5EngineConfig.configureClient() {
if (!isAtLeastQ) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
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(),
)
}.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)
}
}
@@ -176,6 +176,12 @@ fun AdvancedScreen(navigator: DestinationsNavigator) {
useSelectedAsSummary = true,
entries = languages,
)
if (BuildConfig.DEBUG) {
SwitchPreference(
title = "Enable Cronet",
value = Settings::enableCronet,
)
}
if (isAtLeastO) {
IntSliderPreference(
maxValue = 16384,
+3 -2
View File
@@ -81,7 +81,7 @@ 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"
conscrypt = "org.conscrypt:conscrypt-android:2.5.3"
desugar = "com.android.tools:desugar_jdk_libs_nio:2.1.2"
@@ -93,7 +93,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"
@@ -122,6 +122,7 @@ androidx-profileinstaller = { module = "androidx.profileinstaller:profileinstall
[bundles]
coil = ["coil-compose", "coil-gif", "coil-network"]
compose = ["compose-foundation", "compose-material-icons-extended", "compose-material3", "compose-material3-adaptive", "compose-ui-util"]
http-client = ["conscrypt", "ktor-client-apache5"]
kotlinx-serialization = ["serialization-cbor", "serialization-json", "serialization-json-okio", "serialization-xml"]
splitties = ["splitties-appctx", "splitties-arch-room", "splitties-preferences", "splitties-systemservices"]