
Headless background GPS listener with offline caching, automatic sending, retry and network monitoring; position filtering, battery reporting, protocol formatting, permission helpers and callback API.
A headless (no UI) Kotlin Multiplatform library that provides background GPS location listening with offline caching on Android and iOS. Ported from the Traccar GPS tracking clients for both platforms.
dependencies {
implementation("io.github.saggeldi:yedu-kmp-gps-listener:0.0.4")
}dependencies {
implementation 'io.github.saggeldi:yedu-kmp-gps-listener:0.0.4'
}Use GpsFactory to create everything from shared common code. Android requires a one-time initialize(context) call; iOS works out of the box.
Android - call once in Application or Service:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
GpsFactory.initialize(this)
}
}Then from common code (works on both platforms):
// GPS-only mode
val tracker = GpsFactory.createGpsTracker(myListener)
tracker.start(config)
// Full pipeline mode (GPS + caching + sending + retry)
val controller = GpsFactory.createTrackingController(
serverUrl = "https://your-server.com:5055",
buffer = true,
listener = myControllerListener
)
controller.start(config)For full control, create platform-specific components directly:
// Android
val tracker = GpsTracker(AndroidLocationProvider(context), listener)
// iOS (Swift)
let tracker = GpsTracker(locationProvider: IosLocationProvider(), listener: listener)Both modes can coexist. Use GpsFactory for convenience, or construct manually when you need custom implementations.
GpsTracker - GPS-only listener. You receive positions and decide what to do with them.TrackingController - Full pipeline. GPS + offline caching + network monitoring + HTTP sending + retry logic. Handles everything automatically.Use this when you want full control over what happens with positions.
class MyTrackingService : Service() {
private lateinit var gpsTracker: GpsTracker
override fun onCreate() {
super.onCreate()
startForeground(1, createNotification())
// Option A: Common factory (requires GpsFactory.initialize(context) in Application)
gpsTracker = GpsFactory.createGpsTracker(myListener)
// Option B: Direct platform constructor
// gpsTracker = GpsTracker(AndroidLocationProvider(this), myListener)
gpsTracker.start(GpsConfig(
deviceId = "my-device-123",
interval = 300,
accuracy = LocationAccuracy.HIGH
))
}
private val myListener = object : GpsTrackerListener {
override fun onPositionUpdate(position: Position) {
println("${position.latitude}, ${position.longitude}")
println("Battery: ${position.battery.level}%")
}
override fun onError(error: String) {
Log.e("GPS", error)
}
override fun onStatusChange(status: TrackerStatus) {
Log.d("GPS", "Tracker: $status")
}
}
override fun onDestroy() {
gpsTracker.stop()
super.onDestroy()
}
override fun onBind(intent: Intent?) = null
}import YeduKmpGpsListener
class AppDelegate: UIResponder, UIApplicationDelegate {
var gpsTracker: GpsTracker?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIDevice.current.isBatteryMonitoringEnabled = true
let tracker = GpsTracker(
locationProvider: IosLocationProvider(),
listener: MyGpsListener()
)
tracker.start(config: GpsConfig(
deviceId: "my-device-123",
interval: 300,
distance: 0.0,
angle: 0.0,
accuracy: .medium
))
gpsTracker = tracker
return true
}
}
class MyGpsListener: GpsTrackerListener {
func onPositionUpdate(position: Position) {
// Handle position however you want
}
func onError(error: String) {
print("GPS Error: \(error)")
}
func onStatusChange(status: TrackerStatus) {
print("Tracker: \(status)")
}
}Use this when you want automatic offline caching, HTTP sending, and retry - like the original Traccar clients.
The state machine works as follows:
GPS position received
-> write to local database
-> if online: read from database -> send to server -> delete from database -> read next
-> if offline: positions accumulate in database
-> on network reconnect: read -> send -> delete -> read next
-> on send failure: retry after 30 seconds
class MyTrackingService : Service() {
private lateinit var controller: TrackingController
private var wakeLock: PowerManager.WakeLock? = null
override fun onCreate() {
super.onCreate()
startForeground(1, createNotification())
// Acquire wake lock for reliable background tracking
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::GPS")
wakeLock?.acquire()
// Option A: Common factory (requires GpsFactory.initialize(context) in Application)
controller = GpsFactory.createTrackingController(
serverUrl = "https://your-server.com:5055",
buffer = true,
listener = controllerListener
)
// Option B: Direct platform constructors
// controller = TrackingController(
// locationProvider = AndroidLocationProvider(this),
// positionStore = AndroidPositionStore(this),
// positionSender = AndroidPositionSender(),
// networkMonitor = AndroidNetworkMonitor(this),
// retryScheduler = AndroidRetryScheduler(),
// serverUrl = "https://your-server.com:5055",
// buffer = true,
// listener = controllerListener
// )
controller.start(GpsConfig(
deviceId = "my-device-123",
interval = 300,
accuracy = LocationAccuracy.HIGH
))
}
private val controllerListener = object : TrackingControllerListener {
override fun onPositionUpdate(position: Position) {
Log.d("GPS", "New: ${position.latitude}, ${position.longitude}")
}
override fun onPositionSent(position: Position) {
Log.d("GPS", "Sent to server")
}
override fun onSendFailed(position: Position) {
Log.w("GPS", "Send failed, will retry")
}
override fun onError(error: String) {
Log.e("GPS", error)
}
override fun onStatusChange(status: TrackerStatus) {
Log.d("GPS", "Tracker: $status")
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
override fun onDestroy() {
controller.stop()
if (wakeLock?.isHeld == true) wakeLock?.release()
super.onDestroy()
}
override fun onBind(intent: Intent?) = null
}import YeduKmpGpsListener
class AppDelegate: UIResponder, UIApplicationDelegate {
var controller: TrackingController?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIDevice.current.isBatteryMonitoringEnabled = true
let ctrl = TrackingController(
locationProvider: IosLocationProvider(),
positionStore: IosPositionStore(),
positionSender: IosPositionSender(),
networkMonitor: IosNetworkMonitor(),
retryScheduler: IosRetryScheduler(),
serverUrl: "https://your-server.com:5055",
buffer: true,
listener: nil // or provide a TrackingControllerListener
)
ctrl.start(config: GpsConfig(
deviceId: "my-device-123",
interval: 300,
distance: 0.0,
angle: 0.0,
accuracy: .high
))
controller = ctrl
return true
}
}// Update config without restarting
tracker.updateConfig(tracker.currentConfig()!!.copy(interval = 60))
// Change accuracy
tracker.updateConfig(tracker.currentConfig()!!.copy(accuracy = LocationAccuracy.HIGH))
// Request a single immediate position
tracker.requestSingleLocation()
// Check status
if (tracker.isTracking()) { /* ... */ }
// Stop
tracker.stop()| Type | Name | Description |
|---|---|---|
| Object | GpsFactory |
expect/actual factory. Call initialize(context) on Android first |
| Extension | GpsFactory.createGpsTracker(listener) |
Create GPS-only tracker from common code |
| Extension | GpsFactory.createTrackingController(serverUrl, ...) |
Create full pipeline from common code |
| Method | GpsFactory.createLocationPermissionHelper() |
Create permission/location-state helper |
| Type | Name | Description |
|---|---|---|
| Class | GpsTracker |
GPS-only listener. start(), stop(), updateConfig(), requestSingleLocation()
|
| Data class | GpsConfig |
Config: deviceId, interval, distance, angle, accuracy
|
| Interface | GpsTrackerListener |
Callbacks: onPositionUpdate(), onError(), onStatusChange()
|
| Data class | Position |
GPS position with battery info |
| Data class | BatteryStatus |
Battery level (0-100) and charging state |
| Enum | LocationAccuracy |
HIGH, MEDIUM, LOW
|
| Enum | TrackerStatus |
STARTED, STOPPED
|
| Interface | LocationPermissionHelper |
Check/request permissions, query GPS state, open settings |
| Enum | PermissionStatus |
GRANTED, DENIED, NOT_DETERMINED, RESTRICTED
|
| Type | Name | Description |
|---|---|---|
| Class | TrackingController |
Full pipeline: GPS + caching + sending + retry |
| Interface | TrackingControllerListener |
Callbacks: position events + send/fail events |
| Object | ProtocolFormatter |
OsmAnd/Traccar URL formatting |
| Interface | PositionStore |
Local position database |
| Interface | PositionSender |
HTTP position sending |
| Interface | NetworkMonitor |
Network connectivity monitoring |
| Interface | RetryScheduler |
Delayed retry execution |
| Component | Android | iOS |
|---|---|---|
| Location Provider | AndroidLocationProvider(context) |
IosLocationProvider() |
| Battery Provider | AndroidBatteryProvider(context) |
IosBatteryProvider() |
| Position Store | AndroidPositionStore(context) |
IosPositionStore() |
| Position Sender | AndroidPositionSender() |
IosPositionSender() |
| Network Monitor | AndroidNetworkMonitor(context) |
IosNetworkMonitor() |
| Retry Scheduler | AndroidRetryScheduler() |
IosRetryScheduler() |
| Permission Helper | AndroidLocationPermissionHelper(context) |
IosLocationPermissionHelper() |
| Field | Type | Description |
|---|---|---|
id |
Long |
Database ID (0 if not persisted) |
deviceId |
String |
Device identifier |
time |
Instant |
Timestamp (kotlinx-datetime) |
latitude |
Double |
Latitude in degrees |
longitude |
Double |
Longitude in degrees |
altitude |
Double |
Altitude in meters |
speed |
Double |
Speed in knots (converted from m/s) |
course |
Double |
Bearing 0-360 degrees |
accuracy |
Double |
Horizontal accuracy in meters |
battery |
BatteryStatus |
Battery level and charging state |
mock |
Boolean |
Mock location flag (Android only) |
Positions are formatted as OsmAnd/Traccar-compatible URL query parameters:
https://server:5055?id=DEVICE_ID×tamp=EPOCH&lat=LAT&lon=LON&speed=SPEED&bearing=COURSE&altitude=ALT&accuracy=ACC&batt=LEVEL&charge=true&mock=true
Use LocationPermissionHelper to check/request location permissions and query GPS state from common code.
val permissionHelper = GpsFactory.createLocationPermissionHelper()
// Check current permission status
when (permissionHelper.checkPermissionStatus()) {
PermissionStatus.GRANTED -> println("Permission granted")
PermissionStatus.DENIED -> println("Permission denied")
PermissionStatus.NOT_DETERMINED -> println("Permission not yet requested")
PermissionStatus.RESTRICTED -> println("Permission restricted by policy")
}
// Check background permission specifically
if (permissionHelper.hasBackgroundPermission()) { /* always-on tracking OK */ }
// Check if device GPS is enabled
if (!permissionHelper.isLocationEnabled()) {
permissionHelper.openLocationSettings() // prompt user to enable GPS
}permissionHelper.requestPermission(background = false) { status ->
println("Result: $status")
}
// Request background (always) permission
permissionHelper.requestPermission(background = true) { status ->
println("Background result: $status")
}permissionHelper.openLocationSettings() // device GPS settings
permissionHelper.openAppSettings() // app permission settingsOn Android, permission requesting requires an Activity. Without calling setActivity(), requestPermission() returns the current status without showing a dialog.
// In your Activity
val helper = GpsFactory.createLocationPermissionHelper()
as AndroidLocationPermissionHelper
helper.setActivity(this)
helper.requestPermission(background = true) { status ->
// handle result
}
// Relay the result from your Activity
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
helper.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
// Clear when Activity is destroyed
override fun onDestroy() {
helper.clearActivity()
super.onDestroy()
}<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />Register your service:
<service
android:name=".MyTrackingService"
android:foregroundServiceType="location"
android:exported="false" /><key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Required for background GPS tracking</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Required for background GPS tracking</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Required for GPS tracking</string>Filtering is done in shared Kotlin code (GpsTracker.shouldAcceptPosition), not in native. Raw GPS updates from the platform are filtered by:
interval seconds have passed since last positiondistance meters (0 = disabled)angle degrees (0 = disabled)The first position is always accepted. If any condition is met, the position passes.
LocationManager with GPS/Network/Passive providersFOREGROUND_SERVICE_LOCATION typePARTIAL_WAKE_LOCK for reliable trackingisMock / isFromMockProvider
START_STICKY to survive killsCLLocationManager with allowsBackgroundLocationUpdates = true
pausesLocationUpdatesAutomatically = false for continuous trackingstartMonitoringSignificantLocationChanges() for reliable background wake-upsrequestAlwaysAuthorization() for background permissionlocation to UIBackgroundModes in Info.plistUIDevice.currentDevice
# Build all targets
./gradlew :library:build
# Run all tests
./gradlew :library:allTests
# Run Android host tests only
./gradlew :library:testAndroidHostTest
# Run iOS simulator tests
./gradlew :library:iosSimulatorArm64Test
# Publish to Maven Central
./gradlew :library:publishToMavenCentralCopyright 2026 Shageldi Alyyew
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
A headless (no UI) Kotlin Multiplatform library that provides background GPS location listening with offline caching on Android and iOS. Ported from the Traccar GPS tracking clients for both platforms.
dependencies {
implementation("io.github.saggeldi:yedu-kmp-gps-listener:0.0.4")
}dependencies {
implementation 'io.github.saggeldi:yedu-kmp-gps-listener:0.0.4'
}Use GpsFactory to create everything from shared common code. Android requires a one-time initialize(context) call; iOS works out of the box.
Android - call once in Application or Service:
class MyApp : Application() {
override fun onCreate() {
super.onCreate()
GpsFactory.initialize(this)
}
}Then from common code (works on both platforms):
// GPS-only mode
val tracker = GpsFactory.createGpsTracker(myListener)
tracker.start(config)
// Full pipeline mode (GPS + caching + sending + retry)
val controller = GpsFactory.createTrackingController(
serverUrl = "https://your-server.com:5055",
buffer = true,
listener = myControllerListener
)
controller.start(config)For full control, create platform-specific components directly:
// Android
val tracker = GpsTracker(AndroidLocationProvider(context), listener)
// iOS (Swift)
let tracker = GpsTracker(locationProvider: IosLocationProvider(), listener: listener)Both modes can coexist. Use GpsFactory for convenience, or construct manually when you need custom implementations.
GpsTracker - GPS-only listener. You receive positions and decide what to do with them.TrackingController - Full pipeline. GPS + offline caching + network monitoring + HTTP sending + retry logic. Handles everything automatically.Use this when you want full control over what happens with positions.
class MyTrackingService : Service() {
private lateinit var gpsTracker: GpsTracker
override fun onCreate() {
super.onCreate()
startForeground(1, createNotification())
// Option A: Common factory (requires GpsFactory.initialize(context) in Application)
gpsTracker = GpsFactory.createGpsTracker(myListener)
// Option B: Direct platform constructor
// gpsTracker = GpsTracker(AndroidLocationProvider(this), myListener)
gpsTracker.start(GpsConfig(
deviceId = "my-device-123",
interval = 300,
accuracy = LocationAccuracy.HIGH
))
}
private val myListener = object : GpsTrackerListener {
override fun onPositionUpdate(position: Position) {
println("${position.latitude}, ${position.longitude}")
println("Battery: ${position.battery.level}%")
}
override fun onError(error: String) {
Log.e("GPS", error)
}
override fun onStatusChange(status: TrackerStatus) {
Log.d("GPS", "Tracker: $status")
}
}
override fun onDestroy() {
gpsTracker.stop()
super.onDestroy()
}
override fun onBind(intent: Intent?) = null
}import YeduKmpGpsListener
class AppDelegate: UIResponder, UIApplicationDelegate {
var gpsTracker: GpsTracker?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIDevice.current.isBatteryMonitoringEnabled = true
let tracker = GpsTracker(
locationProvider: IosLocationProvider(),
listener: MyGpsListener()
)
tracker.start(config: GpsConfig(
deviceId: "my-device-123",
interval: 300,
distance: 0.0,
angle: 0.0,
accuracy: .medium
))
gpsTracker = tracker
return true
}
}
class MyGpsListener: GpsTrackerListener {
func onPositionUpdate(position: Position) {
// Handle position however you want
}
func onError(error: String) {
print("GPS Error: \(error)")
}
func onStatusChange(status: TrackerStatus) {
print("Tracker: \(status)")
}
}Use this when you want automatic offline caching, HTTP sending, and retry - like the original Traccar clients.
The state machine works as follows:
GPS position received
-> write to local database
-> if online: read from database -> send to server -> delete from database -> read next
-> if offline: positions accumulate in database
-> on network reconnect: read -> send -> delete -> read next
-> on send failure: retry after 30 seconds
class MyTrackingService : Service() {
private lateinit var controller: TrackingController
private var wakeLock: PowerManager.WakeLock? = null
override fun onCreate() {
super.onCreate()
startForeground(1, createNotification())
// Acquire wake lock for reliable background tracking
val powerManager = getSystemService(POWER_SERVICE) as PowerManager
wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::GPS")
wakeLock?.acquire()
// Option A: Common factory (requires GpsFactory.initialize(context) in Application)
controller = GpsFactory.createTrackingController(
serverUrl = "https://your-server.com:5055",
buffer = true,
listener = controllerListener
)
// Option B: Direct platform constructors
// controller = TrackingController(
// locationProvider = AndroidLocationProvider(this),
// positionStore = AndroidPositionStore(this),
// positionSender = AndroidPositionSender(),
// networkMonitor = AndroidNetworkMonitor(this),
// retryScheduler = AndroidRetryScheduler(),
// serverUrl = "https://your-server.com:5055",
// buffer = true,
// listener = controllerListener
// )
controller.start(GpsConfig(
deviceId = "my-device-123",
interval = 300,
accuracy = LocationAccuracy.HIGH
))
}
private val controllerListener = object : TrackingControllerListener {
override fun onPositionUpdate(position: Position) {
Log.d("GPS", "New: ${position.latitude}, ${position.longitude}")
}
override fun onPositionSent(position: Position) {
Log.d("GPS", "Sent to server")
}
override fun onSendFailed(position: Position) {
Log.w("GPS", "Send failed, will retry")
}
override fun onError(error: String) {
Log.e("GPS", error)
}
override fun onStatusChange(status: TrackerStatus) {
Log.d("GPS", "Tracker: $status")
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return START_STICKY
}
override fun onDestroy() {
controller.stop()
if (wakeLock?.isHeld == true) wakeLock?.release()
super.onDestroy()
}
override fun onBind(intent: Intent?) = null
}import YeduKmpGpsListener
class AppDelegate: UIResponder, UIApplicationDelegate {
var controller: TrackingController?
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
UIDevice.current.isBatteryMonitoringEnabled = true
let ctrl = TrackingController(
locationProvider: IosLocationProvider(),
positionStore: IosPositionStore(),
positionSender: IosPositionSender(),
networkMonitor: IosNetworkMonitor(),
retryScheduler: IosRetryScheduler(),
serverUrl: "https://your-server.com:5055",
buffer: true,
listener: nil // or provide a TrackingControllerListener
)
ctrl.start(config: GpsConfig(
deviceId: "my-device-123",
interval: 300,
distance: 0.0,
angle: 0.0,
accuracy: .high
))
controller = ctrl
return true
}
}// Update config without restarting
tracker.updateConfig(tracker.currentConfig()!!.copy(interval = 60))
// Change accuracy
tracker.updateConfig(tracker.currentConfig()!!.copy(accuracy = LocationAccuracy.HIGH))
// Request a single immediate position
tracker.requestSingleLocation()
// Check status
if (tracker.isTracking()) { /* ... */ }
// Stop
tracker.stop()| Type | Name | Description |
|---|---|---|
| Object | GpsFactory |
expect/actual factory. Call initialize(context) on Android first |
| Extension | GpsFactory.createGpsTracker(listener) |
Create GPS-only tracker from common code |
| Extension | GpsFactory.createTrackingController(serverUrl, ...) |
Create full pipeline from common code |
| Method | GpsFactory.createLocationPermissionHelper() |
Create permission/location-state helper |
| Type | Name | Description |
|---|---|---|
| Class | GpsTracker |
GPS-only listener. start(), stop(), updateConfig(), requestSingleLocation()
|
| Data class | GpsConfig |
Config: deviceId, interval, distance, angle, accuracy
|
| Interface | GpsTrackerListener |
Callbacks: onPositionUpdate(), onError(), onStatusChange()
|
| Data class | Position |
GPS position with battery info |
| Data class | BatteryStatus |
Battery level (0-100) and charging state |
| Enum | LocationAccuracy |
HIGH, MEDIUM, LOW
|
| Enum | TrackerStatus |
STARTED, STOPPED
|
| Interface | LocationPermissionHelper |
Check/request permissions, query GPS state, open settings |
| Enum | PermissionStatus |
GRANTED, DENIED, NOT_DETERMINED, RESTRICTED
|
| Type | Name | Description |
|---|---|---|
| Class | TrackingController |
Full pipeline: GPS + caching + sending + retry |
| Interface | TrackingControllerListener |
Callbacks: position events + send/fail events |
| Object | ProtocolFormatter |
OsmAnd/Traccar URL formatting |
| Interface | PositionStore |
Local position database |
| Interface | PositionSender |
HTTP position sending |
| Interface | NetworkMonitor |
Network connectivity monitoring |
| Interface | RetryScheduler |
Delayed retry execution |
| Component | Android | iOS |
|---|---|---|
| Location Provider | AndroidLocationProvider(context) |
IosLocationProvider() |
| Battery Provider | AndroidBatteryProvider(context) |
IosBatteryProvider() |
| Position Store | AndroidPositionStore(context) |
IosPositionStore() |
| Position Sender | AndroidPositionSender() |
IosPositionSender() |
| Network Monitor | AndroidNetworkMonitor(context) |
IosNetworkMonitor() |
| Retry Scheduler | AndroidRetryScheduler() |
IosRetryScheduler() |
| Permission Helper | AndroidLocationPermissionHelper(context) |
IosLocationPermissionHelper() |
| Field | Type | Description |
|---|---|---|
id |
Long |
Database ID (0 if not persisted) |
deviceId |
String |
Device identifier |
time |
Instant |
Timestamp (kotlinx-datetime) |
latitude |
Double |
Latitude in degrees |
longitude |
Double |
Longitude in degrees |
altitude |
Double |
Altitude in meters |
speed |
Double |
Speed in knots (converted from m/s) |
course |
Double |
Bearing 0-360 degrees |
accuracy |
Double |
Horizontal accuracy in meters |
battery |
BatteryStatus |
Battery level and charging state |
mock |
Boolean |
Mock location flag (Android only) |
Positions are formatted as OsmAnd/Traccar-compatible URL query parameters:
https://server:5055?id=DEVICE_ID×tamp=EPOCH&lat=LAT&lon=LON&speed=SPEED&bearing=COURSE&altitude=ALT&accuracy=ACC&batt=LEVEL&charge=true&mock=true
Use LocationPermissionHelper to check/request location permissions and query GPS state from common code.
val permissionHelper = GpsFactory.createLocationPermissionHelper()
// Check current permission status
when (permissionHelper.checkPermissionStatus()) {
PermissionStatus.GRANTED -> println("Permission granted")
PermissionStatus.DENIED -> println("Permission denied")
PermissionStatus.NOT_DETERMINED -> println("Permission not yet requested")
PermissionStatus.RESTRICTED -> println("Permission restricted by policy")
}
// Check background permission specifically
if (permissionHelper.hasBackgroundPermission()) { /* always-on tracking OK */ }
// Check if device GPS is enabled
if (!permissionHelper.isLocationEnabled()) {
permissionHelper.openLocationSettings() // prompt user to enable GPS
}permissionHelper.requestPermission(background = false) { status ->
println("Result: $status")
}
// Request background (always) permission
permissionHelper.requestPermission(background = true) { status ->
println("Background result: $status")
}permissionHelper.openLocationSettings() // device GPS settings
permissionHelper.openAppSettings() // app permission settingsOn Android, permission requesting requires an Activity. Without calling setActivity(), requestPermission() returns the current status without showing a dialog.
// In your Activity
val helper = GpsFactory.createLocationPermissionHelper()
as AndroidLocationPermissionHelper
helper.setActivity(this)
helper.requestPermission(background = true) { status ->
// handle result
}
// Relay the result from your Activity
override fun onRequestPermissionsResult(
requestCode: Int, permissions: Array<String>, grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
helper.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
// Clear when Activity is destroyed
override fun onDestroy() {
helper.clearActivity()
super.onDestroy()
}<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />Register your service:
<service
android:name=".MyTrackingService"
android:foregroundServiceType="location"
android:exported="false" /><key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Required for background GPS tracking</string>
<key>NSLocationAlwaysUsageDescription</key>
<string>Required for background GPS tracking</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Required for GPS tracking</string>Filtering is done in shared Kotlin code (GpsTracker.shouldAcceptPosition), not in native. Raw GPS updates from the platform are filtered by:
interval seconds have passed since last positiondistance meters (0 = disabled)angle degrees (0 = disabled)The first position is always accepted. If any condition is met, the position passes.
LocationManager with GPS/Network/Passive providersFOREGROUND_SERVICE_LOCATION typePARTIAL_WAKE_LOCK for reliable trackingisMock / isFromMockProvider
START_STICKY to survive killsCLLocationManager with allowsBackgroundLocationUpdates = true
pausesLocationUpdatesAutomatically = false for continuous trackingstartMonitoringSignificantLocationChanges() for reliable background wake-upsrequestAlwaysAuthorization() for background permissionlocation to UIBackgroundModes in Info.plistUIDevice.currentDevice
# Build all targets
./gradlew :library:build
# Run all tests
./gradlew :library:allTests
# Run Android host tests only
./gradlew :library:testAndroidHostTest
# Run iOS simulator tests
./gradlew :library:iosSimulatorArm64Test
# Publish to Maven Central
./gradlew :library:publishToMavenCentralCopyright 2026 Shageldi Alyyew
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.