Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 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
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
124 changes: 115 additions & 9 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,19 @@ 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

steps:
- name: Checkout
Expand Down Expand Up @@ -338,7 +346,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 +359,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
# No window, no audio, and use swiftshader for headless environments
emulator-options: >
-no-window
Expand All @@ -367,14 +377,111 @@ 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
# Match the snapshot creation options. The action default includes -no-snapshot,
# which forces 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 }}

- name: Build Release App
run: ./gradlew android-test-app:lint android-test-app:assembleRelease

android37:
runs-on: ubuntu-latest
timeout-minutes: 30

steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd

- name: Configure JDK
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654
with:
distribution: 'temurin'
java-version: 21

- name: Enable KVM group perms
# https://github.blog/changelog/2023-02-23-hardware-accelerated-android-virtualization-on-actions-windows-and-linux-larger-hosted-runners/
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm

- name: Verify KVM
run: |
sudo apt-get install -y cpu-checker
kvm-ok || echo "KVM is not accelerated"
kvm-ok || exit 1

- name: Setup Gradle
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c

- name: Gradle cache
run: ./gradlew :android-test:test

- name: Run Android 37 Tests
run: |
SDKMANAGER="$(find "$ANDROID_HOME/cmdline-tools" -path '*/bin/sdkmanager' | sort | tail -n 1)"
AVDMANAGER="$(find "$ANDROID_HOME/cmdline-tools" -path '*/bin/avdmanager' | sort | tail -n 1)"
ADB="$ANDROID_HOME/platform-tools/adb"
EMULATOR="$ANDROID_HOME/emulator/emulator"
export ANDROID_AVD_HOME="$HOME/.android/avd"
mkdir -p "$ANDROID_AVD_HOME"

yes | "$SDKMANAGER" --licenses > /dev/null
"$SDKMANAGER" --install \
'build-tools;36.0.0' \
platform-tools \
'platforms;android-37.0' \
emulator \
'system-images;android-37.0;google_apis_playstore_ps16k;x86_64' \
--channel=0 > /dev/null

printf 'no\n' | "$AVDMANAGER" create avd \
--force \
--name test \
--package 'system-images;android-37.0;google_apis_playstore_ps16k;x86_64'

"$AVDMANAGER" list avd

"$EMULATOR" \
-port 5554 \
-avd test \
-no-window \
-gpu swiftshader_indirect \
-no-snapshot \
-noaudio \
-no-boot-anim \
-camera-back none \
-memory 4096 &

"$ADB" -s emulator-5554 wait-for-device
timeout 1200 bash -c 'until [[ "$("$1" -s emulator-5554 shell getprop sys.boot_completed | tr -d "\r")" == "1" ]]; do sleep 2; done' -- "$ADB"
timeout 300 bash -c 'until "$1" -s emulator-5554 shell service check input | grep -q "found"; do sleep 2; done' -- "$ADB"
timeout 300 bash -c 'until "$1" -s emulator-5554 shell service check package | grep -q "found"; do sleep 2; done' -- "$ADB"
timeout 300 bash -c 'until "$1" -s emulator-5554 shell service check activity | grep -q "found"; do sleep 2; done' -- "$ADB"
"$ADB" -s emulator-5554 shell getprop ro.build.version.sdk
"$ADB" -s emulator-5554 shell getprop ro.build.version.release

./gradlew -PandroidBuild=true connectedCheck
env:
API_LEVEL: 37.0

- name: Stop Android 37 Emulator
if: always()
run: |
"$ANDROID_HOME/platform-tools/adb" -s emulator-5554 emu kill || true

loom:
runs-on: ubuntu-latest

Expand Down Expand Up @@ -440,4 +547,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
35 changes: 23 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 Expand Up @@ -114,3 +120,8 @@ junitPlatform {
excludeTags("Remote")
}
}

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

@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>
Loading
Loading