Skip to content

Extract Android host into federated packages and add JNI command transport - #612

Draft
liodali wants to merge 60 commits into
mainfrom
jni_integration
Draft

Extract Android host into federated packages and add JNI command transport#612
liodali wants to merge 60 commits into
mainfrom
jni_integration

Conversation

@liodali

@liodali liodali commented Aug 29, 2026

Copy link
Copy Markdown
Owner

Problem

The Android implementation was bundled directly inside the main plugin, which made it hard to evolve independently and left no clean boundary for alternative command transports like JNI.

Solution

Split Android into a proper federated plugin setup and add an optional JNI command plane.

  • Extract the Android host code out of flutter_osm_plugin into a new endorsed flutter_osm_android package (platform view, map sessions, MethodChannel transport).
  • Add an optional flutter_osm_android_jni package that routes typed map commands through generated JNI bindings into the Kotlin bridge, while MethodChannel keeps ownership of events, acknowledgements, location, permissions, and lifecycle.
  • Move the typed Android transport contract, event codec, and stable ID value types (MarkerId, RoadId, ShapeId, StaticPositionId) into flutter_osm_interface.
  • Add AndroidMapController with an attach-only backend selection (methodChannel, jni, or auto fallback) — commands are never replayed across transports after initialization.
  • Implement camera, marker, overlay (shapes, roads, static positions), tile, layer-visibility, and location commands across the session and both transports.
  • Add lifecycle-aware location updates via a resumable subscription, per-view request codes, and event coalescing/throttling.
  • Rework release tooling around a shared package registry so interface, web, android, jni, and root packages are validated, versioned, and published in dependency order.

Testing

  • Dart unit tests for the transport contract, event codec/decoder hardening, and controller attach/fallback routing.
  • Kotlin unit tests for the map session registry, typed command set, location request codes, event coalescer, and resumable location subscription.
  • Android integration tests covering attach policy, JNI smoke probe, multiple map sessions, Phase 3 camera/marker commands, Phase 4 overlays, and Phase 5 location lifecycle across pause/resume.
  • A JNI vs MethodChannel benchmark harness for Phase 4 commands.
  • Python tests for the updated release and version tooling.

liodali and others added 30 commits August 24, 2026 02:05
Introduce MarkerId, AndroidMapException and sealed AndroidMapEvent
hierarchy plus AndroidMapPlatform/AndroidMapBackend contract. Barrier
file re-exports the new domain so web/iOS imports stay free of JNI types.
Validate MarkerId equality, AndroidMapException context and typed event
payload retention without importing any platform transport.
Move dispose onto IBaseOSMController, make BaseMapController timer nullable
and keep legacy initialization on the legacy path. Typed Android controllers
own their lifecycle via AndroidMapPlatform instead.
Add AndroidMapTransport contract and factory plus JNI/MethodChannel
transports. Phase 2 keeps JNI in capability-probe mode so auto can still
select MethodChannel before any stateful command is replayed.
Introduce opt-in controller covering attach, activeBackend/ready/events,
single-attach guard, view-filtered event propagation, disposal during
attach, and auto fallback restricted to JNI attach before initialization.
Initialization errors are terminal and never replayed via fallback.
…arrier

Re-export Android typed domain and transports from lib/android.dart and
align MapController/OSM disposal to the IBaseOSMController contract so the
typed path does not depend on a concrete controller cast.
Handle AndroidMapPlatform in MobileOsmFlutter, forward typed events to
controller/observer callbacks, guard non-Android attach, and make disposal
idempotent for the transport event subscription.
…harness

Cover attach/ready/event filtering, disposal/fallback semantics and
non-Android guard. Add integration harness asserting MethodChannel active
backend and readiness delivery over OSMFlutter.
…rations

Surface initialize, setZoom, moveTo, and setRotation as Boolean returns so
the JNI bridge can detect command rejection. Add emitAcknowledgement,
emitError, emitMarkerTap, and emitReady helpers for the typed event
contract. Add getZoomSnapshot so JNI queries from off the main thread
stay thread-safe.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…apter

Add a TypedMapCommand enum so the typed Android controller can dispatch
android#camera#rotation and android#marker#add/remove without colliding
with the legacy method set. Forward unknown methods to notImplemented
once both legacy and typed lookups miss.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…MapSession

