VeloSpot v1.0.20
VeloSpot v1.0.20
Added
- GitHub community health files β added the standard community documents to meet the GitHub Community Standards and make the project easier and safer to contribute to: a Code of Conduct (
CODE_OF_CONDUCT.md, Contributor Covenant v2.1), a Contributing guide (CONTRIBUTING.md, tailored to VeloSpot'sgooglePlay/fdroidflavours, JDK 17, BRouter submodule and the real Gradle build/test commands), a Security policy (SECURITY.md, private vulnerability reporting via GitHub Security Advisories), structured issue templates (.github/ISSUE_TEMPLATE/β bug report, feature request and a chooser config), a pull request template (.github/PULL_REQUEST_TEMPLATE.md) and a funding link (.github/FUNDING.yml, Buy Me a Coffee). - Comprehensive ride statistics dashboard in "My rides" β the My rides sheet now leads with a rich, collapsible Statistics card that crunches your whole ride history into every metric a data nerd could want, all derived purely from the already-stored rides (no extra storage). It's collapsed by default (tap the header to expand) so the ride list stays uncluttered, and groups the numbers into five sections:
- Totals β ride count, total distance, total & moving time, cumulative elevation gain β and loss β.
- Averages β Γ distance, Γ duration, Γ speed (distance-weighted across moving time) and Γ climb per ride.
- Personal records (highlighted chips) β top speed, longest ride, longest duration, best Γ speed and biggest single climb.
- Activity β first-ride date, distinct active days, current & longest day streak (consecutive calendar days), plus rides + distance this week and this month.
- Fun facts β COβ saved vs. an average car (~120 g/km), calories burned (~30 kcal/km) and your share of a full lap around the Earth (40,075 km).
- Pure, side-effect-free computation (
computeRideStatistics) feeding a chip-based, wrapping flow layout (RideStatisticsSection). Fully localised across all eight supported languages.
- Share a recorded ride as a "VeloSpot Wrapped" card β a new Share ride button in the ride detail sheet ("My rides" β tap a ride) opens a preview dialog that renders the ride as a bold, vertical 1080Γ1350 (4:5) social-media tile, ready for WhatsApp, Telegram, Instagram and the like. The card shows a real 2D map cutout of the route behind a glowing GPS track, the headline distance, the date and the key ride statistics (time, Γ speed, elevation gain, max speed).
- Off-screen, deterministic rendering β the card is drawn directly onto a
Bitmapwith the platformCanvas(no Compose lifecycle, no charting dependency) on a background thread (RideShareCardRenderer), so it is fully reproducible. The tile has slightly rounded corners. - Map cutout that lines up with the route β
RideRouteMapSnapshotteruses MapLibre'sMapSnapshotterto render the ride's bounding box off-screen (no on-screenMapView); track points are projected with the snapshot's ownpixelForLatLng, so the polyline sits exactly on the streets. OSM attribution is drawn into the panel. If the snapshot can't be produced (offline, error or 8 s timeout), the card falls back to a clean themed-gradient panel, so sharing always works. - Colour theme picker with live preview β six hand-picked themes (Aurora, Sunset, Forest, Ocean, Berry, Midnight) restyle the gradient, accent and route/marker colours; tapping a swatch re-renders the preview live. The (expensive) map snapshot is fetched once and reused across theme changes. The route uses a contrast halo + a theme-coloured glow over a tinted scrim so it stays legible on any basemap.
- Privacy-friendly sharing β the image is written to the app's private cache and handed to the system share sheet via a
FileProvider(ImageSharer); nothing is uploaded by VeloSpot itself β the image only leaves the device once you pick a target app. Fully localised across all eight supported languages.
- Off-screen, deterministic rendering β the card is drawn directly onto a
- Ride recording keeps running in the background β with a notification, a Quick Settings tile and a home-screen widget β recording a ride no longer freezes the moment you leave the map screen. The recording lifecycle (the GPS feed, the live stats and the persistence) moved out of the
viewModelScopeinto a process-level@SingletonRideRecordingManager, paired with alocation-typed foreground service (RideRecordingService) that keeps the process + GPS alive while the app is backgrounded or closed.- Persistent notification β while recording, an ongoing notification shows the live time β’ distance β’ speed and offers Stop & save and Discard actions, so you can finish the ride straight from the shade without reopening the app. Tapping it returns to the map. A dedicated low-importance notification channel is created on Android 8+, and
POST_NOTIFICATIONSis requested on Android 13+ when a recording starts (the recording still runs if denied β only the notification is hidden). - Quick Settings tile β a
RideRecordingTileServicetile starts/stops a recording with a single tap from the notification shade, reflecting the active/inactive state with label + subtitle; it bounces you into the app to grant location permission if it's missing. - Home-screen widget β a
RideRecordingWidget(AppWidgetProvider) with a single start/stop control that shows the live time + distance while recording. Both the tile and the widget share the very same singleton manager as the in-app FAB, the notification and the service, so the recording state stays consistent across every entry point; the manager pushes state changes to them (AppWidgetManagerrefresh broadcast +TileService.requestListeningState) even while the app's UI is closed. - GPS stays alive only while needed β
MapViewModelno longer tears down location updates on background/onClearedwhile a recording is active (the manager owns the GPS radio then, at high accuracy); when nothing is recording the existing battery-friendly teardown is unchanged. New permissions:FOREGROUND_SERVICE,FOREGROUND_SERVICE_LOCATION,POST_NOTIFICATIONS. Works in both the Google-Play (Fused) and F-Droid (LocationManager) flavours via the sharedLocationRepository. Fully localised across all eight supported languages.
- Persistent notification β while recording, an ongoing notification shows the live time β’ distance β’ speed and offers Stop & save and Discard actions, so you can finish the ride straight from the shade without reopening the app. Tapping it returns to the map. A dedicated low-importance notification channel is created on Android 8+, and
Changed
- Privacy policy: disclose the ride-share card β the new share-card feature is now documented across
PRIVACY.md,docs/PRIVACY.mdanddocs/privacy.html: a new section "3.4 Ride Sharing (Share Card)" explains that the card image is generated and cached locally and only leaves the device when you pick a target app in the Android share sheet (after which that app's policy applies), and that drawing the map cutout loads OpenFreeMap tiles for the ride's area (the OpenFreeMap row and notes were updated accordingly). - Privacy policy: disclose the one-time BRouter offline-routing download β the policy previously implied BRouter is entirely offline, but the one-time download of the offline routing data fetches map-segment tiles from
brouter.de(the requested 5Β°Γ5Β° tile name reveals the rider's approximate region + IP). This connection is now listed in the third-party services table and the BRouter note is clarified acrossPRIVACY.md,docs/PRIVACY.mdanddocs/privacy.html; theINTERNETpermission description was updated accordingly.
Removed
- Dead "parking photos" feature β the parking-photo UI was never functional: the bundled OpenStreetMap dataset always stores
imageUrl = NULL(the extraction script never populates it), so theAsyncImageblock inSelectedSpaceSheetcould never render and no image request was ever made. Removed the dead photo UI and the now-unused Coil image-loading dependency (coil-compose, plus its version-catalog and attribution entries), and stripped the misleading "parking photos" mentions from the README, the store descriptions and the privacy policy. The dormantimageUrlcolumn is intentionally kept in the Room schema to avoid a destructive migration / regenerating the bundled databases.
Fixed
- Navigation now reliably detects arrival and ends itself β for every destination β turn-by-turn navigation previously only auto-finished when riding to a genuine bike parking spot (via the auto-park path), so navigating to an address-search result, a saved place, a custom map pin or the parked bike would keep running indefinitely even after you'd arrived; you had to stop it by hand. On top of that, arrival detection relied solely on the along-route remaining distance and was suppressed entirely while off-route, so pulling onto the pavement at the door (a couple of metres of GPS noise) or a BRouter route that stops just short of the destination could leave navigation stuck. Arrival is now handled centrally for all destinations (
maybeHandleArrivalinNavigationController):- Works off-route β when the rider is off the route line near the destination, a straight-line (crow-flies) distance from the raw GPS fix to the actual destination coordinate is used as a fallback, independent of the route, so arriving a few metres beside the line still registers; while on-route the precise along-route remaining distance is still used.
- Debounced β two consecutive fixes inside the 25 m arrival radius are required before navigation ends, rejecting a single stray GPS sample that briefly snaps onto the destination.
- Ends every navigation β reaching a real parking spot still auto-parks the bike with the "arrived β bike parked here" confirmation; reaching any other destination now ends navigation with a new generic "you've arrived at your destination" confirmation. Covered by updated and new unit tests; the new string is localised (English + German).
- Cyclist marker no longer disappears behind 3D buildings β in the tilted 3D view the live-location cyclist (and the parking pins / route line) could vanish wherever a building footprint overlapped them on screen. The
fill-extrusion3D building layer was being added withstyle.addLayeron top of every other layer, and because MapLibre draws symbol/line layers without a depth test against the extrusion, visibility is purely a matter of paint order β so the buildings simply painted over the markers wherever they overlapped (hence "sometimes hidden", only near tall buildings).ensureBuildingExtrusionLayernow slots the extrusion beneath the lowest of our own overlays (route β track β parking β location β pins) viaaddLayerBelow, so the cyclist avatar and all other overlays stay reliably in the foreground while the volumes still rise above the flat base building fill (MapStyleLayers.kt). - Routing could put a bike onto trunk roads / motorway feeders (e.g. the A1) β the bundled BRouter profiles forbade motorways but treated
trunk/trunk_linkways (KraftfahrstraΓen β the high-speed feeders and on/off ramps around a motorway like the A1) too cheaply, so on a longer route BRouter could route a bike onto one. All bundled profiles are now hardened (motorroad=yestrunks were already forbidden everywhere via the access rules; the motorway itself was already cost 10000 / forbidden):- gravel & mtb β a trunk is only ridden when it explicitly carries bike infrastructure (a cycleway, a cycle route or
bicycle=yes|designated, cost 20β60); otherwise it is strongly avoided (cost 200) and forbidden outright where cycling is signed off (bicycle=no|private|dismount). - trekking (the default), fastbike and shortest β trunks where cycling is signed off (
bicycle=no|private|use_sidepath) are now forbidden, while legally-cyclable trunks keep their existing tuned cost (shortestpreviously treated a trunk like any other road at cost 1). - All five edited profiles were validated with BRouter's own
IntegrityCheckProfile(way + node contexts). - Because
BRouterEngine.ensureProfiles()previously copied a profile only when it was missing, the bundled profiles are now versioned (PROFILES_VERSION) and re-extracted to internal storage whenever the version changes, so existing installs actually pick up profile fixes.
- gravel & mtb β a trunk is only ridden when it explicitly carries bike infrastructure (a cycleway, a cycle route or
- Startup crash on upgrade:
no such table: room_table_modification_logβ when upgrading over an install that already had the bundled parking database on disk, the app could crash on launch the first time any favourites/rides/placesFlowwas observed. The one-time "isolate favourites" migration opened the newFavoritesDatabasethrough its low-levelopenHelper.writableDatabaseand ran a rawATTACH DATABASE β¦ INSERT β¦ SELECT. That bypasses Room's (2.7+) connection-pool initialisation, so Room's internalroom_table_modification_loginvalidation table was never created β and the firstInvalidationTrackerobserver then crashed withSQLiteException: no such table: room_table_modification_log. The legacy favourites are now read through a separate read-onlySQLiteDatabaseconnection and re-inserted via the Room DAO, so the database is only ever opened on Room's normal path (noopenHelper/ATTACH), which correctly initialises the invalidation tracker. Clean installs were unaffected. - Private data no longer backed up to Google Drive (
allowBackuphardening) βandroid:allowBackupwastruewith empty backup-rule templates, so Android Auto Backup could upload recorded GPS ride tracks, favourites, saved places and settings to the user's Google Drive β undisclosed and at odds with VeloSpot's "all data stays exclusively on your device" promise. Backup is now disabled (android:allowBackup="false"), and thedata_extraction_rules.xml/backup_rules.xmlare filled with explicitexcluderules (covering thedatabase,sharedprefandfiledomains β including Room's-wal/-shmsidecar files) as defense-in-depth should backup ever be re-enabled. No third party receives any user data. (#86) - Ride-recording timer now ticks continuously β the elapsed-time counter on the live recording card was derived from the last GPS fix's timestamp, so it only advanced when a new fix arrived (it would freeze at a red light or during a brief GPS dropout). The elapsed time is now wall-clock based (
RideTracker.currentStats(now)) and a 1 Hz ticker inRideTrackingControllerrepublishes the live stats every second, so the timer counts up smoothly and honestly regardless of the GPS cadence. - No accidental pins while navigating or recording β tapping an empty spot on the map during turn-by-turn navigation or an active ride recording no longer drops a custom pin (which also triggered reverse-geocoding and a camera jump mid-trip).
MapViewModel.onMapTappednow ignores empty-map taps while a follow session is active; tapping existing parking spots / saved places still works.
π₯ APK Variants
| File | Description |
|---|---|
VeloSpot-v1.0.20.apk |
F-Droid flavor β no Google services, ready to sideload; this is the APK F-Droid reproducibly verifies |
VeloSpot-v1.0.20-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.