GPX share-sheet import: parser, simplification, intent filter (#24) #78
No reviewers
Labels
No labels
area:companion
area:docs
area:shared
area:tooling
area:watchapp
blocker
kind:chore
kind:feature
kind:spike
kind:test
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
robert/PedalPebble!78
Loading…
Reference in a new issue
No description provided.
Delete branch "area/gpx-share-import"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements #24: GPX share-sheet import intent, parser and simplification, including the acceptance
criteria added in the 2026-09-01 edge-case update.
:companion:core — the parser (pure Kotlin/JVM)
GpxImporter.kt: a streaming SAX parser handling both<trk>/<trkpt>tracks and<rte>/<rtept>routes (bikerouter.de and RideWithGPS emit routes, not tracks). Multiple
<trkseg>are flattened intoone continuous polyline in document order, so the seam is never mistaken for a turn (there is no
per-point turn detection at import time at all — that is the enrichment chain in #63). A second
<trk>or
<rte>element is rejected by name and count rather than silently truncated to the first. Missing<ele>and missing/duplicate<time>are tolerated. Waypoints (<wpt>) are preserved separately fromthe polyline (FR-N12, #63). Malformed XML and GPX with no usable geometry are rejected with a message
naming the specific problem.
PolylineSimplifier.kt: iterative (non-recursive, so a pathological zigzag input can't blow thestack) Ramer-Douglas-Peucker simplification at a 5 m perpendicular-distance tolerance — corner-preserving
by construction, which is what "roughly 5 m spacing, preserving corners" actually calls for.
EntityResolveras the real XXE guarantee (not reliant on Xerces-only feature strings, since Android'sbuilt-in parser is Expat-backed and won't necessarily recognise them — those are applied best-effort,
wrapped so an unsupported feature doesn't fail parser setup), and hard caps on input size (32 MiB),
point count (200,000) and element nesting depth (40) so a pathological file aborts mid-parse instead of
exhausting memory.
multi-segment, multi-track/route rejection, missing elevation, missing/duplicate timestamps, waypoints,
malformed/empty) plus the XXE, oversize, point-cap and nesting-depth guards, and a dedicated
PolylineSimplifierTestproving straight runs collapse and corners survive../gradlew :companion:core:testis green (21 tests total including the pre-existing skeleton test, 0 skipped/failed).
:companion:route — the share-sheet landing point
AndroidManifest.xml:GpxShareImportActivityregistered forACTION_SENDandACTION_VIEW, on boththe correct
application/gpx+xmlMIME type and a*/*+*.gpxpathPatternfallback for sourcesthat share a
.gpxunder a generic or missing MIME type.GpxShareImportActivity.kt: reads the sharedUriviaContentResolveronDispatchers.IO, callsinto
GpxImporter, and shows the outcome. Deliberately plainandroid.app.Activityrather than AndroidXComponentActivity(not worth pulling AndroidX Activity/Lifecycle into this module for one screen), andDispatchers.IOrather than.Main—kotlinx-coroutines-androidisn't on this module's classpath, soDispatchers.Mainwould throw at runtime; the result is posted back viarunOnUiThreadinstead.de/enstring resources for the status messages.Left out of scope, tracked separately
11+ package-visibility behaviour for the intent filters (D36) — needs Robert's phone, not something a
JVM test can prove.
file parse, and if not, why".
<rtept>name/desc turn-cue text (bikerouter.de's tier-0 cues, FR-N15/D26) is deliberately notextracted here — it belongs to the enrichment chain in #63, which reads the source GPX itself.
*/*+pathPatternintent-filter fallback only matches when the sharedUri's path isfilename-shaped, which isn't guaranteed for every content provider (some hand back opaque document ids)
— documented as a known limitation in the activity's kdoc, not something fixable without a real share
from a real app.
docs/REQUIREMENTS.mdalready listed FR-N1 as "Covered — #24"; verified still accurate, no change made.https://claude.ai/code/session_01DAoXbRmJUf2uxNYBfdAXPt