Implement initialize, setZoom, moveTo, setRotation, and addMarker through
the Mapscore camera. Emit acknowledgements and errors through the
existing per-view event sink using the android#event envelope. Track a
volatile zoomSnapshot so JNI queries stay thread-safe and stable marker
identity is preserved through receiveGeoPoint taps. Wire the typed
dispatch path into handleLegacyChannelCommands.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Add a companion resolve(viewId) that returns a MapSession only when a
single Flutter engine exposes it, failing closed when two engines happen
to share an overlapping view ID. Expose and hide registries from the
factory lifecycle so disposing a factory withdraws its sessions from
JNI lookup.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Queue every JNI mutation on Android's main thread, then acknowledge it
through the per-view event sink once Mapscore accepts or rejects. attach
selects a session before any state-changing command; close withdraws
it. Each command resolves the session lazily to defend against disposal
between queueing and execution.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…contract

Verify resolve returns the only exposed session, fails closed when two
engines expose the same view ID, and clears when hide is called. Update
the FakeMapSession to honour the expanded interface, including the new
emit helpers and Boolean return values.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Add jnigen bindings for attach, initialize, moveTo, setZoom, getZoom,
setRotation, addMarker, removeMarker, and close on OsmAndroidBridge.
These mirror the Kotlin façade so the Dart transport can call straight
through the JVM with one native hop.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…ker commands

Add moveTo, setZoom, getZoom, setRotation, addMarker, and removeMarker
to the typed transport. Document that stateful commands are never
replayed through another transport after initialization begins, so
attach-only fallback stays safe.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…pTransport

Implement moveTo, setZoom, getZoom, setRotation, addMarker, and
removeMarker against the existing per-view channel with the matching
legacy method names. Centralize error handling through _invokeVoid and
_invokeValue so PlatformException and MissingPluginException map to the
typed AndroidMapException.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…owledgements

Replace the Phase 2 capability gate with a real command transport that
queues every mutation through jnigen and waits for the matching
android#event acknowledgement before completing the Dart future. Pull
shared event decoding into android_event_decoder.dart so the JNI and
MethodChannel transports stay in sync on the envelope. Failed
acknowledgements and timeouts surface as typed AndroidMapException and
are never replayed through MethodChannel.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…ontroller

Expose AndroidMapCamera (moveTo, setZoom, getZoom, setRotation) and
AndroidMapMarkers (add, remove) on the controller. Each method awaits
ready before dispatching and routes through whichever transport won
attach, so the same Dart API works on both the JNI and MethodChannel
backends. Issue stable MarkerIds per controller so removeMarker can
target the right native pin.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Verify the typed command slice goes through the attached transport,
records the issued marker IDs, and rejects duplicate IDs before any
command reaches the backend. Add an event decoder test that decodes the
ack and error envelopes and rejects malformed payloads.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Replace the Phase 2 attach harness with the JNI-ready slice that runs
camera move/zoom/rotation and a stable-ID marker add/remove through the
JNI backend. Add a second view isolation test that proves commands stay
routed to the right platform view even when both share a marker ID.
Keep a methodChannel fallback test so the typed slice stays available
when JNI is unavailable.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Bump objectVersion to 54, switch the thin script to embed_and_thin, drop
the stale App.framework and Flutter.framework references, and raise the
iOS deployment target to 15.0. Update the shared scheme to match the
new project upgrade version.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…ypes

Introduce value-typed overlay identifiers alongside MarkerId so the typed
Android controller can issue stable IDs for road geometry, polygon shapes,
and static-position groups. Reject empty values through an assertion
since the JNI side uses them as native keys.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…e commands

Extend the session contract with bulk add/update/remove for markers,
circle and rectangle shape overlays, static-position groups, road
geometry with options, raster/vector tile sources, and overlay-layer
visibility. Each new method returns Boolean so the bridge can detect
rejection from Mapscore without ambiguity.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
liodali and others added 30 commits August 27, 2026 10:22
…dapter

Add the Phase 4 method names to TypedMapCommand so the per-view MethodChannel
adapter can dispatch bulk marker updates, shape overlays, static-position
groups, road geometry, tile sources, and overlay visibility alongside
the existing camera and marker commands. Forward unknown methods to
notImplemented once both lookups miss.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…isibility

