diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0008eed26..d52cb090d 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -136,9 +136,6 @@ android { dex { useLegacyPackaging = false } - resources { - excludes += "META-INF/DEPENDENCIES" - } } dependenciesInfo.includeInApk = false @@ -248,7 +245,7 @@ dependencies { implementation(libs.telephoto.zoomable) - implementation(libs.ktor.client.apache5) + implementation(libs.ktor.client.core) implementation(libs.bundles.kotlinx.serialization) @@ -258,6 +255,8 @@ dependencies { coreLibraryDesugaring(libs.desugar) + implementation(libs.cronet.embedded) + implementation(libs.androidx.profileinstaller) "baselineProfile"(project(":benchmark")) diff --git a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt index 6b2b37cc9..27bbcabbf 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/EhApplication.kt @@ -43,6 +43,7 @@ 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 @@ -50,7 +51,6 @@ 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,13 +64,11 @@ 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 @@ -195,23 +193,12 @@ class EhApplication : companion object { val ktorClient by lazy { - if (isAtLeastSExtension7) { - HttpClient(Cronet) { - engine { - configureClient() - } - install(HttpCookies) { - storage = EhCookieStore - } + HttpClient(Cronet) { + engine { + client = cronetHttpClient } - } else { - HttpClient(Apache5) { - engine { - configureClient() - } - install(HttpCookies) { - storage = EhCookieStore - } + install(HttpCookies) { + storage = EhCookieStore } } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt b/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt new file mode 100644 index 000000000..7952919db --- /dev/null +++ b/app/src/main/kotlin/com/hippo/ehviewer/cronet/CronetKt.kt @@ -0,0 +1,20 @@ +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() diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ktor/Apache5Extensions.kt b/app/src/main/kotlin/com/hippo/ehviewer/ktor/Apache5Extensions.kt deleted file mode 100644 index 9cefb1944..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ktor/Apache5Extensions.kt +++ /dev/null @@ -1,70 +0,0 @@ -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) { - 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(), - ) - } -} diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ktor/Cronet.kt b/app/src/main/kotlin/com/hippo/ehviewer/ktor/Cronet.kt index 1616d7c1f..773f6748a 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ktor/Cronet.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ktor/Cronet.kt @@ -1,12 +1,8 @@ -@file:RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) - package com.hippo.ehviewer.ktor -import android.os.Build -import androidx.annotation.RequiresExtension +import io.ktor.client.engine.HttpClientEngine import io.ktor.client.engine.HttpClientEngineFactory -import splitties.init.appCtx object Cronet : HttpClientEngineFactory { - override fun create(block: CronetConfig.() -> Unit) = CronetEngine(CronetConfig(appCtx).apply(block)) + override fun create(block: CronetConfig.() -> Unit): HttpClientEngine = CronetEngine(CronetConfig().apply(block)) } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetConfig.kt b/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetConfig.kt index e95ccf8d1..b9f35892e 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetConfig.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetConfig.kt @@ -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(val context: Context) : HttpClientEngineConfig() { - var config: HttpEngine.Builder.() -> Unit = {} +class CronetConfig : HttpClientEngineConfig() { + var client: CronetEngine? = null + + fun config(context: Context, block: CronetEngine.Builder.() -> Unit) { + client = CronetEngine.Builder(context).apply(block).build() + } } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt b/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt index 8493eacc7..804e338ea 100644 --- a/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt +++ b/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetEngine.kt @@ -1,15 +1,5 @@ -@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 @@ -37,15 +27,17 @@ 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 by lazy { - with(config) { HttpEngine.Builder(context).apply(config).build() } - } + private val client = config.client ?: error("Cronet client is not configured") @InternalAPI override suspend fun execute(data: HttpRequestData) = executeHttpRequest(callContext(), data) @@ -56,7 +48,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() override fun onRedirectReceived(request: UrlRequest, info: UrlResponseInfo, newLocationUrl: String) { continuation.resume( @@ -95,18 +87,16 @@ class CronetEngine(override val config: CronetConfig) : HttpClientEngineBase("Cr chunkChan.close() } - override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: HttpException) { + override fun onFailed(request: UrlRequest, info: UrlResponseInfo?, error: CronetException) { if (continuation.isActive) { continuation.resumeWithException(error) } else { chunkChan.close(error) } } - - override fun onCanceled(request: UrlRequest, info: UrlResponseInfo?) = Unit } - client.newUrlRequestBuilder(data.url.toString(), executor, callback).apply { + client.newUrlRequestBuilder(data.url.toString(), callback, executor).apply { setHttpMethod(data.method.value) data.headers.flattenForEach { key, value -> addHeader(key, value) } data.body.contentType?.let { addHeader(HttpHeaders.ContentType, it.toString()) } @@ -127,7 +117,7 @@ private fun UrlResponseInfo.toHttpResponseData( statusCode = HttpStatusCode.fromValue(httpStatusCode), requestTime = requestTime, headers = headers { - headers.asMap.forEach { (key, value) -> + allHeaders.forEach { (key, value) -> appendAll(key, value) } }, @@ -143,17 +133,6 @@ private fun UrlResponseInfo.toHttpResponseData( private fun OutgoingContent.toUploadDataProvider() = when (this) { is OutgoingContent.NoContent -> null - 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() - } - } + is OutgoingContent.ByteArrayContent -> UploadDataProviders.create(bytes()) else -> error("UnsupportedContentType $this") } diff --git a/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetExtensions.kt b/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetExtensions.kt deleted file mode 100644 index c27585a06..000000000 --- a/app/src/main/kotlin/com/hippo/ehviewer/ktor/CronetExtensions.kt +++ /dev/null @@ -1,25 +0,0 @@ -@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) - } -} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 270e4a20c..8703c9dd1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -81,6 +81,8 @@ 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" @@ -91,7 +93,7 @@ kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version. ktlint = "com.pinterest:ktlint:1.4.0-kotlin-dev-SNAPSHOT" -ktor-client-apache5 = { module = "io.ktor:ktor-client-apache5", version.ref = "ktor" } +ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" } logcat = "com.squareup.logcat:logcat:0.1"