Skip to content

VeloSpot v1.0.23

Choose a tag to compare

@github-actions github-actions released this 23 Jun 21:52

VeloSpot v1.0.23

Added

  • GPX export & import for ridesMy rides now has Import and Export actions. Tapping Export turns the list into a multi-select (checkboxes + a Cancel/Export bar); confirming with several rides asks whether to combine them into one GPX file or write one file per ride, then asks for the destination: Share (system share sheet → save to Files, Drive, e-mail, …) or Save to file (a Storage Access Framework picker — Create document for a single file, or pick a folder for separate files). A single selection skips the layout question, the file named after the ride. Import opens a document picker and reads each <trk> of the chosen GPX file(s) back into a ride (keeping its <name>), deriving distance, elevation and — when the GPX is time-stamped — duration/speeds. Built on a dependency-free GpxWriter/GpxParser/GpxRideFactory (core/gpx) and a cache + FileProvider share path / SAF save path (GpxExporter). Fully localised across all eight supported languages.
  • Named rides — recorded rides now carry a name instead of just a date. An auto-recorded navigation ride is named after its destination (the reverse-geocoded place of the destination coordinate, e.g. "Trier", falling back to the destination's own label); a generated round trip becomes "Round trip - {current place}" (localised, ride_round_trip_name). Finishing a manual recording now opens a name prompt pre-filled with the reverse-geocoded current place (cancel keeps the ride recording, an empty field saves it unnamed). The name is shown in the My rides list and the ride-detail sheet, and can be edited anytime via the pencil in the detail sheet. Resolved in MapViewModel.resolveAndSetAutoRideName / requestStopRideTracking (reverse-geocoded via NominatimGeocoder.reverseGeocodePlace) and persisted with the ride (RecordedRide.name, Room migration). Fully localised across all eight supported languages.
  • Pedalling cyclist avatar during navigation — the live-location cyclist now visibly pedals while you ride. The rider's legs, shoes and pedals are no longer baked into the static ic_cyclist_avatar vector; they're drawn programmatically (MarkerIconFactory.drawCyclistLegs) as two knees-bent legs whose feet sweep forward/back 180° out of phase, mimicking a turning crank seen from above. The navigation controller pre-renders a strip of pedal frames (NAV_PEDAL_FRAME_COUNT, navPedalFrameImageId) and its per-frame Choreographer loop swaps the avatar image each tick, with the crank phase tied to the rider's along-route distance (PEDAL_METERS_PER_REV) so the cadence matches the real ground speed — and naturally freezes when you stop. Whenever the rider has (nearly) stopped — whether idle on the map or waiting at a traffic light mid-navigation — the avatar now drops into a believable standstill pose: the rider plants one foot flat on the ground beside the bike (a wider, flattened shoe sells the ground contact) while the other foot stays on the raised pedal, instead of hovering frozen mid-stroke. Off-route the resting marker uses it by default; during navigation a dedicated foot-down frame (navIdleFrameImageId) is shown once the eased ground speed drops below PEDAL_STANDSTILL_SPEED_MPS (idle flag in createLocationMarkerIcon / drawCyclistLegs).
  • "Keep screen on while riding" toggle — the display is now kept awake during both active navigation and a live ride recording (previously only navigation prevented the screen from dimming/locking). A new switch in the Settings → Appearance & map sheet lets you turn this off (e.g. to save battery and rely on voice guidance). It's on by default and persisted (KeepScreenOnPreferences, MapViewModel.keepScreenOnEnabled). Fully localised across all eight supported languages.
  • Legal notice (Impressum) — the project now carries a proper imprint with the responsible party's name and postal address (§ 5 DDG). It's reachable everywhere: a new Imprint section in the in-app About sheet (shown inline, so it stays offline-accessible and localised across all eight languages), a dedicated imprint.html page on the website linked from the navigation and footers of the home and privacy pages, a root IMPRINT.md, and the contact/data-controller blocks of both PRIVACY.md files and the privacy page now name the full address.
  • CODEOWNERS — a .github/CODEOWNERS file (* @drzeeb) so GitHub automatically requests the maintainer's review on every pull request (including Renovate bot PRs) and branch protection can require code-owner reviews.
  • "Ridden tracks" map layer — a new Layers overlay draws every recorded ride as its own thin, translucent line, so you can see everywhere you've been at a glance. Because the lines are semi-transparent, overlapping passes build up colour and frequently used streets read stronger — a lightweight, route-preserving complement to the existing Ride heatmap. Each track is reduced with Ramer–Douglas–Peucker simplification (RideTrackLines, ~8 m tolerance, typically −80–95 % points) off the main thread before drawing, the hairline width scales with zoom, and the layer sits beneath the map pins (which stay tappable). It's off by default and persisted (MapLayerCategory.TRACKS, updateTracksHistoryLayer); the pure aggregation is unit-tested (RideTrackLinesTest). Fully localised across all eight supported languages.

Changed

  • Closing a ride returns to the rides list — closing a recorded ride's detail sheet (the or the back gesture) now reopens the My rides list it was opened from, instead of dropping back to the bare map, so browsing several rides in a row no longer means reopening the list each time (MainMapScreen).
  • Saved-ride detail no longer blocks the map — opening a recorded ride from My rides used to show its statistics in a modal bottom sheet, whose scrim swallowed all touches so the drawn ride track couldn't be panned, pinched or zoomed. The detail view is now a non-modal, draggable sheet: it overlays the map without a scrim, only its own surface consumes touches, and it can be dragged down to a small peek (or closed with the button) to free up the map while the ride polyline stays drawn. It starts fully expanded so all stats are visible immediately (RideDetailSheet, rendered inside the map layout). Fully localised across all eight supported languages (ride_detail_close, ride_detail_drag_hint).

Fixed

  • Debug route simulation can now be paused & resumed (and no longer makes the rider run away) — the (debug-only) GPS route simulator's play/stop button now works as a proper play / pause: pressing it again pauses the run (keeping the position) and pressing play resumes from where it left off instead of restarting at the route start (RouteSimulator.travelledMeters + startOffsetMeters; a reroute/new route still restarts fresh). Separately, stopping the simulator used to leave the navigation avatar coasting on to the end of the route: the simulator stops sending fixes, but the navigation puck advances by dead-reckoning (NavigationManager predicts position from the last speed between sparse GPS fixes) and never saw a "standing still" fix to slow it down. Pausing/stopping now feeds one final stationary fix (speed 0) at the current position so the puck eases to a stop. Covered by RouteSimulatorTest (resume/reset) and NavigationControllerTest (stationary fix).
  • Flaky MapViewModelTest on CI — the test built fresh MapViewModels but never tore them down, so two kinds of coroutine outlived each test and kept running on background threads: the view-models' own viewModelScope collectors (location, favorites, route-simulation flows) and — because navigation auto-starts a ride recording that the tests never stop — the process-level RideRecordingManager's endless 1 s stats ticker and GPS collector. An exception from such a leaked coroutine (often after resetMain()) then surfaced against the next test as UncaughtExceptionsBeforeTest (e.g. toggleFavorite…, startInAppNavigation…), failing the suite intermittently. RideRecordingManager's background scope is now injectable (Hilt still supplies the real Dispatchers.Default one); the test hands each manager a real, cancellable scope and, in tearDown, cancels every manager scope and clears every view-model — so nothing leaks across tests. (A test scheduler is deliberately not used for the manager, as advanceUntilIdle() would spin forever on the ticker's endless delay loop.)
  • Bogus ride "max speed" from GPS Doppler spikes — a recorded ride could report a wildly wrong top speed (e.g. 70 km/h / 52 km/h on rides where nothing close was ridden). The cause: RideTracker took the GPS-reported instantaneous speed (speedMps, the receiver's Doppler velocity) directly as the peak, gated only by an absolute 90 km/h ceiling. That sensor value can briefly glitch to 2–5× the real speed — typically on a low-accuracy fix — while the position barely moved; the existing "teleport" filter only validates the position-derived speed and so never caught these. A peak-speed sample is now accepted only when corroborated by the track geometry: there must be a reliable position-derived baseline (fixes ≥ 1 s apart) and the reported speed may not exceed it by more than 1.5× (SPEED_CORROBORATION_FACTOR), which discards the Doppler spikes while still honouring genuine fast (e.g. downhill) stretches. Covered by a new RideTrackerTest case.
  • Accessibility (a11y) — TalkBack/screen-reader support across the map UI: sheet titles are now exposed as headings (SheetHeader + every sheet title) so screen-reader users can navigate by heading; the record-ride FAB (idle red-dot state) now has an accessible name (ride_start); the 2D/3D navigation tiles and the offline routing-profile rows expose Role.RadioButton + selected state so the active choice is announced; layer toggle cards and address-search result rows merge their contents into a single focusable element. New strings localised across all eight languages.

📥 APK Variants

File Description
VeloSpot-v1.0.23.apk F-Droid flavor – no Google services, ready to sideload; this is the APK F-Droid reproducibly verifies
VeloSpot-v1.0.23-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.