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:
12 files changed
+143
-51
No files matched your search
@@ -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"))
|
||||
|
||||
|
||||
Vendored
+3
-1
@@ -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,
|
||||
|
||||
@@ -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"]
|
||||
|
||||
|
||||
Reference in new issue
Block a user