diff --git a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt index 4666a83..65fafa4 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt @@ -427,6 +427,10 @@ class DumonGeolocation : Plugin() { BgPrefs.setBackgroundMinPostIntervalMs(context, v) LogUtils.d("DUMON_GEOLOCATION", "Set background min post interval = ${v} ms") } + call.getBoolean("backgroundUseImuFallback")?.let { + BgPrefs.setBackgroundUseImuFallback(context, it) + LogUtils.d("DUMON_GEOLOCATION", "Set backgroundUseImuFallback = ${it}") + } call.resolve() } diff --git a/android/src/main/java/com/dumon/plugin/geolocation/bg/BackgroundLocationService.kt b/android/src/main/java/com/dumon/plugin/geolocation/bg/BackgroundLocationService.kt index dfc0093..b75e7f8 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/bg/BackgroundLocationService.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/bg/BackgroundLocationService.kt @@ -13,6 +13,8 @@ import androidx.core.content.ContextCompat import com.dumon.plugin.geolocation.gps.GpsStatusManager import com.dumon.plugin.geolocation.gps.GpsTrackingMode import com.dumon.plugin.geolocation.gps.SatelliteStatus +import com.dumon.plugin.geolocation.imu.ImuData +import com.dumon.plugin.geolocation.imu.ImuSensorManager import com.dumon.plugin.geolocation.utils.BgPrefs import com.dumon.plugin.geolocation.utils.LogUtils import com.dumon.plugin.geolocation.utils.AuthStore @@ -27,12 +29,19 @@ import kotlin.math.* class BackgroundLocationService : Service() { private var gpsManager: GpsStatusManager? = null + private var imuManager: ImuSensorManager? = null private var postUrl: String? = null private val postExecutor = Executors.newSingleThreadExecutor() @Volatile private var pendingPostedLat: Double? = null @Volatile private var pendingPostedLon: Double? = null @Volatile private var pendingPostedTs: Long? = null @Volatile private var lastPostAttemptTs: Long = 0L + @Volatile private var latestImu: ImuData? = null + + // Previous fix for derived speed + @Volatile private var prevLat: Double? = null + @Volatile private var prevLon: Double? = null + @Volatile private var prevTs: Long? = null override fun onCreate() { super.onCreate() @@ -56,12 +65,35 @@ class BackgroundLocationService : Service() { postUrl = intent?.getStringExtra(EXTRA_POST_URL) postUrl?.let { BgPrefs.setPostUrl(applicationContext, it) } - // Start GNSS continuous updates; avoid Wi-Fi scans in background for efficiency + // Start GNSS updates; avoid Wi-Fi scans in background for efficiency gpsManager = GpsStatusManager( applicationContext, onSatelliteStatusUpdate = { /* no-op in background by default */ }, onLocationUpdate = { location, isMocked -> if (location.latitude == 0.0 && location.longitude == 0.0) return@GpsStatusManager + // Compute speed with priority: GNSS > IMU > Derived > 0 + val gnssSpeed: Float? = if (location.hasSpeed()) location.speed else null + val imuSnap = latestImu + val imuSpeed: Float? = imuSnap?.speed + val imuAccel: Float? = imuSnap?.acceleration + val imuDir: Float? = imuSnap?.directionRad + + val derivedSpeed: Float? = run { + val pLat = prevLat; val pLon = prevLon; val pTs = prevTs + if (pLat != null && pLon != null && pTs != null) { + val dtSec = (location.time - pTs).toDouble() / 1000.0 + if (dtSec >= 3.0 && dtSec <= 30.0) { + val dMeters = haversineMeters(pLat, pLon, location.latitude, location.longitude) + (dMeters / dtSec).toFloat() + } else null + } else null + } + val finalSpeed: Float = when { + gnssSpeed != null -> gnssSpeed + imuSpeed != null -> imuSpeed + derivedSpeed != null -> derivedSpeed + else -> 0f + } BgPrefs.saveLatestFix( context = applicationContext, latitude = location.latitude, @@ -70,9 +102,9 @@ class BackgroundLocationService : Service() { timestamp = location.time, source = if (isMocked) "MOCK" else "GNSS", isMocked = isMocked, - speed = null, - acceleration = null, - directionRad = null, + speed = finalSpeed, + acceleration = imuAccel, + directionRad = imuDir, ) val endpoint = postUrl ?: BgPrefs.getPostUrl(applicationContext) @@ -128,6 +160,7 @@ class BackgroundLocationService : Service() { latitude = location.latitude, longitude = location.longitude, accuracy = location.accuracy.toDouble(), + speed = finalSpeed.toDouble(), timestamp = location.time, source = if (isMocked) "MOCK" else "GNSS", isMocked = isMocked @@ -142,6 +175,10 @@ class BackgroundLocationService : Service() { } } } + // Update previous for next derived computation + prevLat = location.latitude + prevLon = location.longitude + prevTs = location.time } ) @@ -150,6 +187,14 @@ class BackgroundLocationService : Service() { val interval = BgPrefs.getBackgroundIntervalMs(applicationContext, 5000L) gpsManager?.setPollingInterval(interval) + // Start IMU to provide speed fallback in background (optional) + if (BgPrefs.getBackgroundUseImuFallback(applicationContext, false)) { + imuManager = ImuSensorManager(applicationContext) { data -> + latestImu = data + } + try { imuManager?.start() } catch (_: Exception) {} + } + return START_STICKY } @@ -157,6 +202,8 @@ class BackgroundLocationService : Service() { LogUtils.d("BG_SERVICE", "onDestroy") gpsManager?.stop() gpsManager = null + try { imuManager?.stop() } catch (_: Exception) {} + imuManager = null BgPrefs.setActive(this, false) try { postExecutor.shutdownNow() } catch (_: Exception) {} super.onDestroy() @@ -209,11 +256,12 @@ class BackgroundLocationService : Service() { latitude: Double, longitude: Double, accuracy: Double, + speed: Double, timestamp: Long, source: String, isMocked: Boolean ) { - val json = """{"source":"$source","timestamp":$timestamp,"latitude":$latitude,"longitude":$longitude,"accuracy":$accuracy,"isMocked":$isMocked}""" + val json = """{"source":"$source","timestamp":$timestamp,"latitude":$latitude,"longitude":$longitude,"accuracy":$accuracy,"speed":$speed,"isMocked":$isMocked}""" val url = URL(endpoint) val conn = (url.openConnection() as HttpURLConnection).apply { requestMethod = "POST" diff --git a/android/src/main/java/com/dumon/plugin/geolocation/utils/BgPrefs.kt b/android/src/main/java/com/dumon/plugin/geolocation/utils/BgPrefs.kt index ddd406d..f0af71c 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/utils/BgPrefs.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/utils/BgPrefs.kt @@ -23,6 +23,7 @@ object BgPrefs { private const val KEY_POST_MIN_DIST_M = "bg_post_min_dist_m" private const val KEY_POST_MIN_ACC_M = "bg_post_min_acc_m" private const val KEY_POST_MIN_INTERVAL_MS = "bg_post_min_interval_ms" + private const val KEY_BG_USE_IMU_FALLBACK = "bg_use_imu_fallback" fun setActive(context: Context, active: Boolean) { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) @@ -146,6 +147,18 @@ object BgPrefs { .coerceAtLeast(0L) } + fun setBackgroundUseImuFallback(context: Context, enabled: Boolean) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .edit() + .putBoolean(KEY_BG_USE_IMU_FALLBACK, enabled) + .apply() + } + + fun getBackgroundUseImuFallback(context: Context, defaultValue: Boolean = false): Boolean { + return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + .getBoolean(KEY_BG_USE_IMU_FALLBACK, defaultValue) + } + fun setPostUrl(context: Context, url: String?) { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) .edit() diff --git a/dist/docs.json b/dist/docs.json index 09b74a1..a188451 100644 --- a/dist/docs.json +++ b/dist/docs.json @@ -565,6 +565,13 @@ "docs": "", "complexTypes": [], "type": "number | undefined" + }, + { + "name": "backgroundUseImuFallback", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "boolean | undefined" } ] }, diff --git a/dist/esm/definitions.d.ts b/dist/esm/definitions.d.ts index 11cbae1..d457eb2 100644 --- a/dist/esm/definitions.d.ts +++ b/dist/esm/definitions.d.ts @@ -41,6 +41,7 @@ export interface DumonGeoOptions { backgroundPostMinDistanceMeters?: number; backgroundPostMinAccuracyMeters?: number; backgroundMinPostIntervalMs?: number; + backgroundUseImuFallback?: boolean; } export interface PermissionStatus { location: 'granted' | 'denied'; diff --git a/dist/esm/definitions.js.map b/dist/esm/definitions.js.map index 7eb0307..9de2d5b 100644 --- a/dist/esm/definitions.js.map +++ b/dist/esm/definitions.js.map @@ -1 +1 @@ -{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// export interface SatelliteStatus {\n// satellitesInView: number;\n// usedInFix: number;\n// constellationCounts: { [key: string]: number };\n// }\n\n// export interface WifiAp {\n// ssid: string;\n// bssid: string;\n// rssi: number;\n// distance?: number;\n// }\n\n// export interface WifiScanResult {\n// apCount: number;\n// aps: WifiAp[];\n// }\n\n// export interface ImuData {\n// accelX: number;\n// accelY: number;\n// accelZ: number;\n// gyroX: number;\n// gyroY: number;\n// gyroZ: number;\n// speed?: number;\n// acceleration?: number;\n// directionRad?: number;\n// }\n\n// export interface GpsData {\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n// satellitesInView?: number;\n// usedInFix?: number;\n// constellationCounts?: { [key: string]: number };\n// }\n\n// export interface PositioningData {\n// source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n// timestamp: number;\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n\n// gnssData?: SatelliteStatus;\n// wifiData?: WifiAp[];\n// imuData?: ImuData;\n// }\n\nexport interface PositioningData {\n source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n timestamp: number;\n latitude: number;\n longitude: number;\n accuracy: number;\n speed: number;\n acceleration: number;\n directionRad: number;\n isMocked: boolean;\n mode: 'normal' | 'driving';\n predicted?: boolean;\n // Optional detailed speed fields and provenance\n speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';\n speedGnss?: number; // m/s from Location.getSpeed when available and fresh\n speedImu?: number; // m/s from IMU fusion (internal heuristic)\n speedDerived?: number; // m/s from delta-position / delta-time (coarse)\n}\n\nexport interface SatelliteStatus {\n satellitesInView: number;\n usedInFix: number;\n constellationCounts: { [key: string]: number };\n}\n\nexport interface DumonGeoOptions {\n distanceThresholdMeters?: number;\n speedChangeThreshold?: number;\n directionChangeThreshold?: number;\n emitDebounceMs?: number;\n drivingEmitIntervalMs?: number;\n wifiScanIntervalMs?: number;\n enableWifiRtt?: boolean;\n enableLogging?: boolean;\n enableForwardPrediction?: boolean;\n maxPredictionSeconds?: number;\n emitGnssStatus?: boolean;\n suppressMockedUpdates?: boolean;\n keepScreenOn?: boolean;\n backgroundPollingIntervalMs?: number; // Android background polling interval\n backgroundPostMinDistanceMeters?: number; // Android background min distance to post\n backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters)\n backgroundMinPostIntervalMs?: number; // Android background minimum interval between POST attempts\n}\n\nexport interface PermissionStatus {\n location: 'granted' | 'denied';\n wifi: 'granted' | 'denied';\n}\n\nexport interface DumonGeolocationPlugin {\n startPositioning(): Promise;\n stopPositioning(): Promise;\n getLatestPosition(): Promise;\n checkAndRequestPermissions(): Promise;\n setOptions(options: DumonGeoOptions): Promise;\n getGnssStatus(): Promise;\n getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;\n // Background tracking (Android)\n startBackgroundTracking(options?: {\n title?: string;\n text?: string;\n channelId?: string;\n channelName?: string;\n postUrl?: string; // optional: service will POST latest fixes here as JSON\n }): Promise;\n stopBackgroundTracking(): Promise;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise;\n openBackgroundPermissionSettings(): Promise;\n openNotificationPermissionSettings(): Promise;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise;\n clearAuthTokens(): Promise;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise;\n\n addListener(\n eventName: 'onPositionUpdate',\n listenerFunc: (data: PositioningData) => void\n ): PluginListenerHandle;\n\n addListener(\n eventName: 'onGnssStatus',\n listenerFunc: (data: SatelliteStatus) => void\n ): PluginListenerHandle;\n}\n"]} \ No newline at end of file +{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// export interface SatelliteStatus {\n// satellitesInView: number;\n// usedInFix: number;\n// constellationCounts: { [key: string]: number };\n// }\n\n// export interface WifiAp {\n// ssid: string;\n// bssid: string;\n// rssi: number;\n// distance?: number;\n// }\n\n// export interface WifiScanResult {\n// apCount: number;\n// aps: WifiAp[];\n// }\n\n// export interface ImuData {\n// accelX: number;\n// accelY: number;\n// accelZ: number;\n// gyroX: number;\n// gyroY: number;\n// gyroZ: number;\n// speed?: number;\n// acceleration?: number;\n// directionRad?: number;\n// }\n\n// export interface GpsData {\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n// satellitesInView?: number;\n// usedInFix?: number;\n// constellationCounts?: { [key: string]: number };\n// }\n\n// export interface PositioningData {\n// source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n// timestamp: number;\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n\n// gnssData?: SatelliteStatus;\n// wifiData?: WifiAp[];\n// imuData?: ImuData;\n// }\n\nexport interface PositioningData {\n source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n timestamp: number;\n latitude: number;\n longitude: number;\n accuracy: number;\n speed: number;\n acceleration: number;\n directionRad: number;\n isMocked: boolean;\n mode: 'normal' | 'driving';\n predicted?: boolean;\n // Optional detailed speed fields and provenance\n speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';\n speedGnss?: number; // m/s from Location.getSpeed when available and fresh\n speedImu?: number; // m/s from IMU fusion (internal heuristic)\n speedDerived?: number; // m/s from delta-position / delta-time (coarse)\n}\n\nexport interface SatelliteStatus {\n satellitesInView: number;\n usedInFix: number;\n constellationCounts: { [key: string]: number };\n}\n\nexport interface DumonGeoOptions {\n distanceThresholdMeters?: number;\n speedChangeThreshold?: number;\n directionChangeThreshold?: number;\n emitDebounceMs?: number;\n drivingEmitIntervalMs?: number;\n wifiScanIntervalMs?: number;\n enableWifiRtt?: boolean;\n enableLogging?: boolean;\n enableForwardPrediction?: boolean;\n maxPredictionSeconds?: number;\n emitGnssStatus?: boolean;\n suppressMockedUpdates?: boolean;\n keepScreenOn?: boolean;\n backgroundPollingIntervalMs?: number; // Android background polling interval\n backgroundPostMinDistanceMeters?: number; // Android background min distance to post\n backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters)\n backgroundMinPostIntervalMs?: number; // Android background minimum interval between POST attempts\n backgroundUseImuFallback?: boolean; // Android background: enable IMU-based speed fallback (default true)\n}\n\nexport interface PermissionStatus {\n location: 'granted' | 'denied';\n wifi: 'granted' | 'denied';\n}\n\nexport interface DumonGeolocationPlugin {\n startPositioning(): Promise;\n stopPositioning(): Promise;\n getLatestPosition(): Promise;\n checkAndRequestPermissions(): Promise;\n setOptions(options: DumonGeoOptions): Promise;\n getGnssStatus(): Promise;\n getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;\n // Background tracking (Android)\n startBackgroundTracking(options?: {\n title?: string;\n text?: string;\n channelId?: string;\n channelName?: string;\n postUrl?: string; // optional: service will POST latest fixes here as JSON\n }): Promise;\n stopBackgroundTracking(): Promise;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise;\n openBackgroundPermissionSettings(): Promise;\n openNotificationPermissionSettings(): Promise;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise;\n clearAuthTokens(): Promise;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise;\n\n addListener(\n eventName: 'onPositionUpdate',\n listenerFunc: (data: PositioningData) => void\n ): PluginListenerHandle;\n\n addListener(\n eventName: 'onGnssStatus',\n listenerFunc: (data: SatelliteStatus) => void\n ): PluginListenerHandle;\n}\n"]} \ No newline at end of file diff --git a/documentation/PLUGIN_REFERENCE.md b/documentation/PLUGIN_REFERENCE.md index eb3ef9f..4f0de59 100644 --- a/documentation/PLUGIN_REFERENCE.md +++ b/documentation/PLUGIN_REFERENCE.md @@ -62,6 +62,7 @@ export interface DumonGeoOptions { backgroundPostMinDistanceMeters?: number; // Android: minimum perpindahan untuk POST (default 10m) backgroundPostMinAccuracyMeters?: number; // Android: minimum akurasi fix untuk POST (meter); kosong = tidak dibatasi backgroundMinPostIntervalMs?: number; // Android: interval minimum antar upaya POST (default ~10000 ms) + backgroundUseImuFallback?: boolean; // Android: aktif/nonaktifkan fallback speed via IMU saat background (default false) } export interface PermissionStatus { @@ -131,6 +132,19 @@ getBackgroundPostUrl(): Promise<{ url: string | null }> ``` - Menjalankan tracking lokasi saat app di‑pause menggunakan Foreground Service. Menyimpan latest fix ke penyimpanan lokal agar bisa diambil saat kembali ke foreground. Opsi `postUrl` (opsional) akan mengirim setiap pembaruan lokasi via HTTP POST (JSON) ke endpoint tersebut. Untuk autentikasi header, panggil `setAuthTokens` (menyimpan token secara native) agar setiap POST menyertakan header `Authorization: Bearer ` dan `refresh-token: `. Di web: no‑op. +Catatan baterai (Android, background): +- Secara default fallback IMU dimatikan. Aktifkan `backgroundUseImuFallback: true` bila Anda butuh estimasi speed yang lebih halus saat sinyal GNSS kurang baik, dengan konsekuensi konsumsi baterai meningkat. Saat dimatikan, speed di background hanya berasal dari GNSS (jika tersedia) atau turunan Δpos/Δt; `acceleration` dan `directionRad` akan terset ke 0. + +Format payload POST (background): + +- `source`: string (`GNSS` | `MOCK`) +- `timestamp`: number (epoch ms) +- `latitude`: number +- `longitude`: number +- `accuracy`: number (meter) +- `speed`: number (m/s) +- `isMocked`: boolean + ### Events ```ts diff --git a/src/definitions.ts b/src/definitions.ts index 507e6dd..3efe297 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -94,6 +94,7 @@ export interface DumonGeoOptions { backgroundPostMinDistanceMeters?: number; // Android background min distance to post backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters) backgroundMinPostIntervalMs?: number; // Android background minimum interval between POST attempts + backgroundUseImuFallback?: boolean; // Android background: enable IMU-based speed fallback (default false) } export interface PermissionStatus {