feat(location): Kotlin Multiplatform geolocation module - #726
Draft
kdroidFilter wants to merge 3 commits into
Draft
kdroidFilter wants to merge 3 commits into
kdroidFilter wants to merge 3 commits into
Conversation
Desktop through a Rust JNI bridge (Windows Geolocator, macOS Core Location, Linux XDG portal / GeoClue), Android LocationManager, iOS Core Location and the browser Geolocation API for js / wasmJs, behind one LocationProvider. nucleus.native-module now wires the jvm() target of multiplatform modules, and detekt covers their source sets.
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
nucleus.location, a Kotlin Multiplatform geolocation module modelled on robius-location. Rust is used on desktop only; every other target talks to the platform from Kotlin.Windows.Devices.Geolocation.Geolocator(Rust JNI bridge,windows-rs)objc2) on a dedicatedCFRunLoopthreadzbus, no libdbus)LocationManager— fused provider on API 31+, GPS / network below. No Play servicesiosArm64,iosSimulatorArm64)js,wasmJs)navigator.geolocation+ Permissions API)Documentation
Installation
dependencies { implementation("dev.nucleusframework:nucleus.location:<version>") }Usage
Both ask for authorization when it has not been determined yet, and throw
LocationException(LocationError.AuthorizationDenied)when it is refused. To choose when the prompt appears — or to ask for background access — call it first:GeolocationimplementsLocationProvider; depend on the interface to substitute a fake in tests.API
isAvailableauthorization()LocationAuthorization(NotDetermined,Denied,Restricted,Foreground,Background), no promptrequestAuthorization(access, accuracy)currentLocation(accuracy, maxAge = ZERO, timeout = 30.seconds)locationUpdates(accuracy, minInterval = 1.seconds, minDistanceMeters = 0.0)Flow<Location>Location:latitude,longitude,altitude?,horizontalAccuracy?,verticalAccuracy?,bearing?,speed?,timestampMillis,isCached. Optional values arenullwhen the platform did not measure them.LocationError:AuthorizationDenied,TemporarilyUnavailable,PermanentlyUnavailable,Network,Timeout,Unknown.Semantics
currentLocation(maxAge = Duration.ZERO)(the default) always measures a new fix. A positivemaxAgelets the platform answer from its cache (Location.isCached), which is instant.timeoutbounds the whole request and fails withLocationError.Timeout.locationUpdatesis cold: each collector opens its own platform session, stopped when the collection ends. Transient outages (TemporarilyUnavailable,Network) are ridden out; the flow fails onAuthorizationDenied/PermanentlyUnavailable.minIntervalandminDistanceMetersare hints the platform may not honour exactly.Platform setup
Android
Declare the permissions you use — the library declares none, since which ones an app holds is its own decision:
The prompt needs a resumed activity; the library tracks it through a
ContentProvidermerged into the app manifest (noandroidx.startupdependency). Removing that provider disables the library. On Android 12+ a precise request shows the system's precise / approximate choice in one dialog; background access is requested separately after a foreground grant, as the platform requires.iOS and macOS
Core Location only prompts an app whose
Info.plistexplains why:NSLocationWhenInUseUsageDescription(plusNSLocationAlwaysAndWhenInUseUsageDescriptionfor background; macOS also readsNSLocationUsageDescription). With the Nucleus plugin:nucleus.application { nativeDistributions { macOS { infoPlist { extraKeysRawXml = """ <key>NSLocationUsageDescription</key> <string>Shows the weather where you are.</string> <key>NSLocationWhenInUseUsageDescription</key> <string>Shows the weather where you are.</string> """.trimIndent() } } } }An unpackaged run (
./gradlew run, the IDE) has no suchInfo.plistand is never authorized on macOS — userunDistributableor a packaged build. A sandboxed (App Store) build also needs thecom.apple.security.personal-information.locationentitlement.Windows
No manifest capability. Access follows Settings → Privacy & security → Location → Let desktop apps access your location; Windows 11 24H2+ prompts on the first request.
authorization()reportsNotDetermineduntil a request in the current process has been answered — WinRT cannot tell "allowed" from "never asked" without asking.Linux
The XDG Location portal is used whenever present, sandboxed or not: it shows the desktop's consent dialog and needs nothing from the app. Without a portal (or without its Location interface), an unsandboxed app talks to GeoClue 2 directly, attributing the request to its
.desktopid (the installed desktop entry, elseNucleusApp.appId). A portal refusal is final — GeoClue is never tried behind the user's back.authorization()reportsNotDetermineduntil a request in this process succeeded.Web
Secure context only (HTTPS or
localhost). Browsers prompt only when a position is requested, sorequestAuthorizationasks for one.authorization()reads the Permissions API state, queried on first use and kept current by itschangeevent — the very first call may still reportNotDetermined.watchPositionhas no interval or distance options:minInterval/minDistanceMetersare ignored and the browser reports when the position changes.LocationAccess.Backgroundmeans nothing to a page.Native library
libnucleus_locationis built fromlocation/src/main/native(Rust crate;cargo+rustuprequired) bybuildNativeWindows/buildNativeMacOs/buildNativeLinuxon the matching host and ships in the JVM artifact undernucleus/native/<platform>-<arch>/. GraalVM reachability metadata is included.Architecture
commonMainholds the API and all the semantics:BackendLocationProviderimplements the implicit authorization request, cached-fix acceptance bymaxAge, timeouts and the flow once, on top of an internalLocationBackend.LocationBackend: authorization plus sessions — a stream of fixes and errors betweenstartandstop.jvmMain+src/main/native): sessions are identified by Kotlin-allocated ids; the native side reserves a slot before starting, so a stop racing the start (a one-shot can finish beforenativeStartreturns) drops the session instead of leaking it. Pending JNI exceptions go toJniExceptionReporter, never a silent clear.CLLocationManagerlives on onenucleus-locationthread running aCFRunLoop(Core Location delivers on the creating thread's run loop; the JVM main thread is not ours to rely on). Requests made while authorization is undetermined are deferred until the answer, as in robius-location.checkSelfPermissionon the activity's pause → resume (the dialog is an activity of its own); a request answered without a dialog ("don't ask again") is detected by the absence of a pause.js()-bodied helpers passing primitives and lambdas, so a singlewebMainsource compiles for Kotlin/JS and Kotlin/Wasm.Build changes outside the module
nucleus.native-modulesupports KMP modules: libraries stay undersrc/main/resources/nucleus/native(the path every CI workflow caches / uploads / verifies) and are copied into thejvmtarget only viajvmProcessResources(matched by name — buildSrc needs no KGP on its classpath), together with the native-libraries manifest.detekttask thatcheckruns only looked atsrc/main/kotlin(NO-SOURCE on a KMP module); it now covers every*Main/kotlinsource set so the KDoc rules apply.cleannow comes from thebaseplugin: Kotlin/JS applies the lifecycle plugin to the root project, which rejects a hand-registeredclean. Behaviour unchanged (deletes the rootbuild/).kotlin-js-store/— yarn locks for JS and Wasm (kotlinUpgradeYarnLock/kotlinWasmUpgradeYarnLock).com.android.kotlin.multiplatform.libraryplugin,kotlinx-coroutines-test.gradle.properties:kotlin.native.ignoreDisabledTargets=true— iOS klibs cross-compile on any host (Kotlin 2.4); only their tests need macOS.build-natives.yamlbuild steps + verify lists;pre-merge.yaml/publish-maven.yamlexpected natives.examples/location-demo(console, desktop),examples/location-web-demo(jsBrowserDevelopmentRun).jvmAPI only (location/api/jvm/location.api); the other targets expose the same common API.Test plan
cargo checkclean (0 warnings) forx86_64-pc-windows-msvc,aarch64-apple-darwin,x86_64-unknown-linux-gnu; Windows release DLL builds:location:check— ktlint, detekt, apiCheck; common tests 9/9 on JVM, JS (Node) and Wasm (Node)compileKotlinIosArm64/compileKotlinIosSimulatorArm64/compileAndroidMain/compileKotlinJs/compileKotlinWasmJsexamples/location-demo—NotDetermined→Foreground, cached one-shot fix, 3 live updates ~1 s apartDenied→LocationException(AuthorizationDenied))nucleus.native-modulestill builds a JVM JNI module (:fs-watcher:jar);location-jvm.jarcarries both Windows DLLs + the manifestlocation-demo(runDistributable) — prompt, one-shot, updates