Skip to content

Modernize the app: Compose Material 3 rewrite, engine fixes, tests and CI - #49

Open
BKPepe wants to merge 10 commits into
librespeed:masterfrom
BKPepe:feature/modern-client
Open

Modernize the app: Compose Material 3 rewrite, engine fixes, tests and CI#49
BKPepe wants to merge 10 commits into
librespeed:masterfrom
BKPepe:feature/modern-client

Conversation

@BKPepe

@BKPepe BKPepe commented Aug 11, 2026

Copy link
Copy Markdown

Summary

This PR turns the template into a production-oriented LibreSpeed Android client: Jetpack Compose + Material 3 UI, a fixed and extended test engine, opt-in telemetry with a tested privacy contract, and CI/release workflows.

It is intentionally large, but the history is structured for review — 9 commits, each a self-contained block (build system → engine → UI → tests → CI → docs → features → hardening → coverage). Reviewing commit by commit is recommended.

⚠️ New application ID

The app now uses applicationId org.librespeed.speedtest (previously com.dosse.speedtest). On Google Play this is a new app, not an update — existing installations will not upgrade to it. This was a deliberate choice to move the app under the project's own namespace, but it is a decision the maintainers should explicitly confirm before any store publishing.

Screenshots

Result (dark) Test in progress (light) Settings

What's new for users

Engine fixes (kept in Java, same architecture)

  • Malformed User-Agent header (CRLF injection into the request) fixed; UA is now set at runtime with the real version
  • Any 2xx response accepted instead of only 200 (fixes ping against servers returning 204)
  • IP version and server id tracked per test point; loss reported from unreturned pings
  • Telemetry submits to the tested server's results backend and returns the result link
  • Two implicit narrowing casts flagged by CodeQL made explicit

Privacy

Telemetry is off by default. When enabled, the result (including the IP address) goes to the tested server's results backend — the app has no central telemetry collection endpoint; the in-app "What is sent?" dialog explains this. The IP address is shown only in the technical details screen and is never part of any shared text or image. The new runtime permissions are optional and degrade gracefully: ACCESS_COARSE_LOCATION (server distances on demand, on-device only), READ_PHONE_STATE (4G/5G label) and POST_NOTIFICATIONS (scheduled-test results).

Testing

  • 51 JVM unit tests, including a telemetry privacy contract asserted through the real app path against MockWebServer (telemetry off → no request may leave; on → exactly one, to the tested server), database migration tests (v1–v3 → v4), export escaping, network-type detection with permission states, and boundary tests that caught two real bugs before merge
  • Screenshot regression tests (Roborazzi goldens, verified in CI)
  • Instrumentation smoke + recreation tests on an emulator matrix: API 26 (minSdk), 33, 35, 36 (targetSdk)
  • Integration job running the whole engine against the official LibreSpeed docker image
  • Manually verified on Pixel 7 and Pixel 9 Pro Fold emulators (both postures) and against public servers, including an end-to-end telemetry check with a real result link

