Skip to content

Android parity 2/3: getting data in fast - #35

Merged
roznet merged 5 commits into
mainfrom
claude/eloquent-maxwell-3bi8vk
Sep 26, 2026
Merged

roznet merged 5 commits into
mainfrom
claude/eloquent-maxwell-3bi8vk

Conversation

@roznet

@roznet roznet commented Sep 26, 2026

Copy link
Copy Markdown
Owner

PR 2 of 3 of the Android parity work (#30): people, aircraft and flights go in as fast as on iOS. One commit per section; the checklist and decisions are in designs/future/android-parity.md §5 and §8, ticked in the same commits.

Sections

  • 2a People and aircraft (5fc16bb)
    • People tab: search, Sort by Recent, crew pill, CSV import and export through SAF.
    • CSV import matches rows by name + date of birth across the file and the app (iOS 1173620).
    • Full-screen crew/passenger picker: usual crew first, "Frequent with" groups, crew/passenger toggle, crew order with PIC first, add someone the search did not find (iOS 83e47fd).
    • Per-flight document choice for people with several documents: flight.chosenDocNumbers, the first real Room migration (1 → 2), carried by leg actions and Move my data.
    • Aircraft editor: category, company operator, owner picked from People.
  • 2b Scan decisions (74dde14): MRZResultProcessor ported to :core-logic with its Swift tests (duplicates across everyone, name mismatch, matching people). The result shows in a bottom sheet before anything is stored. Standalone scan from People and from the picker. Scan from a photo (Photo Picker) or a PDF (PdfRenderer).
  • 2c Airports and local time (323656e)
    • Route and usual-base pickers over the bundled airports.db.
    • An airport → IANA time zone table generated by scripts/airport_timezones.py.
    • Schedule shown in UTC or local time.
    • Airport notices from maps.flyfun.aero, through their own client so the forms token is never sent there.
  • 2d New-flight flow (f9af07a): two steps (route, then people), a "same crew as last time" suggestion, "Choose another flight…" to copy an earlier crew, and Previous Flight import rescheduled to its next local time of day.
  • 2e Contact import (d837b9e): PickContact() with no READ_CONTACTS, then create or merge (Fill Missing Only / Override All). Matching and merging are in :core-logic with tests.

Testing

Built in a cloud session with no route to Google Maven, as PR 1's 1a was, so ./gradlew :app:… could not run.

  • :core-logic: 173 tests pass (43 new, most ported from the Swift suites), after every commit.
  • app/src/main + androidTest: type-checked against Compose Multiplatform desktop plus stubs for the Android APIs, clean after every commit. The 27 app/src/test tests pass.
  • Not verified:
    • Room's KSP output and the migration. app/schemas/…/2.json is written by the next real build and needs committing.
    • Resources and the manifest.
    • Anything at runtime.

Owed before merge: ./gradlew :app:testDebugUnitTest :core-logic:test, assembleDebug, the instrumented tests, and a drive on the emulator:

  • the 1 → 2 upgrade on an existing database
  • the picker and document choice
  • CSV round trip
  • scan result sheet, and photo/PDF scan
  • route picker and local times
  • the new-flight flow
  • a contact import: check that phones, e-mails and addresses come through the picker's grant

Decisions worth a look

  • Time zones: a committed 180 KB icao,zone table rather than a column added to airports.db at build time. Re-run the script when airports.db changes; a missing airport only loses the local-time option.
  • Route entry: the route card and picker replace the two ICAO text fields. An unknown four-letter code can still be used as typed.
  • New-flight imports: only Previous Flight is offered. FPL paste is blocked on G4; Weather and Autorouter are PR 3.
  • Document scanner: ML Kit GmsDocumentScanning was not adopted for now.
  • New gap from the iOS re-sync: fix-validation-errors-in-place (iOS 7af8d61), listed in §7 as a PR 3 candidate.

Closes #30

🤖 Generated with Claude Code

https://claude.ai/code/session_016NgSweh5v5Fx5omrHvvjAk


Generated by Claude Code

- People tab: search by name, Sort by Recent (each person's latest live
  flight), crew pill, + menu with CSV import, overflow with CSV export (SAF).
- CSV import matches rows by name + date of birth across the file and the
  app, so one person with several documents stays one person (iOS 1173620);
  the plan is pure in :core-logic, written in one transaction.
- Full-screen crew and passenger picker: search, usual crew first, Frequent
  with / Usual Crew groups, crew/passenger toggle, reorderable crew (PIC
  first), add a person the search did not find. A person created from the
  picker joins the flight (iOS 83e47fd).
- Per-flight document choice for people with several active documents,
  stored as flight.chosenDocNumbers (Room migration 1 -> 2), carried by leg
  actions and Move my data, and winning over the region pick.
- Aircraft editor: airplane/helicopter, company operator, owner picked from
  People with owner/address kept in step.

Refs #30

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NgSweh5v5Fx5omrHvvjAk
…or PDF

- Port MRZResultProcessor to :core-logic with its Swift tests: duplicate
  document across everyone, name mismatch, matching people. Replaces the
  silent upsert applyScan did.
- The scan result is a bottom sheet over the camera with the choices that
  fit where the scan started (add to this person, update the name, create a
  new person; or pick a matching person). Rescanning a document the person
  already holds offers to refresh it.
- Scan Document from the People + menu and the crew picker opens whoever
  the scan ends with, and puts them on the flight from the picker.
- Scan from a photo (Photo Picker) or a PDF (SAF + PdfRenderer), no
  permission and nothing kept.

Refs #30

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NgSweh5v5Fx5omrHvvjAk
…ices

- Route picker over the bundled airports.db (FROM/TO, ranked search, recent
  routes; an unknown four-letter code can still be used), and a single
  airport picker for an aircraft's usual base.
- Airport time zones as an icao,zone table generated from airports.db by
  scripts/airport_timezones.py: Android's Geocoder has no time zone.
- Schedule fields offer UTC and the route's local times, moving to the
  airport's zone once known unless the pilot picked one.
- Airport notices from maps.flyfun.aero under the route, through their own
  client so the forms token never goes there.

Refs #30

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NgSweh5v5Fx5omrHvvjAk
…ight

- + opens New Flight in two steps (route, schedule, aircraft; then people)
  on the same draft; Create Flight stores it and the editor takes over.
- The people step suggests who was on board last time (same aircraft
  first, then the latest flight, then usual crew), and "Choose another
  flight..." copies any earlier crew, one row per distinct set of people.
- Previous Flight copies an earlier flight's route, aircraft, people and
  settings, rescheduled to the next time its local time of day comes round.
- PeopleSuggestion and NextOccurrence ported to :core-logic with the iOS
  tests.

Refs #30

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NgSweh5v5Fx5omrHvvjAk
- Import from Contact on the People + menu and the crew picker: the system
  contact picker (no READ_CONTACTS), then create a new person or update a
  close match, filling missing fields only or overriding all, with a choice
  when the contact has several phones, e-mails or addresses.
- Matching (same surname with a shared first-name prefix, or both names
  within two edits) and merging ported to :core-logic with tests.

Closes #30 once driven on the emulator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016NgSweh5v5Fx5omrHvvjAk
@roznet

roznet commented Sep 26, 2026

Copy link
Copy Markdown
Owner Author

Code Review

Verdict: merge, with the fixes below on main. No blocking bugs. The core-logic ports match their Swift originals; where they differ, the difference is deliberate or makes the Kotlin more correct.

Verified locally (covers the "owed before merge" list, except the manual emulator drive)

  • ./gradlew :core-logic:test :app:testDebugUnitTest :app:assembleDebug :app:assembleDebugAndroidTest: pass.
  • 1 → 2 migration on a real v1 database. The emulator install had 2 flights and 2 people. After upgrading it in place, user_version is 2, all rows are kept, chosenDocNumbers is NULL, and the app launches with no crash.
  • Instrumented tests (DataTransferTest, FlyFunDatabaseTest): 19/19 pass.

Fix on main

  1. app/schemas/.../FlyFunDatabase/2.json is not committed. The build generates it, and it is the baseline for the next migration and for any MigrationTestHelper test.
  2. No migration test for the first real migration. Add a MigrationTestHelper test for 1 → 2: a row survives and chosenDocNumbers is null.
  3. CSV import/export blocks the UI thread. FlyFunApp.kt:552-566: rememberCoroutineScope() runs on the Main dispatcher, so openInputStream().readBytes() and openOutputStream().write() run on the UI thread. That risks an ANR with a cloud document provider. Wrap both in withContext(Dispatchers.IO). Settings' import at :962 has the same pattern and predates this PR.
  4. A zone the pilot chose is lost after a picker. ScheduleField.kt:54-55 keeps zoneId/zoneChosen in rememberSaveable. The route and people pickers early-return, and the new-flight flow switches steps; in both cases ScheduleField leaves composition and its state is dropped.
    • Scenario: set Departure to UTC, open the route picker, come back. The field has snapped to the origin's local zone, and the next time typed is read in a zone the pilot didn't pick.
    • Fix: a SaveableStateHolder around the flight screens and around each step.
  5. scripts/airport_timezones.py:48: the fallback is inverted. timezone_at already returns an Etc/GMT±N sea zone, so the timezone_at_land fallback never runs. The committed CSV has 0 Etc/ rows, so nothing is affected today. Swap the order so a coastal airfield never gets a zone with no DST.

Follow-ups (not fixed here)

  • ContactReader and the picker's grant. ContactReader reads <contact>/entities under the picker's grant. If the provider refuses the sub-path, runCatching hides it, and only the name comes through. This is already open in android-parity.md; it needs a device check. Main will log the failure so it can be seen.
  • Photo scan memory. ImageMrzReader's InputImage.fromFilePath probably decodes photos at full size. PDF pages are capped at 3000 px; photos could be capped the same way if a large photo runs out of memory.
  • CSV line breaks. A quoted field containing a line break does not round-trip, because parse splits lines before it looks at quotes. Swift behaves the same, and the exported fields never contain line breaks.

@roznet
roznet merged commit cf7fe51 into main Sep 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Android parity 2/3: getting data in fast

2 participants