From 236a23f65b5919ec812f53c92eb743d9baa880de Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Tue, 30 Sep 2025 07:53:48 +0000 Subject: [PATCH] =?UTF-8?q?=F0=9F=93=9D=20Add=20docstrings=20to=20`android?= =?UTF-8?q?14`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Docstrings generation was requested by @yschimke. * https://github.com/ktorio/ktor/pull/4013#issuecomment-3350481531 The following files were modified: * `ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/Android14URLConnectionFactory.kt` * `ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidClientEngine.kt` * `ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidURLConnectionUtils.kt` * `ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/URLConnectionFactory.kt` --- .../android/Android14URLConnectionFactory.kt | 42 +++++++++++++++++ .../engine/android/AndroidClientEngine.kt | 44 ++++++++++++++---- .../android/AndroidURLConnectionUtils.kt | 20 ++++++++- .../engine/android/URLConnectionFactory.kt | 45 +++++++++++++++++++ 4 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/Android14URLConnectionFactory.kt create mode 100644 ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/URLConnectionFactory.kt diff --git a/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/Android14URLConnectionFactory.kt b/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/Android14URLConnectionFactory.kt new file mode 100644 index 00000000000..1d848ef6369 --- /dev/null +++ b/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/Android14URLConnectionFactory.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.client.engine.android + +import android.net.http.HttpEngine +import java.net.HttpURLConnection +import java.net.URI + +// @RequiresExtension(extension = Build.VERSION_CODES.S, version = 7) +internal class AndroidNetHttpEngineFactory(private val config: AndroidEngineConfig) : URLConnectionFactory { + private val engine by lazy { buildEngine() } + + /** + * Builds an HttpEngine configured with the Android application context from the provided config. + * + * Retrieves the configuration's context (must be non-null) and uses its applicationContext to construct + * an HttpEngine with the settings from `config.httpEngineConfig`. + * + * @return A configured `HttpEngine` instance. + * @throws IllegalArgumentException if `config.context` is null. + */ + private fun buildEngine(): HttpEngine { + val ctx = requireNotNull(config.context) { + "AndroidEngineConfig.context must be set when using HttpEngine; prefer applicationContext." + }.applicationContext + return HttpEngine.Builder(ctx) + .apply(config.httpEngineConfig) + .build() + } + + /** + * Create an HttpURLConnection for the given URL string using the configured HttpEngine. + * + * @param urlString The URL to open, as a string. + * @return An `HttpURLConnection` connected to the specified URL. + */ + override operator fun invoke(urlString: String): HttpURLConnection { + return engine.openConnection(URI.create(urlString).toURL()) as HttpURLConnection + } +} diff --git a/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidClientEngine.kt b/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidClientEngine.kt index bd550faa2f2..193506cf3ef 100644 --- a/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidClientEngine.kt +++ b/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidClientEngine.kt @@ -34,6 +34,27 @@ public class AndroidClientEngine(override val config: AndroidEngineConfig) : Htt override val supportedCapabilities: Set> = setOf(HttpTimeoutCapability, SSECapability) + private val urlFactory = if (config.httpEngineDisabled || + !isHttpEngineAvailable() || + config.proxy != null || + config.context == null + ) { + URLConnectionFactory.StandardURLConnectionFactory(config) + } else { + AndroidNetHttpEngineFactory(config) + } + + /** + * Executes the given HTTP request and returns the resulting response. + * + * The request described by `data` is sent over a configured HttpURLConnection; the request body + * (if any) is written to the connection, and the response status, headers, protocol version, and + * body (or an adapted representation when a ResponseAdapterAttribute is present) are returned. + * + * @param data The HTTP request data to execute (URL, method, headers, body, and attributes). + * @return The HttpResponseData containing the response status, headers, protocol version, body, and call context. + * @throws IllegalStateException If the request method does not allow a body but a non-empty body is provided. + */ override suspend fun execute(data: HttpRequestData): HttpResponseData { val callContext = callContext() @@ -44,12 +65,13 @@ public class AndroidClientEngine(override val config: AndroidEngineConfig) : Htt val contentLength: Long? = data.headers[HttpHeaders.ContentLength]?.toLong() ?: outgoingContent.contentLength - val connection: HttpURLConnection = getProxyAwareConnection(url).apply { + val connection: HttpURLConnection = urlFactory(url).apply { connectTimeout = config.connectTimeout readTimeout = config.socketTimeout setupTimeoutAttributes(data) + // TODO document not active on Android 14 if (this is HttpsURLConnection) { config.sslManager(this) } @@ -91,7 +113,7 @@ public class AndroidClientEngine(override val config: AndroidEngineConfig) : Htt .mapKeys { it.key?.lowercase(Locale.getDefault()) ?: "" } .filter { it.key.isNotBlank() } - val version: HttpProtocolVersion = HttpProtocolVersion.HTTP_1_1 + val version: HttpProtocolVersion = urlFactory.protocolFromRequest(connection) val responseHeaders = HeadersImpl(headerFields) val responseBody: Any = data.attributes.getOrNull(ResponseAdapterAttributeKey) @@ -101,14 +123,20 @@ public class AndroidClientEngine(override val config: AndroidEngineConfig) : Htt HttpResponseData(statusCode, requestTime, responseHeaders, version, responseBody, callContext) } } - - private fun getProxyAwareConnection(urlString: String): HttpURLConnection { - val url = URL(urlString) - val connection: URLConnection = config.proxy?.let { url.openConnection(it) } ?: url.openConnection() - return connection as HttpURLConnection - } } +/** + * Writes this [OutgoingContent] into the provided [stream], honoring the supplied [callContext]. + * + * Supports ByteArrayContent, ReadChannelContent, WriteChannelContent, NoContent and ContentWrapper; + * for WriteChannelContent a writer is launched with [callContext] and its resulting channel is copied to the stream. + * The stream is closed after writing completes. + * + * @param stream The destination [OutputStream] to write the content into; it will be closed when writing finishes. + * @param callContext The coroutine context used when producing content for `WriteChannelContent`. + * + * @throws UnsupportedContentTypeException if this content is a [OutgoingContent.ProtocolUpgrade]. + */ @OptIn(DelicateCoroutinesApi::class) internal suspend fun OutgoingContent.writeTo( stream: OutputStream, diff --git a/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidURLConnectionUtils.kt b/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidURLConnectionUtils.kt index 7c83aee467d..574508ddda5 100644 --- a/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidURLConnectionUtils.kt +++ b/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/AndroidURLConnectionUtils.kt @@ -4,6 +4,8 @@ package io.ktor.client.engine.android +import android.os.* +import android.os.ext.SdkExtensions import io.ktor.client.network.sockets.* import io.ktor.client.plugins.* import io.ktor.client.request.* @@ -85,7 +87,21 @@ internal fun HttpURLConnection.content(status: Int, callContext: CoroutineContex } /** - * Checks the exception and identifies timeout exception by it. - */ + * Determines whether this throwable represents a network timeout. + * + * @return `true` if the throwable is a `SocketTimeoutException` or a `ConnectException` whose message contains "timed out", `false` otherwise. + */ private fun Throwable.isTimeoutException(): Boolean = this is java.net.SocketTimeoutException || (this is ConnectException && message?.contains("timed out") ?: false) + +internal val isAndroid: Boolean = "Dalvik" == System.getProperty("java.vm.name") + +/** + * Determines whether the Android HTTP engine is supported on the current runtime. + * + * @return `true` if the runtime VM is Dalvik, the OS SDK level is at least 30, and the S platform + * extensions version is at least 7; `false` otherwise. + */ + internal fun isHttpEngineAvailable() = isAndroid && + Build.VERSION.SDK_INT >= 30 && + SdkExtensions.getExtensionVersion(Build.VERSION_CODES.S) >= 7 diff --git a/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/URLConnectionFactory.kt b/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/URLConnectionFactory.kt new file mode 100644 index 00000000000..7fb881c1711 --- /dev/null +++ b/ktor-client/ktor-client-android/jvm/src/io/ktor/client/engine/android/URLConnectionFactory.kt @@ -0,0 +1,45 @@ +/* + * Copyright 2014-2025 JetBrains s.r.o and contributors. Use of this source code is governed by the Apache 2.0 license. + */ + +package io.ktor.client.engine.android + +import io.ktor.http.* +import io.ktor.util.* +import java.net.* + +@InternalAPI +internal interface URLConnectionFactory { + /** + * Open an HttpURLConnection for the specified URL string, using the factory's configured proxy if present. + * + * @param urlString The URL to connect to, expressed as a string. + * @return An HttpURLConnection for the specified URL. + */ +operator fun invoke(urlString: String): HttpURLConnection + /** + * Determine the HTTP protocol version associated with the given URL connection. + * + * @param connection The HttpURLConnection to inspect. + * @return The HTTP protocol version for the connection. The default implementation returns `HttpProtocolVersion.HTTP_1_1`. + */ + fun protocolFromRequest(connection: HttpURLConnection): HttpProtocolVersion { + // This is not exposed with HttpEngine + return HttpProtocolVersion.HTTP_1_1 + } + + @InternalAPI + class StandardURLConnectionFactory(val config: AndroidEngineConfig) : URLConnectionFactory { + /** + * Create an HttpURLConnection for the given URL string using the engine's proxy when configured. + * + * @param urlString The URL as a string to open a connection to. + * @return An HttpURLConnection for the specified URL; if the engine config defines a proxy, the connection is opened via that proxy. + */ + override operator fun invoke(urlString: String): HttpURLConnection { + val url = URL(urlString) + val connection: URLConnection = config.proxy?.let { url.openConnection(it) } ?: url.openConnection() + return connection as HttpURLConnection + } + } +}