From 7c291fb5cb3a74e7b73b2e4a86bf16ab9ae430ed Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 25 Jul 2026 21:55:18 +0100 Subject: [PATCH 1/3] Add EchAwareDns as the default on Android 37 --- gradle/libs.versions.toml | 2 +- okhttp/api/android/okhttp.api | 13 ++ okhttp/build.gradle.kts | 4 + .../kotlin/okhttp3/android/EchAwareDnsTest.kt | 199 ++++++++++++++++++ .../android/ShadowNetworkSecurityPolicy.kt | 46 ++++ .../kotlin/okhttp3/android/AndroidDns.kt | 8 +- .../kotlin/okhttp3/android/EchAwareDns.kt | 91 ++++++++ .../internal/platform/Android10Platform.kt | 6 + .../kotlin/okhttp3/OkHttpClient.kt | 2 +- .../okhttp3/internal/platform/Platform.kt | 6 + 10 files changed, 371 insertions(+), 6 deletions(-) create mode 100644 okhttp/src/androidHostTest/kotlin/okhttp3/android/EchAwareDnsTest.kt create mode 100644 okhttp/src/androidHostTest/kotlin/okhttp3/android/ShadowNetworkSecurityPolicy.kt create mode 100644 okhttp/src/androidMain/kotlin/okhttp3/android/EchAwareDns.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 8f48c42d142e..c32debd0ef29 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -48,7 +48,7 @@ org-conscrypt = "2.6.1" org-junit-jupiter = "5.13.4" playservices-safetynet = "18.1.0" robolectric = "4.17-beta-2" -robolectric-android = "16-robolectric-13921718" +robolectric-android = "17-robolectric-15733970" serialization = "1.11.0" shadow-plugin = "9.5.1" signature-android-apilevel21 = "5.0.1_r2" diff --git a/okhttp/api/android/okhttp.api b/okhttp/api/android/okhttp.api index 7855a24ceb7f..c7f1534c713f 100644 --- a/okhttp/api/android/okhttp.api +++ b/okhttp/api/android/okhttp.api @@ -1389,3 +1389,16 @@ public final class okhttp3/android/AndroidDns : okhttp3/Dns { public fun newCall (Lokhttp3/Dns$Request;)Lokhttp3/Dns$Call; } +public final class okhttp3/android/EchAwareDns : okhttp3/Dns { + public static final field Companion Lokhttp3/android/EchAwareDns$Companion; + public fun ()V + public fun (Lokhttp3/Dns;Lokhttp3/Dns;Landroid/security/NetworkSecurityPolicy;)V + public synthetic fun (Lokhttp3/Dns;Lokhttp3/Dns;Landroid/security/NetworkSecurityPolicy;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun lookup (Ljava/lang/String;)Ljava/util/List; + public fun newCall (Lokhttp3/Dns$Request;)Lokhttp3/Dns$Call; +} + +public final class okhttp3/android/EchAwareDns$Companion { + public final fun forNetwork (Landroid/net/Network;)Lokhttp3/android/EchAwareDns; +} + diff --git a/okhttp/build.gradle.kts b/okhttp/build.gradle.kts index 15d24f092949..96866a1f2be6 100644 --- a/okhttp/build.gradle.kts +++ b/okhttp/build.gradle.kts @@ -334,6 +334,10 @@ afterEvaluate { // Work around robolectric requirements and limitations // https://cs.android.com/android-studio/platform/tools/base/+/mirror-goog-studio-main:build-system/gradle-core/src/main/java/com/android/build/gradle/tasks/factory/AndroidUnitTest.java;l=339 allJvmArgs = allJvmArgs.filter { !it.startsWith("--add-opens") } + } else { + // Robolectric's FileDescriptor interceptor reaches into jdk.internal.access, which isn't + // exported to the unnamed module. Android 17 (API 37) images hit it on startup. + jvmArgs("--add-exports=java.base/jdk.internal.access=ALL-UNNAMED") } } } diff --git a/okhttp/src/androidHostTest/kotlin/okhttp3/android/EchAwareDnsTest.kt b/okhttp/src/androidHostTest/kotlin/okhttp3/android/EchAwareDnsTest.kt new file mode 100644 index 000000000000..36b338b438a6 --- /dev/null +++ b/okhttp/src/androidHostTest/kotlin/okhttp3/android/EchAwareDnsTest.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(OkHttpInternalApi::class) + +package okhttp3.android + +import android.annotation.SuppressLint +import android.net.Network +import android.security.NetworkSecurityPolicy +import android.security.NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_DISABLED +import android.security.NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_ENABLED +import android.security.NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC +import android.security.NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_UNKNOWN +import assertk.assertThat +import assertk.assertions.containsExactly +import assertk.assertions.isEmpty +import assertk.assertions.isEqualTo +import assertk.assertions.isFalse +import assertk.assertions.isTrue +import java.net.InetAddress +import okhttp3.Dns +import okhttp3.FakeDns +import okhttp3.android.ShadowNetworkSecurityPolicy.Companion.newNetworkSecurityPolicy +import okhttp3.internal.OkHttpInternalApi +import okhttp3.internal.SuppressSignatureCheck +import okhttp3.internal.dns.ResourceRecord +import okhttp3.internal.dns.execute +import okio.ByteString +import okio.ByteString.Companion.encodeUtf8 +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config +import org.robolectric.shadows.ShadowNetwork + +@SuppressLint("NewApi") +@SuppressSignatureCheck +@RunWith(RobolectricTestRunner::class) +@Config(sdk = [37], shadows = [ShadowNetworkSecurityPolicy::class]) +class EchAwareDnsTest { + private val echAddress = InetAddress.getByAddress("publicobject.com", byteArrayOf(1, 1, 1, 1)) + private val addressOnlyAddress = InetAddress.getByAddress("publicobject.com", byteArrayOf(2, 2, 2, 2)) + private val echConfigList = "ech config list".encodeUtf8() + + /** Carries service metadata, as [AndroidDns] does when it queries the `HTTPS` record. */ + private val echDns = + FakeDns().apply { + setRecords("publicobject.com", echAddress, echConfigList) + } + + private val addressOnlyDns = + FakeDns().apply { + setRecords("publicobject.com", addressOnlyAddress) + } + + @Test + fun encryptionEnabledUsesEchDns() { + val records = recordsFor(DOMAIN_ENCRYPTION_MODE_ENABLED) + + assertThat(records.addresses()).containsExactly(echAddress) + assertThat(records.echConfigLists()).containsExactly(echConfigList) + } + + @Test + fun encryptionOpportunisticUsesEchDns() { + val records = recordsFor(DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC) + + assertThat(records.addresses()).containsExactly(echAddress) + assertThat(records.echConfigLists()).containsExactly(echConfigList) + } + + @Test + fun encryptionDisabledUsesAddressOnlyDns() { + val records = recordsFor(DOMAIN_ENCRYPTION_MODE_DISABLED) + + assertThat(records.addresses()).containsExactly(addressOnlyAddress) + assertThat(records.echConfigLists()).isEmpty() + } + + @Test + fun encryptionUnknownUsesAddressOnlyDns() { + val records = recordsFor(DOMAIN_ENCRYPTION_MODE_UNKNOWN) + + assertThat(records.addresses()).containsExactly(addressOnlyAddress) + assertThat(records.echConfigLists()).isEmpty() + } + + /** Hosts the policy says nothing about don't pay for an `HTTPS` query. */ + @Test + fun unconfiguredHostUsesAddressOnlyDns() { + val records = echAwareDns(newNetworkSecurityPolicy()).recordsFor("publicobject.com") + + assertThat(records.addresses()).containsExactly(addressOnlyAddress) + assertThat(records.echConfigLists()).isEmpty() + } + + /** Each host is judged on its own policy, even though both are available from [echDns]. */ + @Test + fun policyIsPerHost() { + val deniedEchAddress = InetAddress.getByAddress("denied.example.com", byteArrayOf(1, 1, 1, 2)) + val deniedEchConfigList = "denied ech config list".encodeUtf8() + val deniedAddress = InetAddress.getByAddress("denied.example.com", byteArrayOf(2, 2, 2, 2)) + echDns.setRecords("denied.example.com", deniedEchAddress, deniedEchConfigList) + addressOnlyDns.setRecords("denied.example.com", deniedAddress) + + val dns = + echAwareDns( + newNetworkSecurityPolicy( + "publicobject.com" to DOMAIN_ENCRYPTION_MODE_ENABLED, + "denied.example.com" to DOMAIN_ENCRYPTION_MODE_DISABLED, + ), + ) + + val allowedRecords = dns.recordsFor("publicobject.com") + assertThat(allowedRecords.addresses()).containsExactly(echAddress) + assertThat(allowedRecords.echConfigLists()).containsExactly(echConfigList) + + val deniedRecords = dns.recordsFor("denied.example.com") + assertThat(deniedRecords.addresses()).containsExactly(deniedAddress) + assertThat(deniedRecords.echConfigLists()).isEmpty() + } + + /** [Dns.lookup] can't carry HTTPS records, so it never pays for the `HTTPS` query. */ + @Test + fun lookupAlwaysUsesAddressOnlyDns() { + val dns = echAwareDns(newNetworkSecurityPolicy("publicobject.com" to DOMAIN_ENCRYPTION_MODE_ENABLED)) + + assertThat(dns.lookup("publicobject.com")).containsExactly(addressOnlyAddress) + } + + /** Both arms of [EchAwareDns.forNetwork] resolve on the network, not just the ECH one. */ + @Test + fun forNetworkScopesBothArms() { + val network = ShadowNetwork.newInstance(1234) + + val dns = EchAwareDns.forNetwork(network) + + val echDns = dns.echDns as AndroidDns + assertThat(echDns.network).isEqualTo(network) + assertThat(echDns.includeServiceMetadata).isTrue() + + val addressOnlyDns = dns.addressOnlyDns as AndroidDns + assertThat(addressOnlyDns.network).isEqualTo(network) + assertThat(addressOnlyDns.includeServiceMetadata).isFalse() + } + + private fun recordsFor(domainEncryptionMode: Int): List = + echAwareDns(newNetworkSecurityPolicy("publicobject.com" to domainEncryptionMode)) + .recordsFor("publicobject.com") + + private fun Dns.recordsFor(hostname: String): List = newCall(Dns.Request(hostname)).execute() + + private fun List.addresses() = filterIsInstance().map { it.address } + + private fun List.echConfigLists() = filterIsInstance().mapNotNull { it.echConfigList } + + private fun echAwareDns(policy: NetworkSecurityPolicy) = + EchAwareDns( + echDns = echDns, + addressOnlyDns = addressOnlyDns, + policy = policy, + ) +} + +/** Serves [address] for [hostname], plus an `HTTPS` record when [echConfigList] is non-null. */ +private fun FakeDns.setRecords( + hostname: String, + address: InetAddress, + echConfigList: ByteString? = null, +) { + this[hostname] = + listOfNotNull( + echConfigList?.let { + ResourceRecord.Https( + name = hostname, + timeToLive = 5, + echConfigList = it, + ) + }, + ResourceRecord.IpAddress( + name = hostname, + timeToLive = 5, + address = address, + ), + ) +} diff --git a/okhttp/src/androidHostTest/kotlin/okhttp3/android/ShadowNetworkSecurityPolicy.kt b/okhttp/src/androidHostTest/kotlin/okhttp3/android/ShadowNetworkSecurityPolicy.kt new file mode 100644 index 000000000000..2d80157b0062 --- /dev/null +++ b/okhttp/src/androidHostTest/kotlin/okhttp3/android/ShadowNetworkSecurityPolicy.kt @@ -0,0 +1,46 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.android + +import android.security.NetworkSecurityPolicy +import android.security.NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_UNKNOWN +import okhttp3.internal.SuppressSignatureCheck +import org.robolectric.annotation.Implementation +import org.robolectric.annotation.Implements +import org.robolectric.shadow.api.Shadow + +/** + * Gives tests a [NetworkSecurityPolicy] with domain encryption modes of their choosing. The + * platform constructor is private, so instances come from [newNetworkSecurityPolicy] rather than + * from a subclass. + */ +@SuppressSignatureCheck +@Implements(NetworkSecurityPolicy::class) +class ShadowNetworkSecurityPolicy { + var domainEncryptionModes: Map = mapOf() + + @Implementation + fun getDomainEncryptionMode(hostname: String): Int = domainEncryptionModes[hostname] ?: DOMAIN_ENCRYPTION_MODE_UNKNOWN + + companion object { + /** Returns a policy that reports [domainEncryptionModes], and [DOMAIN_ENCRYPTION_MODE_UNKNOWN] for other hosts. */ + fun newNetworkSecurityPolicy(vararg domainEncryptionModes: Pair): NetworkSecurityPolicy { + val policy = Shadow.newInstanceOf(NetworkSecurityPolicy::class.java) + Shadow.extract(policy).domainEncryptionModes = domainEncryptionModes.toMap() + return policy + } + } +} diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt index ea884a7a77cc..2de449e62bc0 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/AndroidDns.kt @@ -52,17 +52,17 @@ import okio.Buffer @RequiresApi(29) @SuppressSignatureCheck class AndroidDns( - private val dnsResolver: DnsResolver = DnsResolver.getInstance(), - private val network: Network? = null, + internal val dnsResolver: DnsResolver = DnsResolver.getInstance(), + internal val network: Network? = null, dnsCache: DnsCache = DnsCache(), /** * True to also query the `HTTPS` record for service metadata. Keep this on: it enables privacy * features such as Encrypted Client Hello (ECH) for the HTTPS call. Set it to false only when * you want to disable ECH. */ - private val includeServiceMetadata: Boolean = true, + internal val includeServiceMetadata: Boolean = true, // Runs inline; the executor only hands off DnsResolver's callbacks. - private val executor: Executor = Executor { it.run() }, + internal val executor: Executor = Executor { it.run() }, ) : Dns { private val taskRunner: TaskRunner = TaskRunner.INSTANCE diff --git a/okhttp/src/androidMain/kotlin/okhttp3/android/EchAwareDns.kt b/okhttp/src/androidMain/kotlin/okhttp3/android/EchAwareDns.kt new file mode 100644 index 000000000000..591f2d3d4e5d --- /dev/null +++ b/okhttp/src/androidMain/kotlin/okhttp3/android/EchAwareDns.kt @@ -0,0 +1,91 @@ +/* + * Copyright (c) 2026 OkHttp Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package okhttp3.android + +import android.annotation.SuppressLint +import android.net.Network +import android.os.Build +import android.security.NetworkSecurityPolicy +import android.security.NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_ENABLED +import android.security.NetworkSecurityPolicy.DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC +import androidx.annotation.ChecksSdkIntAtLeast +import androidx.annotation.RequiresApi +import java.net.InetAddress +import okhttp3.Dns +import okhttp3.internal.SuppressSignatureCheck +import okhttp3.internal.platform.Platform.Companion.isAndroid + +/** + * ECH Aware DNS Wrapper for Android 37. + * + * Uses the [NetworkSecurityPolicy] to read the domain encryption settings for + * a given host. + * + * Usage: + * ``` + * val dns = EchAwareDns(AndroidDns()) + * val client = OkHttpClient.Builder().dns(dns).build() + * ``` + * + * @param echDns resolves hosts that the policy permits ECH for. It should carry service metadata, + * such as [AndroidDns] with `includeServiceMetadata` enabled. + * @param addressOnlyDns resolves hosts that the policy denies ECH for. + * The default, [Dns.SYSTEM], resolves on the default network. + * @param policy the source of each host's domain encryption mode. + */ +@SuppressLint("NewApi") +@SuppressSignatureCheck +class EchAwareDns + @RequiresApi(37) + constructor( + internal val echDns: Dns = AndroidDns(), + internal val addressOnlyDns: Dns = Dns.SYSTEM, + internal val policy: NetworkSecurityPolicy = NetworkSecurityPolicy.getInstance(), + ) : Dns { + // Safe to call lookup as it doesn't carry HTTPS records + override fun lookup(hostname: String): List = addressOnlyDns.lookup(hostname) + + override fun newCall(request: Dns.Request): Dns.Call { + val dns = if (echAllowed(request.hostname)) echDns else addressOnlyDns + + return dns.newCall(request) + } + + /** + * Allows avoiding waiting for HTTPS records, when we know that Android Conscrypt won't use them. + */ + private fun echAllowed(hostname: String): Boolean = + when (policy.getDomainEncryptionMode(hostname)) { + DOMAIN_ENCRYPTION_MODE_ENABLED, DOMAIN_ENCRYPTION_MODE_OPPORTUNISTIC -> true + else -> false + } + + @SuppressSignatureCheck + companion object { + /** Returns a [Dns] that resolves on [network]. */ + @RequiresApi(37) + fun forNetwork(network: Network): EchAwareDns = + EchAwareDns( + echDns = AndroidDns(network = network), + addressOnlyDns = AndroidDns(network = network, includeServiceMetadata = false), + ) + + @ChecksSdkIntAtLeast(api = 37) + internal val isSupported: Boolean = isAndroid && Build.VERSION.SDK_INT >= 37 + + internal fun buildIfSupported(): Dns? = if (isSupported) EchAwareDns() else null + } + } diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt index e89e0260f59c..84f676d33f89 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt @@ -26,7 +26,9 @@ import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager +import okhttp3.Dns import okhttp3.Protocol +import okhttp3.android.EchAwareDns import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.AndroidPlatform.Companion.Tag import okhttp3.internal.platform.android.Android10SocketAdapter @@ -90,6 +92,10 @@ class Android10Platform : // No TLS extensions if the socket class is custom. socketAdapters.find { it.matchesSocket(sslSocket) }?.getSelectedProtocol(sslSocket) + private val platformDns: Dns by lazy { EchAwareDns.buildIfSupported() ?: Dns.SYSTEM } + + override fun platformDns(): Dns = platformDns + override fun getStackTraceForCloseable(closer: String): Any? = if (Build.VERSION.SDK_INT >= 30) { CloseGuard().apply { open(closer) } diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt index bea47f62e899..ff478913b0d2 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/OkHttpClient.kt @@ -597,7 +597,7 @@ open class OkHttpClient internal constructor( internal var followSslRedirects = true internal var cookieJar: CookieJar = CookieJar.NO_COOKIES internal var cache: Cache? = null - internal var dns: Dns = Dns.SYSTEM + internal var dns: Dns = Platform.get().platformDns() internal var proxy: Proxy? = null internal var proxySelector: ProxySelector? = null internal var proxyAuthenticator: Authenticator = Authenticator.NONE diff --git a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt index d65df2df3df3..3633ab55801d 100644 --- a/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt +++ b/okhttp/src/commonJvmAndroid/kotlin/okhttp3/internal/platform/Platform.kt @@ -31,6 +31,7 @@ import javax.net.ssl.SSLSocketFactory import javax.net.ssl.TrustManager import javax.net.ssl.TrustManagerFactory import javax.net.ssl.X509TrustManager +import okhttp3.Dns import okhttp3.OkHttpClient import okhttp3.Protocol import okhttp3.internal.publicsuffix.PublicSuffixDatabase @@ -142,6 +143,11 @@ open class Platform { } } + /** + * Provides the default [Dns] for the system, by default [Dns.SYSTEM]. + */ + open fun platformDns(): Dns = Dns.SYSTEM + @Throws(IOException::class) open fun connectSocket( socket: Socket, From 520939fd4d4724a47aaabbdb2a823be682690040 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 25 Jul 2026 22:46:06 +0100 Subject: [PATCH 2/3] Remove default --- okhttp/build.gradle.kts | 8 ++++++++ .../kotlin/okhttp3/internal/platform/Android10Platform.kt | 6 ------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/okhttp/build.gradle.kts b/okhttp/build.gradle.kts index 96866a1f2be6..052c5be1828a 100644 --- a/okhttp/build.gradle.kts +++ b/okhttp/build.gradle.kts @@ -339,6 +339,14 @@ afterEvaluate { // exported to the unnamed module. Android 17 (API 37) images hit it on startup. jvmArgs("--add-exports=java.base/jdk.internal.access=ALL-UNNAMED") } + + if (testJavaVersion < 21) { + // Robolectric can't build an Android SDK 37 sandbox on Java 17. + filter { + excludeTestsMatching("okhttp3.android.EchAwareDnsTest") + isFailOnNoMatchingTests = false + } + } } } diff --git a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt index 84f676d33f89..e89e0260f59c 100644 --- a/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt +++ b/okhttp/src/androidMain/kotlin/okhttp3/internal/platform/Android10Platform.kt @@ -26,9 +26,7 @@ import javax.net.ssl.SSLContext import javax.net.ssl.SSLSocket import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager -import okhttp3.Dns import okhttp3.Protocol -import okhttp3.android.EchAwareDns import okhttp3.internal.SuppressSignatureCheck import okhttp3.internal.platform.AndroidPlatform.Companion.Tag import okhttp3.internal.platform.android.Android10SocketAdapter @@ -92,10 +90,6 @@ class Android10Platform : // No TLS extensions if the socket class is custom. socketAdapters.find { it.matchesSocket(sslSocket) }?.getSelectedProtocol(sslSocket) - private val platformDns: Dns by lazy { EchAwareDns.buildIfSupported() ?: Dns.SYSTEM } - - override fun platformDns(): Dns = platformDns - override fun getStackTraceForCloseable(closer: String): Any? = if (Build.VERSION.SDK_INT >= 30) { CloseGuard().apply { open(closer) } From 51d8e8ebd95eed02ff64846e0a6a571af48397c3 Mon Sep 17 00:00:00 2001 From: Yuri Schimke Date: Sat, 25 Jul 2026 22:59:27 +0100 Subject: [PATCH 3/3] better method of filtering. --- .../src/main/kotlin/okhttp.testing-conventions.gradle.kts | 6 ++++++ okhttp/build.gradle.kts | 8 -------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts b/build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts index 08dc0e1759d4..9cfd009e733b 100644 --- a/build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts +++ b/build-logic/src/main/kotlin/okhttp.testing-conventions.gradle.kts @@ -46,6 +46,12 @@ tasks.withType { systemProperty("okhttp.platform", platform) systemProperty("junit.jupiter.extensions.autodetection.enabled", "true") + + if (testJavaVersion < 21) { + // Robolectric needs Java 21 to sandbox Android SDK 37. Tests configured only for SDKs + // outside this set are skipped rather than failing to create a sandbox. + systemProperty("robolectric.enabledSdks", (21..36).joinToString(",")) + } } tasks.withType().configureEach { diff --git a/okhttp/build.gradle.kts b/okhttp/build.gradle.kts index 052c5be1828a..96866a1f2be6 100644 --- a/okhttp/build.gradle.kts +++ b/okhttp/build.gradle.kts @@ -339,14 +339,6 @@ afterEvaluate { // exported to the unnamed module. Android 17 (API 37) images hit it on startup. jvmArgs("--add-exports=java.base/jdk.internal.access=ALL-UNNAMED") } - - if (testJavaVersion < 21) { - // Robolectric can't build an Android SDK 37 sandbox on Java 17. - filter { - excludeTestsMatching("okhttp3.android.EchAwareDnsTest") - isFailOnNoMatchingTests = false - } - } } }