From 5416c8ccc9220bc36e7af3febcbdd9d86e88cf30 Mon Sep 17 00:00:00 2001 From: Peter Stone Date: Thu, 2 Apr 2026 11:28:00 -1000 Subject: fix(ui): make record track FAB visible (#1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add GpsPosition data class and NMEA RMC parser with tests - GpsPosition: latitude, longitude, sog (knots), cog (degrees true), timestampMs - NmeaParser.parseRmc: handles GP/GN talker IDs, void status, malformed input - SOG/COG default to 0.0 when fields absent; S/W coords are negative - 13 unit tests: GpsPositionTest (2), NmeaParserTest (11) — all GREEN Co-Authored-By: Claude Sonnet 4.6 * feat: add harmonic tide height predictions (Section 3.2 / 4.2) Implement offline harmonic tide prediction as specified in COMPONENT_DESIGN.md: - TideConstituent: name, speedDegPerHour, amplitudeMeters, phaseDeg - TidePrediction: timestampMs, heightMeters - TideStation: id, name, lat, lon, datumOffsetMeters, constituents - HarmonicTideCalculator: predictHeight(), predictRange(), findHighLow() Formula: h(t) = Z0 + Σ [ Hi × cos( ωi × (t − t0) − φi ) ] - 15 unit tests covering all calculation paths Co-Authored-By: Claude Sonnet 4.6 * feat: implement isochrone-based weather routing (Section 3.4) * feat: implement PDF logbook export (Section 4.8) - LogbookEntry data class: timestampMs, lat/lon, SOG, COG, wind, baro, depth, event/notes - LogbookFormatter: UTC time, position (deg/dec-min), 16-pt compass, row/page builders - LogbookPdfExporter: landscape A4 PDF via android.graphics.pdf.PdfDocument with column headers, alternating row shading, and table border - 20 unit tests covering all formatting helpers and data model behaviour Co-Authored-By: Claude Sonnet 4.6 * feat: offline GRIB staleness checker, ViewModel integration, and UI badge - Add GribRegion, GribFile data models and GribFileManager interface - Add InMemoryGribFileManager for testing and default use - Add GribStalenessChecker with FreshnessResult sealed class (Fresh/Stale/NoData) - Integrate weatherStaleness StateFlow into MainViewModel (checked after loadWeather) - Add yellow staleness banner TextView to fragment_map.xml - Wire staleness banner in MapFragment (shown on Stale, hidden on Fresh/NoData) - Add GribStalenessCheckerTest (4 TDD tests) Co-Authored-By: Claude Sonnet 4.6 * feat: satellite GRIB download with bandwidth optimisation (§9.1) Implements weather data download over Iridium satellite links: - GribParameter enum with SATELLITE_MINIMAL set (wind + pressure only) - SatelliteDownloadRequest data class (region, params, forecast hours, resolution) - SatelliteGribDownloader: size/time estimation, abort-on-oversized, pluggable fetcher - 8 unit tests covering estimation scaling, minimal param set, and download outcomes Co-Authored-By: Claude Sonnet 4.6 * feat: add AnchorWatchHandler UI with Depth/Rode Out inputs and suggested radius - Add AnchorWatchState with calculateRecommendedWatchCircleRadius, which uses ScopeCalculator.watchCircleRadius (Pythagorean scope formula) and falls back to rode length when geometry is invalid - Add AnchorWatchHandler Fragment with EditText inputs for Depth (m) and Rode Out (m); updates suggested watch circle radius live via TextWatcher - Add fragment_anchor_watch.xml layout - Wire AnchorWatchHandler into bottom navigation (MainActivity + menu) - Add AnchorWatchStateTest covering valid geometry, short-rode fallback Co-Authored-By: Claude Sonnet 4.6 * feat(safety): log wind and current conditions at MOB activation (Section 4.6) Per COMPONENT_DESIGN.md Section 4.6, the MOB navigation view must display wind and current conditions at the time of the event. - MobEvent: add nullable windSpeedKt, windDirectionDeg, currentSpeedKt, currentDirectionDeg fields captured at the exact moment of activation - MobAlarmManager.activate(): accept optional wind/current params and forward them into MobEvent (defaults to null for backward compatibility) - LocationService (new): aggregates live SensorData (resolves true wind via TrueWindCalculator) and marine-forecast current conditions; snapshot() provides a point-in-time EnvironmentalSnapshot for safety-critical logging - MobAlarmManagerTest: add tests for wind/current storage and null defaults - LocationServiceTest (new): covers snapshot, true-wind resolution, current-condition updates, and the latestSensor flow Co-Authored-By: Claude Sonnet 4.6 * feat(gps): implement NMEA/Android GPS sensor fusion in LocationService Adds priority-based selection between NMEA GPS (dedicated marine GPS, higher priority) and Android system GPS (fallback) within LocationService. Selection policy: 1. Prefer NMEA when its most recent fix is fresh (≤ nmeaStalenessThresholdMs, default 5 s) 2. Fall back to Android GPS when NMEA is stale 3. Use stale NMEA only when Android has never reported a fix 4. bestPosition is null until at least one source reports New public API: - GpsSource enum (NONE, NMEA, ANDROID) - LocationService.updateNmeaGps(GpsPosition) - LocationService.updateAndroidGps(GpsPosition) - LocationService.bestPosition: StateFlow - LocationService.activeGpsSource: StateFlow - Injectable clockMs parameter for deterministic unit tests Adds 7 unit tests covering: no-data state, fresh NMEA priority, stale NMEA fallback, only-NMEA/only-Android scenarios, exact-threshold edge case, and NMEA recovery after Android takeover. Co-Authored-By: Claude Sonnet 4.6 * feat(gps): add fix-quality (accuracy) tier to GPS sensor fusion Extend LocationService's source-selection policy with a quality-aware "marginal staleness" zone between the primary and a new extended staleness threshold (default 10 s): 1. Fresh NMEA (≤ primary threshold, 5 s) → always prefer NMEA 2. Marginally stale NMEA (5–10 s) → prefer NMEA only when GpsPosition.accuracyMeters is strictly better than Android's; fall back to Android conservatively when accuracy data is absent 3. Very stale NMEA (> 10 s) → always prefer Android 4. Only one source available → use it regardless of age Changes: - GpsPosition: add nullable accuracyMeters field (default null, no breaking change to existing callers) - LocationService: add nmeaExtendedThresholdMs constructor parameter; recomputeBestPosition() now implements three-tier logic; extract GpsPosition.hasStrictlyBetterAccuracyThan() helper - LocationServiceTest: expose nmeaExtendedThresholdMs in fusionService helper; add posWithAccuracy helper; add 4 new test cases covering accuracy-based NMEA preference, worse-accuracy fallback, no-accuracy conservative fallback, and very-stale unconditional fallback Co-Authored-By: Claude Sonnet 4.6 * chore: add .gitignore, pull-crash-logs script, updated agent permissions - .gitignore: exclude agent artifacts (.claudomator-env, .agent-home/, crash-logs/) - scripts/pull-crash-logs: download Crashlytics crash reports via Firebase CLI - .claude/settings.local.json: add Android SDK/emulator/adb permission rules Co-Authored-By: Claude Sonnet 4.6 * chore: update CI workflow to target main branch Co-Authored-By: Claude Sonnet 4.6 * fix: resolve all Kotlin compilation errors blocking CI build - Migrate kapt → KSP (Kotlin 2.0 + kapt is broken; KSP is the supported path) - Fix duplicate onResume() override in MainActivity - Fix wrong package imports: com.example.androidapp.data.model → org.terst.nav.data.model across GribFileManager, GribStalenessChecker, SatelliteGribDownloader, LogbookFormatter, LogbookPdfExporter, IsochroneRouter, AnchorWatchHandler - Create missing SensorData, ApparentWind, TrueWindData, TrueWindCalculator classes - Inline missing ScopeCalculator formula (Pythagorean) in AnchorWatchState Co-Authored-By: Claude Sonnet 4.6 * feat(track): implement GPS track recording with map overlay - TrackRepository + TrackPoint wired into MainViewModel: isRecording/trackPoints StateFlows, startTrack/stopTrack/addGpsPoint - MapHandler.updateTrackLayer(): lazily initialises a red LineLayer and updates GeoJSON polyline from List - fab_record_track FAB in activity_main.xml (top|end of bottom nav); icon toggles between ic_track_record and ic_close while recording - MainActivity feeds every GPS fix into viewModel.addGpsPoint() and observes trackPoints to redraw the polyline in real time - ic_track_record.xml vector drawable (red record dot) - 8 TrackRepositoryTest tests all GREEN Co-Authored-By: Claude Sonnet 4.6 * test(ci): share APKs between jobs and expand smoke tests CI — build job now uploads both APKs as the 'test-apks' artifact. smoke-test job downloads them and passes -x assembleDebug -x assembleDebugAndroidTest to skip recompilation (~4 min saved). Test results uploaded as 'smoke-test-results' artifact on every run. Smoke tests expanded from 1 → 11 tests covering: - MainActivity launches without crash - All 4 bottom-nav tabs are displayed - Safety tab: Safety Dashboard, ACTIVATE MOB, ANCHOR WATCH visible - Log tab: voice-log mic FAB visible - Instruments tab: bottom sheet displayed - Map tab: returns from overlay, mapView visible - MOB FAB: always visible, visible on Safety tab - Record Track FAB: displayed, toggles to Stop Recording, toggles back MainActivity: moved isRecording observer to initializeUI() so the FAB content description updates without requiring GPS permission (needed for emulator tests that run without location permission). Co-Authored-By: Claude Sonnet 4.6 * refactor: address simplify review findings TrackRepository.addPoint() now returns Boolean (true if point was added). MainViewModel.addGpsPoint() only updates _trackPoints StateFlow when a point is actually appended — eliminates ~3,600 no-op flow emissions per hour when not recording. MainActivity: loadedStyle promoted from nullable field to MutableStateFlow; trackPoints observer uses filterNotNull + combine so no track points are silently dropped if the style loads after the first GPS fix. Smoke tests: replaced 11× ActivityScenario.launch().use{} with a single @get:Rule ActivityScenarioRule — same isolation, less boilerplate. CI: removed redundant app-debug artifact upload (app-debug.apk is already included inside the test-apks artifact). Removed stale/placeholder comments from MainActivity. Co-Authored-By: Claude Sonnet 4.6 * fix(ui): anchor record track FAB to instrument sheet to prevent it being hidden The FAB was anchored to bottom_navigation, placing it in the peek zone of the instrument bottom sheet (120dp) and making it invisible to users. Re-anchoring to instrument_bottom_sheet keeps the button visible and makes it track naturally with sheet drag gestures. Co-Authored-By: Claude Sonnet 4.6 * fix(ui): anchor MOB FAB to instrument sheet to match record-track FAB The fab_mob was still anchored to bottom_navigation using the same top|start pattern that caused fab_record_track to be hidden behind the instrument sheet's 16dp elevation. Apply the same fix so the safety-critical MOB button is not occluded. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Claudomator Agent Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Agent Co-authored-by: Claude Agent --- .github/workflows/android.yml | 33 +++++++++++++++++++++++++-------- 1 file changed, 25 insertions(+), 8 deletions(-) (limited to '.github') diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index aa292f2..2c28410 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -2,9 +2,9 @@ name: Android CI/CD on: push: - branches: [ master ] + branches: [ main ] pull_request: - branches: [ master ] + branches: [ main ] jobs: build: @@ -27,14 +27,16 @@ jobs: run: ./gradlew assembleDebug assembleDebugAndroidTest working-directory: android-app - - name: Upload artifact + - name: Upload test APKs (shared with smoke-test job) uses: actions/upload-artifact@v4 with: - name: app-debug - path: android-app/app/build/outputs/apk/debug/app-debug.apk + name: test-apks + path: | + android-app/app/build/outputs/apk/debug/app-debug.apk + android-app/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk - name: upload artifact to Firebase App Distribution - if: github.ref == 'refs/heads/master' + if: github.ref == 'refs/heads/main' uses: wzieba/Firebase-Distribution-Github-Action@v1 with: appId: ${{secrets.FIREBASE_APP_ID}} @@ -66,7 +68,7 @@ jobs: smoke-test: runs-on: ubuntu-latest - # Run after build succeeds so we don't spin up an emulator for a broken build + # Run after build succeeds — no point spinning up an emulator for a broken build needs: build steps: @@ -82,21 +84,36 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x android-app/gradlew + # Restore pre-built APKs so the emulator job skips the compile step + - name: Download test APKs + uses: actions/download-artifact@v4 + with: + name: test-apks + path: . # preserves android-app/app/build/outputs/… directory structure + - name: Enable KVM (faster emulator) run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm + # -x assembleDebug -x assembleDebugAndroidTest: skip recompile, use downloaded APKs - name: Run smoke tests on emulator uses: reactivecircus/android-emulator-runner@v2 with: api-level: 30 arch: x86_64 profile: pixel_3a - script: ./gradlew connectedDebugAndroidTest + script: ./gradlew connectedDebugAndroidTest -x assembleDebug -x assembleDebugAndroidTest working-directory: android-app + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: smoke-test-results + path: android-app/app/build/outputs/androidTest-results/ + - name: Notify claudomator if: always() env: -- cgit v1.2.3