Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
4f8e286
Testing ECH in Android Platform
yschimke Jan 16, 2026
e21fd68
More testing
yschimke Jan 16, 2026
8b062b8
Fixes
yschimke Jan 16, 2026
292d40a
Merge branch 'master' into testing_ech
yschimke Mar 15, 2026
5b46c86
Fixes
yschimke Mar 15, 2026
7e08fcb
Fixes
yschimke Mar 15, 2026
9692d67
Fixes
yschimke Mar 15, 2026
7ec38bf
Fixes
yschimke Mar 15, 2026
33c62cd
More fixes
yschimke Mar 16, 2026
2ea576e
More fixes
yschimke Mar 16, 2026
cc06a75
More fixes
yschimke Mar 16, 2026
cbecbaa
More fixes
yschimke Mar 16, 2026
487bc99
Refactor
yschimke Mar 17, 2026
2d8237e
Merge branch 'master' into testing_ech
yschimke Apr 4, 2026
7029d12
Android API 37
yschimke Apr 4, 2026
ae060bb
Testing with robolectric also
yschimke Apr 5, 2026
dc0f8ca
Merge branch 'master' into testing_ech
yschimke Apr 30, 2026
1a05f84
More fixes
yschimke Apr 30, 2026
e034658
Fix ECH branch CI failures
yschimke May 4, 2026
04f4a43
Avoid AGP source set cast for android tests
yschimke May 4, 2026
8702756
Permit localhost cleartext in Android tests
yschimke May 4, 2026
ffe3d69
Document ECH public APIs
yschimke May 4, 2026
b2652bb
Harden Android ECH support
yschimke May 4, 2026
ccde3d3
Temporarily focus CI on Android API 37
yschimke May 4, 2026
1b74946
Use explicit adb path for Android 37 CI
yschimke May 4, 2026
d71c148
Fix Android 37 cleanup workflow syntax
yschimke May 4, 2026
1fb1f76
Split Android 37 CI into separate job
yschimke May 4, 2026
5febd47
Wait for Android 37 package services
yschimke May 4, 2026
b890bb3
Try Android 37 Play Store system image
yschimke May 4, 2026
ec7e36d
Isolate Android 37 emulator job
yschimke May 4, 2026
892472b
Initialize OkHttp in public suffix Android test
yschimke May 4, 2026
72742fe
Re-enable regular build jobs
yschimke May 4, 2026
8cd005f
Restore conditional build job gates
yschimke May 4, 2026
994fe6f
Pin Android 37 workflow actions
yschimke May 4, 2026
6276a39
Merge branch 'master' into testing_ech
yschimke May 25, 2026
6ea7990
Merge branch 'master' into testing_ech
yschimke Jun 16, 2026
4e61bab
Address ECH PR review feedback
yschimke Jun 16, 2026
16f1ec7
Use google_apis_playstore_ps16k image for API 37
yschimke Jun 16, 2026
fa55cb0
Fix EchAware visibility so it compiles
yschimke Jun 16, 2026
cb2a521
Add experimental AsyncDns for HTTPS/SVCB + ECH (foundation)
yschimke Jun 16, 2026
43e605c
Inline the android37 emulator job with the 16 KB system image
yschimke Jun 16, 2026
2099333
Move API 37 into the android matrix job
yschimke Jun 16, 2026
5839d9f
Suppress NewApi lint for AndroidAsyncDns DnsResolver constructor
yschimke Jun 16, 2026
3b3043e
AndroidAsyncDns: query A/AAAA/HTTPS separately, add addressesOnly
yschimke Jun 16, 2026
05f6306
Make ECH types experimental public API
yschimke Jun 16, 2026
ad2e3de
Keep ECH and AsyncDns internal instead of experimental public API
yschimke Jun 16, 2026
11f85d5
Carry ECH state on RealCall fields instead of tags
yschimke Jun 16, 2026
e652838
Resolve ECH via a call-aware internal DNS path; drop EchAware
yschimke Jun 16, 2026
18bd5dc
Merge pull request #24 from yschimke/agent/ech-review-fixes
yschimke Jun 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 35 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,24 @@ jobs:
strategy:
fail-fast: false
matrix:
api-level:
- 21
- 23
- 29
- 34
include:
- api-level: 21
arch: x86
target: default
- api-level: 23
arch: x86
target: default
- api-level: 29
arch: x86
target: default
- api-level: 34
arch: x86_64
target: default
# API 37 only ships 16 KB page-size images; playstore_ps16k resolves to
# google_apis_playstore_ps16k. Quoted so YAML keeps the ".0".
- api-level: "37.0"
arch: x86_64
target: playstore_ps16k

