updated 151025-03

This commit is contained in:
wengki81 2025-10-16 01:20:23 +08:00
parent c306b59b9e
commit a392b97e50
8 changed files with 94 additions and 6 deletions

View File

@ -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()
}

View File

@ -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"

View File

@ -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()

7
dist/docs.json vendored
View File

@ -565,6 +565,13 @@
"docs": "",
"complexTypes": [],
"type": "number | undefined"
},
{
"name": "backgroundUseImuFallback",
"tags": [],
"docs": "",
"complexTypes": [],
"type": "boolean | undefined"
}
]
},

View File

@ -41,6 +41,7 @@ export interface DumonGeoOptions {
backgroundPostMinDistanceMeters?: number;
backgroundPostMinAccuracyMeters?: number;
backgroundMinPostIntervalMs?: number;
backgroundUseImuFallback?: boolean;
}
export interface PermissionStatus {
location: 'granted' | 'denied';

View File

@ -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<void>;\n stopPositioning(): Promise<void>;\n getLatestPosition(): Promise<PositioningData>;\n checkAndRequestPermissions(): Promise<PermissionStatus>;\n setOptions(options: DumonGeoOptions): Promise<void>;\n getGnssStatus(): Promise<SatelliteStatus | null>;\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<void>;\n stopBackgroundTracking(): Promise<void>;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise<PositioningData | null>;\n openBackgroundPermissionSettings(): Promise<void>;\n openNotificationPermissionSettings(): Promise<void>;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>;\n clearAuthTokens(): Promise<void>;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise<void>;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise<void>;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise<void>;\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"]}
{"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<void>;\n stopPositioning(): Promise<void>;\n getLatestPosition(): Promise<PositioningData>;\n checkAndRequestPermissions(): Promise<PermissionStatus>;\n setOptions(options: DumonGeoOptions): Promise<void>;\n getGnssStatus(): Promise<SatelliteStatus | null>;\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<void>;\n stopBackgroundTracking(): Promise<void>;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise<PositioningData | null>;\n openBackgroundPermissionSettings(): Promise<void>;\n openNotificationPermissionSettings(): Promise<void>;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>;\n clearAuthTokens(): Promise<void>;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise<void>;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise<void>;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise<void>;\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"]}

View File

@ -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 dipause 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 <token>` dan `refresh-token: <token>`. Di web: noop.
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

View File

@ -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 {