Skip to content

VeloSpot v1.0.24

Choose a tag to compare

@github-actions github-actions released this 28 Jun 14:56
250a6cf

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 via splash_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 by RideMaxSpeedPointTest and RideSpeedSegmentsTest. 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, new isMock column 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 via computeRideStatistics) 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 new RideStatisticsTest cases. 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, new archivedAt column 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 (the RideRecordingService, RideRecordingManager, the Quick Settings tile and the home-screen widget) used to import types and helpers straight from the feature.map.presentation package β€” 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 holder RideTrackingUiState now lives next to its producer in core.tracking, the locale-neutral ride formatters (formatRideDistance/Duration/Speed/Elevation) moved to a new core.format package, and the hasLocationPermission helper moved to a new core.location package. The presentation layer now depends on core (the correct direction) and no core/** file imports from feature/** 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 tiny SOURCE_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 (ContentResolver streams + DocumentsContract document creation) lived directly in MapViewModel, mixing Android framework / data-layer concerns into the presentation layer. It now lives in a dedicated, Hilt-injected GpxFileStore (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 off RecordedRide.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 so begin β†’ append … β†’ clear never race, mock (simulator) rides are excluded, and the whole flow degrades to a no-op if storage is unavailable. Covered by a new RideRecordingPersistenceTest.

  • 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. RideRecordingService now holds a PARTIAL_WAKE_LOCK for 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 the WAKE_LOCK permission.

  • 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_link and highway=trunk/trunk_link ("Kraftfahrstraßen", e.g. A1 feeders & ramps) are now hard-blocked for bikes (cost 10000, placed before the engine's min 9999 cap 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: a highway=cycleway, and any road carrying a mapped cycle track / lane (cycleway=track|lane and the cycleway:left|right|both variants, detected via new hascycleway / hasonroadcycleway / has_cycleway_infra helpers), 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 standard lookups.dat are 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 raw Double.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>, via GpxValidator) 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 by GpxWriterTest and a new GpxValidatorTest.

  • Offline routing no longer fails with "route data incomplete" when you start next to a sidewalk β€” every bundled profile declares noStartWay=footway,sidewalk so 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 a footway=sidewalk with 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. BRouterEngine now 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 fixes shortest.brf failing to parse β€” it referenced an invalid bicycle=…|official lookup value (official exists for other tags but not bicycle), which threw a ParseException and broke the BRouter profile tests in CI β€” and bumps PROFILES_VERSION 8 β†’ 9 so the corrected profiles actually replace the stale copies in internal storage on existing installs. A new BRouterProfileRoutabilityTest validates 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), RideTracker now 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 new RideTrackerTest cases.

  • 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)

  1. Download the APK below.
  2. On your Android device: Settings β†’ Apps β†’ Install unknown apps β†’ allow your browser or file manager.
  3. Open the APK and tap Install.

The F-Droid flavor is also available on F-Droid.