steps:
- name: Checkout
Expand Down Expand Up @@ -338,7 +351,7 @@ jobs:
uses: actions/cache@v5
id: avd-cache
with:
key: avd-${{ runner.os }}-${{ matrix.api-level }}-${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }}
key: avd-${{ runner.os }}-${{ matrix.api-level }}-${{ matrix.target }}-${{ matrix.arch }}
path: |
~/.android/avd/*
~/.android/adb*
Expand All @@ -351,7 +364,9 @@ jobs:
with:
api-level: ${{ matrix.api-level }}
force-avd-creation: false
arch: ${{ matrix.api-level >= 30 && 'x86_64' || 'x86' }}
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
emulator-boot-timeout: 1200

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

20 minutes? woof

# No window, no audio, and use swiftshader for headless environments
emulator-options: >
-no-window
Expand All @@ -367,7 +382,19 @@ jobs:
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: ${{ matrix.api-level }}
arch: ${{ matrix.api-level == '34' && 'x86_64' || 'x86' }}
target: ${{ matrix.target }}
arch: ${{ matrix.arch }}
emulator-boot-timeout: 1200
# These must match the options used to create the snapshot above: the emulator only
# restores a cached snapshot when the boot options are identical. The action default
# includes -no-snapshot, which would otherwise force a slow cold boot.
emulator-options: >
-no-window
-gpu swiftshader_indirect
-noaudio
-no-boot-anim
-camera-back none
-memory 2048
script: ./gradlew -PandroidBuild=true connectedCheck
env:
API_LEVEL: ${{ matrix.api-level }}
Expand Down Expand Up @@ -440,4 +467,3 @@ jobs:

- name: Run with Jlink
run: ./gradlew module-tests:imageRun -PokhttpModuleTests=true

9 changes: 4 additions & 5 deletions android-test-app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
@file:Suppress("UnstableApiUsage")

import okhttp3.buildsupport.testJavaVersion


plugins {
id("okhttp.base-conventions")
id("com.android.application")
}

android {
compileSdk = 36
compileSdk {
version = release(37)
}

namespace = "okhttp.android.testapp"

Expand All @@ -18,7 +17,7 @@ android {

defaultConfig {
minSdk = 21
targetSdk = 36
targetSdk = 37
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,23 @@
*/
package okhttp3.android

import androidx.test.platform.app.InstrumentationRegistry
import assertk.assertThat
import assertk.assertions.isEqualTo
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttp
import org.junit.Before
import org.junit.Test

