- Kotlin 99%
- Shell 1%
Top-align the content, trim vertical padding (12->6dp) and shrink the travel-time figure (28->23sp), so the route, time and updated-at lines all fit a 1-cell-high widget instead of the timestamp clipping at the bottom. Bump to 0.1.4 (5). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UQD9jG6DUrEwVM2ar5j7Ao |
||
|---|---|---|
| .woodpecker | ||
| app | ||
| gradle | ||
| .gitignore | ||
| build.gradle.kts | ||
| build.sh | ||
| gradle.properties | ||
| gradlew | ||
| gradlew.bat | ||
| logo.png | ||
| README.md | ||
| settings.gradle.kts | ||
trafikapp
Android companion app for the Historic traffic API (trafikhistorik) — the same data the web UI shows, on your phone, plus home-screen widgets for live travel times.
It reads from the live environment at
https://api.trafikhistorik.jaduer.dk/; there is no login (the API is
read-only and unauthenticated).
Screens
Bottom-navigation tabs, mirroring the web UI:
- Begivenheder — traffic incident posts with nested status updates, filterable by Alle / Aktive / Afsluttede, keyset "Vis flere" pagination.
- Rejsetid — travel-time routes paired by opposite direction (A ↔ B), each
with a live "now" badge (current minutes + congestion factor ×) and a history
chart. Range picker
2t / 6t / 48t / 7d / 3u, same windows as the web. - Bus — bus delay over the day (minutes vs. planned departure time) plus a list of the largest delays.
- Odin — Odin messages, searchable by first report / station, offset pagination.
- Vejdirektoratet — road announcements, searchable by title / summary, offset pagination.
Charts use Vico (Compose, Material 3).
Rejsetid home-screen widget
Add the Rejsetid widget from your launcher and pick a route in the configuration screen. Each widget instance tracks one route and shows its current travel time, congestion factor and last-updated time. A WorkManager job refreshes every placed widget about every 15 minutes — matching the API's sampling cadence — so the value stays current without opening the app. Tapping the widget opens the app.
Stack
Native Kotlin + Jetpack Compose, minSdk 33 (Android 13+), targetSdk 36. Dependencies kept close to the API surface: Compose BOM + Material 3, Navigation, Lifecycle/ViewModel, OkHttp + kotlinx.serialization (hand-rolled typed client mirroring the API — no Retrofit/codegen), Vico (charts), Glance + WorkManager (widget). Tests: JUnit + MockWebServer.
The typed client and its models live in
app/src/main/kotlin/dk/jaduer/trafikapp/data;
note the /api/travel-times endpoints are not in the API's openapi.yaml
(they live in the Flask source), so the models there were derived from
trafikhistorik/app/routes/travel_times.py.
Building
Everything runs in Docker; no local Android toolchain needed.
./build.sh # unit tests + debug APK (default)
./build.sh test # just the unit tests
./build.sh assembleDebug # just the APK -> app/build/outputs/apk/debug/
./build.sh assembleRelease # R8-minified release APK -> app/build/outputs/apk/release/
Unit tests are plain JVM: the typed API client runs against a MockWebServer fake of the traffic API with fixtures shaped like the live responses.
assembleRelease works locally without any signing secrets — it just produces
an unsigned app-release-unsigned.apk, useful for checking that R8 shrinking
still builds cleanly. Signed release APKs are built by CI on tags; see
"Release/signing" below.
Installing
Signed APKs are attached to Forgejo releases on every version tag. Two ways to install:
- Direct: download
trafikapp-<tag>.apkfrom the latest release and open it on the device (enable "install unknown apps" for your browser/file manager once, when prompted). - Obtainium: add this repo as an app source (it auto-detects Forgejo/Gitea releases) and Obtainium tracks new tags. There is no Play Store listing.
Release/signing (for maintainers)
Release builds are signed with a dedicated app-signing keystore (never the
debug key) and R8-minified. The signing config in app/build.gradle.kts reads
the keystore path and passwords from environment variables and is only
applied when they're set — this is what lets local ./build.sh assembleRelease succeed unsigned, and lets CI sign only on tag builds where the
secrets are injected.
One-time: generate the keystore
keytool -genkeypair -v \
-keystore trafikapp-release.jks \
-alias trafikapp \
-keyalg RSA -keysize 4096 -validity 10000
Keep the resulting .jks and its passwords somewhere durable and private
(a password manager, not this repo) — losing it means future releases can no
longer update installs signed with it.
Woodpecker secrets
.woodpecker/build.yaml builds and signs the release APK only on tag events,
using these repo secrets:
| Secret | Value |
|---|---|
trafikapp_keystore |
base64 -w0 trafikapp-release.jks output |
trafikapp_keystore_password |
keystore password |
trafikapp_key_alias |
key alias (trafikapp above) |
trafikapp_key_password |
key password |
gitea_token |
Forgejo API token with write access to releases |
Set them in the Woodpecker UI under the repo's Secrets settings. On a tag push,
CI decodes the keystore to a temp file, runs assembleRelease with
TRAFIKAPP_KEYSTORE/TRAFIKAPP_KEYSTORE_PASSWORD/TRAFIKAPP_KEY_ALIAS/
TRAFIKAPP_KEY_PASSWORD set, renames the output to
trafikapp-${CI_COMMIT_TAG}.apk, and attaches it to the matching Forgejo
release.