Skip to content

Android parity 3/3: platform and integrations - #37

Merged
roznet merged 8 commits into
mainfrom
claude/confident-mccarthy-v85j9a
Sep 27, 2026
Merged

roznet merged 8 commits into
mainfrom
claude/confident-mccarthy-v85j9a

Conversation

@roznet

@roznet roznet commented Sep 26, 2026

Copy link
Copy Markdown
Owner

Closes #31. This is PR 3 of the Android parity work: the app now looks and behaves like an Android app, and can import flights from FlyFun Weather. The checklist and the decisions made along the way are in designs/future/android-parity.md §6 and §8. There is one commit per section.

Not compiled. Google Maven is blocked from this cloud session, so ./gradlew could not run. Please build it and run it on the emulator before merging. The :core-logic tests are new too and have not been run yet.

3a: look and layout

  • Launcher icon. An adaptive icon made from the iOS artwork, plus a monochrome layer for Android 13 themed icons. scripts/android_launcher_icon.py generates it; re-run it when the iOS icon changes.
  • Theme. FlyFunTheme: dynamic colour (Material You), light or dark following the system, drawn edge to edge. There is no fallback palette: dynamic colour needs API 31 and the app's minimum is 33.
  • Navigation. NavigationSuiteScaffold replaces the bottom bar: a bar on phones and a rail on tablets.
  • Tablet layout. ListDetailPaneScaffold shows the list beside the open flight, person or aircraft on wide screens. The navigation back stack decides which panes show, so phones behave exactly as before and ViewModel scoping, Back and the pickers are unchanged.
    • If you tap another flight in the side list while the open one has unsaved edits, it asks Save / Discard, as Back does.
  • New dependencies. androidx.compose.material3.adaptive:adaptive and adaptive-layout pinned at 1.2.0, and material3-adaptive-navigation-suite taken from the Compose BOM. I could not check these versions against Google Maven, so bump them if the build complains.

3b: auth and platform

  • Sign in with Apple through the server's existing web flow (/auth/login/apple, form_post callback, then the auth code). It sits beside Google on the sign-in screen and in Settings.
  • Token storage. TokenStore now encrypts the token with its own Keystore AES-GCM key, and the deprecated security-crypto dependency is gone.
    • The old encrypted file is deleted, not migrated, so testers will have to sign in once more.
  • Rolling sessions (bug fix). Android ignored the server's X-Renewed-Token header, so every session expired at the token's lifetime however often the app was used. The interceptor now keeps the renewed token.
  • Launcher shortcuts: New Flight and Scan Passport.

3c: server-backed imports

  • FlyFun Weather import. The new-flight import list now has a "FlyFun Weather" entry. It lists the pilot's flights on weather.flyfun.aero using the forms token, and applies the chosen flight's route, schedule and aircraft to the draft.
    • The aircraft is matched by registration. If it isn't on file, it is staged and saved only when the flight is created.
    • A 401 from Weather does not sign the pilot out of forms; it shows a message asking them to sign in with the same account instead.
    • The cross-app flight format (FlightExchange) and the mapping onto the draft are in :core-logic, covered by the new FlightExchangeTest.
  • Autorouter: skipped, as agreed. Its routes come as raw ICAO flight plans. It is moved to §7 to wait on gate G4.

3d: localisation

  • Strings. Every user-visible literal is now in res/values/strings*.xml, one file per area.
  • Translations. scripts/android_strings.py writes values-fr/de/es from the iOS Localizable.xcstrings, matching on the English text. 133 of the 350 strings get a translation today; the rest fall back to English.
  • Language setting. locales_config.xml lists en/fr/de/es for the system's per-app language setting.
  • Still in English. Text built in :core-logic, which has no access to Android resources: the CSV import summary, the merge summary, the "Anne, Bob and 2 more" line, and parser error messages. The details are in §8.

To check when you build

  • The Compose adaptive APIs (calculateThreePaneScaffoldValue, ThreePaneScaffoldDestinationItem, NavigationSuiteType.None) are the likeliest compile errors, because their names have changed between adaptive versions.
  • NavController.currentBackStack is used to find which tab's list a screen was opened from.
  • Tablet or foldable emulator: check the rail, the list beside an item, and the Save / Discard prompt when switching flights.
  • Weather import against a real account.
  • Apple sign-in end to end.

🤖 Generated with Claude Code

https://claude.ai/code/session_017wLwwiewwvrkxjTmXNfwNg


Generated by Claude Code

claude and others added 7 commits September 27, 2026 07:05
… helpers

