Re-exported at 46a0726, which adds README.md plus docs/ARCHITECTURE, DEVELOPMENT, TESTING, v1 history including the original brief, and a v3 backlog. 112 files, and the bundle now carries 19 commits. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
117 lines
5.6 KiB
Markdown
117 lines
5.6 KiB
Markdown
# Rippr
|
||
|
||
A native Android GPS ride recorder for motorcycles. Records telemetry from an
|
||
un-killable foreground service into a local SQLite database, renders the traversed path
|
||
on a map afterwards, and exports to GPX/GeoJSON.
|
||
|
||
Built for one specific use: **hit start, put the phone in a pocket, ride, hit stop.**
|
||
Every design decision below follows from that.
|
||
|
||
```
|
||
Package com.rippr minSdk 26 (Android 8.0) targetSdk 35
|
||
Kotlin 2.0.21 AGP 8.7.3 Gradle 8.11.1
|
||
~5,800 lines Kotlin 84 unit tests 46 instrumented tests
|
||
```
|
||
|
||
## Current state — v2.0.1
|
||
|
||
| Feature | Status |
|
||
|---|---|
|
||
| GPS recording via foreground service | Working, validated on real rides |
|
||
| Trips with pause/resume/stop/discard | Working |
|
||
| Live speed + elapsed clock | Working (fixed in 2.0.1) |
|
||
| Path rendered on OpenStreetMap | Working (fixed in 2.0.1) |
|
||
| Ride statistics + charts | Working; elevation gain has known drift |
|
||
| Rename / delete / merge rides | Working |
|
||
| GPX + GeoJSON export | Working, validated against an external parser |
|
||
| Upload to a REST endpoint | Implemented, **no UI** — see below |
|
||
| Live map during recording | Deliberately absent — see below |
|
||
| Group ride view | Deferred to v3 |
|
||
|
||
## Quick start
|
||
|
||
Requires **JDK 17–21** (AGP does not support 25) and the Android SDK with platform 35 and
|
||
build-tools 35. Full setup in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md).
|
||
|
||
```bash
|
||
echo "sdk.dir=$HOME/Library/Android/sdk" > local.properties
|
||
./gradlew assembleDebug testDebugUnitTest lintDebug
|
||
./gradlew connectedDebugAndroidTest # needs a device or emulator
|
||
```
|
||
|
||
## Architecture in one page
|
||
|
||
```
|
||
TrackingService (foreground)
|
||
└─ FusedLocation callback ──trySend──► unbounded Channel
|
||
│
|
||
single writer coroutine (batches of 25, ~2s)
|
||
│
|
||
┌───────────────────┴────────────────────┐
|
||
▼ ▼
|
||
Room database Accumulator
|
||
Trip → Segment → TrackPoint distance / moving time /
|
||
│ elevation, folded per batch
|
||
▼
|
||
TripRepository (all lifecycle transitions)
|
||
│
|
||
┌─────────────────┼──────────────────┐
|
||
▼ ▼ ▼
|
||
RecordScreen TripsScreen TripDetailScreen
|
||
└─ RideMap (osmdroid)
|
||
```
|
||
|
||
Three decisions carry most of the weight:
|
||
|
||
**The location callback never blocks.** Fixes go into an unbounded `Channel`; a single
|
||
writer coroutine drains it in batches. Slow disk stalls the writer, never GPS, and no fix
|
||
is dropped under back pressure.
|
||
|
||
**Recording state lives in the database, not memory.** A row in `trips` with
|
||
`endedAt IS NULL` *is* the fact of an in-progress ride. It survives process death, which
|
||
an in-memory flag cannot — `START_STICKY` restarts the service with a null intent and a
|
||
flag would come back `false` mid-ride.
|
||
|
||
**Segments make pause correct rather than cosmetic.** Without them, pausing at a gas
|
||
station and resuming across town draws a straight line through terrain never ridden, and
|
||
counts it as distance. Segments propagate all the way through: distance accumulation, map
|
||
polylines, and GPX `<trkseg>`.
|
||
|
||
Full reasoning in [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md).
|
||
|
||
## Two deliberate absences
|
||
|
||
**No live map while recording.** The phone is in a pocket; nobody is looking at it. A live
|
||
map would burn battery on top of GPS and a wake lock for nothing. The map is a post-ride
|
||
artifact, gated behind a toggle, and `TrackingService` holds no reference to any map type.
|
||
|
||
**No UI for the upload endpoint.** The REST uploader works and is tested, but is only
|
||
reachable via `Config.setUploadEndpoint()`. It has no server to talk to yet; the UI arrives
|
||
when group-ride streaming does.
|
||
|
||
## Documentation
|
||
|
||
| Document | Contents |
|
||
|---|---|
|
||
| [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md) | Durable design decisions and the reasoning behind them |
|
||
| [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) | Toolchain setup, build/test commands, emulator harness |
|
||
| [docs/TESTING.md](docs/TESTING.md) | What is covered, what is not, and what the emulator cannot verify |
|
||
| [docs/v1/](docs/v1/) | Original spec, what shipped, and how it deviated |
|
||
| [docs/v2/](docs/v2/) | 18-task plan, per-task docs, and the progress log with every bug found |
|
||
| [docs/v3/BACKLOG.md](docs/v3/BACKLOG.md) | Known gaps, deferred work, and ideas for next |
|
||
|
||
**Start here for v3:** [docs/v3/BACKLOG.md](docs/v3/BACKLOG.md), then
|
||
[docs/v2/PROGRESS.md](docs/v2/PROGRESS.md) for the bugs that were found and why.
|
||
|
||
## Known gaps
|
||
|
||
1. **Elevation gain drifts** ~30 m per ten stationary minutes against synthetic noise.
|
||
Real GPS error is correlated rather than uniform, so the true figure needs a real ride.
|
||
Flat ground should read near zero.
|
||
2. **No Compose UI tests** for any of the six screens. Verification was manual.
|
||
3. **Speed colouring on the map is unvalidated** — the emulator reports zero velocity, so
|
||
the path renders uniformly there.
|
||
4. **Migrations are now mandatory.** `fallbackToDestructiveMigration()` was removed in v2;
|
||
any schema change must ship a `Migration` against
|
||
`app/schemas/com.rippr.data.AppDatabase/2.json`.
|