CI / release

  • PRs: lint, unit + screenshot tests, debug build, docker integration, dependency review, CodeQL, device matrix
  • Tag vX.Y.Z: release build that fails without signing secrets, verifies tag vs versionName, attaches the signed APK to a draft release (suitable for Obtainium; enables F-Droid Auto Update #45, Download .apk from github directly with Obtainium #41) and keeps the AAB as an artifact
  • Play publishing is a separate manual workflow (internal/alpha/beta only, never production)
  • Dependabot for Gradle and Actions; third-party actions pinned to commit SHAs

Maintainer checklist: secrets and Google Play

Nothing in this PR publishes anywhere by itself. To get signed releases and a Play listing, the repository owner has to do the following one-time setup:

1. Release signing (required for tagged releases)

The release workflow refuses to build without these repository secrets:

Secret Value
SIGNING_KEYSTORE_BASE64 the upload keystore, base64-encoded (base64 -i upload.jks)
SIGNING_STORE_PASSWORD keystore password
SIGNING_KEY_ALIAS key alias
SIGNING_KEY_PASSWORD key password

Create the keystore once with keytool -genkeypair -v -keystore upload.jks -alias upload -keyalg RSA -keysize 4096 -validity 9125 and keep an offline backup — with Play App Signing (recommended, see below) a lost upload key can be reset, but a lost keystore without it means losing the ability to update the app. The keystore is written only to the runner's temp directory and never appears in logs or artifacts.

2. Google Play (optional, for the store listing)

Because of the new org.librespeed.speedtest application ID this is a brand-new app in the Play Console:

  1. Create the app in the Play Console and opt in to Google Play App Signing (Google holds the app signing key, the keystore above becomes just the upload key).
  2. Upload the first AAB manually through the Console (the publish workflow can only upload to an app that already exists). The AAB is produced by the release workflow as the librespeed-release artifact.
  3. Complete the mandatory listing steps: store listing texts and screenshots (the fastlane/metadata directory in this PR has en-US and cs-CZ ready), content rating questionnaire, privacy policy URL (PRIVACY.md needs to be reachable as a public URL — the raw GitHub link works), and the Data safety form — declare: no data collected by default; optional telemetry sends measurements, client info and IP address to the user-chosen test server; optional coarse location used on-device only, never transmitted.
  4. For workflow-driven uploads afterwards: create a service account in Google Cloud, link it under Play Console → API access, grant it the Release manager role, and store its JSON key as the PLAY_SERVICE_ACCOUNT_JSON secret. The manual publish workflow then uploads to internal/alpha/beta only (production is deliberately not offered).
  5. Per release: bump versionCode and versionName in app/build.gradle.kts, tag v<versionName> — the workflow fails if the tag and versionName disagree.

targetSdk 36 meets the Google Play target-API requirement for new apps and updates effective 31 Aug 2026.

3. Without Play

Tagged releases attach the signed APK to a draft GitHub release — that alone is enough for Obtainium users and for an F-Droid inclusion request later (reproducible-build tweaks for F-Droid would be a separate PR).

Known limitations / possible follow-ups

  • Language switch is Android 13+ (per-app locales); older versions follow the system language
  • The baseline profile uses hand-written wildcard rules; a macrobenchmark-generated profile is planned once the androidx.baselineprofile plugin supports AGP 9
  • Loss and bufferbloat are estimates derived from the ping side-channel, not raw packet captures
  • A full UI test suite with a mocked engine and history paging for very large datasets are intended follow-ups

BKPepe and others added 9 commits August 11, 2026 14:04
Gradle 9 with AGP 9 built-in Kotlin, a version catalog, SDK 37 and the
org.librespeed.speedtest application id.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Corrects the malformed User-Agent header, accepts any 2xx response,
tracks the IP version, estimates loss from unreturned pings and submits
telemetry to the tested server.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Four-tab UI following the design mockups: gauge with live graphs, server
management with favorites and distances, history with result and test
details, sharing as image, link or text, Czech translation and an
adaptive icon.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pull requests run lint, unit tests and a debug build plus dependency
review; tags build signed releases and fail without signing secrets.
Dependabot watches Gradle and Actions weekly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
README with current screenshots, privacy policy, security policy and
fastlane metadata in English and Czech.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Stability and bufferbloat measurements, server comparison, history
filters with export and trends, a diagnostic report, a licenses screen,
cellular details and a tabletop layout for foldables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Licenses generated by licensee, scheduled background tests, bufferbloat
guidance, a per-app language switch, 4G/5G labels, fold-crease layouts,
a baseline profile, NaN-safe stats, stricter URL validation and the
telemetry contract asserted through the real app path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Database migration, export, network detection and boundary tests,
screenshot goldens verified in CI, a recreation smoke test, the device
matrix with API 33, a docker LibreSpeed integration job, buildless
CodeQL and hardened workflows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 11, 2026 12:41

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@sstidl

sstidl commented Aug 12, 2026

Copy link
Copy Markdown

Since Google Play verification and costs are hindering us to use Play Store, the way for using F-Droid should be described better

@BKPepe

BKPepe commented Aug 12, 2026

Copy link
Copy Markdown
Author

F-Droid is a fine alternative, but I wouldn't frame it as F-Droid instead of Google Play. Most regular users will only look for apps on Google Play or the App Store, and since there are already many existing alternatives for speed tests, I doubt they will specifically look for this app on F-Droid.

Google Play is a one-time $25 registration with no recurring fees. The initial verification and closed testing are a one-off hurdle, but I don't think that's a reason to avoid it. Sideloading isn't a realistic option for ordinary users either.

So I'd go with both: Play for mainstream users, F-Droid/Obtainium as alternatives. Over time, we could drop some of the alternative distribution channels to reduce the maintenance and support burden.

The CLIs send <product>/<version> (<os>; <arch>; <platform detail>), so
send that same shape here: the app version, the Android release, the
primary ABI, and the device product, which bounds what a connection can
show in the first place.

Build properties are set by whoever built the ROM, so they are reduced
to a conservative alphabet and bounded before going into a header, where
a stray line break would split the request itself. The engine's built-in
default follows the same shape for the case where the app never sets
one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

3 participants