VeloSpot v1.0.24
VeloSpot v1.0.24
Added
- Animated branded launch screen β opening the app no longer shows a blank white screen while the map style and tiles load. A full-screen VeloSpot splash now covers the load with the brand-green gradient and the app logo: the location pin drops in with a bounce, gently breathes, and emits expanding GPS-ping rings (as if acquiring a fix), under the app name and a row of pulsing loading dots. It fades and scales away the moment the map is ready (
styleVersion > 0, with a short minimum so the entrance animation is seen). The cold-start window background is also themed (green gradient + logo viasplash_window_background) so even the very first pre-Compose frame is branded instead of white (VeloSpotSplash,MainMapScreen). - Top-speed marker & speed-coloured track when reviewing a ride β opening a recorded ride from My rides can now visualise where and how fast you rode. A red speech bubble marks the exact spot the ride hit its top speed and shows that value (anchored to the GPS fix whose recorded speed best matches the ride's stored peak,
RideMaxSpeedPoint), and the drawn track can be coloured by the speed ridden β a smooth green β amber β red ramp with red mapped to the ride's own peak (RideSpeedSegments, the polyline split into β€ 240 speed-tagged segments off the main thread,updateTrackSpeedLayer). Both overlays are driven by a compact toggle stack on the right edge, just below the menu button, that appears only while a ride is open; the two switches are persisted globally (RideViewOptions/RideViewPreferences) so your last-used choices apply to every ride you open next. Pure helpers covered byRideMaxSpeedPointTestandRideSpeedSegmentsTest. Localised in English and German. - Mock rides are flagged and kept out of your statistics β a ride recorded with the (debug) route simulator / "Mock tool" is now persisted as such (
RecordedRide.isMock, newisMockcolumn via Room migration v2 β v3) and clearly marked in My rides with a small "Mock" badge in the list and a "Recorded mock ride" indicator in the detail sheet. Synthetic rides no longer skew your real numbers: they are excluded from the statistics dashboard (totals, averages, personal records, streaks, fun facts viacomputeRideStatistics) and from the Ride heatmap and Ridden tracks map layers. The manager tags the ride the moment it receives its first simulated fix (RideRecordingManager.feedExternal). Covered by newRideStatisticsTestcases. Fully localised across all eight supported languages. - Archive rides β recorded rides can now be archived to declutter the timeline without deleting them. The ride-detail sheet gains an Archive / Restore action; archived rides drop out of the main My rides list (and its ride count) into a collapsible "Show archived (n)" section from which they can be reopened and restored at any time. The archive state is persisted (
RecordedRide.archivedAt, newarchivedAtcolumn via Room migration v2 β v3) and exposed through the repository (setRideArchived). Fully localised across all eight supported languages.
Changed
- Recording stack no longer depends "upwards" on the UI layer β the background ride-recording components in
core.tracking(theRideRecordingService,RideRecordingManager, the Quick Settings tile and the home-screen widget) used to import types and helpers straight from thefeature.map.presentationpackage β a Clean-Architecture inversion (inner layer depending on the outer UI). The three shared symbols were moved down into the core layer: the recording state holderRideTrackingUiStatenow lives next to its producer incore.tracking, the locale-neutral ride formatters (formatRideDistance/Duration/Speed/Elevation) moved to a newcore.formatpackage, and thehasLocationPermissionhelper moved to a newcore.locationpackage. The presentation layer now depends on core (the correct direction) and nocore/**file imports fromfeature/**anymore. Pure refactor β no behaviour change. - Far less map work on every GPS fix β a fresh GPS fix used to re-run the whole marker pass, re-serialising the entire parking / favourites / saved-places / route GeoJSON just to move the blue location dot. The live-location dot now has its own lightweight effect (
updateLocationDot, a tinySOURCE_LOCATION-only update) and the heavy marker pass is no longer keyed on the live position, so panning a fix no longer rebuilds every other layer (MainMapScreen,MapMarkerRenderer). Separately, the live recording track redraw β which rebuilds the full polyline each time β is now coalesced: a burst of fixes (e.g. a batched delivery after Doze, or the debug simulator) collapses into a single redraw via a short debounce instead of one redraw per fix. Together these cut the per-fix UI cost during navigation and ride recording. - GPX file I/O moved out of the map ViewModel β the ride GPX export/import Storage-Access-Framework plumbing (
ContentResolverstreams +DocumentsContractdocument creation) lived directly inMapViewModel, mixing Android framework / data-layer concerns into the presentation layer. It now lives in a dedicated, Hilt-injectedGpxFileStore(data.gpx) exposing three off-main-thread suspend functions βwriteDocument,writeDocumentsToTree,importRidesβ so the ViewModel just hands over the picked URIs / validated documents and reacts to a simple success / count / rides result. Pure refactor β no behaviour change.
Fixed
-
A mock ride's track is drawn again when you open it β a ride recorded with the debug route simulator carries no speed samples (its max speed is
0), so with the globally-persisted "colour track by speed" option on, the speed-coloured line rendered invisible (the colour ramp needs a positive peak) while the plain line was suppressed β leaving the mock ride with no track on the map. Mock rides are now always drawn as the plain line regardless of the toggle, and the "colour by speed" switch in the ride-inspection controls is disabled (greyed out) while a mock ride is open, keyed offRecordedRide.isMock(MainMapScreen,RideViewOptionsControls). -
A ride recording is no longer lost when the system kills the app mid-ride β the active recording lives in an in-memory singleton kept alive by the foreground service, but the OS can still kill the process under memory pressure, which used to drop the whole partial track. Every accepted fix is now streamed to disk as the ride is recorded (an append-only point file plus a tiny running-aggregates meta file,
RideRecordingPersistence); on the next launch an orphaned session is recovered and saved as a finished ride (held to the same "too short to keep" bar as a normal stop) so the track survives a crash/kill, then the recovery files are cleared. Writes are funnelled through a single ordered IO worker sobegin β append β¦ β clearnever race, mock (simulator) rides are excluded, and the whole flow degrades to a no-op if storage is unavailable. Covered by a newRideRecordingPersistenceTest. -
Recorded ride no longer thins out or freezes when the screen is off β a
location-typed foreground service keeps the process alive and location access allowed, but does not keep the CPU awake, so with the screen off and the device dozing the OS could defer/batch location callbacks and leave gaps in the recorded track.RideRecordingServicenow holds aPARTIAL_WAKE_LOCKfor the duration of an active recording (acquired when it goes foreground, released the moment recording ends, with a 12 h safety-net timeout against a leaked lock), so every GPS fix is delivered on time throughout a screen-off ride. Adds theWAKE_LOCKpermission. -
Bike routing no longer sends you onto motorways / trunk roads when a parallel cycleway exists β across all five BRouter profiles (
trekking,fastbike,gravel,mtb,shortest)highway=motorway/motorway_linkandhighway=trunk/trunk_link("KraftfahrstraΓen", e.g. A1 feeders & ramps) are now hard-blocked for bikes (cost10000, placed before the engine'smin 9999cap so it is a real exclusion just like motorways β not a high-but-passable cost) unless cycling is explicitly permitted there (bicycle=yes|designated|permissive) or the way carries its own bike infrastructure. In return, dedicated cycling infrastructure is rewarded: ahighway=cycleway, and any road carrying a mapped cycle track / lane (cycleway=track|laneand thecycleway:left|right|bothvariants, detected via newhascycleway/hasonroadcycleway/has_cycleway_infrahelpers), now gets a tangible discount so it clearly beats the bare main road of the same class running right next to it. Only tags present in the standardlookups.datare used. -
Cyclist avatar redrawn as a proper 3rd-person rider (no more "wheelie" or flat look) β the live-location avatar used to be a top-down sprite, which on the tilted 3D navigation map either stood straight up on its rear wheel (a "wheelie") or, when laid flat, looked squished. The vector (
ic_cyclist_avatar.xml) is now drawn as a true rear view: the back of the rider, the back of the helmet and the rear wheel nearest the viewer, with the arms reaching forward to a (narrowed) handlebar. Rendered as the existing upright billboard it now simply reads as a rider standing on the map seen from behind β no perspective faking needed, so the earlier bitmap pre-tilt (tiltBackwards) was removed. The programmatic pedalling/standstill legs (MarkerIconFactory.drawCyclistLegs) were re-positioned to straddle the rear wheel, and the navigation avatar is rendered a bit larger for presence (MarkerIconFactory.kt,MapStyleLayers.kt). -
Exported GPX files some apps refused to open β the ride GPX export is now hardened to be strictly standards-compliant so Garmin Connect, Strava and other readers accept it. Coordinates and elevation are written as fixed-precision, dot-separated decimals (
%.7f/%.1f,Locale.US) instead of rawDouble.toString, which could emit long float tails or scientific notation; XML-illegal control characters are stripped from the ride name (a single one made the whole file unparseable everywhere); and each track now carries a<type>cycling</type>. Every generated document is additionally validated (well-formed XML with at least one<trkpt>, viaGpxValidator) before it is shared or saved β a malformed file is never handed to the user; instead a clear error is shown (ride_export_invalid). Covered byGpxWriterTestand a newGpxValidatorTest. -
Offline routing no longer fails with "route data incomplete" when you start next to a sidewalk β every bundled profile declares
noStartWay=footway,sidewalkso a route never snaps its start onto the pavement, but BRouter enforces this by refusing to match the start waypoint to such a way; when the rider actually stood on / beside afootway=sidewalkwith no carriageway in catching range the start matched nothing, BRouter threw "from-position not mapped" internally, and the caller only saw an empty track β surfacing as "route data incomplete" for every profile the moment navigation started from such a spot.BRouterEnginenow retries once with the start-way guard disabled (check_start_way=0) as a fallback, so the rider still gets a route while the no-sidewalk-start preference is kept for the normal case (a genuinely impossible route, e.g. a missing segment tile, still fails on the retry β which is correct). The same pass also fixesshortest.brffailing to parse β it referenced an invalidbicycle=β¦|officiallookup value (officialexists for other tags but notbicycle), which threw aParseExceptionand broke the BRouter profile tests in CI β and bumpsPROFILES_VERSION8 β 9 so the corrected profiles actually replace the stale copies in internal storage on existing installs. A newBRouterProfileRoutabilityTestvalidates against the real BRouter engine that every bundled profile keeps ordinary cyclable roads (residential,tertiary,cycleway, β¦) routable, guarding against future profile edits that accidentally block normal roads. -
Phantom speed & distance from GPS drift while (nearly) stationary β a drifting GPS fix in the standstill / poor-reception case could be logged as real movement, inflating a recorded ride's speed and distance. On top of the existing guards (drop fixes whose reported accuracy is worse than 30 m, reject "teleport" outliers above the ~90 km/h absolute cap, smooth stored positions with a 3-fix moving average),
RideTrackernow also rejects a fix whose implied acceleration is physically impossible: between two reliable segments (β₯ 1 s apart) a change in speed exceeding 6 m/sΒ² (~0.6 g β e.g. 0 β 40 km/h in a single second) is discarded, while genuine sprints, hard braking and fast descents (~1β4 m/sΒ²) still pass. Distance/speed/moving-time stay measured on the raw fixes so the totals remain accurate. Covered by two newRideTrackerTestcases. -
2D/3D map-view tiles no longer show a muddy drop shadow β the selected perspective tile in the Map view sheet combined a translucent container colour (
accent @ 14 %) with an 8 dp elevation, so the Card's shadow bled through the semi-transparent tile and dirtied its interior and corners (visible as the grey smudge around the active 2D/3D button). The selected tile now uses an opaque tint β the accent colour composited over the sheet surface β and an explicit rounded shape, so the "lifted" state reads as a clean shadow that follows the rounded corners (NavigationViewSheet).
π₯ APK Variants
| File | Description |
|---|---|
VeloSpot-v1.0.24.apk |
F-Droid flavor β no Google services, ready to sideload; this is the APK F-Droid reproducibly verifies |
VeloSpot-v1.0.24-googlePlay.apk |
Google Play flavor β includes Google Play Services Location |
Installation (sideload)
- Download the APK below.
- On your Android device: Settings β Apps β Install unknown apps β allow your browser or file manager.
- Open the APK and tap Install.
The F-Droid flavor is also available on F-Droid.