Skip to content

Decisions (Android port)

Moved from vulture-android/docs/decisions.md

Originally titled “Decisions”.

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.

#QuirkWho noticesRecommendationDecision
1Landing detection reads a fix near the start of the fileLanding time / flight duration can be offFixFix
2Release (tow/winch release) index is one fix too early~1 s on release timeFixFix
3Midnight-UTC wrap compared with the first fix onlyFlights crossing 00:00 UTC (Australia, Americas)FixFix
4Vario smoothing feeds back already-smoothed valuesNobody; it’s a smoothing styleCopyCopy
5Two Earth radii (6371 km vs 6378.137 km)~0.1 % on optimised/task distancesCopy (changing it moves every distance)Copy
6distanceTraveled stored in km, optimisations in mNobody (internal units)CopyCopy
7Wind direction = upwind bearing; first wind sample keyed at the first fixNobody, if the UI labels it rightCopyCopy
8Thermal “relative strength” explodes when every thermal is negativeRare weird thermal colouringFixFix
9Rejected thermal candidates still mark their fixes as “thermal”Subtle (thermal membership of a fix)CopyCopy
10Turn-direction count skips the last two fixes of a thermalNobodyCopyCopy
11No legs when a flight has no thermals and no engine run; the leg distance is a 5-TP optimised distanceStraight glides show no legsCopyCopy
12A C-record declaration with a task name after it is droppedDeclared task silently missingFixFix
13HFDTEDATE:ddmmyy without ,NN gives no dateFlight without a date from some loggersFixFix
14Generated filename and recording dates use the device time zoneSame flight, different name on other devicesFixFix
15Time-zone offset taken at 00:00 UTC of the flight dateOnly on DST change daysCopyCopy
16Default observation zones are all zero unless configuredScoring falls back to line crossings / closest pointCopy (it’s a default, not a bug)Copy
17“Triangle” has no closing leg and no FAI 28 % ruleFeature gapCopy (feature for later)Copy
18ft/min, hPa and % print 6 decimalsVisible ugly numbersFix (ios-changes.md B9)Fix
19Live takeoff detector uses a 100 m turn radius vs 50 m in the IGC analysisRecording only (not in Android v1)CopyCopy
2064-bit ints and Int.max “no value” sentinelsNobodyCopy (Kotlin Long)Copy
21Optimiser ranges crash Swift when bounds invertiOS crash on odd input; Android would silently skipFix (guard on iOS)Fix
22Elevation download has a 10 s budget; any failure means no terrain for the whole flightNumbers differ online vs offlineCopy for nowCopy
23A tile still loading when the 10 s budget ends gets 0 m terrain, yet the flight counts as having terrainWrong AGL and pressure correction on slow connectionsFix (decided 2026-10-10)Fix

Firebase (Auth with Google sign-in, Firestore, Cloud Storage), free for everyone. CloudKit dropped (decided 2026-10-10).

  • applicationId: be.fousa.vulture (debug: be.fousa.vulture.debug)
  • minSdk: 29 (Android 10)

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.