Implement bulk addMarkers/updateMarkerIcon/removeMarkers, circle and
rectangle shape overlays through the polygon layer, static-position
groups through the static icon layer, road geometry through the line
layer, raster/vector tile configuration, and overlay-layer visibility
across every overlay layer. Return Boolean acceptance so the JNI bridge
can detect rejection. Validate geometry, lat/lon, and zoom ranges before
mutating Mapscore state. Clear all overlay state on dispose so a recycled
session cannot leak markers, shapes, or roads.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Add bulk addMarkers/updateMarkerIcon/removeMarkers, circle and rectangle
shape overlays, static-position groups, road geometry with options,
raster/vector tile sources, overlay-layer visibility, and a resetTile
entry point through the same main-thread dispatch path as Phase 3.
Empty API key/value strings are normalised to null before reaching
Mapscore so unset credentials do not corrupt the layer cache.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Verify bulk marker, shape, road, and overlay-visibility calls route
through MapSessionRegistry to one session only. Confirm the Phase 4
typed method names are accepted by TypedMapCommand and stay separate
from LegacyMapCommand lookups.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Regenerate jnigen bindings covering bulk addMarkers, updateMarkerIcon,
removeMarkers, circle and rectangle shape overlays, static-position
groups, road geometry, raster/vector tile sources, overlay visibility,
and resetTile. The addMarker signature now accepts an optional icon
ByteArray; native string arrays pass through JArray<JString>.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
… and tiles

Add bulk marker commands (addAll/updateIcon/removeAll), circle and
rectangle shape overlays, static-position groups, road geometry with
options, raster/vector tile configuration, and overlay-layer visibility
to the typed transport contract. Document that the icon argument is
encoded image bytes such as PNG.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…MapTransport

Implement bulk marker, shape, static-position, road, tile, and overlay
visibility commands over the existing per-view channel. Encode icon
bytes inline, encode road option colors as signed ARGB, and use
android#tile#set for both raster and vector sources. Reuse the existing
_invokeVoid helper so every new path benefits from the typed
AndroidMapException mapping.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Implement bulk addMarkers, updateMarkerIcon, removeMarkers, circle and
rectangle shapes, static-position groups, road geometry with options,
raster/vector tile sources, overlay visibility, and resetTile through
the JNI bridge. Encode icon bytes through JByteArray, string collections
through JArray<JString>, and coordinates through JDoubleArray. Release
every native reference after the call so JNI references do not leak
when commands reject.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
…oidMapController

Expose AndroidMapShapes (circle/rectangle add/remove/clear),
AndroidMapStaticPositions (set/remove), AndroidMapRoads (draw/remove/clear),
and AndroidMapLayers (setTile/setOverlaysVisible) on the controller.
Each method awaits ready, routes through whichever transport won
attach, issues stable IDs per controller, and validates geometry so
failures surface as AndroidMapException before any native call. Re-export
the new overlay types through android.dart.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Verify that bulk markers, shapes, static positions, roads, tile
configuration, and overlay visibility route through the attached
transport on either backend. Reject duplicate shape IDs, invalid
geometry, and empty position lists before any command reaches the
backend. Add a MethodChannelAndroidMapTransport test that asserts the
serialised envelopes for marker, shape, road, and tile commands.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Drive the JNI backend through bulk addAll/updateIcon, shape overlays,
static-position groups with icon bytes, road geometry with options,
overlay-layer visibility toggles, and raster tile configuration. Verify
every Phase 4 operation reaches Mapscore by collecting the
acknowledgement event names. Add bulk marker and overlay commands to
the fallback test so the typed slice stays available on MethodChannel.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Drive bulk markers, shape overlays, static positions, road geometry,
and overlay visibility through both JNI and MethodChannel backends in
the same row so the timings compare equal payloads under one tester.
Add the missing integration_test_driver so `flutter drive` can pick up
the Phase 4 benchmark alongside the controller attach suite.

Co-authored-by: CommandCodeBot <noreply@commandcode.ai>
Generated by a tool misparsing a CLI flag as a path. Not part of the
plugin source.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Coalesces region events to 100ms and location events to 1s, emitting
the first event immediately and trailing events with the newest
payload. Close cancels all pending tasks.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…updates

