Add an Android Auto Surface - #31
Open
NicholasFeldman wants to merge 373 commits into
Open
NicholasFeldman wants to merge 373 commits into
NicholasFeldman wants to merge 373 commits into
Conversation
…egional boundary line and labels
… actually renders on shorter lines instead of being hidden or unreadable
…ead of showing as anonymous RX observations. Log tab, noise floor graph, and map popup show "Direct Repeats" and "Multi-hop Repeats" sections. TX markers are green (direct echo), RX-colored (multi-hop only), or red (no response).
…nd every ping is tagged with it. You can now filter region coverage by radio preset.
- TX pings now send a short opaque tag (e.g. "MM:zpCFQwc") on the #wardriving channel instead of your GPS coordinates. Your location still reaches the server over the API, it's just no longer readable off the air by anyone with the channel key. - New "Broadcast My Coordinates" toggle (Settings -> Ping Settings, off by default) lets you opt back into putting your real coordinates on the air, now sent as a compact "MM:lat,lon". - Very long sessions now end cleanly: when the per-session ping limit is reached the app uploads any pending pings, then disconnects with "Reached session limit, please reconnect."
…oJson tile(hack cause we cant invalidate a single tile with LibreMaps)
…heat fix) Root cause of phones overheating during wardriving: the MapLibre MapWidget (the most expensive subtree) did context.watch<AppStateProvider>() and so rebuilt on EVERY one of the provider's ~166 notifyListeners() calls — including high-frequency UI-only notifies (noise floor every 5s, battery, live stats) and the dense-mesh passive-RX pin storm (10-20x/sec). This kept the CPU/GPU pinned. Present in the weeks-old build too, which matched the reports. Fix (Tier B): - AppStateProvider gains `mapRevision` plus `_notifyMapNow()` (immediate) and `_notifyMapThrottled()` (250ms leading+trailing coalescing). Only map-relevant mutations (TX/RX/disc/trace markers, echoes, GPS position, repeater load, history view, marker/log clears, marker-style prefs) bump mapRevision; the RX-pin/echo storm goes through the throttle (~4/sec cap). UI-only notifies (noise floor, battery, stats) leave mapRevision untouched. - MapWidget is wrapped in a Selector keyed on (mapRevision, focus, history, padding, controls) in home_screen.dart, so it is cached across all UI-only notifies, and its top-level subscription switches from context.watch to read. Result: the map rebuilds only when map data/layout actually changes, at a bounded rate, instead of continuously. Investigated and found NOT to be causes (no change): - 500ms countdown timer: already isolated into its own ChangeNotifier. - graph_screen 2s live timer: it's a pushed MaterialPageRoute (disposed on pop), not an offscreen IndexedStack child — no continuous offscreen cost. - _saveLastPosition: already throttled to 30s; checkDistanceTriggers gates at 25m. - _computeMarkerDataVersion heard/discovered sums: they drive marker color transitions, so they must stay; Tier B makes the O(N) loop run rarely. flutter analyze: clean. flutter test: 39/39 pass.
Add the "Map Rebuild Isolation" section to DEVELOPMENT.md and CLAUDE.md, and a new Critical Rule 9 in CLAUDE.md: map-rendered state must bump mapRevision via _notifyMapNow()/_notifyMapThrottled() (not plain notifyListeners()) so the map, now isolated behind a Selector + context.read, still updates.
…e pump) The Active/Hybrid/Passive mode buttons ran a repeating 1200ms pulse AnimationController (_pulseController.repeat) the entire time a mode was running. A repeating AnimationController keeps Flutter's vsync ticker alive, so the GPU/compositor never idled — on a 120Hz device the app rendered ~120fps for the whole wardriving session for a purely cosmetic breathing glow, even parked. Profile-mode measurement (SchedulerBinding.addTimingsCallback FPS logger): during active Hybrid mode the app sat at ~113-129 fps continuously; with the pulse removed it drops to ~25-28 fps (renders only when something actually changes). The button still reads as active via color, the indicator dot, the "Active" text, and the live countdown — only the animated breathing is gone. Removes the AnimationController / AnimatedBuilder from _ActionButton, _LandscapeIconButton, and _CompactActionButton; the active-state background opacity is now a static value. flutter analyze: clean. flutter test: 39/39 pass.
The MapWidget's Selector was being silently defeated on every HomeScreen rebuild. HomeScreen.build() uses context.watch, so it rebuilds on every notifyListeners() — including the ~2Hz GPS notify. Each rebuild constructed a fresh inline Selector instance, and provider's Selector invalidates its cache whenever `oldWidget != widget` (selector.dart:77), so the cached MapWidget was rebuilt BEFORE the value comparison ran — relayouting the iOS platform view (~24ms) on every GPS position update. Measured on-device (iPhone, profile mode): GPS-driven map builds dropped from 2-3x/sec @ ~24ms to 0/sec; per-frame build time fell ~24ms -> ~1ms while the camera still follows in real time. Two changes: - Decouple GPS from mapRevision: the GPS listener now calls plain notifyListeners(); camera-follow, derived heading and the GPS puck are driven from a direct provider listener (_onPositionNotify -> _handleGpsPosition) via the native controller (animateCamera / updateSymbol), preserving the real-time nav feel with no widget rebuild. - Memoize the map Selector (home_screen _buildMapSelector) so its widget identity is stable across HomeScreen's per-notify rebuilds, letting the Selector's value comparison actually gate the map.
…ge Grid" -> "Grid Mode"
…ution for cell MAX-DIST Brings the web's coverage "Cell Click" (GRID SUMMARY: per-status counts, AVG SNR/NOISE, MAX DIST, proportional bar graph) and "Repeater Click" (online status, fingerprint, hop bytes, schedule + clock-skew warning, first heard, max range, BIDIR/TX/RX/DISC/DEAD totals) into the app, with client-side aggregation mirroring the web. Coverage is fetched lazily via the key-gated app_coverage.php proxy. Cell MAX-DIST (GridSummary) resolves tokens via _repForToken (full-hex / narrowCandidates) instead of a bare id lookup, so it stays correct as the server normalizes coverage path tokens to a wider-than-id canonical hex. - lib/utils/coverage_summary.dart: GridSummary / RepeaterStats / RepeaterLookup / GridCell; _repForToken full-hex resolver. - lib/utils/repeater_format.dart, distance_formatter.dart: date / clock-skew / coverage-distance formatting. - lib/widgets/cell_summary_sheet.dart, map_widget.dart: cell-tap + repeater detail sheets. - lib/services/api_service.dart, providers/app_state_provider.dart: coverage fetchers. lib/models/repeater.dart: time_offset. - tests in test/utils/.
The multi-byte path warning told users the radio would stay in multi-byte mode until they change it. That's wrong: _restorePathHashMode() restores the original setting on a clean disconnect (unless the user changed it themselves). Reword to say the app restores it on a normal disconnect and warn that an unclean drop needs a manual revert in Settings.
Tapping a coverage cell in Detailed mode now (a) fetches and keeps the pings whose 3x3 blob colours the tapped cell, matching the web's lazyShowPingsAt (GridCell.filterWithinBlob / blobFetchRadiusMeters, used by _showCellSummary), and (b) draws an outline-only 3x3 highlight block centred on the tapped tile (GridCell.blockRing + a self-healing line-layer overlay, cleared when the summary sheet closes). Simplified mode collapses to the single tapped cell. Adds GridCell unit tests.
With a session and 'Broadcast My Coordinates' on, the on-air body is now MM:<tag>:lat,lon (keyed tag plus plaintext coords) instead of coords-only. The bare wire tag still goes to the API (txWireTag), so /wardrive validation and tx_pings are unchanged, and the 11-bit session-limit counter guard now applies to both privacy and broadcast modes. No session yet still falls back to plaintext MM:lat,lon.
Mobile offline-session download was a 'coming soon' stub (web already worked). Add AppStateProvider.shareOfflineSession: write the session's pretty JSON to a temp file and open the OS share sheet (Save to Files, Drive, email, ...) via share_plus, mirroring shareDebugLog. settings_screen's download handler now calls it on mobile; the web blob-download path is unchanged. Downloads work before upload and after a failed upload (those sessions stay listed).
setMapStyle and setColorVisionType called plain notifyListeners(), which does not bump mapRevision — so the MapWidget (isolated behind the mapRevision Selector, Critical Rule 9) never rebuilt and the new style/palette never reached the native map. Switch both to _notifyMapNow() so the change actually applies.
…h event) The map re-pushed every accumulated TX/RX/DISC/trace marker to the native layer on every ping event — hundreds of awaited platform round-trips per event late in a session — so marker/ping display lag grew with session length and the coverage tile often repainted before its ping marker showed. Markers now skip the native update when their icon/size is unchanged, bounding per-event work to actually-changed pins. Adds a [MAP] Coverage sync diag line.
…e server to accept the upload session before sending, and never discards pings it couldn't upload (they're kept and retried instead of being lost as a "partial upload").
…at record time for parity with live uploads — model was previously sent as the generic 'Offline Upload' and power/version were derived at upload time instead of when the pings were recorded. Snapshotted fields are sent on upload auth with fallbacks for legacy sessions.
…coordinates All 10 TestFlight crash reports share one root cause: an invalid GPS or stored coordinate (NaN / infinite / out-of-range) reached MapLibre's camera, whose native LatLng constructor throws std::domain_error — uncaught across the C++/Obj-C boundary, so it aborts the app (SIGABRT). iOS can briefly report an invalid CLLocation right after the app resumes from background (the "brought it back up and it crashed" reports), and a corrupted/stale lastKnownPosition from Hive explains the "instacrash on launch" reports. The prior _cameraAnimationReady one-frame delay only addressed GL-surface timing, not the invalid input. Two layers of defense: - Layer A (crash-stopper): validate every coordinate before it reaches the map camera. New shared isValidLatLng() guards the animate helpers, the three center builders feeding initialCameraPosition + style-load zoom, the focus/history fit-bounds (invalid points filtered), and the cluster zoom-ins in map_widget, plus the offline_maps_screen center. - Layer B (stop the bad data at the source): drop invalid fixes at the GPS stream, simulator, getCurrentPosition and getFreshPosition; ignore invalid stored last-known-position on load and never persist invalid coords. This also keeps invalid coordinates out of the API upload payloads. Adds lib/utils/geo_validation.dart + unit test. flutter analyze clean; full suite green (72 tests).
…kdrop (web parity) Replaces the cyan outline box around a tapped cell's 3x3 block with the web front end's spot-click look (highlightSpotCoverage): the block fills as a grid of cells in one dominant colour and the coverage backdrop dims to 0.15 so the footprint pops, restored when the summary sheet closes (honouring ping-focus). The dominant colour is the highest-priority status (green > cyan > orange > purple > grey > red) among only the pings whose blob covers the tapped cell, so a red-dominant block repaints green-looking neighbours that were coloured by a ping outside the blob (the intentional smear). Applies in both grid modes (3x3 Detailed, single cell Simplified). Highlight + dim appear with the fetched pings, driven off the same fetch the summary sheet already uses (no extra network call).
…ze button Both tap popups now match the ping-focus sheets: a transparent modal barrier so the map and markers stay fully bright (the tile footprint's 3x3 highlight + 0.15 coverage dim still pop), plus a minimize button that collapses the sheet to a tappable bottom pill, leaving the map interactive. Adds a generic _MinimizedInfoPopup + pill (kept separate from focus state), extracts _presentCellSummarySheet for reuse on reshow, and threads an optional onMinimize into CellSummarySheet. Opening a new popup supersedes a minimized one (a minimized cell's footprint is cleaned up when superseded by a repeater; cell-to-cell keeps the old footprint until the new data lands, so no flash).
The minimize pill sat underneath the ping control panel. Mirror focus mode's mechanism: add an AppStateProvider.infoPopupMinimized flag and OR it into the same control-panel visibility conditions + map bottom-padding calc in home_screen that isFocusModeActive/viewingHistorySession already use. The pill set/clear is centralized in _setMinimizedInfoPopup/_clearMinimizedInfoPopup so the flag stays in lockstep across every show/reshow/close/supersede path.
App Shortcuts are indexed from the app, so an AppShortcutsProvider that lives in the App Intents extension is never registered: its phrases resolved to nothing when spoken, with no error to explain it. Status, Recent Repeaters and Find Repeater were all declared that way. Merge the two providers into one MeshMapperAppShortcuts in Runner and give the read intents and their entities dual target membership, which is Apple's arrangement for an intent that backs an App Shortcut and must also run in an extension. Reads still execute in the extension and never launch Flutter. Withdraw the Find Repeater phrase for now. Spoken lookup still failed on device, and a registered phrase that fails is worse than one that was never offered. The intent, entity and query stay built and usable from the Shortcuts app, with a TODO naming the two suspects worth checking.
Implement iOS App Intents and enhance Siri shortcuts functionality
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds an android auto surface, bringing in flutter_carplay as a dependency.
Testing must be done with Desktop Head Unit (https://developer.android.com/training/cars/testing/dhu), and can not be used against a physical head unit.
This was created as a POI App (https://developer.android.com/training/cars).
See here for distribution requirements (developer.android.com/training/cars/distribute).
Claude Code was used for the integration of flutter_carplay, some dart code, test generation, and self review.