Decisions (Android port)
Moved from vulture-android/docs/decisions.md
Originally titled “Decisions”.
1. Domain quirks: copy or fix?
Section titled “1. Domain quirks: copy or fix?”Copy = Android reproduces iOS exactly. Fix = fix on iOS first (see ios-changes.md §C), then port.
Details per quirk: analysis/01-flight-domain.md §13.
| # | Quirk | Who notices | Recommendation | Decision |
|---|---|---|---|---|
| 1 | Landing detection reads a fix near the start of the file | Landing time / flight duration can be off | Fix | Fix |
| 2 | Release (tow/winch release) index is one fix too early | ~1 s on release time | Fix | Fix |
| 3 | Midnight-UTC wrap compared with the first fix only | Flights crossing 00:00 UTC (Australia, Americas) | Fix | Fix |
| 4 | Vario smoothing feeds back already-smoothed values | Nobody; it’s a smoothing style | Copy | Copy |
| 5 | Two Earth radii (6371 km vs 6378.137 km) | ~0.1 % on optimised/task distances | Copy (changing it moves every distance) | Copy |
| 6 | distanceTraveled stored in km, optimisations in m | Nobody (internal units) | Copy | Copy |
| 7 | Wind direction = upwind bearing; first wind sample keyed at the first fix | Nobody, if the UI labels it right | Copy | Copy |
| 8 | Thermal “relative strength” explodes when every thermal is negative | Rare weird thermal colouring | Fix | Fix |
| 9 | Rejected thermal candidates still mark their fixes as “thermal” | Subtle (thermal membership of a fix) | Copy | Copy |
| 10 | Turn-direction count skips the last two fixes of a thermal | Nobody | Copy | Copy |
| 11 | No legs when a flight has no thermals and no engine run; the leg distance is a 5-TP optimised distance | Straight glides show no legs | Copy | Copy |
| 12 | A C-record declaration with a task name after it is dropped | Declared task silently missing | Fix | Fix |
| 13 | HFDTEDATE:ddmmyy without ,NN gives no date | Flight without a date from some loggers | Fix | Fix |
| 14 | Generated filename and recording dates use the device time zone | Same flight, different name on other devices | Fix | Fix |
| 15 | Time-zone offset taken at 00:00 UTC of the flight date | Only on DST change days | Copy | Copy |
| 16 | Default observation zones are all zero unless configured | Scoring falls back to line crossings / closest point | Copy (it’s a default, not a bug) | Copy |
| 17 | “Triangle” has no closing leg and no FAI 28 % rule | Feature gap | Copy (feature for later) | Copy |
| 18 | ft/min, hPa and % print 6 decimals | Visible ugly numbers | Fix (ios-changes.md B9) | Fix |
| 19 | Live takeoff detector uses a 100 m turn radius vs 50 m in the IGC analysis | Recording only (not in Android v1) | Copy | Copy |
| 20 | 64-bit ints and Int.max “no value” sentinels | Nobody | Copy (Kotlin Long) | Copy |
| 21 | Optimiser ranges crash Swift when bounds invert | iOS crash on odd input; Android would silently skip | Fix (guard on iOS) | Fix |
| 22 | Elevation download has a 10 s budget; any failure means no terrain for the whole flight | Numbers differ online vs offline | Copy for now | Copy |
| 23 | A tile still loading when the 10 s budget ends gets 0 m terrain, yet the flight counts as having terrain | Wrong AGL and pressure correction on slow connections | Fix (decided 2026-10-10) | Fix |
2. Sync
Section titled “2. Sync”Firebase (Auth with Google sign-in, Firestore, Cloud Storage), free for everyone. CloudKit dropped (decided 2026-10-10).
3. App identity
Section titled “3. App identity”- applicationId:
be.fousa.vulture(debug:be.fousa.vulture.debug) - minSdk: 29 (Android 10)
4. iOS bug fixes
Section titled “4. iOS bug fixes”Yes; see ios-changes.md §B.
5. Android applies the fixes first (decided 2026-10-10)
Section titled “5. Android applies the fixes first (decided 2026-10-10)”Android implements every Fix above and every bug in ios-changes.md §B/§C now, as each
area is ported, without waiting for iOS; iOS follows later. In code and tests each such place
is tagged FIXED(<id>) (e.g. FIXED(B9), FIXED(Q12)) with a note on what iOS did before.
Consequence for parity testing: golden data exported from an iOS build without a fix will
differ at exactly those tags. Export golden data after the iOS fixes land, or treat
differences at FIXED(...) places as expected.
| Fix | Android status |
|---|---|
| B1 week-based year | done (:domain:waypoints, :domain:services ServiceDates); iOS done (1c986f21f) |
| B2 SkyLines date format, B3 Charron season (October, UTC), B5 WeGlide pin | done in Phase 7 (:domain:services, :data:skylines, :data:charron, :data:weglide), ported from iOS main: iOS done (ffec35d26, d7dc98bb2, 986024c60; P7-D1) |
| B4 Charron competition field | open on both platforms: the field name is behind the Charron login; Android keeps parity with TODO(B4) (:data:charron, P7-D2) until the owner looks it up |
| B40–B49 Phase 7 candidates (Charron website season, form encoding, SkyLines refused upload, WeGlide user id, task update, unknown-service Update, upload Retry, empty SkyLines results, Soaring Spot country order and day zone) | owner, 2026-10-12: B40–B42 and B44–B49 accepted, FIXED(Bxx) (B41 and B48 were PARITY; done); B43 rejected, Android copies iOS (PARITY). iOS prompt: ios-fix-prompt-5.md |
| B6 infringement altitude reference, B7 OpenAir GND/SFC, B8 OpenAIP tile units | done (:domain:airspace, ported from iOS main, where they are fixed: iOS done, decision P6-D2) |
| B9 / Q18 unit decimals and ft/min | done (:core:units) |
| Q3, Q12, Q13, Q14 | done (:domain:flight parser) |
| Q8 | done (flight maxima in the parser, thermal relative strength in :domain:flight enrichers) |
| Q1, Q2, Q21 | done (:domain:flight enrichers; Q21 also guards task scoring) |
| Q23 elevation timeout = no elevations | done (:domain:flight) |
| B10 statistics rounding, B11 preview duration past 24 h, B13 blank pilot names, B14 single-year grid corners | done (:domain:logbook) |
| B12 white-space trimming (row comment, Library caption) | done (:domain:logbook, :feature:library); task filenames keep the iOS helper |
| B15 highlight border below its line (Phase 4 D7) | done in the layer order (:core:map FlightMapLayerIds); drawn by WP-M2; confirmed 2026-10-11 |
| B16 flight-map layers emissive on Standard at night | done (:core:map FlightMapLayers); confirmed 2026-10-11 |
| B17 barogram handle reports itself active (was P4-1) | done (:core:barogram BarogramState, BarogramGestures) |
| B18 coalescer drops a pending trailing push (was P4-2) | done (:core:map CoalescingPusher) |
| B19 wind arrow also for a wind from 180° (was P4-3) | done (:feature:flightmap WindPill) |
| B20 shared task by value (was P4-6) | done (:domain:flightmap ReplayRanking.isSameTask, :domain:compare CompareData) |
| B21 race ends at the first engine run after the start (was P4-7) | done (:domain:flight RaceProgress) |
| B22–B25 barogram: highlight offset, axis minimum, interval pairs, ft/min readout (from P4-4) | done (:domain:barogram) |
| B26–B28 flight detail: glide ratio “42:1”, water ballast measure, left/right average vario (Phase 5 D9) | done (:domain:flightdetail); confirmed 2026-10-11 |
| B29 Map Image in a folder of its own (Phase 5 D7) | done (:core:map FlightSnapshotFile); confirmed 2026-10-11 |
| B30 catalog typos “Competion” / “Minumum” | iOS catalog only; Android picks it up when strings_ios.xml is regenerated |
| B31–B33 Debrief leg numbering: final glide on the last leg, legs 1…n, no hole after a dropped leg (were P5-1…P5-3) | done (:domain:debrief task and detour answers; B33 also :domain:flightdetail DetailOptimizationItem, OptimizationDetail) |
| B34 water ballast slider in 10 L steps | done (:feature:editflight, already so since WP-E1) |
| B35 measure end dots’ white rings stay (Phase 6 spike S2) | done (:core:map measure/); confirmed 2026-10-11 |
| B36 airspace sheet “inside” under an UNL ceiling (P6-D5) | done (:domain:airspace AirspaceDetailModel); confirmed 2026-10-11 |
| B37 measure distance white on white in dark mode / Satellite (WP-M2) | done (:core:map MeasureLayers); confirmed 2026-10-11 |
| B38 openAIP tile fragments and MultiPolygon parts in the scan (P6-D3) | done (:domain:airspace, :core:map); confirmed 2026-10-11 |
| B39 the scan reads only the airspace source the map shows (was P6-3) | done (:feature:flightmap scanInput); decided 2026-10-11 |
| B50–B51 Phase 8a candidates (a cancelled purchase alerts, a restore that finds nothing says “failed to activate”) | owner, 2026-10-12: both accepted: Android says nothing on a cancel (FIXED(B50), done) and a restore that finds nothing gets its own message (FIXED(B51)). iOS prompt: ios-fix-prompt-5.md |
6. Importing and opening files (decided 2026-10-10)
Section titled “6. Importing and opening files (decided 2026-10-10)”- No feedback after a successful import, as on iOS: no “3 flights imported” message, duplicates are skipped silently. Only failed files are reported, in the iOS alert (one file: its name and the error; several: a list).
- Folder and ZIP import stay (Android-only additions).
- No
text/plainin the share filter: Vulture does not appear in the share sheet for every shared text or link. - Pro gating of files from other apps follows iOS (02 §4.7) when the paywall lands: wait for the paywall, and without several flights keep only the first flight of the batch.
- Back while a shared file is still opening cancels the open: accepted.
- Live takeoff detector and Q2: not decided; revisit when recording is ported.
7. Phase 3: shell and flight list (decided 2026-10-10)
Section titled “7. Phase 3: shell and flight list (decided 2026-10-10)”- Fix on Android now, iOS later (
ios-changes.md§B, tagFIXED(<id>)):- B10 statistics cut speeds, distances and altitudes to whole SI units before converting (25.9 m/s → 90 km/h instead of 93);
- B11 the preview duration wraps after 24 h;
- B12 comment trimming keeps one leading/trailing space instead of removing it;
- B13 empty-string pilot and passenger show ”, ” instead of ”—”;
- B14 a statistics grid with a single year reports only left corners.
- Match iOS: on a compact window the app reopens the list that was open (iOS
Defaults[.listVisible]), instead of always starting on the Library. - Keep Android’s behaviour:
- a shared flight without a stored filename gets the generated name
{compId|registration|pilot}_{ddMMyyyy}.igc, its day in UTC (asFIXED(Q14)); iOS skips such flights and uses the device zone; - the statistics update live while open, and an empty logbook shows “No flights yet”;
- a flight opened from another app closes the statistics;
- a thin progress bar while files are imported (no message).
- a shared flight without a stored filename gets the generated name
- Phase 3 scope, confirmed: the Library shows only rows whose screens exist (others arrive with their phase);
“Start flying” (recording) is not in v1; until Phase 4 a flight opens on a summary screen;
ProAccessunlocks everything until the paywall phase; the countries filter stays hidden until airports are ported (since 2026-10-20 the airports table and the take-off lookup are ported, analysis 2026-10-takeoff-airfield-android; the airport screens are not); the look and feel details from QA (small + on the empty Logbook, filled Files icon, picked-row thumbnail) are accepted as they are. - Mapbox: the public token lives in
local.propertiesasMAPBOX_ACCESS_TOKEN; gallery artwork (WP10) goes ahead. The gallery images carry no Mapbox logo or attribution, as on iOS (owner’s choice, knowing Mapbox’s guidelines ask for it on static images too). The interactive maps (Phase 4) keep their attribution.
8. Phase 4: flight map, barogram, replay, compare, ranking (confirmed by the owner on 2026-10-11)
Section titled “8. Phase 4: flight map, barogram, replay, compare, ranking (confirmed by the owner on 2026-10-11)”Taken by the lead during Phase 4 so the work could go on; confirmed by the owner on 2026-10-11 (D1–D15, B16 and the deviations below). Numbers refer to the Phase 4 plan’s decision table.
- D1 Gliders are one Mapbox
SymbolLayerfor all flights (the registration pill is text on a stretchable image) instead of iOS view annotations; the pill looks slightly different. - D2 The map is an
AndroidView(MapView)driven by a controller (:core:mapFlightMapController); no Mapbox Compose extension. - D3 The Mapbox logo and attribution ride above the panel’s top edge (bottom-left), at the bottom when the panel is hidden; the compass top-end below the toolbar. The interactive maps keep their attribution (§7).
- D4 The
ibutton opens an interim summary card until the Phase 5 detail exists. - D5 Out of Phase 4: SkySight clouds (hidden), Debrief / thermal sheet / photos / map image (Phase 5), measure and long press (Phase 6), drag and drop and keyboard shortcuts (Phase 9). A thermal tap moves the handle to the thermal’s centre only.
- D6 Play is an M3 split button (play / stop + speed menu) instead of iOS’s tap-menu / long-press button.
- D7 The highlight border is drawn below its line on Android now (
FIXED(B15),ios-changes.mdB15). - D8 Compare is a full-screen dialog on every width; companions are a bottom card (compact) or a floating
400 dp card (wider); no
SupportingPaneScaffolduntil Phase 5. - D9 Chip reordering uses the
sh.calvin.reorderablelibrary. - D10 No iOS golden export for barogram ranges and readouts for now; checked by hand against the Swift code.
- D11 Performance and ranking QA on a real competition day: OBGN 2026 task 5 (15 m: 16 flights, 18 m/Open:
13 flights), kept git-ignored in
testdata-local/(seedocs/setup.md, “Local test data”; never committed). The lab’s synthetic copies remain for 50-flight runs. - D12 The glider, hang-glider and paraglider markers are vector drawables traced from the iOS PDFs.
- D13 Roborazzi screenshot tests (Robolectric,
@GraphicsMode(NATIVE)). - D14 A debug-only map settings sheet (the lab’s
settingsscreen) until the Phase 9 settings exist. - D15 The barogram quirks are copied (
PARITY) and listed as iOS candidates (ios-changes.mdP4-4). On 2026-10-11 four of them became fixes (B22–B25, below); the other six stayPARITYcandidates. - B16 (proposed by WP-M2) Line, fill, circle and symbol layers of the flight map get emissive strength 1, so
Standard’s night preset does not turn them black (
FIXED(B16),ios-changes.mdB16). Confirmed 2026-10-11, as is B15 (D7).
Deviations recorded in the module READMEs (confirmed by the owner on 2026-10-11; details and reasons in the
linked sections). Those that became iOS fixes are now tagged FIXED(<id>) instead of deviations:
:core:barogram(vulture-android/core/barogram/README.md): the handle really reports itself active (FIXED(B17), was P4-1); zoomed without a handle, only the handle moves; the AAT label on one line; no pointer hover; with nothing selected every flight’s overlays show; TalkBack actions instead of VoiceOver’s audio graph.:core:map(vulture-android/core/map/README.md): the coalescer drops the pending trailing push (FIXED(B18), was P4-2); the highlight’s layers stay on the map, empty; lines with fewer than 2 points are left out; event label and glider tint follow light / dark on Standard without a reload.:domain:flightmap(vulture-android/domain/flightmap/README.md): shared task by value, not identity (FIXED(B20), was P4-6); thermals filtered in the data; colours asrgbastrings; no feature ids where nothing reads them.:domain:compare(vulture-android/domain/compare/README.md): the compare table’s shared task by value (FIXED(B20)).:domain:flight(vulture-android/domain/flight/README.md): aFlightno longer keeps its IGC text (memory).:feature:flightmap(vulture-android/feature/flightmap/README.md): flights are swapped as fresh copies, never changed in place; a replacement analyses and swaps one flight at a time (memory); missing or broken stored flights are reported; Play as a split button (D6); the wind arrow also at 180° (FIXED(B19), was P4-3); the ranking card follows the evolved iOSfeature/race-rankingplacement (side card ≥ 600 dp, slim 64 dp column, expanded) and has no shadow; the task picker has only the flights tab; nokeepingVisibleRegionwhen the ranking card comes and goes; a glider without a bearing points north; the pinned follow lets its camera ease finish (Phase 4 QA fix); the replay time is not restored after process death (the flights and the shell state are).:feature:compare(vulture-android/feature/compare/README.md): a spinner while the model is built; initials from the first and last names; system band colours; ▲/▼ as text glyphs.
Owner decisions of 2026-10-11 on the Phase 4 candidates (ios-changes.md §B; Android applies them now, §5):
- P4-7 → B21: the race ends at the first engine run after the start, so a motored finish is a land-out at the engine start (as Soaring Spot). On OBGN 2026 task 5 the ranking’s finishers now match the official results.
- P4-6 → B20: shared tasks are compared by value, as Android already does; iOS will too. No longer a deviation.
- P4-1 → B17, P4-2 → B18, P4-3 → B19: iOS fixes; Android already behaves this way.
- P4-4: four barogram quirks are fixed (B22 highlights with the alignment offset, B23 non-altitude axes start
at the grid line at or below the minimum instead of one interval lower, B24 the inverted small/large interval
pairs, B25 the ft/min vario readout without decimals); the other six stay
PARITYcandidates. - P4-5 (whole-degree bisector on the map) stays a
PARITYcandidate. - Physical-device testing comes later (an open item in the roadmap).
9. Phase 5: flight detail, companions, editing, photos and the map image (2026-10-11)
Section titled “9. Phase 5: flight detail, companions, editing, photos and the map image (2026-10-11)”Taken by the lead so the work could go on (the Phase 5 plan’s decision table, D1–D19); D1–D19 confirmed by the owner on 2026-10-11.
- D1 The detail is a modal bottom sheet on a compact window (iOS page sheet; it closes when a row acts on the
map) and a floating 400 dp card at the end side, below the top bar, on wider windows (iPad popover; it stays open
and follows the selection). No
SupportingPaneScaffold. - D2 The detail and the Debrief keep their own small back stack (
rememberSaveable+AnimatedContent+PredictiveBackHandler), not a nestedNavDisplay. - D3
FlightRating,FlightScope,FlightQuestionsmoved to:domain:debrief;DetailOptimizationItem,OptimizedLegto:domain:flightdetail(:domain:comparedepends on both). - D4 The detail, Debrief, thermal sheet and photos are modules the map depends on; Compare and Edit flight stay app slots.
- D5 Edit flight is a full-screen dialog (compact) / a 560 dp dialog (wider), saved on Done and on dismiss with no
Cancel (parity), plus a save at
ON_STOP; it writes only when a value changed. - D6
Photos: (a) the system photo picker (no media permission), placed by EXIF GPS or else by time on the track, kept per logbook flight in Room (schema version 2, local only). Confirmed by spike S2. Motion photos are stills.Replaced 2026-10-20: photos left Android, as Google Play does not allow the automatic lookup iOS does (Remove flight photos from Android); Room schema 5 drops the table. - D7 Share › Map Image is a capture of the live map (gliders included), iOS’s composer and a drawn attribution
pill, written to
cache/snapshots/<uuid>/(FIXED(B29)). - D8 The optimisation detail’s mini map is a Mapbox
Snapshotterimage. - D9 02 §6.11 quirks: glide ratio “42:1” (
FIXED(B26)), water ballast as a plural measure (FIXED(B27)), “Average vario (left / right)” (FIXED(B28)); the thermal diameter in the altitude unit and the histogram bucket edges are copied (PARITY); the catalog typos “Competion class” / “Minumum” are an iOS catalog fix (B30). - D10 The report mail goes to
me+vulture@fousa.be(as iOS), iOS subject, body with the Android version and build, the IGC attached. Address confirmed by the owner (2026-10-11). - D11 No Flight Coach and no Siri intents in v1.
- D12 Debrief entry: a toolbar button on windows at least 600 dp wide, an overflow item below (parity).
- D13 A thermal tap (and Go to › Nearest thermal) opens the Pro thermal sheet; without Pro only the paywall (wired to the real paywall in Phase 8a, §12). This replaces Phase 4’s D5.
- D14 Airspace rows, page and “Center infringements” are Phase 6; Phase 5 ships an empty contract.
- D15 Every logbook field updates live in the open detail (Room flows; iOS: water ballast only).
- D16 The Android-only
FlightEntity.ratingcolumn stays unused. - D17
Coil 3 for the photo pager.Void 2026-10-20: the pager and Coil left with the photos. - D18 No Mac-style inspector tabs on wide windows: the card pushes pages as the phone does.
- D19 The stale
.claude/worktreesare left alone (the owner removes them).
Confirmed by the owner (2026-10-11):
- The report address
me+vulture@fousa.be(D10). - Photos picked outside the recording (or without a usable time) are skipped, with a short message (“1 photo was
not taken during this flight and was not added.”, Android-only plurals
photos_message_skipped). - “Show your photos” (
arePhotosVisible) stays off by default, as iOS; adding photos turns it on (PhotoStore.add); the map’s ⋮ › Photos › “Show your photos” toggle turns it off again. - The Debrief leg numbering becomes an iOS fix that Android applies now: “Final glide” on the last leg (
B31, was P5-1), every leg numbered 1…n by its position (B32, was P5-2), and no hole or repeated turnpoint after a dropped zero-length leg (B33, was P5-3; the unflown legs continue after the last reached turnpoint). TaggedFIXED(B31)…FIXED(B33)in:domain:debriefand:domain:flightdetail. - Edit flight’s water ballast slider moves in 10 L steps on both platforms (
B34; Android already did, iOS’s is continuous today). A stored value off the grid shows as it is until the slider moves.
Phase 5 integration (WP-F5) and QA (WP-Q) choices, recorded in the module READMEs:
:feature:flightmap(vulture-android/feature/flightmap/README.md) “WP-F5”: the “Photos” submenu (Add photos…, Show your photos) is Android only, for logbook flights; Go to › Nearest thermal opens the thermal sheet as iOS’s menu item does (the phase pill still only goes to the thermal); the detail card on a wide window covers the side ranking card (as iOS’s popover) and the camera keeps clear of an open card there (QA fix).:core:share(vulture-android/core/share/README.md): the report mail is aimed at the apps answeringmailto:(one by package, several through a chooser) instead of amailto:selector, which did not resolve on Android 16 (QA fix).:core:barogram(vulture-android/core/barogram/README.md): the TalkBack actions no longer hold the old model across a task override (QA fix: the 16-flight override ran out of memory with an accessibility service on).
Still open for the owner (Android keeps what is described until decided):
- Map Image chip text in dark mode follows the app theme (iOS
inverseColoris dynamic), so a dark flight colour gets near-black text on the always-dark strip (PARITY, feature/flightmap README “WP-U5”): worth a look on iOS.
10. Phase 6: airspace (2026-10-11)
Section titled “10. Phase 6: airspace (2026-10-11)”Taken by the lead so the work could go on (the Phase 6 plan’s decision table, P6-D1…P6-D20); confirmed by the owner
on 2026-10-11 (P6-D4 refined by the owner’s decisions on P6-2 / P6-3, below). Where each lives is in the module READMEs (:domain:airspace, :data:airspace,
:core:map “Airspace” and “Gestures”, :feature:airspace, :feature:airspacefiles, :feature:flightmap “WP-F6”).
- P6-D1 openAIP key:
OPENAIP_API_KEYinlocal.properties/ CI (for now the key the iOS app uses); blank → no openAIP layers (logged once), no crash. No backend proxy. - P6-D2 B6 / B7 / B8: ported from iOS
main(fixed there), taggedFIXED(B6)…FIXED(B8); iOS done (§5). - P6-D3 openAIP tile fragments and MultiPolygon parts are merged per airspace for the scan (
FIXED(B38)); spike S1 confirmed the iOS quirk. - P6-D4 Scan input. Hidden sectors are still scanned (
PARITY, candidate P6-2: kept by the owner on 2026-10-11; hiding a sector on the map does not make it safe to fly through). The scan reads only the airspace source the map shows: the openAIP airspace on screen while openAIP is shown, the visible files (hidden sectors included) while custom airspace is on (FIXED(B39), candidate P6-3, fixed by the owner’s decision of 2026-10-11; iOS scanned the files under openAIP too, also without Pro, and listed an airspace in both twice, seen on the OBGN day). - P6-D5 The airspace sheet treats an UNL ceiling as above any altitude (
FIXED(B36)). - P6-D6 Only the selected flight is scanned (it is also the detail’s flight); iOS scans every flight on the map.
- P6-D7 Library › Airspaces shows list rows only; the gallery layout waits for Phase 9.
- P6-D8 Import: + › Files (any type, sniffed), + › XCSoar (the catalogue), “Open with” routed by sniffing with
iOS’s “Import successful” alert; no
text/plaininSEND(§6 stays), no folder import for airspace, no openAIP XML catalogue. - P6-D9 Custom Airspace, Show hidden sectors and Automatic updates live on an “Airspace settings” page under Library › Airspaces › ⋮ until the Settings screens (Phase 9). Since 2026-10-20 Automatic updates is only in Settings › Map, as on iOS (Airspace automatic updates).
- P6-D10 Measure gesture: two-finger long press (parity); spike S2 found no conflict with Mapbox’s gestures.
- P6-D11 The query marker is a Mapbox view annotation with a pulse.
- P6-D12 A tap while the sheet is open: a glider first, then the airspace query; photos and thermals are not tappable meanwhile.
- P6-D13 Hidden sectors: iOS’s JSON format and day rules,
filesDir/airspace/<yyyy-MM-dd>-hidden-airspace.json, local only (sync decided in Phase 8). - P6-D14 The airspace id hash is ported exactly (
swiftDescription+ iOS’s djb2 variant), tested against Swift. - P6-D15 Catalogue update check: one WorkManager job per app start (network constrained), the shell’s “Updates available” alert, automatic update when the setting is on.
- P6-D16 Waypoint files and the explore / task maps: out of scope; their layer slots stay reserved.
- P6-D17 ”© openAIP” as the vector source’s attribution (Mapbox’s (i) dialog).
- P6-D18 Every airspace layer is fully emissive so Standard at night keeps the colours (extends
FIXED(B16)). - P6-D19 Infringements against openAIP only cover the airspace on screen (
PARITY, the sheet’s footer says so). - P6-D20 iOS airspace golden exporter:
ios-changes.mdA3; meanwhile golden data from a Swift copy on this Mac.
iOS fixes from Phase 6 (ios-changes.md §B, Android applies them now): B35 (measure end rings) and B37 (measure
text on a white pill) proposed by the work packages; B36 (UNL ceiling, P6-D5) and B38 (openAIP fragments, P6-D3) lead
decisions; all four confirmed by the owner on 2026-10-11. B39 (the scan reads only the shown source, was P6-3) decided
by the owner on 2026-10-11. iOS prompt: ios-fix-prompt-4.md.
Still open for the owner:
- OpenAir arc ids (found in WP-D1, a Phase 8 item). Airspace drawn with OpenAir arcs (
DA,DB,DC) goes throughsin/cos/atan2, and Apple’s libm is not correctly rounded, so an arc point can differ from iOS in the last bits and the airspace’s id differs (Belgium/Luxembourg: 148 of 233 ids equal to iOS; Germany: 453 of 692; all others have arcs). Android usesStrictMathso its ids are stable across devices. It matters only if hidden sectors are synced between iOS and Android (Phase 8): then either both platforms compute arcs the same way, or hidden sectors are matched by name + class + file instead of id (domain/airspace/README.md, “Ids and the last bit”). - Not yet compared: the airspace sheet’s band names breaking mid-word in narrow bands against iOS
AirspaceScreens, and one shared class-label table (three copies today::domain:flightdetailInfringementRow.classLabel,:feature:airspaceAirspaceTexts.classLabelRes,:feature:airspacefilesclassLabelRes).
11. Phase 7: integrations (2026-10-11; confirmed by the owner on 2026-10-12)
Section titled “11. Phase 7: integrations (2026-10-11; confirmed by the owner on 2026-10-12)”Taken by the lead so the work could go on (the owner said “do what looks best”; the Phase 7 plan’s decision table,
P7-D1…P7-D20, all as recommended). Owner, 2026-10-12: P7-D1…P7-D20 confirmed as written, except P7-D14 (one shared
token, no separate Android token) and P7-D15 (the vulture-web route now exists), both updated below; of the WP-F7
choices, the service stack and the sheets are confirmed, and two are changed to match iOS: service flights open the
map at once and download in it, and plain HTTP is allowed everywhere. The logos’ usage rights: the owner has them, as
for iOS. Where each lives is in the module
READMEs (:domain:services, :data:credentials, :data:soaringspot, :data:weglide, :data:skylines,
:data:charron, :data:remoteflights, :feature:soaringspot, :feature:charron, :feature:serviceflights,
:feature:services, :feature:upload, :feature:taskmap, app “Discover services” and “App Links”).
- P7-D1 B2 / B3 / B5 (and B1): ported from iOS
main, taggedFIXED; season = October, Gregorian, UTC. iOS done (§5). - P7-D2 B4 competition field: unknown (behind the login); parity,
TODO(B4), until the owner logs in once. - P7-D3 Credentials: Tink AEAD with an Android Keystore-wrapped keyset over a DataStore file in
noBackupFilesDir, excluded from backup, wiped (signed out) on any decrypt failure; never synced (as the iOS Keychain item). - P7-D4 No Credential Manager password saving in v1.
- P7-D5 Interim account screens: a “Connected services” screen shown over the shell, reached from the Library’s ⋮ Settings, each browser’s Account button and the WeGlide connect prompt; moves into Settings in Phase 9.
- P7-D6 A minimal read-only task map (
:feature:taskmap): the task alone, its page, Save, Update. - P7-D7 WeGlide task fetch + mapper + Update on the task map; reachable for Soaring Spot tasks now.
- P7-D8 Recents: only the selected downloaded flight becomes a recent (iOS parity); staged rows are cleaned up.
- P7-D9 The WeGlide / SkyLines flight id is recorded on the flight after an upload (Android only, no UI yet).
- P7-D10 Uploads run in process (an app-scoped coroutine) behind the iOS step card; no WorkManager.
P7-D11 Replay Day asksChanged by iOS build 563 (RACE_RANKINGfirst, thenSEVERAL_FLIGHTS, before anything is downloaded (iOSed23fc896); each gate puts its paywall up since Phase 8a (§12).e4093d7c5), ported 2026-10-17: the ranking stays down when a day is opened. Replay Day is “Open all flights” (f90ed1e50,b8fbcbd9e,a43789ba2) and asksSEVERAL_FLIGHTSalone before anything is downloaded; the race ranking is the pilot’s to open from the map’s bar, where it asks forRACE_RANKINGitself. The ranks and handicaps of the day still go to the map (they feed that ranking). TheshowsRanking/showsRankingWhenAvailableflag that carried it is removed (2026-10-17).- P7-D12 Read-only endpoints recorded once as scrubbed fixtures; uploads to the real services only by the owner, by hand, never in CI.
- P7-D13 The owner’s own accounts, typed on the device only; nothing in
local.properties. - P7-D14 Soaring Spot goes through vulture.aero with the existing
VULTURE_API_TOKEN. Owner, 2026-10-12: one shared token for iOS and Android; no separate Android token, also not before the public release. - P7-D15 App Links:
/.well-known/assetlinks.jsonon vulture-web. Owner, 2026-10-12: the lead added the route at the owner’s request (vulture-web commitadc7fff,app/asset_links.rb, registered inapp.rb, specs inspec/app_spec.rb), with the debug fingerprint only; the release fingerprints (Play App Signing and upload key) go intoRELEASE_FINGERPRINTSonce Play App Signing exists. The owner deploys it; until then links are verified with the dev override only (vulture-android/docs/app-links.md). - P7-D16 Plain OkHttp + kotlinx.serialization, no Retrofit.
- P7-D17 Charron upload: the multipart POST with OkHttp, the answer in a WebView (
loadDataWithBaseURL) that shares the cookies. - P7-D18 Out: WeGlide stories / feed / latest / daily, SGP, Soaring Spot
open?url=, the iOS widget link. - P7-D19 iOS candidate B40 (Charron website season) logged; Android uses
CharronSeason. Owner, 2026-10-12: B40 accepted (iOS fixes it; Android’s behaviour is the fixed one,FIXED(B40)). - P7-D20 Analytics events:
TODO(analytics), wired in Phase 8.
Also taken in WP-F7 (owner, 2026-10-12: two confirmed, two changed to match iOS):
Service flights download before the map opensChanged by the owner, 2026-10-12: match iOS. The map opens at once and the flights download in it (iOS pushes the map and lets it download), with the progress and the alert for the flights that failed shown on the map. Done: an emptyAppKey.Flightopens over the browser (for a link over the Logbook, or the map on top); the map’s HUD counts the downloads; the batch joins with its ranks and handicaps when the last download ends (iOSfinishAdding); failures are listed in one alert. Android deviations: closing the map cancels its download (iOS finishes it silently), and a killed app comes back with an empty map. Android still stages the flights and opens them by id (memory rules).- A service’s stack comes back as it was left (iOS
putService), without the flight / task maps it opened (iOSreleaseServiceMaps); a list picked drops them too. Confirmed. - Modal screens are “sheets” over the shell (Connected services, account screens, the Charron upload form), above the statistics; Back pops them first. Confirmed.
Plain HTTP only for the Charron.line hostsChanged by the owner, 2026-10-12: all HTTP, as iOS. Cleartext traffic is allowed for every host (iOS allows any HTTP load); the WP-F7 rule allowed onlylvzc.beandcharronline.be, whose IGC links arehttp://(found in WP-Q). Done (base-config, as iOSNSAllowsArbitraryLoads).- Service logos are PNGs rendered from the iOS asset catalog. Owner, 2026-10-12: the owner has the usage rights of the Soaring Spot, WeGlide, SkyLines and Charron.line marks, as for iOS.
iOS candidates from Phase 7 (ios-changes.md §B): owner, 2026-10-12: B40, B41, B42 and B44–B49 accepted (iOS
fixes them, Android applies them now, FIXED(Bxx)); B43 rejected (Android copies iOS, PARITY). Prompt:
vulture-android/docs/ios-fix-prompt-5.md.
Still open for the owner: the B4 field (TODO(B4) stays until the owner logs in to Charron once); deploying the
vulture-web assetlinks.json route (commit adc7fff) and adding the release fingerprints once Play App Signing
exists (P7-D15); a real upload to WeGlide, SkyLines and Charron on the phone (P7-D12).
12. Phase 8a: Vulture Pro with RevenueCat and the paywall (2026-10-11; confirmed by the owner on 2026-10-12)
Section titled “12. Phase 8a: Vulture Pro with RevenueCat and the paywall (2026-10-11; confirmed by the owner on 2026-10-12)”Taken by the Phase 8a agent so the work could go on. Owner, 2026-10-12: P8-D1…P8-D20 confirmed, except three
changed below: the paywall waits for the products as iOS does and “no products” is an alert (P8-D6); the hero
images become Android captures now (P8-D7); Settings › Vulture Pro loses its Restore row (P8-D12 / P8-D13). B50 and
B51 are accepted (P8-D11). All four changes are done. Firebase sync and accounts
are not part of 8a. Where each lives: :data:billing (vulture-android/data/billing/README.md),
:feature:paywall (vulture-android/feature/paywall/README.md), app README “Vulture Pro (Phase 8a)”; the owner’s set-up steps are
in vulture-android/docs/billing-setup.md.
- P8-D1 RevenueCat Android SDK (
com.revenuecat.purchases:purchases10.26.0) in the same RevenueCat project as iOS: entitlementall, the current offering (no id in the code) with the iOS fallbacks (the known products, then “no products”). OnlyRevenueCatStoreimports the SDK; the rest sees theSubscriptionStoreinterface (fakes in tests). - P8-D2 Google Play products: one subscription
vulture_prowith base plansmonthlyandyearlyand free-trial offers mirroring the App Store’s (“new customers only”); RevenueCat idsvulture_pro:monthly/vulture_pro:yearly. The debug build (be.fousa.vulture.debug) is not on Play and has no products (iOS debug has its own App Store products). - P8-D3 The free tier is always on (
ProAccessPolicy.IS_FREE_TIER_ENABLED = true). iOS’s debug-only hard paywall (-VultureHardPaywall, thelaunchtrigger,isAppOpen) and promoted purchases (none on Play) are not ported. - P8-D4
ProAccessPolicyis a plain Kotlin class in:data:billingnext toProFeature(no new JVM module, no Android in it); the paywall’s arithmetic (PaywallPlans) and wording (PaywallTexts) live in:feature:paywall. Both are tested against the iOS tests (ProAccessPolicyTests,SubscriptionViewModelTests) case for case. - P8-D5 The gate is iOS’s
ProAccessAPI on theProAccessinterface:allows(feature)andrequestsFlights(count)put the paywall up when they say no,presentPaywall,isUnlockedNow. A view model posts the request to the app-widePaywallPresenter; onePaywallHostover every screen draws it; nothing happens while a paywall is up (iOS). Every formerTODO(pro)goes through it; the existingProRequiredevents stay as notices. - P8-D6 The paywall is a full-screen dialog on a phone and a 560 dp card on windows ≥ 600 dp; Back or × closes it,
a tap outside does not; it closes itself once Pro is unlocked. Changed by the owner, 2026-10-12: match iOS
exactly. The paywall is not shown until the products have loaded (iOS waits for them); when there are no products,
an alert says so instead of a paywall. This replaces the interim choice (open at once with a spinner, “no products”
as a state in the paywall with Try again). Done: a request loads the plans with nothing on screen (iOS
withProducts), then the paywall comes up; with no plans iOS’s alert (Error, “There are no subscriptions found.”, Retry) shows instead. Android deviation: Back or a tap outside dismisses that alert and drops the request (iOS’s alert offers Retry alone; a request left waiting would block every later paywall). - P8-D7 The recording, SkySight and task-planning heroes are not copied (those paywalls never show on Android).
Changed by the owner, 2026-10-12: Android captures now. The hero images (light / dark,
drawable-night-nodpi) are screenshots of the Android app, replacing the iOS@2xJPEGs, some of which show iOS chrome. Done: all eight heroes, light and dark, 1200×800, captured on the emulator with the OBGN 2026 task-5 flights and OpenAir files; how to redo them::feature:paywallREADME “Hero images”. - P8-D8 Copy: no cross-platform promise (iOS’s “One subscription for iPhone, iPad and Mac.” is not shown);
Android adds “Manage or cancel anytime in Google Play.”; the welcome line is Android’s own (iOS’s starts with “Plan
your tasks”); “Everything in Pro” leaves out task planning, recording and SkySight (not in Android v1). Android-only
strings in 8 languages (
:feature:paywall). - P8-D9 The merged manifest removes
com.google.android.gms.permission.AD_ID(pulled in by RevenueCat’s advertising-id dependency): no ads, the Play “Advertising ID” declaration can say No. - P8-D10 Trial eligibility comes from the product: Play only lists offers the account may still start (RevenueCat’s Android eligibility check always answers “unknown”).
- P8-D11 A cancelled purchase shows nothing; a restore that finds nothing shows iOS’s “failed to activate” text.
Owner, 2026-10-12: B50 and B51 accepted: the cancel is
FIXED(B50)(Android’s behaviour is the fix) and the restore gets its own “No subscription found for this account.” (FIXED(B51), Android stringpaywall_restore_nothing_foundin 8 languages until iOS adds its own catalog key). Done. - P8-D12 Debug builds have a Pro override (Store / Unlocked / Locked, in the billing DataStore) on the
subscription settings screen; release builds ignore it and never show it (iOS:
-VultureBypassSubscription). Owner, 2026-10-12: the debug override stays. - P8-D13 The subscription settings are an interim sheet (
AppKey.Subscription,:feature:paywallSubscriptionSettingsScreen) reached from Library ⋮ › Settings, whose Services screen gains an “Account” section with a “Vulture Pro” row, until the Settings tree (Phase 9). It shows the offer (opening the paywall) or the user id, “Subscription valid until” (long date, short time) and the trial note, plus “Manage subscriptions” (RevenueCat’s management URL, else Play’s subscriptions page). Changed by the owner, 2026-10-12: no Restore row. The Restore row (an Android addition) is removed; Restore is only on the paywall, as on iOS. Done. - P8-D14 Customers are anonymous RevenueCat users;
SubscriptionRepository.logIn(appUserId)/logOut()exist for Phase 8’s Firebase sign-in (03 §6.4) and are not called yet. Owner, 2026-10-12: confirmed: anonymous now; later the Firebase uid becomes the shared RevenueCat app user id on both platforms (iOS follows), which also answers RA-24. - P8-D15 The first-launch paywall (offering metadata
show_paywall_on_first_launch, once per install) is asked once per process, on a fresh start that opened no file or link (iOS: once a launch, unless something is presented). - P8-D16 Files from other apps ask the store afresh (
isUnlockedNow, ≤ 5 s for its first answer) before trimming a batch to its first flight, then put the several-flights paywall up (decisions §6). When the store fails,refreshedIsActivekeeps the last known state (iOS: not active). - P8-D17 Analytics:
SubscriptionAnalyticswith the iOS paywall events and properties; a logging implementation until Mixpanel lands (TODO(analytics)). - P8-D18
strings_ios.xmlregenerated from iOSmain(the race ranking paywall strings); the Android-onlysettings_labels_unit_ftminis dropped now that iOS has the key (B9 landed there). - P8-D19 A price worked out on the device (per month) is written in the device’s locale with the plan’s currency; iOS uses the store’s region, which Play does not give.
- P8-D20 CI: the owner adds the
REVENUECAT_API_KEYrepository secret and its line indeploy.yml’s environment (8a does not touch the workflows);ci.ymlneeds no key.
iOS candidates from Phase 8a (ios-changes.md §B): owner, 2026-10-12: B50 and B51 accepted (FIXED).
Still open for the owner: the Play / RevenueCat set-up: the Play Console subscription, the service account, the
RevenueCat Android app and the goog_ key (billing-setup.md); the REVENUECAT_API_KEY secret (P8-D20); a real test
purchase with a license tester; the iOS prices and trial lengths to mirror.
13. Repo audit (2026-10-12; decided by the owner on 2026-10-12)
Section titled “13. Repo audit (2026-10-12; decided by the owner on 2026-10-12)”Five agents audited the repo in parallel (domain + core maths; data + network; the flight-UI modules; the design system, the service screens and the app; cross-cutting build, docs and lint), fixed what was safe and in scope, and deferred everything that changes behaviour, needs a shared file or an owner decision. Those proposals are gathered here, numbered RA-n, each with where it applies, the recommendation and an effort (S: an hour, M: a day, L: more). Group (d) is what the audit decided on its own and already applies. Parity with iOS stays the requirement throughout: none of (a)–(c) changes what a screen does, only how it is built.
Owner, 2026-10-12: each item of (a)–(c) is answered in its own “Owner” line below. In short: yes to RA-1, RA-2,
RA-3 (changed: the floating button everywhere), RA-4, RA-5, RA-6, RA-8, RA-11, RA-13 (changed: rename all now), RA-15
(detekt), RA-16, RA-18, RA-19, RA-20, RA-23; written down only for RA-9 and RA-10
(vulture-android/docs/android-primer.md, “Conventions in this repo”); no to RA-7, RA-12 and RA-17; RA-24 is
answered by P8-D14; RA-14 needed no answer (applied by the audit’s follow-up). Every yes is implemented (2026-10-14).
(a) Material 3 Expressive and UI rules
Section titled “(a) Material 3 Expressive and UI rules”- RA-1 Motion-scheme sweep. Standard enter / exit / fade animations use ad-hoc
tweendurations instead ofMaterialTheme.motionScheme:feature/flightdetailFlightDetailContent.kt:160(300 ms page push),feature/debriefDebriefContent.kt:159,feature/flightmappanel/FloatingPanel.kt:232-256,panel/PanelPills.kt:92,panel/FlightPanelContent.kt:62,panel/PanelControls.kt:268,FlightMapScreen.kt:919,feature/flightlistquicklook/FlightPreviewCard.kt:118,core/barogramEventPills.kt:127. Recommendation: one sweep,motionScheme.defaultEffectsSpec()for fades anddefaultSpatialSpec()for slides, screenshots re-recorded; keep the documented iOS timings (feature/thermalCenteringPlanChart.kt350 / 700 ms,feature/photosPhotoPager.kt:340spring).feature/charron’s result fade already moved. Effort M. Owner, 2026-10-12: yes, keeping the documented iOS timings. Done: every standard fade and slide in the flight map, barogram, flight detail, Debrief and quick look usesMaterialTheme.motionScheme(the spec nearest the iOS duration, noted in a comment); the thermal chart’s 350 / 700 ms and the photo pager’s spring stay. - RA-2 One top-app-bar rule per screen class. List roots: Library and Statistics use
LargeFlexibleTopAppBar+exitUntilCollapsed, the flight list a plainTopAppBarwith a two-line title and no scroll behaviour (feature/flightlistFlightListScreen.kt:339); modal screens (Compare, Edit flight) a plainTopAppBar; the detailTopAppBar(windowInsets = 0); Thermal and Debrief hand-roll a header row. The service screens now use material3 1.5’sTopAppBar(title, subtitle)for a subtitle (iOSnavigationItem.subtitle). Recommendation: write the rule down (large flexible bar +exitUntilCollapsedfor list roots, small bar for modal screens and companions,TopAppBar(subtitle)for a count or a date) and give the flight listpinnedScrollBehaviorso its bar tints on scroll like the others (iOS keeps the count under the title, so no large title there). Thermal, Debrief and the task picker take theCompanionHeaderof RA-8. Effort M. Owner, 2026-10-12: yes, the rule as recommended (the flight list: small bar that tints on scroll; the companions get the shared header). Done (the rule is the “Top app bars” table incore/designsystem/README.md; the flight list’s count is the bar’s subtitle, withpinnedScrollBehavior). - RA-3 One bottom-action pattern. “Open N flights” / “Replay Day” is built three ways:
feature/charronday =ExtendedFloatingActionButton,feature/soaringspotday =Surface+Button,feature/serviceflights=BottomAppBar. Recommendation: oneBottomAppBarwith the primaryButtontrailing (serviceflights’ shape, which also holds Select all and the day) for charron and soaringspot too; layout, tags and screenshots change. Effort M. Owner, 2026-10-12: changed: the floating button everywhere. “Open N flights” / “Replay Day” is anExtendedFloatingActionButton(charron’s shape) infeature/charron,feature/soaringspotandfeature/serviceflights, not aBottomAppBar. Done; the WeGlide / SkyLines browser moved Select all and the calendar into its top bar (where iOS puts them in its wide and Mac layouts). - RA-4 One wide-window rule,
SheetWindow. The paywall is a 560 dp card over a scrim from 600 dp (P8-D6), Edit flight a 560 dp dialog (§9 D5), the task-map banner 560 dp, the detail card 400 dp (feature/flightdetailFlightDetailHost.kt:251), the companion card 400 dp (CompanionHost.kt:224), the filter dialog 360 dp (feature/flightlistfilter/FlightFilterSheet.kt:268, the iOS popover); the Phase 7 form sheets (Services, ServiceAccount, Subscription, CharronUpload) are full screen even on a tablet. Recommendation: the rule “a screen iOS presents as a page / form sheet is full screen on a compact window and a 560 dp × 90 % card over a scrim from 600 dp; inspectors 400 dp; popovers 360 dp”,PaywallWindowgeneralised into a design-systemSheetWindow, andSheetScreen(app/navigation/ServiceScreens.kt:107) wrapped in it. Effort M (plus screenshots). Owner, 2026-10-12: yes, with a design-systemSheetWindow. Done: the paywall, Edit flight and the app’s sheets (Connected services, accounts, Subscription settings, the Charron upload form) are a 560 dp card over one scrim from 600 dp. The shell stays live under a sheet (as iOS’s presenting view), so the map does not reload when a sheet closes. - RA-5
ListItemor hand-rolled rows.feature/serviceflights,feature/servicesandfeature/paywalluseListItem;feature/charron,feature/soaringspotandfeature/airspacefileshand-roll their rows;feature/library(RowGroup),feature/debrief(Section),feature/flightdetail(GroupedRow) andfeature/editflight(ListItemonsurfaceContainer) each build the iOS inset-grouped list their own way. Every module is consistent inside itself. Recommendation: oneGroupedSection { rows }+GroupedRowDivider(inset)in the design system (RA-8) withListItemrows inside, adopted module by module; the deprecatedListItemoverload the compiler warns about goes at the same time. Effort L. Owner, 2026-10-12: yes, the grouped components (with RA-8), and every screen migrated. Done (GroupedSection& co. in the design system; every iOS grouped list in the app uses them; switch rows usetoggleItemColors()). - RA-6 Caption / footnote typography tokens and iOS system colours. Raw
sptext styles for iOScaption1,caption2andfootnoteinfeature/flightmappanel/PanelPills.kt:257-260andpanel/FlightStrip.kt:488-497(the strip’s 22 / 15 / 11 pt),core/barogramBarogram.kt:399-402;feature/soaringspotPilotsSheet.kt:1155keeps 9 / 12 sp on purpose (the 32 dp badge). iOS system colours are defined twice:systemGray#8E8E93incore/barogramBarogramPalette.kt:131,systemGray6andlabelinfeature/flightmappanel/MatchingTextColor.kt:51-60(a documented port ofUIColor.matchingTextColor()). Recommendation:caption1/caption2/footnoteonVultureTypographyand oneIosSystemColorsobject in the design system, swapped in one change. Effort S. Owner, 2026-10-12: yes (the typography tokens and the iOS system colours). Done (VultureTheme.typography.caption1/caption2/footnote,IosSystemColors; the map panel, barogram and glider pill use them). - RA-7 The task banner’s logo. iOS puts the service’s logo before the task’s title on the task map; the design
system now has
logo_<service>_menu, sofeature/taskmapTaskBannercan show it throughExternalTaskKind.of(externalUrl). A parity gain and a UI addition. Effort S. Owner, 2026-10-12: no: the task banner keeps no logo. - RA-8 Three shared components the flight-UI modules asked for. (1)
CompanionHeader(title, navigation = Close | Back, actions)forfeature/thermalThermalDetailContent.kt:154,feature/debriefDebriefContent.kt:189, the task pickerfeature/flightmapdialogs/FlightMapDialogs.kt:180andfeature/flightdetailFlightDetailContent.kt:272; (2)GroupedSectionHeader(text)withheading()plus theGroupedSectionof RA-5 forfeature/libraryLibraryScreen.kt:148 / 175,feature/editflightEditFlightContent.kt:158,feature/debriefDebriefPages.kt:441 / 460,feature/flightdetailDetailList.kt:123,feature/flightlistfilter/FlightFilterSheet.kt:140(four text styles for one iOS grouped-list header;flightdetailanddebriefuseonSurfaceVarianton purpose for iOS’sDetailSectionTitle); (3)FileErrorsDialog(rows)forfeature/importImportErrorText.kt:70andfeature/flightmapdialogs/FlightMapDialogs.kt:291(one file = a titled alert, several = a list undererror.label.multiple). With them: apreview/source of synthetic flights in the design system so the flight-UI screens can carry@Previews (today their fixtures are test-only and Roborazzi stands in),VultureIcons.Restore, and the local SF-Symbol tables (panel/PanelSymbols.kt,DetailStyle.kt) moving toVultureIconswhen a second screen needs them. The design system’s requests to the flight-UI modules go with it:FlightListScreen.kt:406→ the sharedSearchTopBar,EditFlightContent.kt:158→ the sharedSectionHeader,LibraryRows.kt:103→ServiceLogos.sidebar, and the glyphs still named directly (flightmap54,flightdetail29,import4,editflight1; the Filled / Outlined ones →VultureIconsor the Rounded family). Effort M–L. Owner, 2026-10-12: yes, the components added and every screen migrated (with RA-5). Done (CompanionHeaderon the thermal sheet, Debrief, flight detail and task picker;FileErrorsDialogfor import and the map; the airspace sheet’s header stays its own for now).
(b) Code conventions
Section titled “(b) Code conventions”- RA-9 Route / screen naming and the stateless screen. The convention (routes bind the view model with
hiltViewModel(), screens take state + callbacks, hosts own dialogs and sheets) holds everywhere butfeature/flightmapFlightMapScreen(viewModel = …), which takes the view model (the map is anAndroidViewand cannot preview anyway). Recommendation: write the rule down indocs/android-primer.mdand leave the map screen as the documented exception. Effort S. Owner, 2026-10-12: write the convention down only: done invulture-android/docs/android-primer.md, “Conventions in this repo”; no code changes. - RA-10 One rule for one-shot events. Navigation and camera commands go through a
Channel(feature/flightmapFlightMapViewModel.kt:190,feature/importImportViewModel.kt:81,feature/taskmapTaskMapViewModel); dialogs use a state flag plus an acknowledgement (pendingDelete/dismissDelete,errors/errorsShown,airspaceImported/airspaceImportShown,isMissing). Both are defensible. Recommendation: write it down (“commands →Channel, dialogs → state + ack”) rather than change code. Effort S. Owner, 2026-10-12: write it down only: done invulture-android/docs/android-primer.md, “Conventions in this repo”; no code changes. - RA-11 Injected dispatchers and an
@IoDispatcherqualifier. No qualifier exists: 14 classes takeioDispatcher: CoroutineDispatcher = Dispatchers.IOplus a secondary@Injectconstructor or a@Providesthat leaves the default;feature/photosdata/PhotoStore.kt:90-171,feature/importAirspaceImports.kt:41-44andfeature/flightlistFlightListStore.kt:119useDispatchers.IO/Defaultdirectly (FlightMapScreen.kt:407, 420, 891do so inside composables, which is fine). Recommendation: an@IoDispatcher/@DefaultDispatcherqualifier pair provided fromcore/network(or a tinycore/coroutines), injected everywhere, the secondary constructors gone. Touchesapp/diand every data module. Effort M. Owner, 2026-10-12: yes, the dispatcher qualifiers. Done in the data and store classes (:core:coroutines); six view models (statistics, flight detail, compare, flight map, Charron upload, Debrief state),MapImageSharer, the app’s artwork and core/map’s gallery artwork now take theirs injected too. Classes Hilt does not build (the map controller, the airspace layers, the barogram composable) still nameDispatchers.Default/Main; a@MainDispatcherqualifier would be needed for those. - RA-12
Context-free view models.feature/compareCompareFlightsViewModel.kt:52holds theApplicationto read the grid’s strings (the model is built off the main thread and the strings follow the system language). Recommendation: inject anAndroidCompareStringsbuilt from@ApplicationContextresources in a Hilt provider; the same forAndroidDebriefStringsif it ever moves into a view model. Effort S. Owner, 2026-10-12: no: the Compare view model keeps theApplication. - RA-13 One test-name style per module.
domain/compare(31 tests) anddomain/debrief(56) use terse camelCase names (format,thresholds,inputs);feature/flightmap(116 / 105),flightlist(66 / 6),statistics(14 / 6),flightdetail(7 / 43) andphotos(14 / 27) mix backtick sentences with camelCase;compare,thermal,debrief,editflightandcore/barogramare camelCase only; every other module (1 350 tests) uses backtick sentences. Recommendation: backtick sentences, renamed module by module when a module is next touched (the 87 domain renames need wording). Effort M, as noise. Owner, 2026-10-12: changed: rename all tests now to backtick sentences, not module by module. Done (2026-10-14): 821 tests renamed, the same 4 545 run; the style is a CLAUDE.md rule. - RA-14 Fixture naming.
StatisticsFixture.kt(singular) indomain/logbook,MockThermalFlight.ktindomain/thermal,MockThermalFlight.ktandMockRaceRanking.ktinfeature/flightmap(“Mock” for hand-built fixtures, while the repo saysFake*/*Fixtures). Recommendation: rename to*Fixtures.kt. Effort S. Applied on 2026-10-12 by the audit’s follow-up:RaceRankingFixtures,ThermalFlightFixtures. - RA-15
@Suppressof detekt rules with no detekt. Nine functions carry@Suppress("CyclomaticComplexMethod", "LongMethod", "NestedBlockDepth", "LongParameterList")(domain/flightBRecordParser.kt:53, 140,IgcParser.kt:153, 196, 285,FlightTakeoffEnricher.kt:80,FlightTimesEnricher.kt:139,FlightPhasesEnricher.kt:109;domain/servicesFuseMatcher.kt:148) andfeature/taskmapTaskMapViewModel.kt:235aTooGenericExceptionCaught, but detekt is not in the build: they are no-ops, and onlyFuseMatcher’s says why. Recommendation: add detekt inbuild-logicwith a small config (then the annotations mean something) or delete them. Effort S (delete) / M (detekt). Owner, 2026-10-12: add detekt. Added (1.23.8,config/detekt/detekt.yml, no baseline); every finding fixed or suppressed at its place with a reason (2026-10-14), and detekt is part oflint, so CI runs it. - RA-16 A
:core:testingfixtures module.IosStringKeys.ktis copied into thecompare,debriefandflightdetailtest sources and four more test files parsestrings_ios.xmlby hand (debriefDebriefFixtures,logbookStatisticsLocalizationTest,thermalThermalDetailLocalizationTest,waypointsWaypointNameTest);MainDispatcherRuleexists only infeature/flightlist/filterwhile the other modules callDispatchers.setMaininline;FakeProAccessis declared five times (app,feature/soaringspot,feature/debrief,feature/flightmap,feature/upload, plus a private one indata/airspace; each needs a per-feature, mutable or paywall-recording answer thatFixedProAccesscannot give, as their KDoc now says) andFakeConnectedServicesRepositorytwice (appShellTestFakes.kt,feature/uploadUploadFakes.kt); 47 test classes carry@OptIn(ExperimentalCoroutinesApi::class). Recommendation: a JVM:core:testingmodule (orjava-test-fixturesinbuild-logic) with oneIosStringKeysandMainDispatcherRule; afake/main package indata/billingwith one mutableFakeProAccess, andFakeConnectedServicesRepositorynext toFakeCredentialStoreindata/credentials’fake/(bothappandfeature/uploadalready depend on:data:credentials, so no build change); the coroutines opt-in on the test source sets inbuild-logic. Effort M. Owner, 2026-10-12: yes,:core:testingand the shared fakes.:core:testingdone (IosStringsXml,IosStringKeys,MainDispatcherRule, the coroutines opt-in in build-logic); the shared fakes (data/billinganddata/credentialsfake/) and every module’sMainDispatcherRulemigration are done too. - RA-17 One base for
AnalysisStringsandDetailTexts.domain/debriefAnalysisStringsanddomain/flightdetailDetailTextsdeclare the samestring(key),format(key, args)andmediumDay(date)with identical KDoc, implemented three times in UI (AndroidDebriefStrings,AndroidDetailTexts,AndroidCompareStrings). A shared interface needs a module both depend on; today onlycore/units(about numbers). Recommendation: aLocalizedTextsinterface incore/unitsorcore/swift, the three implementations reduced to one. Effort S. Owner, 2026-10-12: no: the three text interfaces stay separate. - RA-18 Empty
<resources />files.data/billingships eight emptyvalues*/strings.xml; the same pattern is incore/designsystemandfeature/statistics. Recommendation: delete repo-wide, or keep as the 8-locale template and say so indocs/strings.md. Effort S. Owner, 2026-10-12: yes, delete them repo-wide. Done (24 files;docs/strings.md). - RA-19 One home for the whitespace rules.
String.normalizedWhitespace(the B12 rule) exists indomain/logbookFoundationStrings.kt:91(internal) andfeature/libraryLibraryFormat.kt:95, andLibraryFormat.kt:97keeps a third copy of the ICU\sclass thatcore/swiftnow publishes asSWIFT_WHITE_SPACE_PATTERN. Recommendation:feature/librarytakescore/swiftand uses the pattern;normalizedWhitespacemoves next to it incore/swift(Vulture’s own rule, not Foundation’s) orfeature/librarytakesdomain/logbookand that copy goes public. Effort S. Owner, 2026-10-12: yes, one home incore/swift. Done (String.normalizedWhitespace()next toSWIFT_WHITE_SPACE_PATTERN). - RA-20 Hilt provider names. Providers are
provideX(network, charron, elevation, skylines, weglide, prefs), barex(airspace, credentials, db, billing, remoteflights) andbindX(soaringspot);@Provideswith constructor defaults and@Inject+@Bindsboth occur. All work. Recommendation: bare names (the majority) if one style is wanted; nothing else. Effort S. Owner, 2026-10-12: yes, bare names everywhere. Done. - RA-21 The
Math.fmaguard under Animal Sniffer.core/geoFusedMultiplyAdd.kt:16, 21is the sanctioned run-time probe (hasNativeFmacatchesNoSuchMethodError), which the API-29 check (RA-26) can only skip by excluding the class inbuild-logic. Recommendation:@IgnoreJRERequirement(compile-only, on the classpath) onfusedMultiplyAddand the probe moved out of theby lazylambda into an annotatedprivate fun probeNativeFma(), then the two exclusion lines inbuild-logicdeleted. Applied the same day (commitbe9b971); listed here because it is the one sanctioned exception to RA-26. - RA-22 Two DataStore patterns.
data/prefskeeps typed JSON stores;data/billingBillingPreferencesis a Preferences DataStore (billing.preferences_pb) for the Pro override and the first-launch flag (P8-D12, P8-D15). Acceptable; noted so the next module picks on purpose. Effort —.
(c) Product
Section titled “(c) Product”- RA-23
ACCESS_NETWORK_STATEfor the offline wording.FailureReason.from(error)is called withoutisOnlineeverywhere (the manifest has noACCESS_NETWORK_STATE), so a phone that is offline reads “No Answer” instead of iOS’s offline text on every service screen. Recommendation: the permission (normal, no prompt) and a shared connectivity helper incore/networkthat everyFailureReason.fromcall passes; noted in thesoaringspotandserviceflightsREADMEs. Effort S. Owner, 2026-10-12: yes,ACCESS_NETWORK_STATEand the shared connectivity helper. Done (core/networkConnectivity; every service failure passesisOnline). - RA-24 Cross-platform subscription identity. Android and iOS share one RevenueCat project (P8-D1), but
customers are anonymous per install (P8-D14) and the copy promises nothing across platforms (P8-D8), so a pilot’s
iOS subscription does not unlock Android today. Recommendation: decide whether it should; if yes, it needs the
shared app user id from the Phase 8 Firebase sign-in (
SubscriptionRepository.logIn, 03 §6.4) and RevenueCat’s transfer rules, and the paywall copy changes. Effort L (Phase 8). Owner, 2026-10-12: answered by P8-D14: anonymous now; later the Firebase uid is the shared RevenueCat app user id on both platforms (iOS follows).
(d) Decided by the audit, now a rule (applied)
Section titled “(d) Decided by the audit, now a rule (applied)”- RA-25 Lint is strict.
warningsAsErrorsandabortOnErrorinbuild-logic, one rootlint.xml(GradleDependency/AndroidGradlePluginVersioninformational,AutoboxingStateCreationa warning), no baseline file, issues printed to stdout,checkDependenciesoff (every module lints itself). The 14 findings that existed on 2026-10-12 (app,core/map,core/share,feature/flightdetail,feature/flightlist,feature/paywall) were fixed in their modules, so./gradlew lintis green and must stay so. CLAUDE.md says so. - RA-26 The Android 10 API check for JVM modules. AGP’s lint cannot run
NewApion a non-Android module (itsApiDetectorhas no minSdk there), sovulture.jvm.libraryruns Animal Sniffer (ru.vyarus.animalsniffer, signatureandroid-api-level-29:10_r5) on the main source set, registered as the JVM module’slinttask; it caughtMath.fmaand a Java 21List.removeLast()on a probe. Documented indocs/setup.md, CLAUDE.md andbuild-logic/README.md; the one annotated exception is RA-21. - RA-27 Every Hilt module is
XxxModulein adi/package.core/network,data/charron,data/elevation,data/skylinesanddata/weglidemoved there;AirspaceBindingsbecameAirspaceBindingsModule;CharronFeatureModule/SoaringSpotFeatureModulebecameCharronModule/SoaringSpotModule; pure-@Bindsmodules are interfaces. Left for the flight-UI follow-up:feature/flightlistfilter/FlightFilterModule.ktand the@Moduleinsidequicklook/FlightQuickLookStore.kt. - RA-28 States, search and section components come from the design system.
ErrorState/FetchErrorDialog/failureMessage(reason),EmptyStateText/EmptyStateAction,LoadingState,SearchTopBar,SearchField,SectionHeader/SectionFooter(withheading()), material3’sTopAppBar(title, subtitle)for a subtitle, andVultureIconsfor every glyph two or more screens share (a one-off may stay in its screen with the SF Symbol in a comment). The loading rule:LoadingState(the ExpressiveLoadingIndicator) for a content wait,CircularProgressIndicatoronly inside a small control (a 24 dp Save, a row’s download, the HUD); the one leftover isfeature/editflightEditFlightContent.kt:323(a 24 dpLoadingIndicatorin aTextButton). Applied in the design-system and service modules and incompare/debrief; RA-8 lists what the flight-UI modules still take. Per-file@OptIn(ExperimentalMaterial3Api::class)/ExperimentalMaterial3ExpressiveApiis gone where seen: the compose convention plugin opts every module in. - RA-29 The module README template.
docs/module-readme-template.md: six headings verbatim (Usage, Contents, iOS → Kotlin mapping, How it works, Deviations from iOS, How to test), work-package history folded into “How it works”, every public file in Contents with its iOS file. A README moves onto it the next time it is touched (no sweep);core/mapis on it. - RA-30 The convention plugins own the shared build settings.
build-logicadds JUnit 4 + Truth andisIncludeAndroidResourcesto every Android module, the Compose UI test dependency to every Compose module, and avulture.roborazziplugin (output dir + the three test dependencies) for the 17 screenshot modules; the module build files drop their repeated lines in the follow-up, together withapp’s unusedandroidTestImplementationand the “Phase 3, in progress” headers offlightlist/import/library. Unused catalogue aliases are removed; the catalogue’s “checked” date is the audit’s (every entry at the latest stable on 2026-10-12, no bumps).
Still open for the owner: RA-14 (fixture naming). The lead applies the yeses in the order (a) → (b) → (c).
14. Phase 8b: Google sign-in and Firebase sync (2026-10-14)
Section titled “14. Phase 8b: Google sign-in and Firebase sync (2026-10-14)”Proposed by the Phase 8b plan and answered by the owner on 2026-10-14. Owner, 2026-10-14: P8B-D1…P8B-D18 as
recommended, except three changed below: IGC files travel on any network (P8B-D5); free users get a 100 MB cloud
storage cap, Pro users none (P8B-D14); Crashlytics now (P8B-D15). The Firebase project is vulture-app-store; its
google-services.json stays out of git. Sync stays free for everyone (§2); only the storage cap depends on Pro.
Where each lives: :domain:sync (vulture-android/domain/sync/README.md) (codecs, merges),
:data:sync (vulture-android/data/sync/README.md) (contracts, fakes, engine), :data:firebase (vulture-android/data/firebase/README.md) (the
only module that imports Firebase), :feature:account (vulture-android/feature/account/README.md) (the account screen), the
SyncTrigger / IgcFileFetcher hooks in :data:db (vulture-android/data/db/README.md). Status (2026-10-14):
implemented (WP-S0…S8): the codecs and merges, Room v3, the engine and its workers, the Firebase implementations,
the account screen, the security rules with emulator tests, and the cloud format in
sync-schema.md. Not verified yet: anything that needs the owner’s real project (Google sign-in’s
web client, App Check, the RevenueCat Firebase extension, the deployed rules; plan §7.2).
- P8B-D1 Sign-in is optional, with no prompt: the app works fully signed out, as before. iOS syncs silently through iCloud and needs no account; the entry point is Library ⋮ › Settings › Account › Sync.
- P8B-D2 Sign-out keeps the local copy: every row and file stays on the phone (“Your flights stay on this phone”). Erasing is offered only in Delete account and in the account-switch “Replace” choice.
- P8B-D3 Photos are not synced (iOS has no photo table: PhotoKit at each open, iCloud Photos syncs the images;
Android’s
flight_photoheld per-device content URIs, and since 2026-10-20 Android has no photos at all). Hidden airspaces and service credentials stay local too. - P8B-D4 Firestore and Cloud Storage in
europe-west1(Belgium): EU data, regional price. Permanent once the database and the bucket are created. - P8B-D5 IGC uploads and downloads. Changed by the owner, 2026-10-14: any network for every IGC upload and
download. There is no Wi-Fi-only back-fill of an existing logbook (no
IgcUploadWorker), and the prefetch worker needs only a network, not Wi-Fi + charging. (The recommendation was any network for new flights, the first back-fill on Wi-Fi.) - P8B-D6 Map files: the content of every file syncs (user imports and downloads), gzip, content-addressed in Storage; iOS syncs all content, and Android cannot re-download openAIP-source files (P6-D8).
- P8B-D7 The track preview syncs in the flight document (iOS does), so the gallery shows real thumbnails before any IGC is downloaded.
- P8B-D8 Recents-only flights sync like iOS (at most 50; their IGC fetched on open, never prefetched). Staged service downloads never sync.
- P8B-D9 Deterministic ids for tasks (SHA-256 of
externalUrlwhen set, else a UUID) and map files (SHA-256 ofsource|type|filenamefor downloads, ofUSER|type|contentHashfor imports), re-keyed in the Room v3 migration, as flights and home bases already are; iOS can compute the same ids. - P8B-D10 The debug app gets a second Android app
be.fousa.vulture.debugin the same Firebase project; one git-ignoredgoogle-services.jsonholds both clients; CI gets it from a secret (deploy workflow only). Until the owner adds the debug app, the build’s per-variant guard keeps debug building with sync unavailable. - P8B-D11 Live listeners while the app is in the foreground (≈ iCloud’s “it just appears”); in the background only the workers pull.
- P8B-D12 The sync status shows only on the account screen; nothing in the Library (iOS shows nothing).
- P8B-D13 Play’s web deletion link: a static
vulture.aero/delete-accountpage (vulture-web, deployed by the owner) explaining in-app deletion, with a mail address; the owner deletes by hand in the Firebase console. - P8B-D14 Storage quota. Changed by the owner, 2026-10-14: a 100 MB cloud-storage cap for free users; unlimited
with Pro. Pro reaches the security rules through RevenueCat’s Firebase integration (the custom claim
revenueCatEntitlementson the ID token containingall, refreshed with the token); the usage is counted inusers/{uid}.storageBytes, raised in the same batch as each blob’s metadata write, and the rules check it unless the claim says Pro. The app checks first (StorageQuota: 100 000 000 bytes, shown as “100 MB”) and says “Cloud storage full (100 MB) — Vulture Pro syncs everything”, keeping the rest local while the metadata keeps syncing. Without the RevenueCat integration configured (an owner step), everyone is treated as free. (The recommendation was no cap at launch, only a budget alert.) - P8B-D15 Crash reporting. Changed by the owner, 2026-10-14: Crashlytics now, no Firebase Analytics. Applied
only when
google-services.jsonhas a client for the variant, like the rest of Firebase; collection off in debug builds; the Play Data-safety form gets a note (firebase-setup.md). (The recommendation was neither in 8b.) - P8B-D16 App Check with Play Integrity, in monitor mode at launch, enforced after a release cycle; debug builds use the debug provider.
- P8B-D17 Google sign-in only; Sign in with Apple joins when iOS adopts Firebase.
- P8B-D18 Tombstones are kept 90 days (03 §8.5); a device offline for longer may bring a deleted flight back (documented).
Versions (2026-10-14, latest stable): Firebase BoM 35.0.0 (auth 25.0.0, firestore 27.0.0, storage 23.0.0,
crashlytics 21.0.0, App Check 20.0.0; checked to compile and lint on AGP 9.4.1 / Kotlin 2.4.21), google-services
plugin 4.5.0, Crashlytics plugin 3.0.8, Credential Manager 1.6.0, googleid 1.2.1. Both plugins are declared at the root
with apply false; the app applies them per variant only when a client exists (WP-S4).
Still open for the owner: the console steps (debug app and fingerprints, enabling Google sign-in, the OAuth
branding, downloading the new google-services.json, Blaze and a budget, Firestore and Storage in europe-west1, the
rules, App Check, the RevenueCat Firebase integration with custom claims, the CI secret), the deletion page on
vulture.aero, the Play Data-safety form and the privacy policy.
15. Phase 9: Settings, polish, release prep (2026-10-14)
Section titled “15. Phase 9: Settings, polish, release prep (2026-10-14)”Taken by the Phase 9 plan (2026-10-14). The owner reviews the theme and the map sheets separately (UI mockups,
2026-10-14): Phase 9 does not change the colour scheme, CompanionHost, the thermal sheet or the airspace sheet;
new screens use the existing design system (GroupedSection & co., VultureTheme). Where it lives:
:feature:settings (vulture-android/feature/settings/README.md) (the tree), app navigation/ (the wiring).
Status (2026-10-15): the Settings tree is complete (WP-S0 root and navigation; WP-A Units, Your sport, Flight
declaration with the glider search; WP-B Map style, Flight display, Thermals, Performance; WP-C Default observation
zones; WP-D Export all data, Advanced, Support), the platform polish is done (WP-E: predictive back, edge to edge,
keyboard shortcuts, TalkBack, large screens; vulture-android/docs/accessibility.md) and the baseline profile ships
(WP-F, :baselineprofile (vulture-android/baselineprofile/README.md)). Phase 9 is complete apart from the owner’s checks on
real hardware (docs/alpha-plan.md, block H).
- P9-D1 One new module
:feature:settingsholds the Settings tree (the root and every sub-screen except the ones that already live elsewhere). Existing screens are reused, not moved: Connected services (:feature:services), Vulture Pro (:feature:paywallSubscriptionSettingsScreen), Account & sync (:feature:account), Airspace settings (:feature:airspacefiles,AirspaceSettingsRoute). The app wires them as destinations of the tree. - P9-D2 Settings is a route stack over the shell, as the interim Services screen was: root → sub-screen, Back pops,
predictive back animates. It is the app’s sheet stack (
AppKey.Settings(key)inShellState.sheets), so it is a 560 dp card from 600 dp like every sheet (RA-4) and has no list-detail pane in v1 (iOS shows a sheet). Entry points: Library ⋮ › Settings (the root replaces the interim Services screen as the first page) and “Settings” at the end of the flight map’s overflow menu (iOSsettingsMenu). - P9-D3 The rows, in iOS
MainSettingsVieworder (analysis doc 03 §4): General: identity card → Flight declaration; Your sport; Units (Set your home base omitted: no Android screen uses the home base — no widgets, METAR or in-flight; the synced value stays untouched). Map: Map style; Airspace (→ the existing airspace settings); Automatic updates (a switch; also on the airspace page until 2026-10-20, now only here). Flight: Flight display; Thermals; Performance (In-flight display omitted: no in-flight mode in v1). Task: Default observation zones; Flight declaration (the same screen as the identity card). Services: Connected services (the subtitle lists the connected ones). Account: Vulture Pro (→ subscription settings); Sync (→:feature:account); Export all data. Advanced: Clear map artwork (Mapbox tile/style cache: size + confirm); Mapbox telemetry switch (Client-side tracking omitted until analytics exists; Clear cloud overlay cache omitted: no SkySight). Support: a separate screen (iOS “Support”): Version, Website, FAQ, Send feedback (mailto, iOS address), Rate (Play in-app review), Privacy, Terms; Mapbox / OpenStreetMap / Maxar credits and “Improve this map”; the service and data links (WeGlide, SkyLines, Soaring Spot, Charron, openAIP); links of out-of-scope services (SkySight, AVWX, sunrise-sunset, OurAirports) are left out (since then SkySight came with its satellite clouds, analysis 2026-10-skysight-clouds-android, and OurAirports on 2026-10-20 with the bundled airports table, analysis 2026-10-takeoff-airfield-android; the Data section lists SkySight, openAIP and OurAirports in iOS’s order). - P9-D4 Flight declaration: the six text fields, the glider type search (
:domain:gliders) and the FLARM logger interval, competition mode and stealth mode (synced and part of the declaration file). Sharing to a Bluetooth / Wi-Fi FLARM device is out of scope; “Export flarmcfg” is ported if iOS builds it as a plain text file (shared through:core:share), else omitted and noted. - P9-D5 The interim entry points become links into the tree: Library ⋮ › Settings opens the Settings root; the browsers’ Account buttons and the WeGlide connect prompt keep opening the service screens directly (Connected services under the account screen); Library › Airspaces › ⋮ › Airspace settings stays.
- P9-D6 Strings:
strings_ios.xmlkeys wherever iOS has the text (most rows do). Android-only strings go infeature/settings/src/main/res/values*/strings_<area>.xml, one file per work package, in all 8 locales, so parallel packages never edit the same resource file. - P9-D7 Predictive back:
android:enableOnBackInvokedCallback="true"(if not already set) and every custom back handler checked to animate with the system gesture (Navigation 3 /PredictiveBackHandler). - P9-D8 Keyboard shortcuts (hardware keyboard, Chromebook, desktop windowing): a small set mirroring iOS’s
UIKeyCommands where they exist in Android’s scope, shown in the system shortcut helper (Meta + /) throughActivity.onProvideKeyboardShortcuts. - P9-D9 Baseline profile: a
:baselineprofilemodule (the androidx.baselineprofile plugin; a generator that starts the app, opens the logbook, opens a flight on the map and scrolls), generated on the emulator and checked in (app/src/main/generated/baselineProfiles/baseline-prof.txtor the plugin’s default location). CI does not regenerate it.
How the tree came out (2026-10-15), against the plan above:
- Clear Map Artwork empties the gallery’s map artwork, as iOS’s row does (
.clearCache(.galleryArtwork)), not the Mapbox tile cache P9-D3 named; its footer is the artwork half of iOS’s footer (the other half is SkySight’s). - Rate opens the Play Store listing (falling back to the web page), as iOS opens the App Store’s write-review page. Play In-App Review is not used: Google asks not to start it from a button, and its quota can make a tap show nothing.
- Export flarmcfg is ported (iOS writes plain text): the declaration’s share button writes
flarmcfg.txtand opens the share sheet; the Bluetooth and Wi-Fi device entries of iOS’s menu are out of scope (P9-D4). The declaration’s explainer card has Android-only text (no device tasks or in-flight tracking here). - Default observation zones: the kind is a segmented row under the bar (iOS: a menu on the title); the preview is a
Compose
Canvasdrawing of iOS’s sample task, not a Mapbox map; the steppers are − / + button pairs. - Top bar: the small
TopAppBarof a modal screen (RA-2), as Settings is a sheet; icons are plain Material icons instead of iOS’s coloured tiles while the owner reviews the theme. - Account footer: the export’s explanation, or iOS’s trial note while a trial runs.
- Mapbox telemetry: applied to the SDK at every start and on every change (
appMapTelemetryStartup), so a value imported from iOS reaches it too. - The debug lab’s map settings sheet (D14) is now redundant; it stays in debug builds.
- Removed by the owner (2026-10-15): “only show the settings that are needed”. The identity card, Task › Default
observation zones and Task › Flight declaration (with the glider search and the FLARM file export) are gone from
Settings, and with them the zone editor’s rules and preview in
:domain:waypointsand the:domain:glidersmodule: the Android app has no in-flight or task-planning use for them. Their values stay inSyncedSettings(they round-trip with iOS), and a flight’s task without zones still falls back to the default zones (row 16 above: iOS’s defaults, or those set on an iPhone). P9-D3’s General and Task rows and P9-D4 are void. - Added by the owner (2026-10-20): Settings › General gets “Set your home base” after Units, picked from the
airports table and written to the synced setting and the
homebasesrecords; the flight and task maps start on it (Maps start at the home base); Settings › Advanced gets the client-side tracking switch, which only stores the choice. P9-D3’s home base and tracking omissions are void (Android settings parity).
16. UI rework (theme and map sheets) (2026-10-15)
Section titled “16. UI rework (theme and map sheets) (2026-10-15)”The owner picked these from the mockups of 2026-10-15 (Theme A light / dark; the thermal and airspace sheets). Settled; do not re-open.
- UI-D1 Theme A everywhere: neutral greys (no warm tint) and the brand orange as the only accent of the UI
chrome. Light: grouped background #F2F2F5, cards white, tonal buttons #ECECF0, dividers #E6E6EB, text #1B1B1F,
secondary text #5E5E66, chevrons #8E8E96;
primaryis iOSTint Prominent(P3 #B9552C, white on it 4.65:1; the mockup’s #C4572A gives 4.43:1 and is not used). Dark: background #0E0E10, cards #1C1C1F, tonal buttons and dividers #2C2C31, text #EDEDF0, secondary text #A3A3AD, chevrons #8A8A93,primary#F08A5A with #2B0E00 on it. Materialsecondary*/tertiary*are greys; the brand blue (tintSecondary) is no longer a chrome colour (selection, chips, buttons, links, switches, sliders). The grouped background issurfaceDim, the cardssurfaceBright(the role table is incore/designsystemColorSchemes.kt). Not accents, and unchanged: the meaningful colours ofVultureColors(flight palette, thermal greens and the climb rating, task / turnpoint, airspace palette, terrain, destructive, favourite, podium, unread), and the blue where it is data (the rating band “Outstanding”, a flight detail’s cruise phase and secondary summary tint, a better value in the compare grid).primaryContaineris the filled orange too (#B9552C, white on it), so floating action buttons (“Open N flights”) and the account avatar are solid orange, never a pale salmon; the navigation rail’s and drawer’s symbols are orange like the Library’s rows, the selection being the grey pill behind them. - UI-D2 M3 Expressive connected button groups replace the outlined segmented buttons everywhere: the design
system’s
ConnectedChoiceGroup(selected orange, others tonal grey, inner corners tightening on press), with the segmented buttons’ radio-button semantics. It is aSurfaceper option rather than Material’sToggleButton, because a toggle button reads as a checkbox (“on” / “off”) to TalkBack. - UI-D3…D6: the map sheets, below.
- UI-D7 The deprecated
rememberModalBottomSheetStatebecomesrememberBottomSheetState(initialValue = Hidden)(withoutPartiallyExpandedwhere the sheet skipped it), and Edit flight’s slider uses theSliderStateoverload. Same behaviour.
The map’s sheets (the owner: the thermal sheet “starts small and is dragged up over the map”; the airspace sheet is “a bottom sheet”):
UI-D3a The companion host is a two-detent sheet on a compact window. CompanionHost (:feature:flightmap) shows
the thermal sheet, the airspace sheet and the Debrief as one bottom sheet without a scrim: a peek where it opens
and an up detent at 75 % of the map’s height (never over the top bar). The peek is measured from the content when
it marks a peek part (CompanionScope.companionPeek(): the thermal sheet), else a share of the height (airspace 50 %,
Debrief 40 %, 55 % in landscape). Changed by the owner, 2026-10-15: with the graph panel shown, the thermal and
airspace sheets open level with its top (peekHeight = the panel’s covered height), covering the panel and none of the
map above it; the measured part and the share remain for a hidden panel. It is dragged by its handle, by any part that does not scroll and by the content’s
scroll overscroll (nested scroll); let go, it rests on the nearer detent, moves on in a fling’s direction (> 1000 dp/s),
and goes away below half the peek. The rules are the pure CompanionLayout.settle / back, unit tested. A companion
of the same kind (the next thermal, a new airspace query) keeps the detent; another kind opens at its peek. Built on
plain draggable + NestedScrollConnection, not Material’s BottomSheetScaffold (whose sheet belongs to a scaffold
and would cover the map’s own panel and top bar layout) nor ModalBottomSheet (a scrim and a window of its own).
UI-D3b Back and TalkBack. Back (predictive: the sheet sinks with the gesture) takes the sheet from up to the peek and from the peek away; a page inside the content (the airspace sector page, a Debrief answer) goes back first. The handle carries Material’s sheet semantics: Expand, Collapse and Close actions, a tap toggles the detents.
UI-D3c The camera frames above the sheet. The host reports the height the sheet rests at (not every drag frame);
the map’s frame padding keeps clear of the higher of the panel and the sheet + 8 dp (iOS bottomPadding). A framing
that comes with a sheet coming up waits up to 500 ms for its height. The thermal sheet moved to its other detent
frames its thermal again; the airspace sheet’s Centre frames above the sheet’s resting height.
UI-D3d The thermal sheet is one continuous sheet. Its peek part (round Close; “Thermal i / n” — a tap centres the thermal — over the time span and turn direction; connected Previous / Next; average climb in the map’s thermal green, height gained, duration; a strip of the climb) stays on top when the sheet is up, and under it scroll the Climb / Centering / 3D primary tabs, the chart and the remaining figures as tiles. Deviation from the ThermalHalf mockup (there the tabs replace the three figures): nothing changes layout while the sheet is dragged, and the figures stay in view. Every value and its formatting are iOS’s; the iOS wide (side-by-side) layout is dropped, as no card is ever 600 dp wide. Back at the peek the scrolling part returns to its top.
UI-D4 The airspace sheet: a list page and a sector page. The list (“N airspaces here”, “Glider at … · Inside: n”; per row the class colour, the name on one line with an ellipsis, “floor – ceiling” with INSIDE or the gap, the class) is the first page at every size and font scale (iOS’s profile, large-type list and wide card were alternatives). The iOS cross-section of the stack is kept under the list (seen when the sheet is dragged up), with its long-press menu; a row or band opens the sector page: Back / name / “i of n here” / Close, chips (class or hidden state, “You are inside” or the gap, frequency), a small schematic profile with the glider’s line beside Ceiling / Glider / Floor, and Centre on map (filled) with Hide (menu: today / always) or Show as a connected pair. The list rows’ swipe-to-hide moved to the sector page. The glider’s altitude and gaps are in the pilot’s altitude unit only; limits keep the unit the file declared (as iOS).
UI-D5 The Debrief uses the same sheet unchanged (opens at 40 %, as before).
UI-D6 Wide windows keep the floating 400 dp card (D8); it shows the contents’ whole layout
(CompanionScope.isExpanded is true), and the screen’s own Back closes it.
17. iOS builds 560–564 on Android; HIG versus Material (2026-10-17)
Section titled “17. iOS builds 560–564 on Android; HIG versus Material (2026-10-17)”The owner asked for the iOS changes since the analysis baseline (build 559) to be applied, with one rule: “some changes were done for the HIG on iOS. These could be different on Android: keep the Material guidelines.” So:
- Behaviour, data and wording follow iOS (sections in the Debrief, Open all flights, the ranking no longer up by itself, Report issue in the map’s menu, the panel’s badges and readings, the unit groups inline, Clear recents asking first, the thermal and airspace sheets’ data).
- Where iOS changed its chrome for Apple’s Human Interface Guidelines, Android keeps Material 3 Expressive: sentence
case and no ellipsis on buttons, menu items and labels (
ANDROID_WORDINGinscripts/convert_strings.py, docs/strings.md), Material menus and dialogs instead of context menus and alerts styled as on iOS, Material’s 48 dp touch targets instead of 44 pt, Back and TalkBack instead of Esc and VoiceOver’s escape gesture, no inspector column (§9 D18 stands), the Material split button for Play and the speed (§8 D6). - Not ported, iOS-only: the inspector column and its sheets (iPad), the Mac bar, the sidebar’s and turnpoints’ context menus, undo managers, the Spotlight and UIKit redraw performance work, the iPhone Duo poses.
- Opening Soaring Spot and Charron.line pages from a browser (the owner’s request: “open a Soaring Spot flight from
Chrome like we do on Safari with iOS”). Chrome for Android runs no extensions, so Android takes the pages’ own
addresses instead of injecting a button: “Open in Vulture” in the share sheet (
ShareLinkActivity), unverifiableVIEWfilters for the pages (the pilot turns on “Open supported links” on Android 12+), and the extension’svulture://scheme (vulture-android/docs/app-links.md, “Soaring Spot and Charron.line pages”). This sets aside thetext/plainhalf of §6: Vulture is in the share sheet of every text, under that label, and refuses texts without a link it reads. For the owner to confirm; the alternative is theVIEWfilters only, which on Android 12+ need the one-time setting. - No screen transitions (owner, 2026-10-19). “Remove the custom transitions when opening a detail from the
sidebar or the flights list; no zoom animation; keep it native.” Every
NavDisplay(the shell, the statistics, the sheets, Library › Airspaces, the Charron upload) swaps its screen at once on a push, a pop and the predictive back gesture: Navigation 3’s default cross-fade and its back-gesture zoom (the screen shrinking to 70 %) are gone (appnavigation/ScreenTransitions.kt). The overlays’ own back preview (Compare, the detail card, the paywall’s page;rememberPredictiveBackDismiss, 90 %) and the in-card page slides (the flight detail, the Debrief, the airspace sheet) are not navigation screens and are unchanged.