Separates caller intent (isRequested) from Activity visibility
(isResumed). Updates stop on pause and resume only when explicitly
requested. A failed start clears requested state. Close rejects
further starts.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Adds SHOW_CURRENT_LOCATION, GET_CURRENT_LOCATION,
START/STOP_LOCATION_UPDATES, and START/STOP_LOCATION_TRACKING to
TypedMapCommand so both JNI and MethodChannel backends share the
same typed command set.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…tion

Replaces the coroutine-based currentUserPosition with a main-thread
pending-result model. Adds a 15s timeout, duplicate-request rejection,
and cancellation on pause, stop, destroy, and activity detach. Location
subscriptions now use ResumableLocationSubscription so updates pause
and resume with caller intent.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Routes region/location events through MapEventCoalescer. Replaces the
leaky resultFlutter/skipCheckLocation flags with typed pending-result
fields. Extracts per-view activity request codes into
LocationActivityRequestCodes so simultaneous maps cannot consume each
others settings results. Cancels pending requests on activity detach,
pause, and disposal.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Typed event for foreground user-location updates delivered through the
MethodChannel event plane. Retains the legacy receiveUserLocation
callback alongside the typed path.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Wraps the decoder in try/catch so malformed native events are dropped
instead of failing the MethodChannel handler. Adds explicit type
checks for envelope fields, marker taps, and user-location payloads.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Adds showCurrentLocation, getCurrentLocation, start/stopLocationUpdates,
and start/stopLocationTracking to the transport interface. Both JNI and
MethodChannel implementations route these through the MethodChannel
command plane. Pending invocations are tracked and cancelled with
transport_closed on close.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Exposes showCurrentLocation, getCurrentLocation, start/stopUpdates, and
start/stopTracking on AndroidMapController. Foreground permission is
requested via an injectable AndroidLocationPermissionRequester before
acquisition commands; stop commands skip the permission request.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Adds a JNI integration test exercising the full location command set
and user-location event delivery. Adds a lifecycle harness that
verifies location subscriptions pause and resume with the host
Activity. Expands the MethodChannel fallback path to cover all
Phase 5 location commands.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Decouple the JNI runtime from the core plugin so it ships as an optional
federated add-on rather than a hard dependency. The AndroidMapTransport
contract and event codec move into flutter_osm_interface, the main plugin
now bundles only the MethodChannel transport, and the JNI bindings,
probe, transport, and generator live in the new flutter_osm_android_jni
package. Drop jni/jnigen from the root pubspec and update docs, tests,
and the example accordingly.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Extract the Android host implementation (native Kotlin + MethodChannel
transport) into a new endorsed flutter_osm_android package that
implements flutter_osm_interface. This is the first step of the
federated plugin extraction: the new package owns the platform view,
map sessions, JNI bridge, and MethodChannel transport, while the root
plugin will endorse it as the default Android implementation.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Centralize the five-package registry and dependency graph in
release_packages.py and refactor update_versions.py,
check_pubspec_release.py, and pre_release.py onto the shared helpers.
Add dependency-aware modes (interface/web/android/jni/osm/all), robust
pubspec parsing, cycle detection, pub.dev checks with timeouts, and
sequential pre-release tag pushes. Add fixture tests for all four
scripts.

Update flutter_osm_android_jni to depend on the new
flutter_osm_android host package via path dependency.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Wire the root plugin to endorse flutter_osm_android as the default
Android implementation, update android_map_controller to import the
transport from the new package, and refresh AGENTS.md to document the
five-package federated structure. Ignore the old_android rollback
backup directory.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The Android Gradle setup, Kotlin sources, assets, resources, tests, and
the bundled MethodChannel transport are removed so the main plugin is
transport-agnostic. The default Android transport factory now defers to
the optional flutter_osm_android_jni package for every Android backend.
Add architecture diagram, when-to-use guidance, exported API table,
fallback semantics, binding regeneration notes, platform support
matrix, and manual publish instructions.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The publish job already removes the web and interface plugin directories before
formatting and publishing. The new flutter_osm_android and flutter_osm_android_jni
packages (added for JNI integration) must be cleaned up as well so they aren't
included in the published artifacts.
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.

1 participant