Imported from zingo/Crazy_Capy_Randonneur (
AGENTS.md). Install upstream withnpx skills add zingo/Crazy_Capy_Randonneur. Copyright stays with the author.
AGENTS.md
Guidance for AI coding agents working in this repository.
Project
Crazy Capy Randonneur — open-source, voice-first, battery-extreme GPS bike navigator for Android 13+ (API 33). No Play Services, no accounts, no cloud.
- App package / namespace:
com.crazycapy.randonneur minSdk = 33,targetSdk = compileSdk = 35- Stack: Kotlin 2.1.21, Jetpack Compose (Material 3), MapLibre + OpenFreeMap, Gradle 8.13 / AGP 8.13. No Play Services.
- Package layout under
app/src/main/java/com/crazycapy/randonneur/:gpx— GPX/TCX/KML loading, track/waypoint model, GPX writernav— guidance core:NavEngine,TurnAnalyzer,PoiTracker,Geo(pure Kotlin, unit-tested; no Android deps)voice— spoken phrases (Phrases,TurnSummary), beeps (BeepPlanner)service— foregroundNavigationService: GPS, ghost-ride driver, TTS, beeps, wake lock, living notificationstate—RideStore(shared app state),RouteStore(on-device route library + settings/last-ride persistence)sim— ghost ride simulatorui— Compose overlays: HUD + next-turn preview, off-route ack, dialogsble— BLE heart-rate provider (stub by default)
Build environment
JDK 21 is required and is NOT on the default PATH. Always set:
export JAVA_HOME=/home/zingo/.local/opt/jdk-21.0.12+8
export PATH=$JAVA_HOME/bin:$PATH
adb lives at /home/zingo/Android/Sdk/platform-tools/adb. Known devices:
- Tablet (Wacom DTHA116, Android 14 / API 34) — preferred dev device. Needs a
udev rule for Wacom (vendor
056a) or it shows "no permissions":SUBSYSTEM=="usb", ATTR{idVendor}=="056a", MODE="0666", GROUP="plugdev"in/etc/udev/rules.d/51-android.rules. - Tablet (DOOGEE T20Mini) — second test device, the API 33 (minSdk) floor. Also
needs a udev rule (it enumerates as USB vendor
18d1):SUBSYSTEM=="usb", ATTR{idVendor}=="18d1", MODE="0666", GROUP="plugdev"in/etc/udev/rules.d/51-android.rules. - Phone (Pixel 7 Pro)
- Emulator:
emulator-5554
Commands
# Build debug APK (always run before installing — the APK file is stale otherwise)
./gradlew :app:assembleDebug
# Lint
./gradlew :app:lintDebug
# Unit tests (pure Kotlin core — fast, no device)
./gradlew :app:testDebugUnitTest
# Instrumented tests (runs on ALL attached devices/emulators)
./gradlew :app:connectedDebugAndroidTest
# Install to phone / emulator
adb -s <device-serial> install -r app/build/outputs/apk/debug/app-debug.apk
After changing code, run ./gradlew :app:assembleDebug :app:lintDebug and the
unit tests; run the instrumented suite when behavior touching the service or
notification changed. Currently 119 unit tests + 4 instrumented ghost-ride tests.
Device test notes:
- The phone's notification is blocked at OS level (
dumpsys notificationshowsAppSettings: com.crazycapy.randonneur importance=NONE) — a deliberate block, not an app bug; don't re-investigate as one. - For no-network tests use the per-app network deny on the emulator (rooted),
not airplane mode on the phone:
adb -s emulator-5554 shell cmd connectivity set-package-networking-enabled false com.crazycapy.randonneur(verify withget-package-networking-enabled→:deny). The emulator app data is root-readable, so clone the phone's routes + pre-cache withrun-as/tarto test offline navigation there. - The emulator's software GL renders
MapSnapshotterunreliably (90 s snapshot timeouts between successes), so full pre-cache runs belong on the phone; use the emulator for offline rides and pure-logic verification. - Pre-cache is honest about partial runs: the dialog reports e.g. "Pre-cached 34/347 · rest on next load" when some turns were missed, and later loads resume where they left off (skips existing files, retries the rest).
- Power measurement: an AVHzY/YK-Lab "Shizuku" USB power meter (Korona YK003C,
/dev/ttyACM0) can log ~1 kHz V/A/W for comparing builds/settings/other apps; usetools/avhzy-monitor.py(record/compare/windows/report/diff/promote/condense, seetools/README.md). Runs are chunk- condensed by default (--chunk 300) and live in the git-ignoredpower-runs/condensed/. For repeatable ghost-ride runs usetools/ghost-power-run.sh(drives the ride over adb, starts at the same route point, cycles the screen to a target on-%, waits out charging, and hasfast/nano/small/medium/long/ultrapresets; default routetools/routes/ystad-onnekoppinge-harlosa.gpx). It relies on the debug-onlyDebugControlReceiverinapp/src/debug/, which never ships in release.tools/power-baseline.mdis the committed reference table;tools/power-report.shbuilds a local report, diffs it against the baseline, and can promote it. Light vs dark map makes no measurable power difference.
Conventions
-
Every source file carries the Apache-2.0 SPDX header:
/* * Copyright (c) 2026 Crazy Capy Randonneur contributors * SPDX-License-Identifier: Apache-2.0 */The six files under
es/jjrh/bikeradar/ipc/(in bothaidl/andjava/) are the exception. They are unmodified copies from android-bike-radar-overlay and keep their own Apache-2.0 header and copyright line. Do not edit them and do not normalise their headers: keeping that notice is what the licence asks of a copy, and changing one line makes it a modified file with obligations of its own. See THIRD_PARTY_NOTICES.md. -
Do NOT add code comments unless asked; keep code self-explanatory.
-
Pure logic goes in
nav/voice/gpx(unit-testable, no Android imports). KeepNavigationServicefor Android-touching glue only. -
Distance phrasing: use
Phrases.formatDistance/formatShort(unitsm/km,0→"0 m"). Speed shown as e.g.32.0 km/hviaformatKmh. -
HUD is a 3×2 grid (Speed | distance covered | map zoom / Average | distance remaining | tap-to-cycle ETA mode) with a north-up
TurnPreviewbeside it. The main map is always north-up. -
On-map controls: pinch to zoom (no zoom buttons); a bottom-centre
MapScaleBarsized from MapLibre'sProjection.getMetersPerPixelAtLatitude, and a top-rightMapCompassshown only when the map is rotated (tap = snap north). The bottom-right button stack is centre-on-me (when following is released), dark/light toggle, then the settings gear at the very bottom. -
Beeps are decoupled from the turn popup via a
turnActiveflag inNavigationService; volume comes fromRideStore.beepVolume/navVolume(0 = off), persisted byRouteStore. -
Settings UI lives in
MainActivity.kt(dialogs); new settings need aRideStorefield + aRouteStoresave/load key. -
Tests: JUnit for JVM unit tests (currently 119), instrumented ghost-ride tests in
app/src/androidTest.
User shorthand
- "flash" or "/flash" = build the debug APK and install it on the phone
(see
.agents/commands/flash.mdand.agents/skills/flash/SKILL.md). - "pr" or "/pr" = push the current branch and open a GitHub PR against
main(see.agents/commands/pr.mdand.agents/skills/pr/SKILL.md).
Docs
README.md— user-facing features, build/run/test instructions, screenshots.PLAN.md— development plan / status. Keep both in sync when features change.tools/README.md— developer tools: the power-measurement toolkit, emulator helper, and agent commands. Document new tools there.- Screenshot files are named
docs/screenshots/YYYY-MM-DD-NN-name.pngso stale captures are easy to spot (deleting the old file before capturing the new one).