/**
* Run with "./gradlew :android-test-app:connectedCheck -PandroidBuild=true" and make sure ANDROID_SDK_ROOT is set.
*/
class PublicSuffixDatabaseTest {
@Before
fun setUp() {
OkHttp.initialize(InstrumentationRegistry.getInstrumentation().targetContext)
}

@Test
fun testTopLevelDomain() {
assertThat("https://www.google.com/robots.txt".toHttpUrl().topPrivateDomain()).isEqualTo("google.com")
Expand Down
30 changes: 18 additions & 12 deletions android-test/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ plugins {
}

android {
compileSdk = 36
compileSdk {
version = release(37)
}

namespace = "okhttp.android.test"

Expand All @@ -24,26 +26,16 @@ android {
)
}

if (androidBuild) {
sourceSets["androidTest"].java.srcDirs(
"../okhttp-brotli/src/test/java",
"../okhttp-dnsoverhttps/src/test/java",
"../okhttp-logging-interceptor/src/test/java",
"../okhttp-sse/src/test/java"
)
}

compileOptions {
targetCompatibility(JavaVersion.VERSION_11)
sourceCompatibility(JavaVersion.VERSION_11)
}

testOptions {
targetSdk = 34
targetSdk = 37
unitTests.isIncludeAndroidResources = true
}


// issue merging due to conflict with httpclient and something else
packagingOptions.resources.excludes += setOf(
"META-INF/DEPENDENCIES",
Expand All @@ -55,11 +47,25 @@ android {
)
}

if (androidBuild) {
androidComponents {
onVariants(selector().all()) { variant ->
variant.androidTest?.sources?.java?.apply {
addStaticSourceDirectory("../okhttp-brotli/src/test/java")
addStaticSourceDirectory("../okhttp-dnsoverhttps/src/test/java")
addStaticSourceDirectory("../okhttp-logging-interceptor/src/test/java")
addStaticSourceDirectory("../okhttp-sse/src/test/java")
}
}
}
}

dependencies {
implementation(libs.kotlin.reflect)
implementation(libs.playservices.safetynet)
"friendsImplementation"(projects.okhttp)
"friendsImplementation"(projects.okhttpDnsoverhttps)
implementation(libs.androidx.activity)

testImplementation(projects.okhttp)
testImplementation(libs.junit)
Expand Down
63 changes: 63 additions & 0 deletions android-test/src/androidTest/java/okhttp/android/test/EchTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* 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 okhttp.android.test

import assertk.assertThat
import assertk.assertions.isNotNull
import assertk.assertions.matchesPredicate
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test

@Tag("Remote")
class EchTest {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NEAT


@Test
fun testHttpsRequest() {
val client: OkHttpClient =
OkHttpClient
.Builder()
.build()

val cloudflareEchBody =
client.sendRequest(Request.Builder().url("https://cloudflare-ech.com/").build()) {
it.body.string()
}
assertThat(cloudflareEchBody).matchesPredicate { it.contains("ECH enabled") }

val cloudflareBody = client.sendRequest(
Request.Builder().url("https://crypto.cloudflare.com/cdn-cgi/trace").build()
) {
it.body.string()
}
assertThat(cloudflareBody).matchesPredicate { it.contains("ECH enabled") }

val tlsEchBody = client.sendRequest(Request.Builder().url("https://tls-ech.dev/").build()) {
it.body.string()
}
assertThat(tlsEchBody).matchesPredicate { it.contains("ECH enabled") }
}

private fun <T> OkHttpClient.sendRequest(request: Request, fn: (Response) -> T): T {
val response = newCall(request).execute()

return response.use {
fn(it)
}
}
}
2 changes: 1 addition & 1 deletion android-test/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />

<application android:usesCleartextTraffic="true" tools:targetApi="m"/>
<application android:usesCleartextTraffic="true" tools:targetApi="m" android:networkSecurityConfig="@xml/network_security_config"/>

</manifest>
11 changes: 10 additions & 1 deletion android-test/src/main/res/xml/network_security_config.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,13 @@
<network-security-config>
<base-config cleartextTrafficPermitted="false">
</base-config>
</network-security-config>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">localhost</domain>
</domain-config>
<domain-config>
<domain includeSubdomains="true">cloudflare-ech.com</domain>
<domain includeSubdomains="true">crypto.cloudflare.com</domain>
<domain includeSubdomains="true">tls-ech.dev</domain>
<domainEncryption mode="opportunistic" />
</domain-config>
</network-security-config>
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,12 @@ class AndroidSocketAdapterTest(
val sslSocket = socketFactory.createSocket() as SSLSocket
assertTrue(adapter.matchesSocket(sslSocket))

adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1))
adapter.configureTlsExtensions(
call = null,
sslSocket = sslSocket,
hostname = null,
protocols = listOf(HTTP_2, HTTP_1_1)
)
// not connected
assertNull(adapter.getSelectedProtocol(sslSocket))
}
Expand Down Expand Up @@ -89,7 +94,12 @@ class AndroidSocketAdapterTest(
object : DelegatingSSLSocket(context.socketFactory.createSocket() as SSLSocket) {}
assertFalse(adapter.matchesSocket(sslSocket))

adapter.configureTlsExtensions(sslSocket, null, listOf(HTTP_2, HTTP_1_1))
adapter.configureTlsExtensions(
call = null,
sslSocket = sslSocket,
hostname = null,
protocols = listOf(HTTP_2, HTTP_1_1)
)
// not connected
assertNull(adapter.getSelectedProtocol(sslSocket))
}
Expand Down
1 change: 1 addition & 0 deletions android-test/src/test/resources/robolectric.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sdk=36
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ tasks.withType<KotlinCompile>().configureEach {
friendPaths.from(friendsTestImplementation.incoming.artifactView { }.files)
}

tasks.withType<Test> {
if (testJavaVersion >= 9) {
// Fix for robolectric https://github.com/robolectric/robolectric/pull/10996
jvmArgs("--add-opens", "java.base/jdk.internal.access=ALL-UNNAMED")
}
}

val resolvableConfigurations = configurations.filter { it.isCanBeResolved }
tasks.register("downloadDependencies") {
description = "Download all dependencies to the Gradle cache"
Expand Down
2 changes: 1 addition & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[versions]
agp = "9.1.1"
agp = "9.2.0"
amazon-corretto = "2.5.0"
android-junit5 = "2.0.1"
androidx-activity = "1.11.0"
Expand Down
1 change: 0 additions & 1 deletion mockwebserver-junit5/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ dependencies {
compileOnly(libs.animalsniffer.annotations)

testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.kotlin.junit5)
testImplementation(projects.okhttpTestingSupport)
testImplementation(libs.assertk)
}
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,12 @@ public class MockWebServer : Closeable {
openClientSockets.add(sslSocket)

if (protocolNegotiationEnabled) {
Platform.get().configureTlsExtensions(sslSocket, null, protocols)
Platform.get().configureTlsExtensions(
call = null,
sslSocket = sslSocket,
hostname = null,
protocols = protocols,
)
}

sslSocket.startHandshake()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,12 @@ class Http2Server(
true,
) as SSLSocket
sslSocket.useClientMode = false
Platform.get().configureTlsExtensions(sslSocket, null, listOf(Protocol.HTTP_2))
Platform.get().configureTlsExtensions(
call = null,
sslSocket = sslSocket,
hostname = null,
protocols = listOf(Protocol.HTTP_2),
)
sslSocket.startHandshake()
return sslSocket
}
Expand Down
1 change: 0 additions & 1 deletion native-image-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ dependencies {
testImplementation(projects.mockwebserver3Junit5)
testImplementation(libs.assertk)
testRuntimeOnly(libs.junit.jupiter.engine)
testImplementation(libs.kotlin.junit5)
testImplementation(libs.junit.jupiter.params)
}

Expand Down
1 change: 0 additions & 1 deletion okhttp-osgi-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ dependencies {
testImplementation(projects.okhttpTestingSupport)
testImplementation(libs.junit)
testImplementation(libs.kotlin.test.common)
testImplementation(libs.kotlin.test.junit)
testImplementation(libs.assertk)

testImplementation(libs.aqute.resolve)
Expand Down
Loading
Loading