Work in progress on #31 section 3a; not yet type-checked.
- Adaptive launcher icon generated from the iOS artwork by
  scripts/android_launcher_icon.py, with a monochrome layer for themed icons
- FlyFunTheme: dynamic colour, light or dark with the system; edge to edge
- material3-adaptive and navigation-suite dependencies, ListDetail helper,
  selected-row highlight on the flight, people and aircraft lists

Still to do in 3a: NavigationSuiteScaffold and wiring ListDetail into the routes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wLwwiewwvrkxjTmXNfwNg
NavigationSuiteScaffold replaces the bottom bar: a bar on phones, a rail on
tablets. ListDetailPaneScaffold lays out the existing back stack, so a tablet
shows the list beside the open flight, person or aircraft, and a phone is
unchanged. Picking another flight from the side list with unsaved edits asks
Save / Discard.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wLwwiewwvrkxjTmXNfwNg
…ions, shortcuts

- Sign in with Apple through the server's web flow, beside Google
- TokenStore encrypts the JWT with its own Keystore AES-GCM key; drops the
  deprecated security-crypto dependency
- Take X-Renewed-Token so sessions roll instead of expiring
- Launcher shortcuts: New flight, Scan passport

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wLwwiewwvrkxjTmXNfwNg
Lists the pilot's weather.flyfun.aero flights with the same account's token
and applies the chosen one's FlightExchange to the new flight: route,
schedule and aircraft (matched by registration, or staged and stored with
the flight). The exchange format and mapping live in :core-logic with tests.

Autorouter import is skipped: its routes are raw ICAO plans, waiting on G4.

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

Flights strings and the generated translations follow.

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

Every user-visible literal in the app moves to res/values/strings*.xml, one
file per area. scripts/android_strings.py writes values-fr/de/es from the
iOS Localizable.xcstrings by matching English text (133 of 350 today; the
rest fall back to English). locales_config.xml offers en/fr/de/es in the
system's per-app language setting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wLwwiewwvrkxjTmXNfwNg
The privacy link, passenger note and Delete all data landed on main while
3d was moving strings into resources, so they were still literals, and the
Delete account text 3d extracted predated main's rewording. Both are now
resources, and the translations are regenerated from the iOS catalogue.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@roznet
roznet force-pushed the claude/confident-mccarthy-v85j9a branch from cf5ff78 to edea0b8 Compare September 27, 2026 05:27
@claude

claude Bot commented Sep 27, 2026

Copy link
Copy Markdown

Reviewed 3a-3d (layout/theme, auth/token storage, FlyFun Weather import, localisation).

Bug: orphaned staged-aircraft FK on a second Weather import (high confidence)

app/android/app/src/main/kotlin/aero/flyfun/forms/ui/flights/FlightsViewModel.kt:443-484 (importWeather)

When a Weather import has a registration not on file, a new AircraftEntity is staged and draft.flight.aircraftId is pointed at it (lines 454-460, 471). If the pilot then imports a second Weather flight into the same draft whose exchange has no registration, registration == null takes the else branch: _stagedAircraft.value is reset to null (line 460), but aircraftId/aircraft are left untouched by the if (registration != null) ... else current.flight.aircraftId guards (lines 471, 479) - so they still point at the now-untracked staged aircraft's id.

At save() (FlightsViewModel.kt:509), _stagedAircraft.value?.takeIf { it.id == draft.flight.aircraftId } is null, so the staged aircraft is never persisted via saveAircraft, yet saveFlight(draft.flight) still writes that dangling aircraftId. FlightEntity declares a Room ForeignKey on aircraftId -> AircraftEntity.id (data/Entities.kt:123-131), so the insert will throw SQLiteConstraintException on Save - a crash reachable purely by importing twice from Weather in the same new-flight draft (second import without a resolvable registration).

Fix: only reset _stagedAircraft.value when registration != null, mirroring the conditional treatment of aircraftId/aircraft right below it.

No other high-confidence issues found; the Keystore TokenStore crypto/concurrency, the rolling-session interceptor split between forms/weather clients, the adaptive/tablet pane and Save-Discard wiring, and the string-resource extraction all checked out correctly against the design notes in designs/future/android-parity.md §8.

…r crashes Save

Importing a Weather flight whose aircraft was not on file staged a new
aircraft and pointed the draft at it. A second import into the same draft
with no registration kept that aircraftId but dropped the staged aircraft,
so Save wrote a flight referencing a row that was never inserted and failed
the foreign key. The staged aircraft now changes only when the import names
one, as the draft's aircraft already did.

Found by the PR 37 review.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@roznet
roznet merged commit db59158 into main Sep 27, 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 3/3: platform and integrations

2 participants