Export and share the recorded ride GPX (issue #47) #112

Merged
robert merged 1 commit from area/gpx-export into main 2026-09-05 17:06:01 +02:00
Owner

Closes #47.

Closes the loop #20 opened: a finished GpxRecorder/GpxWriter recording can now be shared straight to komoot/Strava/anywhere via the Android share sheet, named sensibly, and found again later from a ride-history list.

Filename (rideExportFilename, :companion:core, pure)

PedalPebble-<date>-<time>-<route>.gpx — route name sanitised (Windows-reserved characters stripped, whitespace collapsed to -, capped at 60 chars) and omitted entirely when absent or unusable after sanitising. Seconds-resolution so two rides on the same route on the same day never collide. RideGpxFiles (:companion:ride) now names the on-disk file with this same function — no rename-for-export step is needed, since FileProvider reports a shared Uri's display name as the underlying file's real name.

Sharing (GpxExporter, :companion:ride)

Intent.ACTION_SEND + FileProvider, matching current developer.android.com/reference/androidx/core/content/FileProvider guidance (verified 2026-09-05 against the live page, not recalled from memory):

  • <provider> manifest entry, android:exported="false", android:grantUriPermissions="true"
  • res/xml/file_paths.xml: <external-files-path name="gpx" path="gpx" /> — the same subdirectory RideGpxFiles already writes to (getExternalFilesDir("gpx"))
  • FileProvider.getUriForFile() + explicit FLAG_GRANT_READ_URI_PERMISSION

Verified against the real merged manifest: the ${applicationId} placeholder resolves to de.butzei.pedalpebble.debug.fileprovider for the debug variant, matching what GpxExporter mints its Uri against at runtime (context.packageName).

Ride history — what existed vs. what this issue built

Checked before building anything: nothing in this codebase persisted "what did I record" before this issue — #20 built the writer, not an index of what it wrote. So this is real, new, deliberately minimal scope:

  • RideRecord / RideHistoryStorage / RideHistory (:companion:core, pure) — mirrors RouteRecordStorage/RouteLibrary's storage/business-logic split exactly. No rename/favouriting/search — the issue only asks for "listed and re-exportable".
  • RideHistoryDatabase (Room) / RoomRideHistoryStorage / RideHistoryStore (:companion:ride) — this module's first Room database, its own table, deliberately not folded into :companion:route's RouteDatabase (different domain, wrong dependency direction).
  • RideHistoryScreen / RideHistoryActivity (:companion:ride) — this module's first Compose UI, same shape as :companion:route's RouteLibraryScreen: a list, newest first, share + delete per row.
  • RideService now records one RideRecord (file, started-at, active route name, moving distance) when a recording finishes, on a fire-and-forget Dispatchers.IO coroutine — documented honestly in its own KDoc as a best-effort write that could in principle lose a race against process teardown, not silently assumed reliable.

Heart rate / elevation

Already correctly optional end-to-end per #20's GpxWriter/GpxTrackPoint — unchanged by this issue. GpxRecorderTest's existing full round trip (write → GpxImporter re-import, asserting geometry/elevation/HR survive exactly) already covers this and is still green; not duplicated here.

komoot / Strava verification

Cannot be done from this sandbox — no komoot/Strava account, no real device. The closest available proxy — a real round trip through this project's own GpxImporter — already exists from #20 (GpxRecorderTest, "a full recording ... round-trips through GpxImporter") and passes. Real komoot/Strava verification needs Robert on a real device.

Testing

  • ./gradlew :companion:core:test — 257 tests, 0 failures (14 new: filename generation incl. sanitisation/truncation/collision cases, history list/delete ordering, timestamp display formatting in two zones/locales).
  • ./gradlew :companion:assembleDebug — real build succeeds: Room/KSP codegen for the new RideHistoryDatabase runs, Compose compiles (RideHistoryScreen/RideHistoryActivity both need and have @OptIn(ExperimentalMaterial3Api::class)), and the merged manifest's FileProvider entry was inspected directly (see above).
  • No attached device/emulator in this sandbox, so Room's generated code has not been exercised against a real SQLite database — matching RouteDatabase's own pre-existing honesty note from #25. RoomRideHistoryStorage's mapping functions have no Room API surface of their own but are, likewise, not independently unit-tested here (same gap RoomRouteRecordStorage already had).

https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt

Closes #47. Closes the loop #20 opened: a finished `GpxRecorder`/`GpxWriter` recording can now be shared straight to komoot/Strava/anywhere via the Android share sheet, named sensibly, and found again later from a ride-history list. ### Filename (`rideExportFilename`, `:companion:core`, pure) `PedalPebble-<date>-<time>-<route>.gpx` — route name sanitised (Windows-reserved characters stripped, whitespace collapsed to `-`, capped at 60 chars) and omitted entirely when absent or unusable after sanitising. Seconds-resolution so two rides on the same route on the same day never collide. `RideGpxFiles` (`:companion:ride`) now names the **on-disk** file with this same function — no rename-for-export step is needed, since `FileProvider` reports a shared `Uri`'s display name as the underlying file's real name. ### Sharing (`GpxExporter`, `:companion:ride`) `Intent.ACTION_SEND` + `FileProvider`, matching current `developer.android.com/reference/androidx/core/content/FileProvider` guidance (verified 2026-09-05 against the live page, not recalled from memory): - `<provider>` manifest entry, `android:exported="false"`, `android:grantUriPermissions="true"` - `res/xml/file_paths.xml`: `<external-files-path name="gpx" path="gpx" />` — the same subdirectory `RideGpxFiles` already writes to (`getExternalFilesDir("gpx")`) - `FileProvider.getUriForFile()` + explicit `FLAG_GRANT_READ_URI_PERMISSION` Verified against the real merged manifest: the `${applicationId}` placeholder resolves to `de.butzei.pedalpebble.debug.fileprovider` for the debug variant, matching what `GpxExporter` mints its `Uri` against at runtime (`context.packageName`). ### Ride history — what existed vs. what this issue built Checked before building anything: nothing in this codebase persisted "what did I record" before this issue — #20 built the writer, not an index of what it wrote. So this is real, new, deliberately minimal scope: - `RideRecord` / `RideHistoryStorage` / `RideHistory` (`:companion:core`, pure) — mirrors `RouteRecordStorage`/`RouteLibrary`'s storage/business-logic split exactly. No rename/favouriting/search — the issue only asks for "listed and re-exportable". - `RideHistoryDatabase` (Room) / `RoomRideHistoryStorage` / `RideHistoryStore` (`:companion:ride`) — this module's **first** Room database, its own table, deliberately not folded into `:companion:route`'s `RouteDatabase` (different domain, wrong dependency direction). - `RideHistoryScreen` / `RideHistoryActivity` (`:companion:ride`) — this module's **first** Compose UI, same shape as `:companion:route`'s `RouteLibraryScreen`: a list, newest first, share + delete per row. - `RideService` now records one `RideRecord` (file, started-at, active route name, moving distance) when a recording finishes, on a fire-and-forget `Dispatchers.IO` coroutine — documented honestly in its own KDoc as a best-effort write that could in principle lose a race against process teardown, not silently assumed reliable. ### Heart rate / elevation Already correctly optional end-to-end per #20's `GpxWriter`/`GpxTrackPoint` — unchanged by this issue. `GpxRecorderTest`'s existing full round trip (write → `GpxImporter` re-import, asserting geometry/elevation/HR survive exactly) already covers this and is still green; not duplicated here. ### komoot / Strava verification Cannot be done from this sandbox — no komoot/Strava account, no real device. The closest available proxy — a real round trip through this project's own `GpxImporter` — already exists from #20 (`GpxRecorderTest`, "a full recording ... round-trips through GpxImporter") and passes. **Real komoot/Strava verification needs Robert on a real device.** ### Testing - `./gradlew :companion:core:test` — 257 tests, 0 failures (14 new: filename generation incl. sanitisation/truncation/collision cases, history list/delete ordering, timestamp display formatting in two zones/locales). - `./gradlew :companion:assembleDebug` — real build succeeds: Room/KSP codegen for the new `RideHistoryDatabase` runs, Compose compiles (`RideHistoryScreen`/`RideHistoryActivity` both need and have `@OptIn(ExperimentalMaterial3Api::class)`), and the merged manifest's `FileProvider` entry was inspected directly (see above). - No attached device/emulator in this sandbox, so Room's generated code has not been exercised against a real SQLite database — matching `RouteDatabase`'s own pre-existing honesty note from #25. `RoomRideHistoryStorage`'s mapping functions have no Room API surface of their own but are, likewise, not independently unit-tested here (same gap `RoomRouteRecordStorage` already had). https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
Export and share the recorded ride GPX (issue #47)
Some checks failed
dev-artifact / build-pbw (push) Failing after 0s
dev-artifact / build-apk (push) Failing after 0s
dev-artifact / publish (push) Has been skipped
fast-lane / host-c-tests (push) Failing after 0s
fast-lane / jvm-tests (push) Failing after 0s
fast-lane / pebble-build (push) Failing after 0s
fast-lane / lint-and-secrets (push) Failing after 0s
fast-lane / meta-declares-required-jobs (push) Failing after 0s
fast-lane / host-c-tests (pull_request) Failing after 0s
fast-lane / jvm-tests (pull_request) Failing after 0s
fast-lane / pebble-build (pull_request) Failing after 0s
fast-lane / lint-and-secrets (pull_request) Failing after 0s
fast-lane / meta-declares-required-jobs (pull_request) Failing after 0s
e5f7841210
Closes the loop #20 opened: a finished GpxRecorder/GpxWriter recording can now be shared
straight to komoot/Strava/anywhere via the Android share sheet, named sensibly, and found
again later from a ride-history list.

- rideExportFilename (:companion:core, pure): "PedalPebble-<date>-<time>-<route>.gpx",
  route name sanitized (Windows-reserved chars stripped, whitespace collapsed to '-',
  capped at 60 chars) and omitted entirely when absent/unusable. Seconds-resolution so two
  rides on the same route on the same day never collide. RideGpxFiles (:companion:ride) now
  names the on-disk file with this same function, so no rename-for-export step is needed -
  FileProvider reports a shared Uri's display name as the file's real name.
- GpxExporter (:companion:ride): Intent.ACTION_SEND + FileProvider, matching current
  developer.android.com/reference/androidx/core/content/FileProvider guidance (verified
  2026-09-05, not recalled) - a <provider> manifest entry (exported="false",
  grantUriPermissions="true"), file_paths.xml (<external-files-path name="gpx" path="gpx"/>,
  the same subdirectory RideGpxFiles already writes to), FileProvider.getUriForFile() and an
  explicit FLAG_GRANT_READ_URI_PERMISSION.
- Ride history: nothing in this codebase persisted "what did I record" before this issue
  (#20 built the writer, not an index of what it wrote) - checked, not assumed. Added the
  minimal real slice: RideRecord/RideHistoryStorage/RideHistory (:companion:core, pure,
  mirrors RouteRecordStorage/RouteLibrary's storage/business-logic split) plus a Room-backed
  RideHistoryDatabase/RoomRideHistoryStorage/RideHistoryStore and a RideHistoryScreen/Activity
  (:companion:ride, this module's first Room DB and first Compose UI) listing rides newest
  first with share and delete actions. RideService now records one RideRecord (file, started-
  at, active route name, moving distance) when a recording finishes, on a fire-and-forget IO
  coroutine - documented honestly as a best-effort write that could in principle lose a race
  against process teardown, not silently assumed reliable.
- Heart rate and elevation: already correctly optional end-to-end per #20's GpxWriter/
  GpxTrackPoint; unchanged by this issue. GpxRecorderTest's existing full round trip (write ->
  GpxImporter re-import, asserting geometry/elevation/HR survive) already covers this and is
  still green - not duplicated here.
- komoot/Strava: cannot be verified from this sandbox (no accounts, no device). The closest
  available proxy - a real round trip through this project's own GpxImporter - already exists
  from #20 and passes; real komoot/Strava verification needs Robert on a real device.

./gradlew :companion:core:test: 257 tests, 0 failures (includes 14 new: filename generation,
history list/delete ordering, timestamp display formatting).
./gradlew :companion:assembleDebug: real build succeeds, including Room/KSP codegen for the
new RideHistoryDatabase and the merged manifest's FileProvider entry (confirmed authority
resolves to the debug variant's real package name via the ${applicationId} placeholder).
No attached device/emulator in this sandbox - Room's generated code has not been exercised
against a real SQLite database, matching RouteDatabase's own pre-existing honesty note; the
mapping functions in RoomRideHistoryStorage have no Room API surface of their own but are
likewise not independently unit-tested here, same as RoomRouteRecordStorage before it.

Claude-Session: https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt
robert merged commit e77ecdac18 into main 2026-09-05 17:06:01 +02:00
Sign in to join this conversation.
No description provided.