summaryrefslogtreecommitdiff
path: root/android-app/app/src
AgeCommit message (Collapse)Author
2026-05-26Fix ANR, false tack detections, and empty-state flash on track loadClaude
ANR: getSavedTracks() had no IO dispatcher — TackDetector ran on main thread at O(n²) cost (~77M ops for a 2.5h track). Added withContext(IO). TackDetector: replaced O(n) list.filter with O(log n) binary-search subList for before/after windows; O(n²) → O(n log n). Raised MIN_DELTA 60°→75° to stop detecting ordinary course corrections as maneuvers. Removed MAX_DELTA cap entirely — 180° crash-tacks are valid; the circularMAD stability windows are the real quality gate. TrackDetailSheet: dropped the arbitrary abs(delta)>70 Tack/Jibe heuristic. Now uses true wind angle when available for classification; falls back to "Maneuver" when TWD is absent. SavedTracksFragment: added isLoadingTracks StateFlow (MainViewModel); combine() suppresses the empty-state layout while the initial IO load runs so it no longer flashes "No saved tracks yet" before data arrives. 16/16 TackDetector tests pass. https://claude.ai/code/session_01KXYBuAHZkvv63DeUG6XaZD
2026-05-26Redesign tack detection: time-based windows + circularMAD stabilityClaude
Replace the point-count window (HALF_WIN=2) with 30s time-based settle windows that give consistent reliability regardless of GPS update rate (1 Hz FULL vs 0.2 Hz ECONOMY). Key changes: - Before/after windows are time-based (T_SETTLE=30s) around a guard zone (T_MANEUVER=30s), not point counts. - Stability uses circularMAD (<20°) across all fixes in each window, replacing the single-pair spread check. This correctly handles 0°/360° wrap and suppresses random anchor-watch COG noise (MAD≈90°). - No SOG gate: averaging 30 fixes over 30s reduces noise by √30 regardless of boat speed. A boat at anchor has MAD≈90° and is rejected by the stability check. - MAX_DELTA raised to 160° (covers deep jibes the old 140° missed). - START_SKIP reduced to 60s (stability check handles cold-start noise). - De-duplication uses adjacent-candidate comparison so a long stream of raw candidates from one maneuver stays in a single group even if it spans >MIN_GAP_MS in total. - Position refined to max instantaneous heading rate within the maneuver zone. - Mode: CRUISING (conservative). Race mode noted as future backlog. 16/16 unit tests pass at 1 Hz (FULL mode GPS rate). https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-26Add notes to finished tracks, single Nav voice, fix speed coloringClaude
- TrackDetailSheet: + Add note button opens a dialog with text, camera, and gallery. Entry is saved with timestampMs=track.endMs so it lands inside the track's time window and appears automatically in the event log. logEventAdapter.update() refreshes the list after save. - TripReportGenerator: removed NarrativeStyle enum and 4-style branch. generateNarrative() now emits a concise PASSAGE header (date, time range, duration), stats line (nm / avg kt / max kt), optional conditions (seas, temp), and a timestamped deck log. Fragment and layout updated to remove the ChipGroup style picker. - TrackColors: replaced Expression.get("color") — which silently fails for lineColor on LineLayer in MapLibre Android 11.x — with Expression.step(Expression.get("speed"), ...) that maps the numeric "speed" property directly to color literals. "color" string property removed from speedSegments() features. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-26Fix camera to open system camera app; harden logbook storage; add backlogClaude
Camera: - Replace ActivityResultContracts.TakePicture() with explicit StartActivityForResult + Intent(ACTION_IMAGE_CAPTURE) + EXTRA_OUTPUT so the full system camera app always opens (not a sub-view preview) - FLAG_GRANT_WRITE_URI_PERMISSION added for camera app FileProvider access LogbookStorage: - getExternalFilesDir() can return null when external storage is unavailable; fall back to filesDir for both storageFile and photoDir so saves never silently fail on devices without external storage Backlog (worklog.md): - Add notes/photos to finished tracks + include in trip report - Trip report voice: replace generic style picker with single Nav voice - Fix tack detection false positives at low SOG - Investigate/fix speed coloring in MapLibre Android 11.x https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-26Fix LocationService current TODO; show log entries in track detail viewClaude
LocationService: - Add _currentSpeedKt/_currentDirectionDeg to companion state - Add setCurrentConditions() companion function - snapshot() now returns real current data instead of null - MainActivity.applyConditions() calls setCurrentConditions() on each marine conditions update TrackDetailSheet: - buildLogEvents() filters NavApplication.logbookRepository entries by track time window (startMs..endMs); entries without GPS snap to nearest track point by timestamp - Note/photo events added to the event log list with 📝/📷 icons - drawNotes() adds a green CircleLayer for note/photo markers on the map - LogEvent extended with photoPath field - LogEventAdapter shows a downsampled photo thumbnail when present item_log_entry.xml: add iv_log_thumb ImageView (hidden by default) https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-25Fix bugs from static analysis; add test coverage for new codeClaude
Bugs fixed: - LogbookStorage.copyFromUri: return null when ContentResolver returns null stream instead of returning a path to an unwritten file - VoiceLogFragment adapter: decode photos as downsampled thumbnails (128x96 target) using BitmapFactory inSampleSize instead of loading full-resolution bitmaps on the main thread Tests added: - LogbookRepositoryTest: ID sequencing, insertion order, storage delegation, reload from persisted state, lat/lon preservation - TripReportGeneratorTest: log entry time-range filtering, distance calculation, empty-points edge case, all narrative styles smoke-tested https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-25Merge claude/optimize-nav-location-tracking-Vac5Z into mainClaude
Resolve conflict in TackDetector.kt: keep null-safe lastTackMs check from main (branch had a NPE-unsafe subtraction on nullable Long). https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-25Persist log entries and photos; stamp with GPS locationClaude
- LogbookStorage: Moshi-backed JSON persistence to getExternalFilesDir, photos saved to Pictures/Nav/ instead of cache - LogbookRepository: file-backed replacement for in-memory store, loads saved entries on init so they survive app restarts - VoiceLogViewModel: expose entries StateFlow, accept lat/lon in save() - VoiceLogFragment: switch camera to TakePicture(Uri) via FileProvider for full-res photos; gallery copies content URI to persistent storage; passes current GPS position to save(); shows scrollable entries list - MainViewModel: expose currentPosition StateFlow updated on each GPS fix - AndroidManifest: add FileProvider for Pictures/Nav/ directory - TripReportViewModel: updated to use LogbookRepository https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-21Revert detail sheet drawTrack to original single-Feature static colorClaude
The speed-color expression approaches all failed silently. Reverting to the exact pre-speed-color form (single Feature LineString, static #2B8FC4) to re-establish a visible track line while diagnosing why the FeatureCollection/expression approach doesn't render. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-21Fix speed-colored track: pre-compute hex color per segment, use get("color")Claude
Instead of asking MapLibre to interpolate/step colors at render time (which was silently failing), pre-compute the CSS hex color for each segment in speedSegments() and store it as a "color" property. speedColorExpression() is now Expression.get("color") — a simple property lookup that MapLibre parses as a CSS color for line-color. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix savedTracks silently empty when getSavedTracks() throwsClaude
getSavedTracks() was called inside .onSuccess{} with no error boundary; any exception from toSavedTrack() crashed the coroutine silently after _pastTracks was already set, leaving _savedTracks empty (tracks visible on the main map dim layer but missing from the Log tab list). Wrap every getSavedTracks() call in runCatching, and make toSavedTrack() inside getSavedTracks() skip individual bad tracks via mapNotNull so one corrupt GPX can't blank the whole list. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix invisible speed-colored track: use step() with hex string literalsClaude
Expression.color(Color.parseColor()) inside interpolate() stops was silently failing to render on device. Replace with Expression.step() and Expression.literal(hexString) which MapLibre's renderer handles natively for line-color. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix missing imports for speedColorExpression and speedSegments in MapHandlerClaude
https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Color track lines by boat speed using MapLibre data-driven stylingClaude
Each focused track is rendered as per-segment LineStrings tagged with average SOG, interpolated blue→cyan→green→amber→red for 0→13+ knots. Applies to both the main map focus layer and the track detail sheet. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix Long overflow in TackDetector: var lastTackMs = Long.MIN_VALUE caused ↵Claude
subtraction overflow, filtering every tack
2026-05-20Fix build: TrackDetailSheet is Fragment not DialogFragment, use transaction ↵Claude
from VoiceLogFragment
2026-05-20Fix build: TrackDetailSheet is Fragment not DialogFragment, use transaction ↵Claude
from VoiceLogFragment
2026-05-20Optimize nav location tracking: embedded track detail map, tack detection ↵Peter Stone
fixes, dim past tracks * Fix tack detection and convert track detail to embedded-map fragment TackDetector: revert STABILITY_MAX to 30° (was tightened too far to 20°), add 120s start-skip to filter GPS cold-start false positives, keep 45s minimum gap between tacks. TrackDetailSheet: convert from BottomSheetDialogFragment to a regular Fragment with an embedded MapLibreMap in the top half. This fixes the map centering issue (the embedded map is always sized to its own frame), eliminates accidental dismissal when dragging the map, and makes the view self-contained — back navigation restores the track list via the fragment back stack. Tack markers rendered on the embedded map; tapping a log event pans the embedded map directly. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn * Fix XML conflict markers left in layout_track_detail_sheet.xml https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn * Fix Kotlin conflict markers left in TrackDetailSheet.kt https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn * Fix compile errors on feature branch - SavedTracksFragment/VoiceLogFragment: remove references to SafState, safState, launchSafPicker, layout_empty, btn_setup_storage, and layout_log_entry — none of these exist on this branch - TrackStorage: add missing onCancellation lambda to cont.resume() - build.gradle: enable buildConfig generation (fixes unresolved BuildConfig) https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn * Dim past tracks: dark navy 0.12 opacity, solid lines https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn --------- Co-authored-by: Claude <noreply@anthropic.com>
2026-05-20Merge main: preserve embedded track detail map, tack detection fixes, 0.12 ↵Claude
past track opacity - Tack detector: STABILITY_MAX=30, START_SKIP_MS=120s cold-start filter (feature branch) - TrackDetailSheet: embedded Fragment with MapView, back button pops stack (feature branch) - layout_track_detail_sheet: MapView top half, detail panel bottom half (feature branch) - MapHandler past track dim opacity: 0.12 (down from 0.22) - SavedTracksFragment: fragment transaction to TrackDetailSheet + SAF setup (both) - VoiceLogFragment: inline track list, logViewModel refs fixed (main)
2026-05-20Dim past tracks: dark navy 0.12 opacity, solid linesClaude
https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix compile errors on feature branchClaude
- SavedTracksFragment/VoiceLogFragment: remove references to SafState, safState, launchSafPicker, layout_empty, btn_setup_storage, and layout_log_entry — none of these exist on this branch - TrackStorage: add missing onCancellation lambda to cont.resume() - build.gradle: enable buildConfig generation (fixes unresolved BuildConfig) https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix Kotlin conflict markers left in TrackDetailSheet.ktClaude
https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix XML conflict markers left in layout_track_detail_sheet.xmlClaude
https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix tack detection and convert track detail to embedded-map fragmentClaude
TackDetector: revert STABILITY_MAX to 30° (was tightened too far to 20°), add 120s start-skip to filter GPS cold-start false positives, keep 45s minimum gap between tacks. TrackDetailSheet: convert from BottomSheetDialogFragment to a regular Fragment with an embedded MapLibreMap in the top half. This fixes the map centering issue (the embedded map is always sized to its own frame), eliminates accidental dismissal when dragging the map, and makes the view self-contained — back navigation restores the track list via the fragment back stack. Tack markers rendered on the embedded map; tapping a log event pans the embedded map directly. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix track detail sheet expansion; fix tack detection sensitivityClaude
Sheet: set isDraggable=false so scrolling the event list doesn't push the sheet up over the map. Top 50% stays map, bottom 50% stays list. Tack detector: replaced 5-index skip (~5 s at 1 Hz) with a 45-second timestamp gap. Also tightened STABILITY_MAX from 30° to 20° to reduce false positives from GPS heading noise. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Halve past track opacity (0.45 → 0.22)Claude
https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix setGeoJson overload ambiguity in setFocusedTrackPointsClaude
Kotlin inferred Feature|FeatureCollection as their common supertype, which didn't match any single setGeoJson overload. Split into two explicit branches so each call site has an unambiguous type. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Inline saved tracks on Log tab; collapse log entry when not recordingClaude
Removed the 'Saved Tracks' button that opened a separate overlay. Tracks now appear directly on the Log screen in a RecyclerView below the Generate Trip Report button. Tapping a track hides the overlay and opens the existing TrackDetailSheet bottom sheet over the map. The log entry section (text field, mic, camera, save/clear) is hidden with visibility=gone when not recording and shown when a recording is in progress. The storage-setup prompt is now surfaced here too. SavedTrackAdapter and DATE_FMT widened to internal so VoiceLogFragment can reuse them directly. SavedTracksFragment kept for the GPX import flow. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Restyle past tracks: solid dark blue, dim/focus layersClaude
All past tracks render as solid dark navy (#0B3050) at 45% opacity, 2.5px — no dashes. A second focus layer (#2B8FC4, full opacity, 4px) paints on top when a track is selected, making it pop without changing the dim layer's data. Active recording stays red. The dim layer always contains every past track; the focus layer gets the selected track's points independently via setFocusedTrackPoints(). Cleared on dismiss so all tracks return to equal dim opacity. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix Recenter bleed-through; drop noisy speed events; track detail as bottom ↵Claude
sheet Recenter button was reappearing over overlays because the isFollowing flow kept calling fadeIn(fabRecenter) even when an overlay was visible. Added overlayShowing flag — the flow skips FAB updates while an overlay is shown. hideOverlays() now correctly restores fabRecenter visibility based on the actual following state instead of just resetting alpha. Speed up/Slow down events fired on every 1.5 kt GPS jitter, flooding the log with noise. Removed the detection loop entirely — no reliable motor on/off signal is available from GPS alone. TrackDetailSheet converted from a full-screen Fragment to a BottomSheetDialogFragment: opens at half-screen height so the track route is visible on the map behind it, draggable to full height, 20% background dim. SavedTracksFragment now calls hideOverlays() then sheet.show() instead of fragment-replacing the overlay container. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix gap between wave animation and ocean conditions panelClaude
Removed the spurious constraintBottom_toBottomOf="parent" from forecast_row: ConstraintLayout was vertically centering it between the wave and the sheet bottom (default bias 0.5) instead of placing it flush below the wave. Also aligned light-mode wave_sea_bottom (#074B68 → #0D2137) with the forecast_row background so the wave tail blends seamlessly in light theme. https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Move Saved Tracks to Log tab; restore conditions sheet on backClaude
- hideOverlays() now resets bottomSheetBehavior to COLLAPSED/not-hideable, so the conditions sheet reappears correctly when returning from any overlay (previously it stayed hidden after navigating away via the track browser) - Remove btn_saved_tracks from the conditions sheet (layout + click handler) - Add btn_saved_tracks to the Log screen layout and wire it in VoiceLogFragment via MainActivity.showSavedTracks(); tracks live with the log, not with weather https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix blank track list and Recenter button bleed-through in Saved TracksClaude
- SavedTracksFragment was missing adapter.submitList(tracks) after the collect block rewrite — tracks loaded fine (16 found via SAF) but the RecyclerView adapter never received them, leaving the list visually blank - showOverlay() now also hides fabRecenter; it was not in the hide list so the map's Recenter button remained visible on top of the fragment overlay https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix GPX file opening, share sheet visibility, and add SAF track storageClaude
Intent filter fixes: - Remove pathSuffix=".gpx" + scheme restriction from the application/octet-stream ACTION_VIEW filter; pathSuffix matches the URI path, not the filename, so it never fired against file manager content URIs (opaque paths like /provider/uuid). The existing display-name check in handleIncomingGpx() is the correct gate. - Add ACTION_SEND filter for application/octet-stream; Nav was invisible in share sheets because most file managers send this type for .gpx files. SAF-based track storage (API 29+): - Replace fragile SharedPrefs URI cache + OWNER_PACKAGE_NAME MediaStore query with Storage Access Framework as the primary write/read path. The user grants access to their tracks folder once via ACTION_OPEN_DOCUMENT_TREE; DocumentFile.listFiles() finds all GPX files without relying on MediaStore ownership (which Android clears on uninstall). After reinstall, a one-tap "Restore track access" button in Saved Tracks re-authorizes the same folder and all tracks immediately reappear. - MediaStore path (existing) is kept as fallback for tracks saved before this change - Add SafState enum (CONFIGURED / PERMISSION_LOST / NOT_CONFIGURED) in TrackStorage - TrackRepository exposes safState() and initSafDirectory() thin wrappers - MainViewModel exposes safState StateFlow and onSafDirectorySelected() - SavedTracksFragment observes safState and shows contextual setup / re-auth button - MainActivity registers safPickerLauncher and exposes launchSafPicker() - Add androidx.documentfile:documentfile:1.0.1 dependency https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-20Fix two pre-existing compile errors caught by CIClaude
- Enable buildConfig generation explicitly (AGP 8+ disabled it by default); fixes Unresolved reference 'BuildConfig' in MainActivity - Add required onCancellation lambda to cont.resume() in TrackStorage; fixes 'No value passed for parameter onCancellation' with coroutines 1.10.2 https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-19Reduce location resource usage: economy mode by default, lifecycle throttlingClaude
- Start LocationService at ECONOMY (5 s, PRIORITY_BALANCED_POWER_ACCURACY) instead of FULL (1 Hz, PRIORITY_HIGH_ACCURACY); promote to FULL only while a track is actively recording and demote back when recording stops - Use PRIORITY_BALANCED_POWER_ACCURACY for ECONOMY and ANCHOR_WATCH modes so the GPS chip idles and network/passive sources handle light-duty fixes - Add ACTION_START_ECONOMY / ACTION_START_FULL intents so MainActivity can drive mode transitions from lifecycle callbacks - onPause: drop to ECONOMY if not recording or anchor-watching; onResume: restore the appropriate mode so the first visible frame has a fresh rate - Stop-anchor-watch now returns to ECONOMY instead of FULL (anchor drop rarely coincides with active track recording) - Remove ACCESS_BACKGROUND_LOCATION from manifest — the foreground service with foregroundServiceType="location" already covers background access - Add 5 m minimum-distance filter to DeviceGpsProvider so stationary devices don't generate spurious location callbacks https://claude.ai/code/session_01YUbuZNDAoLea4cf9UGQ9qn
2026-05-15fix: recover tracks after reinstall via MediaScanner; add version to startup logClaude
Android clears OWNER_PACKAGE_NAME for all MediaStore entries when an app is uninstalled. Files survive on disk in Documents/Nav/ but the ownership query returns 0 rows after reinstall. Fix: if the first load finds nothing, call MediaScannerConnection.scanFile() on Documents/Nav/. Because our app triggers the scan, the MediaScanner re-registers each file with our package as owner, restoring the query results on the immediate retry. Also log BuildConfig.VERSION_NAME + VERSION_CODE in the startup message so the build is identifiable directly from the in-app debug log. https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G
2026-05-15fix: handle application/octet-stream for .gpx on Android 16 (Google Files)Claude
Google Files uses application/octet-stream for .gpx because Android 16's MimeTypeMap has no entry for that extension. The ExternalStorageProvider content URI encodes the full file path, so pathSuffix=".gpx" keeps the ACTION_VIEW filter targeted without matching all binary files. ACTION_SEND (share sheet) filters added for typed MIME variants; code now handles both actions — EXTRA_STREAM for SEND, intent.data for VIEW. For octet-stream and generic XML types, OpenableColumns.DISPLAY_NAME is checked before parsing (uri.lastPathSegment is an opaque ID for content URIs, not the filename). All decisions logged to NavLogger. https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G
2026-05-15fix: target Android 16 correctly for GPX file handlerClaude
Android's MimeTypeMap does not include application/gpx+xml for .gpx on all devices, so file managers fall back to text/xml or application/xml. Add intent-filters for both XML MIME types and check OpenableColumns DISPLAY_NAME in code before parsing — so Nav appears in the chooser for XML files but only processes ones named *.gpx. Remove the broken file:// pathSuffix filter (irrelevant on Android 16, also syntactically invalid when combined with mimeType in a single <data> element). Remove unnecessary BROWSABLE category from file-open filters. https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G
2026-05-15fix: GPX file handler coverage and track loading after reinstallClaude
- AndroidManifest: add application/gpx and file:// intent-filters so Nav appears in the chooser on devices that don't know application/gpx+xml - MainActivity: check file extension as fallback when MIME type is generic - TrackStorage: filter MediaStore fallback query by OWNER_PACKAGE_NAME so the system grants access to our own files without READ_EXTERNAL_STORAGE, which also survives app reinstall (same package name = same ownership) - TrackRepository: add reloadTracks() to force a fresh load from storage - MainViewModel: add reloadPastTracks() called after location permission grant so the first-launch race condition can self-heal https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G
2026-05-15fix: saved tracks browser, GPX import persistenceClaude
Saved tracks browser: - TrackDetailSheet converted from BottomSheetDialogFragment to Fragment so it fills the overlay instead of floating over the map - Adapter tap pushes detail fragment onto the back stack; back button returns to the list naturally via popBackStack() - SavedTracksFragment back button calls hideOverlays() (made internal) to close the overlay and return to the map - Layout updated to full-screen: header with back button, RecyclerView fills remaining space via layout_weight instead of fixed 300dp GPX import persistence: - TrackRepository.importTrack() saves parsed points to Documents/Nav/ via TrackStorage (URI is persisted to SharedPreferences) and adds to in-memory list - MainViewModel.addImportedTrack() now calls importTrack() so files opened via the GPX handler load on future launches https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G
2026-05-15fix: saved tracks overlay, GPX file handler, nav importClaude
Saved tracks overlay was immediately hidden after showing because bottomNav.selectedItemId = nav_map fires the nav item listener, which calls hideOverlays(). Removed that line — the map tab is already selected when the instrument sheet is open. GPX file handler: - Manifest: intent-filter for application/gpx+xml + singleTop launch mode so onNewIntent is called when app is already running - MainActivity: handleIncomingGpx() called from onCreate + onNewIntent; parses the URI on IO dispatcher and hands points to ViewModel - MainViewModel: addImportedTrack() prepends parsed points to pastTracks and savedTracks state flows (displayed in session only, not persisted) https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G
2026-05-12fix(track): load past tracks via persisted URIs, no MediaStore query needed (#4)Peter Stone
MediaStore.Files.getContentUri("external") queries require READ_EXTERNAL_STORAGE on Android ≤ 12 and have no clean permission path on Android 13+ for non-media files. openInputStream() on a content URI the app owns works on all versions without any permission — so the fix is to persist each URI at save time. On save: call persistUri(uri) after the IS_PENDING=0 update, storing the content URI in SharedPreferences (nav_track_uris). On load: iterate stored URIs first and open each with openInputStream() — no MediaStore query, no permissions needed. The existing MediaStore query is kept as a fallback for tracks saved before this change; any track found there is auto-migrated into SharedPreferences so future launches skip the query too. Invalid URIs (file deleted) are cleaned up from SharedPreferences automatically. https://claude.ai/code/session_01DNjbYxiC1cco83dArNGW3G Co-authored-by: Claude <noreply@anthropic.com>
2026-05-12fix(track): broaden MediaStore query to find saved GPX filesPeter Stone
RELATIVE_PATH equality matching was returning 0 rows — MediaStore normalises the path differently across Android versions and devices. DISPLAY_NAME LIKE nav_%.gpx is unique enough on its own. Also log path + IS_PENDING per found file to aid future diagnosis. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-12Add saved tracks browser, tack detection, and map zoom-to-boundsPeter Stone
- TackDetector: sliding-window COG algorithm (60-140deg delta = tack/jibe) with stability check to prevent mixed-window false positives; 13 tests green - TackEvent, SavedTrack: new models wrapping points + summary + detected tacks - TrackRepository.getSavedTracks(): returns List<SavedTrack> with lazy load - MapHandler: zoomToTrackBounds (LatLngBounds fit with padding), panToPoint (log step-through), updateTackLayer (yellow circle markers) - MainViewModel: savedTracks + selectedTrack StateFlows, panToPosition SharedFlow - SavedTracksFragment: RecyclerView list with date/distance/duration/tack count - TrackDetailSheet: bottom sheet with stats + event log (tacks, speed changes, start/end); tapping an entry pans the map to that position - MainActivity: zoom map on track complete, wire Saved Tracks button in instrument sheet, observe panToPosition to drive map, show tack markers when a saved track is selected Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-07refactor: extract GeoThrottle, fix fully-qualified refs, harden enum parsingPeter Stone
- Extract GeoThrottle inner class in MainViewModel; eliminates 6 state vars and duplicate throttle logic in loadConditions/loadWindGrid - Fix android.util.Log.e and kotlin.math.* fully-qualified refs now that imports are present - UnitPrefs: wrap valueOf() in runCatching to handle corrupt pref values - VesselRepository.init: forEach { add() } → mapTo() for both lists - TrackRepository: remove noisy cached-log else-branch that fired on every stopTrack() call Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05fix: remove crash-risk !! operators in UnitPrefs and MainViewModelPeter Stone
UnitPrefs: getString() returns String? even with a non-null default, so !! crashes on corrupted prefs. Replaced with explicit elvis fallback to the same enum default. MainViewModel: forecastResult.getOrThrow() is logically safe (when-branch guards it) but unnecessarily brittle. Replaced with getOrNull() ?: emptyList(). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05fix: guard past-track load against SecurityException before permissions grantedPeter Stone
MediaStore.query() throws SecurityException if storage permission hasn't been granted yet. The ViewModel init{} fires before the permission dialog result, so the coroutine crashed the app on first launch. - Wrap contentResolver.query() in runCatching in loadViaMediaStore() - Wrap getPastTracks() call in init{} with runCatching; log failure, don't crash Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05fix: load past tracks on startup; add debug logging throughoutPeter Stone
Root cause: getPastTracks() was only called inside stopTrack(), so saved tracks were never loaded unless a new track was recorded in the same session. Added init{} block to MainViewModel to load them at startup. Logging added: - getPastTracks(): first-call vs cached, count + point sizes - loadViaMediaStore(): cursor row count, per-file parse attempt, point count per file, any openInputStream null or parse errors Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-05fix: keep bottom nav visible when panning; expand drag handle touch targetPeter Stone
Bottom nav: remove it from the following/panning fade cycle — only fabRecenter and fabRecordTrack swap; nav stays visible throughout. Drag handle: 4dp × 36dp was too small and competed with BottomSheet drag gestures. Now 40dp tall via inset drawable (visual bar unchanged, touch area is a usable 40dp × 36dp). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>