FixAndroid status
B1 week-based yeardone (:domain:waypoints, :domain:services ServiceDates); iOS done (1c986f21f)
B2 SkyLines date format, B3 Charron season (October, UTC), B5 WeGlide pindone 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 fieldopen 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 unitsdone (:domain:airspace, ported from iOS main, where they are fixed: iOS done, decision P6-D2)
B9 / Q18 unit decimals and ft/mindone (:core:units)
Q3, Q12, Q13, Q14done (:domain:flight parser)
Q8done (flight maxima in the parser, thermal relative strength in :domain:flight enrichers)
Q1, Q2, Q21done (:domain:flight enrichers; Q21 also guards task scoring)
Q23 elevation timeout = no elevationsdone (:domain:flight)
B10 statistics rounding, B11 preview duration past 24 h, B13 blank pilot names, B14 single-year grid cornersdone (: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 nightdone (: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 stepsdone (: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/plain in 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, tag FIXED(<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 (as FIXED(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).
  • 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; ProAccess unlocks 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.properties as MAPBOX_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 SymbolLayer for 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:map FlightMapController); 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 i button 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.md B15).
  • D8 Compare is a full-screen dialog on every width; companions are a bottom card (compact) or a floating 400 dp card (wider); no SupportingPaneScaffold until Phase 5.
  • D9 Chip reordering uses the sh.calvin.reorderable library.
  • 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/ (see docs/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 settings screen) until the Phase 9 settings exist.
  • D15 The barogram quirks are copied (PARITY) and listed as iOS candidates (ios-changes.md P4-4). On 2026-10-11 four of them became fixes (B22–B25, below); the other six stay PARITY candidates.
  • 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.md B16). 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 as rgba strings; 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): a Flight no 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 iOS feature/race-ranking placement (side card ≥ 600 dp, slim 64 dp column, expanded) and has no shadow; the task picker has only the flights tab; no keepingVisibleRegion when 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 PARITY candidates.
  • P4-5 (whole-degree bisector on the map) stays a PARITY candidate.
  • 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 nested NavDisplay.
  • D3 FlightRating, FlightScope, FlightQuestions moved to :domain:debrief; DetailOptimizationItem, OptimizedLeg to :domain:flightdetail (:domain:compare depends 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 Snapshotter image.
  • 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.rating column 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/worktrees are 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). Tagged FIXED(B31) … FIXED(B33) in :domain:debrief and :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 answering mailto: (one by package, several through a chooser) instead of a mailto: 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 inverseColor is 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.

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_KEY in local.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), tagged FIXED(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/plain in SEND (§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.md A3; 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 through sin / 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 uses StrictMath so 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:flightdetail InfringementRow.classLabel, :feature:airspace AirspaceTexts.classLabelRes, :feature:airspacefiles classLabelRes).

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, tagged FIXED; 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 asks RACE_RANKING first, then SEVERAL_FLIGHTS, before anything is downloaded (iOS ed23fc896); each gate puts its paywall up since Phase 8a (§12). Changed by iOS build 563 (e4093d7c5), ported 2026-10-17: the ranking stays down when a day is opened. Replay Day is “Open all flights” (f90ed1e50, b8fbcbd9e, a43789ba2) and asks SEVERAL_FLIGHTS alone before anything is downloaded; the race ranking is the pilot’s to open from the map’s bar, where it asks for RACE_RANKING itself. The ranks and handicaps of the day still go to the map (they feed that ranking). The showsRanking / showsRankingWhenAvailable flag 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.json on vulture-web. Owner, 2026-10-12: the lead added the route at the owner’s request (vulture-web commit adc7fff, app/asset_links.rb, registered in app.rb, specs in spec/app_spec.rb), with the debug fingerprint only; the release fingerprints (Play App Signing and upload key) go into RELEASE_FINGERPRINTS once 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 opens Changed 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 empty AppKey.Flight opens 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 (iOS finishAdding); 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 (iOS releaseServiceMaps); 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 hosts Changed 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 only lvzc.be and charronline.be, whose IGC links are http:// (found in WP-Q). Done (base-config, as iOS NSAllowsArbitraryLoads).
  • 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:purchases 10.26.0) in the same RevenueCat project as iOS: entitlement all, the current offering (no id in the code) with the iOS fallbacks (the known products, then “no products”). Only RevenueCatStore imports the SDK; the rest sees the SubscriptionStore interface (fakes in tests).
  • P8-D2 Google Play products: one subscription vulture_pro with base plans monthly and yearly and free-trial offers mirroring the App Store’s (“new customers only”); RevenueCat ids vulture_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, the launch trigger, isAppOpen) and promoted purchases (none on Play) are not ported.
  • P8-D4 ProAccessPolicy is a plain Kotlin class in :data:billing next to ProFeature (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 ProAccess API on the ProAccess interface: allows(feature) and requestsFlights(count) put the paywall up when they say no, presentPaywall, isUnlockedNow. A view model posts the request to the app-wide PaywallPresenter; one PaywallHost over every screen draws it; nothing happens while a paywall is up (iOS). Every former TODO(pro) goes through it; the existing ProRequired events 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 @2x JPEGs, 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:paywall README “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 string paywall_restore_nothing_found in 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:paywall SubscriptionSettingsScreen) 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, refreshedIsActive keeps the last known state (iOS: not active).
  • P8-D17 Analytics: SubscriptionAnalytics with the iOS paywall events and properties; a logging implementation until Mixpanel lands (TODO(analytics)).
  • P8-D18 strings_ios.xml regenerated from iOS main (the race ranking paywall strings); the Android-only settings_labels_unit_ftmin is 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_KEY repository secret and its line in deploy.yml’s environment (8a does not touch the workflows); ci.yml needs 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).

  • RA-1 Motion-scheme sweep. Standard enter / exit / fade animations use ad-hoc tween durations instead of MaterialTheme.motionScheme: feature/flightdetail FlightDetailContent.kt:160 (300 ms page push), feature/debrief DebriefContent.kt:159, feature/flightmap panel/FloatingPanel.kt:232-256, panel/PanelPills.kt:92, panel/FlightPanelContent.kt:62, panel/PanelControls.kt:268, FlightMapScreen.kt:919, feature/flightlist quicklook/FlightPreviewCard.kt:118, core/barogram EventPills.kt:127. Recommendation: one sweep, motionScheme.defaultEffectsSpec() for fades and defaultSpatialSpec() for slides, screenshots re-recorded; keep the documented iOS timings (feature/thermal CenteringPlanChart.kt 350 / 700 ms, feature/photos PhotoPager.kt:340 spring). 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 uses MaterialTheme.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 plain TopAppBar with a two-line title and no scroll behaviour (feature/flightlist FlightListScreen.kt:339); modal screens (Compare, Edit flight) a plain TopAppBar; the detail TopAppBar(windowInsets = 0); Thermal and Debrief hand-roll a header row. The service screens now use material3 1.5’s TopAppBar(title, subtitle) for a subtitle (iOS navigationItem.subtitle). Recommendation: write the rule down (large flexible bar + exitUntilCollapsed for list roots, small bar for modal screens and companions, TopAppBar(subtitle) for a count or a date) and give the flight list pinnedScrollBehavior so 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 the CompanionHeader of 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 in core/designsystem/README.md; the flight list’s count is the bar’s subtitle, with pinnedScrollBehavior).
  • RA-3 One bottom-action pattern. “Open N flights” / “Replay Day” is built three ways: feature/charron day = ExtendedFloatingActionButton, feature/soaringspot day = Surface + Button, feature/serviceflights = BottomAppBar. Recommendation: one BottomAppBar with the primary Button trailing (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 an ExtendedFloatingActionButton (charron’s shape) in feature/charron, feature/soaringspot and feature/serviceflights, not a BottomAppBar. 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/flightdetail FlightDetailHost.kt:251), the companion card 400 dp (CompanionHost.kt:224), the filter dialog 360 dp (feature/flightlist filter/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”, PaywallWindow generalised into a design-system SheetWindow, and SheetScreen (app/navigation/ServiceScreens.kt:107) wrapped in it. Effort M (plus screenshots). Owner, 2026-10-12: yes, with a design-system SheetWindow. 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 ListItem or hand-rolled rows. feature/serviceflights, feature/services and feature/paywall use ListItem; feature/charron, feature/soaringspot and feature/airspacefiles hand-roll their rows; feature/library (RowGroup), feature/debrief (Section), feature/flightdetail (GroupedRow) and feature/editflight (ListItem on surfaceContainer) each build the iOS inset-grouped list their own way. Every module is consistent inside itself. Recommendation: one GroupedSection { rows } + GroupedRowDivider(inset) in the design system (RA-8) with ListItem rows inside, adopted module by module; the deprecated ListItem overload 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 use toggleItemColors()).
  • RA-6 Caption / footnote typography tokens and iOS system colours. Raw sp text styles for iOS caption1, caption2 and footnote in feature/flightmap panel/PanelPills.kt:257-260 and panel/FlightStrip.kt:488-497 (the strip’s 22 / 15 / 11 pt), core/barogram Barogram.kt:399-402; feature/soaringspot PilotsSheet.kt:1155 keeps 9 / 12 sp on purpose (the 32 dp badge). iOS system colours are defined twice: systemGray #8E8E93 in core/barogram BarogramPalette.kt:131, systemGray6 and label in feature/flightmap panel/MatchingTextColor.kt:51-60 (a documented port of UIColor.matchingTextColor()). Recommendation: caption1 / caption2 / footnote on VultureTypography and one IosSystemColors object 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, so feature/taskmap TaskBanner can show it through ExternalTaskKind.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) for feature/thermal ThermalDetailContent.kt:154, feature/debrief DebriefContent.kt:189, the task picker feature/flightmap dialogs/FlightMapDialogs.kt:180 and feature/flightdetail FlightDetailContent.kt:272; (2) GroupedSectionHeader(text) with heading() plus the GroupedSection of RA-5 for feature/library LibraryScreen.kt:148 / 175, feature/editflight EditFlightContent.kt:158, feature/debrief DebriefPages.kt:441 / 460, feature/flightdetail DetailList.kt:123, feature/flightlist filter/FlightFilterSheet.kt:140 (four text styles for one iOS grouped-list header; flightdetail and debrief use onSurfaceVariant on purpose for iOS’s DetailSectionTitle); (3) FileErrorsDialog(rows) for feature/import ImportErrorText.kt:70 and feature/flightmap dialogs/FlightMapDialogs.kt:291 (one file = a titled alert, several = a list under error.label.multiple). With them: a preview/ 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 to VultureIcons when a second screen needs them. The design system’s requests to the flight-UI modules go with it: FlightListScreen.kt:406 → the shared SearchTopBar, EditFlightContent.kt:158 → the shared SectionHeader, LibraryRows.kt:103 → ServiceLogos.sidebar, and the glyphs still named directly (flightmap 54, flightdetail 29, import 4, editflight 1; the Filled / Outlined ones → VultureIcons or the Rounded family). Effort M–L. Owner, 2026-10-12: yes, the components added and every screen migrated (with RA-5). Done (CompanionHeader on the thermal sheet, Debrief, flight detail and task picker; FileErrorsDialog for import and the map; the airspace sheet’s header stays its own for now).
  • 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 but feature/flightmap FlightMapScreen(viewModel = …), which takes the view model (the map is an AndroidView and cannot preview anyway). Recommendation: write the rule down in docs/android-primer.md and leave the map screen as the documented exception. Effort S. Owner, 2026-10-12: write the convention down only: done in vulture-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/flightmap FlightMapViewModel.kt:190, feature/import ImportViewModel.kt:81, feature/taskmap TaskMapViewModel); 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 in vulture-android/docs/android-primer.md, “Conventions in this repo”; no code changes.
  • RA-11 Injected dispatchers and an @IoDispatcher qualifier. No qualifier exists: 14 classes take ioDispatcher: CoroutineDispatcher = Dispatchers.IO plus a secondary @Inject constructor or a @Provides that leaves the default; feature/photos data/PhotoStore.kt:90-171, feature/import AirspaceImports.kt:41-44 and feature/flightlist FlightListStore.kt:119 use Dispatchers.IO / Default directly (FlightMapScreen.kt:407, 420, 891 do so inside composables, which is fine). Recommendation: an @IoDispatcher / @DefaultDispatcher qualifier pair provided from core/network (or a tiny core/coroutines), injected everywhere, the secondary constructors gone. Touches app/di and 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 name Dispatchers.Default / Main; a @MainDispatcher qualifier would be needed for those.
  • RA-12 Context-free view models. feature/compare CompareFlightsViewModel.kt:52 holds the Application to read the grid’s strings (the model is built off the main thread and the strings follow the system language). Recommendation: inject an AndroidCompareStrings built from @ApplicationContext resources in a Hilt provider; the same for AndroidDebriefStrings if it ever moves into a view model. Effort S. Owner, 2026-10-12: no: the Compare view model keeps the Application.
  • RA-13 One test-name style per module. domain/compare (31 tests) and domain/debrief (56) use terse camelCase names (format, thresholds, inputs); feature/flightmap (116 / 105), flightlist (66 / 6), statistics (14 / 6), flightdetail (7 / 43) and photos (14 / 27) mix backtick sentences with camelCase; compare, thermal, debrief, editflight and core/barogram are 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) in domain/logbook, MockThermalFlight.kt in domain/thermal, MockThermalFlight.kt and MockRaceRanking.kt in feature/flightmap (“Mock” for hand-built fixtures, while the repo says Fake* / *Fixtures). Recommendation: rename to *Fixtures.kt. Effort S. Applied on 2026-10-12 by the audit’s follow-up: RaceRankingFixtures, ThermalFlightFixtures.
  • RA-15 @Suppress of detekt rules with no detekt. Nine functions carry @Suppress("CyclomaticComplexMethod", "LongMethod", "NestedBlockDepth", "LongParameterList") (domain/flight BRecordParser.kt:53, 140, IgcParser.kt:153, 196, 285, FlightTakeoffEnricher.kt:80, FlightTimesEnricher.kt:139, FlightPhasesEnricher.kt:109; domain/services FuseMatcher.kt:148) and feature/taskmap TaskMapViewModel.kt:235 a TooGenericExceptionCaught, but detekt is not in the build: they are no-ops, and only FuseMatcher’s says why. Recommendation: add detekt in build-logic with 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 of lint, so CI runs it.
  • RA-16 A :core:testing fixtures module. IosStringKeys.kt is copied into the compare, debrief and flightdetail test sources and four more test files parse strings_ios.xml by hand (debrief DebriefFixtures, logbook StatisticsLocalizationTest, thermal ThermalDetailLocalizationTest, waypoints WaypointNameTest); MainDispatcherRule exists only in feature/flightlist/filter while the other modules call Dispatchers.setMain inline; FakeProAccess is declared five times (app, feature/soaringspot, feature/debrief, feature/flightmap, feature/upload, plus a private one in data/airspace; each needs a per-feature, mutable or paywall-recording answer that FixedProAccess cannot give, as their KDoc now says) and FakeConnectedServicesRepository twice (app ShellTestFakes.kt, feature/upload UploadFakes.kt); 47 test classes carry @OptIn(ExperimentalCoroutinesApi::class). Recommendation: a JVM :core:testing module (or java-test-fixtures in build-logic) with one IosStringKeys and MainDispatcherRule; a fake/ main package in data/billing with one mutable FakeProAccess, and FakeConnectedServicesRepository next to FakeCredentialStore in data/credentials’ fake/ (both app and feature/upload already depend on :data:credentials, so no build change); the coroutines opt-in on the test source sets in build-logic. Effort M. Owner, 2026-10-12: yes, :core:testing and the shared fakes. :core:testing done (IosStringsXml, IosStringKeys, MainDispatcherRule, the coroutines opt-in in build-logic); the shared fakes (data/billing and data/credentials fake/) and every module’s MainDispatcherRule migration are done too.
  • RA-17 One base for AnalysisStrings and DetailTexts. domain/debrief AnalysisStrings and domain/flightdetail DetailTexts declare the same string(key), format(key, args) and mediumDay(date) with identical KDoc, implemented three times in UI (AndroidDebriefStrings, AndroidDetailTexts, AndroidCompareStrings). A shared interface needs a module both depend on; today only core/units (about numbers). Recommendation: a LocalizedTexts interface in core/units or core/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/billing ships eight empty values*/strings.xml; the same pattern is in core/designsystem and feature/statistics. Recommendation: delete repo-wide, or keep as the 8-locale template and say so in docs/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 in domain/logbook FoundationStrings.kt:91 (internal) and feature/library LibraryFormat.kt:95, and LibraryFormat.kt:97 keeps a third copy of the ICU \s class that core/swift now publishes as SWIFT_WHITE_SPACE_PATTERN. Recommendation: feature/library takes core/swift and uses the pattern; normalizedWhitespace moves next to it in core/swift (Vulture’s own rule, not Foundation’s) or feature/library takes domain/logbook and that copy goes public. Effort S. Owner, 2026-10-12: yes, one home in core/swift. Done (String.normalizedWhitespace() next to SWIFT_WHITE_SPACE_PATTERN).
  • RA-20 Hilt provider names. Providers are provideX (network, charron, elevation, skylines, weglide, prefs), bare x (airspace, credentials, db, billing, remoteflights) and bindX (soaringspot); @Provides with constructor defaults and @Inject + @Binds both 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.fma guard under Animal Sniffer. core/geo FusedMultiplyAdd.kt:16, 21 is the sanctioned run-time probe (hasNativeFma catches NoSuchMethodError), which the API-29 check (RA-26) can only skip by excluding the class in build-logic. Recommendation: @IgnoreJRERequirement (compile-only, on the classpath) on fusedMultiplyAdd and the probe moved out of the by lazy lambda into an annotated private fun probeNativeFma(), then the two exclusion lines in build-logic deleted. Applied the same day (commit be9b971); listed here because it is the one sanctioned exception to RA-26.
  • RA-22 Two DataStore patterns. data/prefs keeps typed JSON stores; data/billing BillingPreferences is 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 —.
  • RA-23 ACCESS_NETWORK_STATE for the offline wording. FailureReason.from(error) is called without isOnline everywhere (the manifest has no ACCESS_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 in core/network that every FailureReason.from call passes; noted in the soaringspot and serviceflights READMEs. Effort S. Owner, 2026-10-12: yes, ACCESS_NETWORK_STATE and the shared connectivity helper. Done (core/network Connectivity; every service failure passes isOnline).
  • 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. warningsAsErrors and abortOnError in build-logic, one root lint.xml (GradleDependency / AndroidGradlePluginVersion informational, AutoboxingStateCreation a warning), no baseline file, issues printed to stdout, checkDependencies off (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 lint is green and must stay so. CLAUDE.md says so.
  • RA-26 The Android 10 API check for JVM modules. AGP’s lint cannot run NewApi on a non-Android module (its ApiDetector has no minSdk there), so vulture.jvm.library runs Animal Sniffer (ru.vyarus.animalsniffer, signature android-api-level-29:10_r5) on the main source set, registered as the JVM module’s lint task; it caught Math.fma and a Java 21 List.removeLast() on a probe. Documented in docs/setup.md, CLAUDE.md and build-logic/README.md; the one annotated exception is RA-21.
  • RA-27 Every Hilt module is XxxModule in a di/ package. core/network, data/charron, data/elevation, data/skylines and data/weglide moved there; AirspaceBindings became AirspaceBindingsModule; CharronFeatureModule / SoaringSpotFeatureModule became CharronModule / SoaringSpotModule; pure-@Binds modules are interfaces. Left for the flight-UI follow-up: feature/flightlist filter/FlightFilterModule.kt and the @Module inside quicklook/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 (with heading()), material3’s TopAppBar(title, subtitle) for a subtitle, and VultureIcons for 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 Expressive LoadingIndicator) for a content wait, CircularProgressIndicator only inside a small control (a 24 dp Save, a row’s download, the HUD); the one leftover is feature/editflight EditFlightContent.kt:323 (a 24 dp LoadingIndicator in a TextButton). Applied in the design-system and service modules and in compare / debrief; RA-8 lists what the flight-UI modules still take. Per-file @OptIn(ExperimentalMaterial3Api::class) / ExperimentalMaterial3ExpressiveApi is 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/map is on it.
  • RA-30 The convention plugins own the shared build settings. build-logic adds JUnit 4 + Truth and isIncludeAndroidResources to every Android module, the Compose UI test dependency to every Compose module, and a vulture.roborazzi plugin (output dir + the three test dependencies) for the 17 screenshot modules; the module build files drop their repeated lines in the follow-up, together with app’s unused androidTestImplementation and the “Phase 3, in progress” headers of flightlist / 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_photo held 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 externalUrl when set, else a UUID) and map files (SHA-256 of source|type|filename for downloads, of USER|type|contentHash for 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.debug in the same Firebase project; one git-ignored google-services.json holds 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-account page (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 revenueCatEntitlements on the ID token containing all, refreshed with the token); the usage is counted in users/{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.json has 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:settings holds 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:paywall SubscriptionSettingsScreen), 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) in ShellState.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 (iOS settingsMenu).
  • P9-D3 The rows, in iOS MainSettingsView order (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.xml keys wherever iOS has the text (most rows do). Android-only strings go in feature/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 + /) through Activity.onProvideKeyboardShortcuts.
  • P9-D9 Baseline profile: a :baselineprofile module (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.txt or 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.txt and 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 Canvas drawing of iOS’s sample task, not a Mapbox map; the steppers are − / + button pairs.
  • Top bar: the small TopAppBar of 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 (app MapTelemetryStartup), 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:waypoints and the :domain:gliders module: the Android app has no in-flight or task-planning use for them. Their values stay in SyncedSettings (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 homebases records; 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; primary is iOS Tint 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. Material secondary* / tertiary* are greys; the brand blue (tintSecondary) is no longer a chrome colour (selection, chips, buttons, links, switches, sliders). The grouped background is surfaceDim, the cards surfaceBright (the role table is in core/designsystem ColorSchemes.kt). Not accents, and unchanged: the meaningful colours of VultureColors (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). primaryContainer is 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 a Surface per option rather than Material’s ToggleButton, because a toggle button reads as a checkbox (“on” / “off”) to TalkBack.
  • UI-D3…D6: the map sheets, below.
  • UI-D7 The deprecated rememberModalBottomSheetState becomes rememberBottomSheetState(initialValue = Hidden) (without PartiallyExpanded where the sheet skipped it), and Edit flight’s slider uses the SliderState overload. 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_WORDING in scripts/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), unverifiable VIEW filters for the pages (the pilot turns on “Open supported links” on Android 12+), and the extension’s vulture:// scheme (vulture-android/docs/app-links.md, “Soaring Spot and Charron.line pages”). This sets aside the text/plain half 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 the VIEW filters 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 (app navigation/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.