updated 121025-01
This commit is contained in:
parent
f5be8180f2
commit
511d903890
30
README.md
30
README.md
@ -4,7 +4,7 @@ Capacitor Plugin untuk Android yang menyediakan **real-time high-accuracy positi
|
|||||||
- 📡 **GNSS multi-konstelasi** (GPS, GLONASS, BeiDou, Galileo)
|
- 📡 **GNSS multi-konstelasi** (GPS, GLONASS, BeiDou, Galileo)
|
||||||
- 📶 **Wi-Fi RTT / RSSI**
|
- 📶 **Wi-Fi RTT / RSSI**
|
||||||
- 🎯 **IMU Sensor** (Accelerometer, Gyroscope, Rotation)
|
- 🎯 **IMU Sensor** (Accelerometer, Gyroscope, Rotation)
|
||||||
- ⚙️ **Kalman Filter Fusion** dan **Forward Prediction**
|
- ⏩ **Forward Prediction (opsional)**
|
||||||
- 🛡️ **Deteksi Lokasi Palsu (Mock Location Detection)**
|
- 🛡️ **Deteksi Lokasi Palsu (Mock Location Detection)**
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -64,7 +64,7 @@ Menghentikan semua sensor dan proses positioning.
|
|||||||
getLatestPosition(): Promise<PositioningData>
|
getLatestPosition(): Promise<PositioningData>
|
||||||
```
|
```
|
||||||
|
|
||||||
Mengembalikan posisi terkini yang telah melalui proses fusion/prediksi.
|
Mengembalikan posisi terkini. Jika opsi prediksi diaktifkan (lihat setOptions), nilai dapat diproyeksikan pendek dan ditandai `predicted: true`.
|
||||||
|
|
||||||
### addListener(‘onPositionUpdate’, …)
|
### addListener(‘onPositionUpdate’, …)
|
||||||
|
|
||||||
@ -94,6 +94,16 @@ configureEdgeToEdge(options: {
|
|||||||
|
|
||||||
Mengatur status bar dan navigasi bar agar transparan, dengan warna dan icon style sesuai UI.
|
Mengatur status bar dan navigasi bar agar transparan, dengan warna dan icon style sesuai UI.
|
||||||
|
|
||||||
|
### setGpsMode()
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
setGpsMode(options: { mode: 'normal' | 'driving' }): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
Mengganti mode GPS:
|
||||||
|
- `normal`: polling adaptif (hemat baterai)
|
||||||
|
- `driving`: continuous updates + loop emisi periodik
|
||||||
|
|
||||||
### setOptions()
|
### setOptions()
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
@ -174,8 +184,9 @@ interface PermissionStatus {
|
|||||||
|
|
||||||
## Fusion dan Prediksi
|
## Fusion dan Prediksi
|
||||||
|
|
||||||
- Fusion posisi dilakukan menggunakan Kalman Filter sederhana untuk latitude & longitude.
|
- Tidak ada Kalman/Fusion saat ini. Data yang dipancarkan berasal dari GNSS, dilengkapi IMU untuk heuristik interval dan estimasi arah/kecepatan.
|
||||||
- Saat GNSS tidak tersedia (signal hilang), plugin akan melakukan forward prediction berbasis speed + heading dari IMU selama 1 detik ke depan.
|
- Prediksi maju (dead‑reckoning) bersifat opsional dan nonaktif secara default. Aktifkan dengan `setOptions({ enableForwardPrediction: true, maxPredictionSeconds?: number })`.
|
||||||
|
- Saat prediksi aktif, posisi dapat diproyeksikan pendek (<= `maxPredictionSeconds`) berdasarkan `speed` dan `directionRad` dari IMU, serta ditandai `predicted: true`. Nilai `source` tidak berubah.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -225,12 +236,13 @@ interface WifiAp {
|
|||||||
|
|
||||||
## Testing (example-app)
|
## Testing (example-app)
|
||||||
|
|
||||||
Contoh implementasi tersedia di folder /example-app dengan tombol:
|
Contoh implementasi tersedia di folder `/example-app` dengan kontrol:
|
||||||
|
|
||||||
- Start Positioning
|
- Start/Stop Positioning, Get Latest Position
|
||||||
- Stop Positioning
|
- Request Permissions, Clear Log
|
||||||
- Get Latest Position
|
- Mode: Driving / Normal
|
||||||
- Realtime log via onPositionUpdate
|
- Apply Options (semua opsi runtime, termasuk keepScreenOn, emitGnssStatus, enableForwardPrediction, interval)
|
||||||
|
- Diagnostics: Get GNSS Status, Get Location Services Status
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,23 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
<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_COARSE_LOCATION" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||||
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
<uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES" />
|
||||||
|
|
||||||
|
<!-- Background + Foreground service permissions -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<!-- Android 14+ dedicated permission for location foreground services -->
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_LOCATION" />
|
||||||
|
<!-- Android 13+ notifications runtime permission to show FGS notification -->
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<application>
|
||||||
|
<service
|
||||||
|
android:name="com.dumon.plugin.geolocation.bg.BackgroundLocationService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="location" />
|
||||||
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
|||||||
@ -27,10 +27,15 @@ import com.dumon.plugin.geolocation.wifi.WifiScanResult
|
|||||||
import com.dumon.plugin.geolocation.utils.PermissionUtils
|
import com.dumon.plugin.geolocation.utils.PermissionUtils
|
||||||
import com.dumon.plugin.geolocation.utils.LogUtils
|
import com.dumon.plugin.geolocation.utils.LogUtils
|
||||||
import com.getcapacitor.annotation.PermissionCallback
|
import com.getcapacitor.annotation.PermissionCallback
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.dumon.plugin.geolocation.bg.BackgroundLocationService
|
||||||
|
import com.dumon.plugin.geolocation.utils.BgPrefs
|
||||||
import org.json.JSONArray
|
import org.json.JSONArray
|
||||||
import org.json.JSONObject
|
import org.json.JSONObject
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
import android.location.Location
|
import android.location.Location
|
||||||
|
import com.dumon.plugin.geolocation.utils.AuthStore
|
||||||
|
|
||||||
@CapacitorPlugin(
|
@CapacitorPlugin(
|
||||||
name = "DumonGeolocation",
|
name = "DumonGeolocation",
|
||||||
@ -199,7 +204,38 @@ class DumonGeolocation : Plugin() {
|
|||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
fun getLatestPosition(call: PluginCall) {
|
fun getLatestPosition(call: PluginCall) {
|
||||||
|
// Try to fetch a fresh single fix first; fallback to cached snapshot if it fails
|
||||||
|
val hasFine = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
val hasCoarse = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
val manager = gpsManager
|
||||||
|
if (manager == null || (!hasFine && !hasCoarse)) {
|
||||||
|
// No manager or no permissions — fallback immediately
|
||||||
call.resolve(buildPositionData())
|
call.resolve(buildPositionData())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Request a fresh single fix (useNetwork=true for faster first fix), short timeout
|
||||||
|
manager.requestSingleFix(timeoutMs = 3000L, useNetworkProvider = true) { location, isMocked ->
|
||||||
|
// If we received a location and it's not suppressed, return it
|
||||||
|
if (location != null) {
|
||||||
|
if (suppressMockedUpdates && isMocked) {
|
||||||
|
// Treat mocked as failure when suppression is enabled
|
||||||
|
call.resolve(buildPositionData())
|
||||||
|
} else {
|
||||||
|
latestLatitude = location.latitude
|
||||||
|
latestLongitude = location.longitude
|
||||||
|
latestAccuracy = location.accuracy.toDouble()
|
||||||
|
latestSource = if (isMocked) "MOCK" else "GNSS"
|
||||||
|
isMockedLocation = isMocked
|
||||||
|
latestTimestamp = location.time
|
||||||
|
call.resolve(buildPositionData())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Timeout or failure — fallback to last known snapshot
|
||||||
|
call.resolve(buildPositionData())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@PluginMethod
|
@PluginMethod
|
||||||
@ -366,6 +402,26 @@ class DumonGeolocation : Plugin() {
|
|||||||
keepScreenOn = it
|
keepScreenOn = it
|
||||||
applyKeepScreenOn(keepScreenOn)
|
applyKeepScreenOn(keepScreenOn)
|
||||||
}
|
}
|
||||||
|
call.getInt("backgroundPollingIntervalMs")?.let {
|
||||||
|
val bgInterval = it.toLong().coerceAtLeast(1000L)
|
||||||
|
BgPrefs.setBackgroundIntervalMs(context, bgInterval)
|
||||||
|
LogUtils.d("DUMON_GEOLOCATION", "Set background polling interval = ${bgInterval} ms")
|
||||||
|
}
|
||||||
|
call.getDouble("backgroundPostMinDistanceMeters")?.let {
|
||||||
|
val v = it.coerceAtLeast(0.0)
|
||||||
|
BgPrefs.setBackgroundPostMinDistanceMeters(context, v)
|
||||||
|
LogUtils.d("DUMON_GEOLOCATION", "Set background min post distance = ${v} m")
|
||||||
|
}
|
||||||
|
call.getDouble("backgroundPostMinAccuracyMeters")?.let {
|
||||||
|
val v = it.coerceAtLeast(0.0)
|
||||||
|
BgPrefs.setBackgroundPostMinAccuracyMeters(context, v)
|
||||||
|
LogUtils.d("DUMON_GEOLOCATION", "Set background min accuracy = ${v} m")
|
||||||
|
}
|
||||||
|
call.getInt("backgroundMinPostIntervalMs")?.let {
|
||||||
|
val v = it.toLong().coerceAtLeast(0L)
|
||||||
|
BgPrefs.setBackgroundMinPostIntervalMs(context, v)
|
||||||
|
LogUtils.d("DUMON_GEOLOCATION", "Set background min post interval = ${v} ms")
|
||||||
|
}
|
||||||
call.resolve()
|
call.resolve()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,6 +469,162 @@ class DumonGeolocation : Plugin() {
|
|||||||
call.resolve(obj)
|
call.resolve(obj)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun startBackgroundTracking(call: PluginCall) {
|
||||||
|
val title = call.getString("title") ?: "Location tracking active"
|
||||||
|
val text = call.getString("text") ?: "Updating location in background"
|
||||||
|
val channelId = call.getString("channelId") ?: "DUMON_GEO_BG"
|
||||||
|
val channelName = call.getString("channelName") ?: "Dumon Geolocation"
|
||||||
|
val postUrl = call.getString("postUrl")
|
||||||
|
|
||||||
|
val fine = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
val coarse = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
if (!fine && !coarse) {
|
||||||
|
call.reject("Location permission not granted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val intent = Intent(context, BackgroundLocationService::class.java).apply {
|
||||||
|
putExtra(BackgroundLocationService.EXTRA_CHANNEL_ID, channelId)
|
||||||
|
putExtra(BackgroundLocationService.EXTRA_CHANNEL_NAME, channelName)
|
||||||
|
putExtra(BackgroundLocationService.EXTRA_TITLE, title)
|
||||||
|
putExtra(BackgroundLocationService.EXTRA_TEXT, text)
|
||||||
|
if (!postUrl.isNullOrBlank()) putExtra(BackgroundLocationService.EXTRA_POST_URL, postUrl)
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
ContextCompat.startForegroundService(context, intent)
|
||||||
|
call.resolve()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.reject("Failed to start background service: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun stopBackgroundTracking(call: PluginCall) {
|
||||||
|
try {
|
||||||
|
val stopped = context.stopService(Intent(context, BackgroundLocationService::class.java))
|
||||||
|
if (!stopped) {
|
||||||
|
// Even if service was not running, ensure flag cleared
|
||||||
|
BgPrefs.setActive(context, false)
|
||||||
|
}
|
||||||
|
call.resolve()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.reject("Failed to stop background service: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun isBackgroundTrackingActive(call: PluginCall) {
|
||||||
|
val active = BgPrefs.isActive(context)
|
||||||
|
val obj = JSObject().apply { put("active", active) }
|
||||||
|
call.resolve(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getBackgroundLatestPosition(call: PluginCall) {
|
||||||
|
val fix = BgPrefs.readLatestFix(context)
|
||||||
|
if (fix == null) {
|
||||||
|
call.resolve(JSObject())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val obj = JSObject().apply {
|
||||||
|
put("source", fix.source)
|
||||||
|
put("timestamp", fix.timestamp)
|
||||||
|
put("latitude", fix.latitude)
|
||||||
|
put("longitude", fix.longitude)
|
||||||
|
put("accuracy", fix.accuracy)
|
||||||
|
put("isMocked", fix.isMocked)
|
||||||
|
put("speed", fix.speed)
|
||||||
|
put("acceleration", fix.acceleration)
|
||||||
|
put("directionRad", fix.directionRad)
|
||||||
|
put("predicted", false)
|
||||||
|
}
|
||||||
|
call.resolve(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Auth token management for background posting ---
|
||||||
|
@PluginMethod
|
||||||
|
fun setAuthTokens(call: PluginCall) {
|
||||||
|
val access = call.getString("accessToken") ?: run {
|
||||||
|
call.reject("accessToken is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val refresh = call.getString("refreshToken") ?: run {
|
||||||
|
call.reject("refreshToken is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
AuthStore.saveTokens(context, access, refresh)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun clearAuthTokens(call: PluginCall) {
|
||||||
|
AuthStore.clear(context)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getAuthState(call: PluginCall) {
|
||||||
|
val present = AuthStore.getTokens(context) != null
|
||||||
|
val obj = JSObject().apply { put("present", present) }
|
||||||
|
call.resolve(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Background posting endpoint management ---
|
||||||
|
@PluginMethod
|
||||||
|
fun setBackgroundPostUrl(call: PluginCall) {
|
||||||
|
val url = call.getString("url")
|
||||||
|
if (url.isNullOrBlank()) {
|
||||||
|
BgPrefs.setPostUrl(context, null)
|
||||||
|
} else {
|
||||||
|
BgPrefs.setPostUrl(context, url)
|
||||||
|
}
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun getBackgroundPostUrl(call: PluginCall) {
|
||||||
|
val url = BgPrefs.getPostUrl(context)
|
||||||
|
val obj = JSObject().apply { put("url", url) }
|
||||||
|
call.resolve(obj)
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun openBackgroundPermissionSettings(call: PluginCall) {
|
||||||
|
try {
|
||||||
|
val intent = Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = android.net.Uri.parse("package:" + context.packageName)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
call.resolve()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.reject("Failed to open settings: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PluginMethod
|
||||||
|
fun openNotificationPermissionSettings(call: PluginCall) {
|
||||||
|
try {
|
||||||
|
val intent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
Intent(android.provider.Settings.ACTION_APP_NOTIFICATION_SETTINGS).apply {
|
||||||
|
putExtra(android.provider.Settings.EXTRA_APP_PACKAGE, context.packageName)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
|
||||||
|
data = android.net.Uri.parse("package:" + context.packageName)
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
context.startActivity(intent)
|
||||||
|
call.resolve()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.reject("Failed to open notification settings: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun emitPositionUpdate(forceEmit: Boolean = false) {
|
private fun emitPositionUpdate(forceEmit: Boolean = false) {
|
||||||
val now = System.currentTimeMillis()
|
val now = System.currentTimeMillis()
|
||||||
if (!forceEmit && now - lastEmitTimestamp < emitIntervalMs) return
|
if (!forceEmit && now - lastEmitTimestamp < emitIntervalMs) return
|
||||||
|
|||||||
@ -0,0 +1,260 @@
|
|||||||
|
package com.dumon.plugin.geolocation.bg
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.IBinder
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
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.utils.BgPrefs
|
||||||
|
import com.dumon.plugin.geolocation.utils.LogUtils
|
||||||
|
import com.dumon.plugin.geolocation.utils.AuthStore
|
||||||
|
import java.io.BufferedOutputStream
|
||||||
|
import java.io.BufferedReader
|
||||||
|
import java.io.InputStreamReader
|
||||||
|
import java.net.HttpURLConnection
|
||||||
|
import java.net.URL
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
import kotlin.math.*
|
||||||
|
|
||||||
|
class BackgroundLocationService : Service() {
|
||||||
|
|
||||||
|
private var gpsManager: GpsStatusManager? = 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
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
LogUtils.d("BG_SERVICE", "onCreate")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
val channelId = intent?.getStringExtra(EXTRA_CHANNEL_ID) ?: DEFAULT_CHANNEL_ID
|
||||||
|
val channelName = intent?.getStringExtra(EXTRA_CHANNEL_NAME) ?: DEFAULT_CHANNEL_NAME
|
||||||
|
val title = intent?.getStringExtra(EXTRA_TITLE) ?: DEFAULT_TITLE
|
||||||
|
val text = intent?.getStringExtra(EXTRA_TEXT) ?: DEFAULT_TEXT
|
||||||
|
|
||||||
|
createChannelIfNeeded(channelId, channelName)
|
||||||
|
val notification = buildNotification(channelId, title, text)
|
||||||
|
startForeground(DEFAULT_NOTIFICATION_ID, notification)
|
||||||
|
|
||||||
|
// Mark active
|
||||||
|
BgPrefs.setActive(this, true)
|
||||||
|
|
||||||
|
// Optional remote posting endpoint (persisted)
|
||||||
|
postUrl = intent?.getStringExtra(EXTRA_POST_URL)
|
||||||
|
postUrl?.let { BgPrefs.setPostUrl(applicationContext, it) }
|
||||||
|
|
||||||
|
// Start GNSS continuous 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
|
||||||
|
BgPrefs.saveLatestFix(
|
||||||
|
context = applicationContext,
|
||||||
|
latitude = location.latitude,
|
||||||
|
longitude = location.longitude,
|
||||||
|
accuracy = location.accuracy.toDouble(),
|
||||||
|
timestamp = location.time,
|
||||||
|
source = if (isMocked) "MOCK" else "GNSS",
|
||||||
|
isMocked = isMocked,
|
||||||
|
speed = null,
|
||||||
|
acceleration = null,
|
||||||
|
directionRad = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
val endpoint = postUrl ?: BgPrefs.getPostUrl(applicationContext)
|
||||||
|
endpoint?.let { url ->
|
||||||
|
// Check movement threshold, accuracy, and in-flight before posting
|
||||||
|
val last = BgPrefs.getLastPostedFix(applicationContext)
|
||||||
|
val minDistM = BgPrefs.getBackgroundPostMinDistanceMeters(applicationContext, 10.0)
|
||||||
|
val minAccM = BgPrefs.getBackgroundPostMinAccuracyMeters(applicationContext, Double.POSITIVE_INFINITY)
|
||||||
|
val minPostIntervalMs = BgPrefs.getBackgroundMinPostIntervalMs(applicationContext, 10_000L)
|
||||||
|
|
||||||
|
// Accuracy gating
|
||||||
|
if (minAccM.isFinite() && location.accuracy.toDouble() > minAccM) {
|
||||||
|
LogUtils.d("BG_SERVICE", "Skip post: accuracy ${location.accuracy} m > minAcc ${minAccM} m")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
if (last != null) {
|
||||||
|
val d = haversineMeters(last.latitude, last.longitude, location.latitude, location.longitude)
|
||||||
|
if (d < minDistM) {
|
||||||
|
LogUtils.d("BG_SERVICE", "Skip post: moved ${"%.1f".format(d)} m < ${minDistM} m")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In-flight pending gating
|
||||||
|
val pendLat = pendingPostedLat
|
||||||
|
val pendLon = pendingPostedLon
|
||||||
|
if (pendLat != null && pendLon != null) {
|
||||||
|
val dPend = haversineMeters(pendLat, pendLon, location.latitude, location.longitude)
|
||||||
|
if (dPend < minDistM) {
|
||||||
|
LogUtils.d("BG_SERVICE", "Skip post: pending in-flight within ${"%.1f".format(dPend)} m < ${minDistM} m")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Min post interval gating
|
||||||
|
val now = System.currentTimeMillis()
|
||||||
|
val sinceLastAttempt = now - lastPostAttemptTs
|
||||||
|
if (minPostIntervalMs > 0 && sinceLastAttempt < minPostIntervalMs) {
|
||||||
|
LogUtils.d("BG_SERVICE", "Skip post: interval ${sinceLastAttempt} ms < min ${minPostIntervalMs} ms")
|
||||||
|
return@let
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark pending and timestamp before enqueue
|
||||||
|
pendingPostedLat = location.latitude
|
||||||
|
pendingPostedLon = location.longitude
|
||||||
|
pendingPostedTs = location.time
|
||||||
|
lastPostAttemptTs = now
|
||||||
|
|
||||||
|
postExecutor.execute {
|
||||||
|
try {
|
||||||
|
postFix(url,
|
||||||
|
latitude = location.latitude,
|
||||||
|
longitude = location.longitude,
|
||||||
|
accuracy = location.accuracy.toDouble(),
|
||||||
|
timestamp = location.time,
|
||||||
|
source = if (isMocked) "MOCK" else "GNSS",
|
||||||
|
isMocked = isMocked
|
||||||
|
)
|
||||||
|
// Only mark posted on success inside postFix (returns via exception or code check)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtils.e("BG_SERVICE", "postFix failed: ${e.message}")
|
||||||
|
} finally {
|
||||||
|
pendingPostedLat = null
|
||||||
|
pendingPostedLon = null
|
||||||
|
pendingPostedTs = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Use polling mode with a configurable interval to save battery in background
|
||||||
|
gpsManager?.start(GpsTrackingMode.NORMAL)
|
||||||
|
val interval = BgPrefs.getBackgroundIntervalMs(applicationContext, 5000L)
|
||||||
|
gpsManager?.setPollingInterval(interval)
|
||||||
|
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
LogUtils.d("BG_SERVICE", "onDestroy")
|
||||||
|
gpsManager?.stop()
|
||||||
|
gpsManager = null
|
||||||
|
BgPrefs.setActive(this, false)
|
||||||
|
try { postExecutor.shutdownNow() } catch (_: Exception) {}
|
||||||
|
super.onDestroy()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder? = null
|
||||||
|
|
||||||
|
private fun createChannelIfNeeded(channelId: String, name: String) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
var ch = nm.getNotificationChannel(channelId)
|
||||||
|
if (ch == null) {
|
||||||
|
ch = NotificationChannel(channelId, name, NotificationManager.IMPORTANCE_LOW).apply {
|
||||||
|
description = "Background geolocation tracking"
|
||||||
|
setShowBadge(false)
|
||||||
|
enableVibration(false)
|
||||||
|
}
|
||||||
|
nm.createNotificationChannel(ch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(channelId: String, title: String, text: String): Notification {
|
||||||
|
val builder = NotificationCompat.Builder(this, channelId)
|
||||||
|
.setSmallIcon(android.R.drawable.ic_menu_mylocation)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(text)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||||
|
return builder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val DEFAULT_NOTIFICATION_ID = 0xD61
|
||||||
|
private const val DEFAULT_CHANNEL_ID = "DUMON_GEO_BG"
|
||||||
|
private const val DEFAULT_CHANNEL_NAME = "Dumon Geolocation"
|
||||||
|
private const val DEFAULT_TITLE = "Location tracking active"
|
||||||
|
private const val DEFAULT_TEXT = "Updating location in background"
|
||||||
|
|
||||||
|
const val EXTRA_CHANNEL_ID = "channelId"
|
||||||
|
const val EXTRA_CHANNEL_NAME = "channelName"
|
||||||
|
const val EXTRA_TITLE = "title"
|
||||||
|
const val EXTRA_TEXT = "text"
|
||||||
|
const val EXTRA_POST_URL = "postUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun postFix(
|
||||||
|
endpoint: String,
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
accuracy: Double,
|
||||||
|
timestamp: Long,
|
||||||
|
source: String,
|
||||||
|
isMocked: Boolean
|
||||||
|
) {
|
||||||
|
val json = """{"source":"$source","timestamp":$timestamp,"latitude":$latitude,"longitude":$longitude,"accuracy":$accuracy,"isMocked":$isMocked}"""
|
||||||
|
val url = URL(endpoint)
|
||||||
|
val conn = (url.openConnection() as HttpURLConnection).apply {
|
||||||
|
requestMethod = "POST"
|
||||||
|
connectTimeout = 5000
|
||||||
|
readTimeout = 5000
|
||||||
|
doOutput = true
|
||||||
|
setRequestProperty("Content-Type", "application/json")
|
||||||
|
// Attach auth headers if stored
|
||||||
|
val tokens = AuthStore.getTokens(applicationContext)
|
||||||
|
if (tokens != null) {
|
||||||
|
setRequestProperty("Authorization", "Bearer ${tokens.accessToken}")
|
||||||
|
setRequestProperty("refresh-token", tokens.refreshToken)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
BufferedOutputStream(conn.outputStream).use { out ->
|
||||||
|
out.write(json.toByteArray(Charsets.UTF_8))
|
||||||
|
out.flush()
|
||||||
|
}
|
||||||
|
val code = conn.responseCode
|
||||||
|
if (code in 200..299) {
|
||||||
|
LogUtils.d("BG_SERVICE", "Posted location successfully (HTTP $code)")
|
||||||
|
// Update last posted marker after success
|
||||||
|
BgPrefs.setLastPostedFix(applicationContext, latitude, longitude, timestamp)
|
||||||
|
} else {
|
||||||
|
val err = try {
|
||||||
|
BufferedReader(InputStreamReader(conn.errorStream ?: conn.inputStream)).readText()
|
||||||
|
} catch (_: Exception) { "" }
|
||||||
|
LogUtils.e("BG_SERVICE", "Posting failed (HTTP $code) $err")
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
conn.disconnect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun haversineMeters(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double {
|
||||||
|
val R = 6371000.0
|
||||||
|
val dLat = Math.toRadians(lat2 - lat1)
|
||||||
|
val dLon = Math.toRadians(lon2 - lon1)
|
||||||
|
val a = sin(dLat / 2).pow(2.0) + cos(Math.toRadians(lat1)) * cos(Math.toRadians(lat2)) * sin(dLon / 2).pow(2.0)
|
||||||
|
val c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
||||||
|
return R * c
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -58,6 +58,101 @@ class GpsStatusManager(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request a single fresh location fix from available providers with a timeout.
|
||||||
|
* - Tries GPS if fine location is granted
|
||||||
|
* - Optionally also tries NETWORK if coarse location is granted (faster, less accurate)
|
||||||
|
* - Returns the first location received and stops listening
|
||||||
|
* - On timeout or missing permissions, returns null
|
||||||
|
*/
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
|
fun requestSingleFix(
|
||||||
|
timeoutMs: Long = 3000L,
|
||||||
|
useNetworkProvider: Boolean = true,
|
||||||
|
callback: (Location?, Boolean) -> Unit
|
||||||
|
) {
|
||||||
|
val hasFine = ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
val hasCoarse = ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
|
if (!hasFine && !hasCoarse) {
|
||||||
|
LogUtils.e("GPS_STATUS", "requestSingleFix: missing location permissions")
|
||||||
|
callback(null, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var done = false
|
||||||
|
val mainHandler = Handler(Looper.getMainLooper())
|
||||||
|
lateinit var timeoutRunnable: Runnable
|
||||||
|
|
||||||
|
val singleListener = object : LocationListener {
|
||||||
|
override fun onLocationChanged(location: Location) {
|
||||||
|
if (done) return
|
||||||
|
done = true
|
||||||
|
|
||||||
|
val isMocked = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) location.isMock else location.isFromMockProvider
|
||||||
|
|
||||||
|
try {
|
||||||
|
locationManager.removeUpdates(this)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
|
||||||
|
// Cancel timeout if we already have a fix
|
||||||
|
try { mainHandler.removeCallbacks(timeoutRunnable) } catch (_: Exception) {}
|
||||||
|
|
||||||
|
callback(location, isMocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
|
||||||
|
override fun onProviderEnabled(provider: String) {}
|
||||||
|
override fun onProviderDisabled(provider: String) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start listening from eligible providers
|
||||||
|
var requested = false
|
||||||
|
if (hasFine) {
|
||||||
|
try {
|
||||||
|
locationManager.requestLocationUpdates(
|
||||||
|
LocationManager.GPS_PROVIDER,
|
||||||
|
0L,
|
||||||
|
0f,
|
||||||
|
singleListener
|
||||||
|
)
|
||||||
|
requested = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtils.e("GPS_STATUS", "requestSingleFix: failed to request GPS provider: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (useNetworkProvider && hasCoarse) {
|
||||||
|
try {
|
||||||
|
locationManager.requestLocationUpdates(
|
||||||
|
LocationManager.NETWORK_PROVIDER,
|
||||||
|
0L,
|
||||||
|
0f,
|
||||||
|
singleListener
|
||||||
|
)
|
||||||
|
requested = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtils.e("GPS_STATUS", "requestSingleFix: failed to request NETWORK provider: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requested) {
|
||||||
|
LogUtils.e("GPS_STATUS", "requestSingleFix: no provider requested (permissions/providers)")
|
||||||
|
callback(null, false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout fallback
|
||||||
|
timeoutRunnable = Runnable {
|
||||||
|
if (done) return@Runnable
|
||||||
|
done = true
|
||||||
|
try {
|
||||||
|
locationManager.removeUpdates(singleListener)
|
||||||
|
} catch (_: Exception) {}
|
||||||
|
callback(null, false)
|
||||||
|
}
|
||||||
|
mainHandler.postDelayed(timeoutRunnable, timeoutMs)
|
||||||
|
}
|
||||||
|
|
||||||
private fun pollOnceAndEmit() {
|
private fun pollOnceAndEmit() {
|
||||||
val oneShotListener = object : LocationListener {
|
val oneShotListener = object : LocationListener {
|
||||||
override fun onLocationChanged(location: Location) {
|
override fun onLocationChanged(location: Location) {
|
||||||
@ -85,16 +180,40 @@ class GpsStatusManager(
|
|||||||
override fun onProviderEnabled(provider: String) {}
|
override fun onProviderEnabled(provider: String) {}
|
||||||
override fun onProviderDisabled(provider: String) {}
|
override fun onProviderDisabled(provider: String) {}
|
||||||
}
|
}
|
||||||
|
val hasFine = ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
val hasCoarse = ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED
|
||||||
|
|
||||||
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED) {
|
var requested = false
|
||||||
|
if (hasFine) {
|
||||||
|
try {
|
||||||
locationManager.requestLocationUpdates(
|
locationManager.requestLocationUpdates(
|
||||||
LocationManager.GPS_PROVIDER,
|
LocationManager.GPS_PROVIDER,
|
||||||
0L,
|
0L,
|
||||||
0f,
|
0f,
|
||||||
oneShotListener
|
oneShotListener
|
||||||
)
|
)
|
||||||
} else {
|
requested = true
|
||||||
LogUtils.e("GPS_STATUS", "Missing location permission")
|
} catch (e: Exception) {
|
||||||
|
LogUtils.e("GPS_STATUS", "Failed GPS one-shot request: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasCoarse) {
|
||||||
|
try {
|
||||||
|
locationManager.requestLocationUpdates(
|
||||||
|
LocationManager.NETWORK_PROVIDER,
|
||||||
|
0L,
|
||||||
|
0f,
|
||||||
|
oneShotListener
|
||||||
|
)
|
||||||
|
requested = true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
LogUtils.e("GPS_STATUS", "Failed NETWORK one-shot request: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requested) {
|
||||||
|
LogUtils.e("GPS_STATUS", "pollOnceAndEmit: no provider requested (missing permissions)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -0,0 +1,42 @@
|
|||||||
|
package com.dumon.plugin.geolocation.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
|
import androidx.annotation.WorkerThread
|
||||||
|
|
||||||
|
// Optional secure storage using EncryptedSharedPreferences if available.
|
||||||
|
// Falls back to regular SharedPreferences if crypto init fails.
|
||||||
|
object AuthStore {
|
||||||
|
private const val PREFS_NAME = "dumon_geo_auth_prefs"
|
||||||
|
private const val KEY_AT = "access_token"
|
||||||
|
private const val KEY_RT = "refresh_token"
|
||||||
|
|
||||||
|
data class Tokens(val accessToken: String, val refreshToken: String)
|
||||||
|
|
||||||
|
fun saveTokens(context: Context, accessToken: String, refreshToken: String) {
|
||||||
|
val sp = prefs(context)
|
||||||
|
sp.edit()
|
||||||
|
.putString(KEY_AT, accessToken)
|
||||||
|
.putString(KEY_RT, refreshToken)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTokens(context: Context): Tokens? {
|
||||||
|
val sp = prefs(context)
|
||||||
|
val at = sp.getString(KEY_AT, null)
|
||||||
|
val rt = sp.getString(KEY_RT, null)
|
||||||
|
return if (!at.isNullOrBlank() && !rt.isNullOrBlank()) Tokens(at, rt) else null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clear(context: Context) {
|
||||||
|
prefs(context).edit().clear().apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prefs(context: Context): android.content.SharedPreferences {
|
||||||
|
// For simplicity and build reliability, use standard SharedPreferences.
|
||||||
|
// If you want strong encryption, migrate to EncryptedSharedPreferences with
|
||||||
|
// androidx.security:security-crypto dependency and MasterKey.
|
||||||
|
return context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@ -0,0 +1,182 @@
|
|||||||
|
package com.dumon.plugin.geolocation.utils
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
|
||||||
|
object BgPrefs {
|
||||||
|
private const val PREFS = "dumon_geo_bg_prefs"
|
||||||
|
|
||||||
|
private const val KEY_ACTIVE = "active"
|
||||||
|
private const val KEY_LAT = "last_lat"
|
||||||
|
private const val KEY_LON = "last_lon"
|
||||||
|
private const val KEY_ACC = "last_acc"
|
||||||
|
private const val KEY_TS = "last_ts"
|
||||||
|
private const val KEY_SRC = "last_src"
|
||||||
|
private const val KEY_MOCK = "last_mock"
|
||||||
|
private const val KEY_SPEED = "last_speed"
|
||||||
|
private const val KEY_ACCEL = "last_accel"
|
||||||
|
private const val KEY_DIR = "last_dir"
|
||||||
|
private const val KEY_BG_INTERVAL = "bg_interval_ms"
|
||||||
|
private const val KEY_POST_URL = "post_url"
|
||||||
|
private const val KEY_POST_LAST_LAT = "post_last_lat"
|
||||||
|
private const val KEY_POST_LAST_LON = "post_last_lon"
|
||||||
|
private const val KEY_POST_LAST_TS = "post_last_ts"
|
||||||
|
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"
|
||||||
|
|
||||||
|
fun setActive(context: Context, active: Boolean) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putBoolean(KEY_ACTIVE, active)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isActive(context: Context): Boolean {
|
||||||
|
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getBoolean(KEY_ACTIVE, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveLatestFix(
|
||||||
|
context: Context,
|
||||||
|
latitude: Double,
|
||||||
|
longitude: Double,
|
||||||
|
accuracy: Double,
|
||||||
|
timestamp: Long,
|
||||||
|
source: String,
|
||||||
|
isMocked: Boolean,
|
||||||
|
speed: Float?,
|
||||||
|
acceleration: Float?,
|
||||||
|
directionRad: Float?,
|
||||||
|
) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putFloat(KEY_LAT, latitude.toFloat())
|
||||||
|
.putFloat(KEY_LON, longitude.toFloat())
|
||||||
|
.putFloat(KEY_ACC, accuracy.toFloat())
|
||||||
|
.putLong(KEY_TS, timestamp)
|
||||||
|
.putString(KEY_SRC, source)
|
||||||
|
.putBoolean(KEY_MOCK, isMocked)
|
||||||
|
.apply()
|
||||||
|
|
||||||
|
// Optional IMU fields
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putFloat(KEY_SPEED, speed ?: 0f)
|
||||||
|
.putFloat(KEY_ACCEL, acceleration ?: 0f)
|
||||||
|
.putFloat(KEY_DIR, directionRad ?: 0f)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
data class BgFix(
|
||||||
|
val latitude: Double,
|
||||||
|
val longitude: Double,
|
||||||
|
val accuracy: Double,
|
||||||
|
val timestamp: Long,
|
||||||
|
val source: String,
|
||||||
|
val isMocked: Boolean,
|
||||||
|
val speed: Float,
|
||||||
|
val acceleration: Float,
|
||||||
|
val directionRad: Float,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun readLatestFix(context: Context): BgFix? {
|
||||||
|
val sp = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
val ts = sp.getLong(KEY_TS, 0L)
|
||||||
|
if (ts == 0L) return null
|
||||||
|
val lat = sp.getFloat(KEY_LAT, 0f).toDouble()
|
||||||
|
val lon = sp.getFloat(KEY_LON, 0f).toDouble()
|
||||||
|
val acc = sp.getFloat(KEY_ACC, 999f).toDouble()
|
||||||
|
val src = sp.getString(KEY_SRC, "GNSS") ?: "GNSS"
|
||||||
|
val mocked = sp.getBoolean(KEY_MOCK, false)
|
||||||
|
val speed = sp.getFloat(KEY_SPEED, 0f)
|
||||||
|
val accel = sp.getFloat(KEY_ACCEL, 0f)
|
||||||
|
val dir = sp.getFloat(KEY_DIR, 0f)
|
||||||
|
return BgFix(lat, lon, acc, ts, src, mocked, speed, accel, dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBackgroundIntervalMs(context: Context, intervalMs: Long) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putLong(KEY_BG_INTERVAL, intervalMs)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBackgroundIntervalMs(context: Context, defaultMs: Long = 5000L): Long {
|
||||||
|
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getLong(KEY_BG_INTERVAL, defaultMs)
|
||||||
|
.coerceAtLeast(1000L)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBackgroundPostMinDistanceMeters(context: Context, meters: Double) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putFloat(KEY_POST_MIN_DIST_M, meters.toFloat())
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBackgroundPostMinDistanceMeters(context: Context, defaultMeters: Double = 10.0): Double {
|
||||||
|
val v = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getFloat(KEY_POST_MIN_DIST_M, defaultMeters.toFloat())
|
||||||
|
return if (v <= 0f) defaultMeters else v.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBackgroundPostMinAccuracyMeters(context: Context, meters: Double) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putFloat(KEY_POST_MIN_ACC_M, meters.toFloat())
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBackgroundPostMinAccuracyMeters(context: Context, defaultMeters: Double = Double.POSITIVE_INFINITY): Double {
|
||||||
|
val v = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getFloat(KEY_POST_MIN_ACC_M, if (defaultMeters.isFinite()) defaultMeters.toFloat() else Float.POSITIVE_INFINITY)
|
||||||
|
return if (v <= 0f || v.isInfinite()) Double.POSITIVE_INFINITY else v.toDouble()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setBackgroundMinPostIntervalMs(context: Context, intervalMs: Long) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putLong(KEY_POST_MIN_INTERVAL_MS, intervalMs)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getBackgroundMinPostIntervalMs(context: Context, defaultMs: Long = 10_000L): Long {
|
||||||
|
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getLong(KEY_POST_MIN_INTERVAL_MS, defaultMs)
|
||||||
|
.coerceAtLeast(0L)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setPostUrl(context: Context, url: String?) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.apply {
|
||||||
|
if (url.isNullOrBlank()) remove(KEY_POST_URL) else putString(KEY_POST_URL, url)
|
||||||
|
}
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPostUrl(context: Context): String? {
|
||||||
|
return context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.getString(KEY_POST_URL, null)
|
||||||
|
}
|
||||||
|
|
||||||
|
data class PostedFix(val latitude: Double, val longitude: Double, val timestamp: Long)
|
||||||
|
|
||||||
|
fun setLastPostedFix(context: Context, lat: Double, lon: Double, ts: Long) {
|
||||||
|
context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
.edit()
|
||||||
|
.putFloat(KEY_POST_LAST_LAT, lat.toFloat())
|
||||||
|
.putFloat(KEY_POST_LAST_LON, lon.toFloat())
|
||||||
|
.putLong(KEY_POST_LAST_TS, ts)
|
||||||
|
.apply()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getLastPostedFix(context: Context): PostedFix? {
|
||||||
|
val sp = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||||
|
val ts = sp.getLong(KEY_POST_LAST_TS, 0L)
|
||||||
|
if (ts == 0L) return null
|
||||||
|
val lat = sp.getFloat(KEY_POST_LAST_LAT, 0f).toDouble()
|
||||||
|
val lon = sp.getFloat(KEY_POST_LAST_LON, 0f).toDouble()
|
||||||
|
return PostedFix(lat, lon, ts)
|
||||||
|
}
|
||||||
|
}
|
||||||
158
dist/docs.json
vendored
158
dist/docs.json
vendored
@ -89,6 +89,136 @@
|
|||||||
"complexTypes": [],
|
"complexTypes": [],
|
||||||
"slug": "getlocationservicesstatus"
|
"slug": "getlocationservicesstatus"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "startBackgroundTracking",
|
||||||
|
"signature": "(options?: { title?: string | undefined; text?: string | undefined; channelId?: string | undefined; channelName?: string | undefined; postUrl?: string | undefined; } | undefined) => Promise<void>",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "options",
|
||||||
|
"docs": "",
|
||||||
|
"type": "{ title?: string | undefined; text?: string | undefined; channelId?: string | undefined; channelName?: string | undefined; postUrl?: string | undefined; } | undefined"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "Promise<void>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "startbackgroundtracking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "stopBackgroundTracking",
|
||||||
|
"signature": "() => Promise<void>",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "Promise<void>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "stopbackgroundtracking"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "isBackgroundTrackingActive",
|
||||||
|
"signature": "() => Promise<{ active: boolean; }>",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "Promise<{ active: boolean; }>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "isbackgroundtrackingactive"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getBackgroundLatestPosition",
|
||||||
|
"signature": "() => Promise<PositioningData | null>",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "Promise<PositioningData | null>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [
|
||||||
|
"PositioningData"
|
||||||
|
],
|
||||||
|
"slug": "getbackgroundlatestposition"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "openBackgroundPermissionSettings",
|
||||||
|
"signature": "() => Promise<void>",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "Promise<void>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "openbackgroundpermissionsettings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "openNotificationPermissionSettings",
|
||||||
|
"signature": "() => Promise<void>",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "Promise<void>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "opennotificationpermissionsettings"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "setAuthTokens",
|
||||||
|
"signature": "(tokens: { accessToken: string; refreshToken: string; }) => Promise<void>",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "tokens",
|
||||||
|
"docs": "",
|
||||||
|
"type": "{ accessToken: string; refreshToken: string; }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "Promise<void>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "setauthtokens"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "clearAuthTokens",
|
||||||
|
"signature": "() => Promise<void>",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "Promise<void>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "clearauthtokens"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getAuthState",
|
||||||
|
"signature": "() => Promise<{ present: boolean; }>",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "Promise<{ present: boolean; }>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "getauthstate"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "setBackgroundPostUrl",
|
||||||
|
"signature": "(options: { url?: string; }) => Promise<void>",
|
||||||
|
"parameters": [
|
||||||
|
{
|
||||||
|
"name": "options",
|
||||||
|
"docs": "",
|
||||||
|
"type": "{ url?: string | undefined; }"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"returns": "Promise<void>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "setbackgroundposturl"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "getBackgroundPostUrl",
|
||||||
|
"signature": "() => Promise<{ url: string | null; }>",
|
||||||
|
"parameters": [],
|
||||||
|
"returns": "Promise<{ url: string | null; }>",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"slug": "getbackgroundposturl"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "configureEdgeToEdge",
|
"name": "configureEdgeToEdge",
|
||||||
"signature": "(options: { bgColor: string; style: 'DARK' | 'LIGHT'; overlay?: boolean; }) => Promise<void>",
|
"signature": "(options: { bgColor: string; style: 'DARK' | 'LIGHT'; overlay?: boolean; }) => Promise<void>",
|
||||||
@ -372,6 +502,34 @@
|
|||||||
"docs": "",
|
"docs": "",
|
||||||
"complexTypes": [],
|
"complexTypes": [],
|
||||||
"type": "boolean | undefined"
|
"type": "boolean | undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "backgroundPollingIntervalMs",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"type": "number | undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "backgroundPostMinDistanceMeters",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"type": "number | undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "backgroundPostMinAccuracyMeters",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"type": "number | undefined"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "backgroundMinPostIntervalMs",
|
||||||
|
"tags": [],
|
||||||
|
"docs": "",
|
||||||
|
"complexTypes": [],
|
||||||
|
"type": "number | undefined"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
32
dist/esm/definitions.d.ts
vendored
32
dist/esm/definitions.d.ts
vendored
@ -32,6 +32,10 @@ export interface DumonGeoOptions {
|
|||||||
emitGnssStatus?: boolean;
|
emitGnssStatus?: boolean;
|
||||||
suppressMockedUpdates?: boolean;
|
suppressMockedUpdates?: boolean;
|
||||||
keepScreenOn?: boolean;
|
keepScreenOn?: boolean;
|
||||||
|
backgroundPollingIntervalMs?: number;
|
||||||
|
backgroundPostMinDistanceMeters?: number;
|
||||||
|
backgroundPostMinAccuracyMeters?: number;
|
||||||
|
backgroundMinPostIntervalMs?: number;
|
||||||
}
|
}
|
||||||
export interface PermissionStatus {
|
export interface PermissionStatus {
|
||||||
location: 'granted' | 'denied';
|
location: 'granted' | 'denied';
|
||||||
@ -48,6 +52,34 @@ export interface DumonGeolocationPlugin {
|
|||||||
gpsEnabled: boolean;
|
gpsEnabled: boolean;
|
||||||
networkEnabled: boolean;
|
networkEnabled: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
startBackgroundTracking(options?: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
channelId?: string;
|
||||||
|
channelName?: string;
|
||||||
|
postUrl?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
stopBackgroundTracking(): Promise<void>;
|
||||||
|
isBackgroundTrackingActive(): Promise<{
|
||||||
|
active: boolean;
|
||||||
|
}>;
|
||||||
|
getBackgroundLatestPosition(): Promise<PositioningData | null>;
|
||||||
|
openBackgroundPermissionSettings(): Promise<void>;
|
||||||
|
openNotificationPermissionSettings(): Promise<void>;
|
||||||
|
setAuthTokens(tokens: {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
clearAuthTokens(): Promise<void>;
|
||||||
|
getAuthState(): Promise<{
|
||||||
|
present: boolean;
|
||||||
|
}>;
|
||||||
|
setBackgroundPostUrl(options: {
|
||||||
|
url?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
getBackgroundPostUrl(): Promise<{
|
||||||
|
url: string | null;
|
||||||
|
}>;
|
||||||
configureEdgeToEdge(options: {
|
configureEdgeToEdge(options: {
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
style: 'DARK' | 'LIGHT';
|
style: 'DARK' | 'LIGHT';
|
||||||
|
|||||||
2
dist/esm/definitions.js.map
vendored
2
dist/esm/definitions.js.map
vendored
@ -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 predicted?: boolean;\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}\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\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 predicted?: boolean;\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"]}
|
||||||
28
dist/esm/web.d.ts
vendored
28
dist/esm/web.d.ts
vendored
@ -19,4 +19,32 @@ export declare class DumonGeolocationWeb extends WebPlugin {
|
|||||||
gpsEnabled: boolean;
|
gpsEnabled: boolean;
|
||||||
networkEnabled: boolean;
|
networkEnabled: boolean;
|
||||||
}>;
|
}>;
|
||||||
|
startBackgroundTracking(_options?: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
channelId?: string;
|
||||||
|
channelName?: string;
|
||||||
|
postUrl?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
stopBackgroundTracking(): Promise<void>;
|
||||||
|
isBackgroundTrackingActive(): Promise<{
|
||||||
|
active: boolean;
|
||||||
|
}>;
|
||||||
|
getBackgroundLatestPosition(): Promise<PositioningData | null>;
|
||||||
|
openBackgroundPermissionSettings(): Promise<void>;
|
||||||
|
openNotificationPermissionSettings(): Promise<void>;
|
||||||
|
setAuthTokens(_tokens: {
|
||||||
|
accessToken: string;
|
||||||
|
refreshToken: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
clearAuthTokens(): Promise<void>;
|
||||||
|
getAuthState(): Promise<{
|
||||||
|
present: boolean;
|
||||||
|
}>;
|
||||||
|
setBackgroundPostUrl(_options: {
|
||||||
|
url?: string;
|
||||||
|
}): Promise<void>;
|
||||||
|
getBackgroundPostUrl(): Promise<{
|
||||||
|
url: string | null;
|
||||||
|
}>;
|
||||||
}
|
}
|
||||||
|
|||||||
34
dist/esm/web.js
vendored
34
dist/esm/web.js
vendored
@ -41,5 +41,39 @@ export class DumonGeolocationWeb extends WebPlugin {
|
|||||||
// Web stub; assume enabled
|
// Web stub; assume enabled
|
||||||
return { gpsEnabled: true, networkEnabled: true };
|
return { gpsEnabled: true, networkEnabled: true };
|
||||||
}
|
}
|
||||||
|
// Background tracking stubs (no-op on web)
|
||||||
|
async startBackgroundTracking(_options) {
|
||||||
|
console.info('[dumon-geolocation] startBackgroundTracking is not supported on web.');
|
||||||
|
}
|
||||||
|
async stopBackgroundTracking() {
|
||||||
|
console.info('[dumon-geolocation] stopBackgroundTracking is not supported on web.');
|
||||||
|
}
|
||||||
|
async isBackgroundTrackingActive() {
|
||||||
|
return { active: false };
|
||||||
|
}
|
||||||
|
async getBackgroundLatestPosition() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async openBackgroundPermissionSettings() {
|
||||||
|
console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');
|
||||||
|
}
|
||||||
|
async openNotificationPermissionSettings() {
|
||||||
|
console.info('[dumon-geolocation] openNotificationPermissionSettings is not supported on web.');
|
||||||
|
}
|
||||||
|
async setAuthTokens(_tokens) {
|
||||||
|
console.info('[dumon-geolocation] setAuthTokens is a no-op on web.');
|
||||||
|
}
|
||||||
|
async clearAuthTokens() {
|
||||||
|
console.info('[dumon-geolocation] clearAuthTokens is a no-op on web.');
|
||||||
|
}
|
||||||
|
async getAuthState() {
|
||||||
|
return { present: false };
|
||||||
|
}
|
||||||
|
async setBackgroundPostUrl(_options) {
|
||||||
|
console.info('[dumon-geolocation] setBackgroundPostUrl is not supported on web.');
|
||||||
|
}
|
||||||
|
async getBackgroundPostUrl() {
|
||||||
|
return { url: null };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//# sourceMappingURL=web.js.map
|
//# sourceMappingURL=web.js.map
|
||||||
2
dist/esm/web.js.map
vendored
2
dist/esm/web.js.map
vendored
File diff suppressed because one or more lines are too long
34
dist/plugin.cjs.js
vendored
34
dist/plugin.cjs.js
vendored
@ -48,6 +48,40 @@ class DumonGeolocationWeb extends core.WebPlugin {
|
|||||||
// Web stub; assume enabled
|
// Web stub; assume enabled
|
||||||
return { gpsEnabled: true, networkEnabled: true };
|
return { gpsEnabled: true, networkEnabled: true };
|
||||||
}
|
}
|
||||||
|
// Background tracking stubs (no-op on web)
|
||||||
|
async startBackgroundTracking(_options) {
|
||||||
|
console.info('[dumon-geolocation] startBackgroundTracking is not supported on web.');
|
||||||
|
}
|
||||||
|
async stopBackgroundTracking() {
|
||||||
|
console.info('[dumon-geolocation] stopBackgroundTracking is not supported on web.');
|
||||||
|
}
|
||||||
|
async isBackgroundTrackingActive() {
|
||||||
|
return { active: false };
|
||||||
|
}
|
||||||
|
async getBackgroundLatestPosition() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async openBackgroundPermissionSettings() {
|
||||||
|
console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');
|
||||||
|
}
|
||||||
|
async openNotificationPermissionSettings() {
|
||||||
|
console.info('[dumon-geolocation] openNotificationPermissionSettings is not supported on web.');
|
||||||
|
}
|
||||||
|
async setAuthTokens(_tokens) {
|
||||||
|
console.info('[dumon-geolocation] setAuthTokens is a no-op on web.');
|
||||||
|
}
|
||||||
|
async clearAuthTokens() {
|
||||||
|
console.info('[dumon-geolocation] clearAuthTokens is a no-op on web.');
|
||||||
|
}
|
||||||
|
async getAuthState() {
|
||||||
|
return { present: false };
|
||||||
|
}
|
||||||
|
async setBackgroundPostUrl(_options) {
|
||||||
|
console.info('[dumon-geolocation] setBackgroundPostUrl is not supported on web.');
|
||||||
|
}
|
||||||
|
async getBackgroundPostUrl() {
|
||||||
|
return { url: null };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var web = /*#__PURE__*/Object.freeze({
|
var web = /*#__PURE__*/Object.freeze({
|
||||||
|
|||||||
2
dist/plugin.cjs.js.map
vendored
2
dist/plugin.cjs.js.map
vendored
File diff suppressed because one or more lines are too long
34
dist/plugin.js
vendored
34
dist/plugin.js
vendored
@ -47,6 +47,40 @@ var capacitorDumonGeolocation = (function (exports, core) {
|
|||||||
// Web stub; assume enabled
|
// Web stub; assume enabled
|
||||||
return { gpsEnabled: true, networkEnabled: true };
|
return { gpsEnabled: true, networkEnabled: true };
|
||||||
}
|
}
|
||||||
|
// Background tracking stubs (no-op on web)
|
||||||
|
async startBackgroundTracking(_options) {
|
||||||
|
console.info('[dumon-geolocation] startBackgroundTracking is not supported on web.');
|
||||||
|
}
|
||||||
|
async stopBackgroundTracking() {
|
||||||
|
console.info('[dumon-geolocation] stopBackgroundTracking is not supported on web.');
|
||||||
|
}
|
||||||
|
async isBackgroundTrackingActive() {
|
||||||
|
return { active: false };
|
||||||
|
}
|
||||||
|
async getBackgroundLatestPosition() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
async openBackgroundPermissionSettings() {
|
||||||
|
console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');
|
||||||
|
}
|
||||||
|
async openNotificationPermissionSettings() {
|
||||||
|
console.info('[dumon-geolocation] openNotificationPermissionSettings is not supported on web.');
|
||||||
|
}
|
||||||
|
async setAuthTokens(_tokens) {
|
||||||
|
console.info('[dumon-geolocation] setAuthTokens is a no-op on web.');
|
||||||
|
}
|
||||||
|
async clearAuthTokens() {
|
||||||
|
console.info('[dumon-geolocation] clearAuthTokens is a no-op on web.');
|
||||||
|
}
|
||||||
|
async getAuthState() {
|
||||||
|
return { present: false };
|
||||||
|
}
|
||||||
|
async setBackgroundPostUrl(_options) {
|
||||||
|
console.info('[dumon-geolocation] setBackgroundPostUrl is not supported on web.');
|
||||||
|
}
|
||||||
|
async getBackgroundPostUrl() {
|
||||||
|
return { url: null };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var web = /*#__PURE__*/Object.freeze({
|
var web = /*#__PURE__*/Object.freeze({
|
||||||
|
|||||||
2
dist/plugin.js.map
vendored
2
dist/plugin.js.map
vendored
File diff suppressed because one or more lines are too long
386
documentation/PLUGIN_REFERENCE.md
Normal file
386
documentation/PLUGIN_REFERENCE.md
Normal file
@ -0,0 +1,386 @@
|
|||||||
|
# Dumon Geolocation Plugin — Reference & Internals
|
||||||
|
|
||||||
|
Dokumen ini merangkum API publik, perilaku runtime, state internal, serta contoh penggunaan plugin dumon‑geolocation (Android). Disusun agar mudah dipahami developer aplikasi.
|
||||||
|
|
||||||
|
## Ringkasan
|
||||||
|
|
||||||
|
- Platform: Android (web memiliki stub; iOS saat ini bukan target utama).
|
||||||
|
- Sumber data: GNSS (GPS/GLONASS/BeiDou/Galileo), IMU (accelerometer/gyro/rotation), Wi‑Fi (RTT/RSSI untuk diagnostik), deteksi lokasi palsu (mock).
|
||||||
|
- Update real‑time melalui event `onPositionUpdate` dengan debounce dan threshold perubahan posisi/kecepatan/arah.
|
||||||
|
- getLatestPosition: mencoba satu kali pembacaan “fresh” (one‑shot) lalu fallback ke snapshot terakhir bila gagal/timeout.
|
||||||
|
- Opsi prediksi maju (dead‑reckoning) opsional untuk memproyeksikan posisi pendek ke depan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API Publik (TypeScript)
|
||||||
|
|
||||||
|
Tipe didefinisikan di `dumon-geolocation/src/definitions.ts`.
|
||||||
|
|
||||||
|
### Interfaces
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export interface PositioningData {
|
||||||
|
source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';
|
||||||
|
timestamp: number; // epoch ms
|
||||||
|
latitude: number;
|
||||||
|
longitude: number;
|
||||||
|
accuracy: number; // meter
|
||||||
|
speed: number; // m/s (dari IMU heuristik)
|
||||||
|
acceleration: number; // m/s^2 (magnitude IMU)
|
||||||
|
directionRad: number; // radian, relatif utara
|
||||||
|
isMocked: boolean; // lokasi terdeteksi palsu
|
||||||
|
predicted?: boolean; // true jika posisi diproyeksikan ke depan
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SatelliteStatus {
|
||||||
|
satellitesInView: number;
|
||||||
|
usedInFix: number;
|
||||||
|
constellationCounts: { [key: string]: number };
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DumonGeoOptions {
|
||||||
|
distanceThresholdMeters?: number; // default ~7.0
|
||||||
|
speedChangeThreshold?: number; // default ~0.5 m/s
|
||||||
|
directionChangeThreshold?: number; // default ~0.17 rad (~10°)
|
||||||
|
emitDebounceMs?: number; // default adaptif; awal 1000 ms
|
||||||
|
drivingEmitIntervalMs?: number; // default ~1600 ms
|
||||||
|
wifiScanIntervalMs?: number; // default ~3000 ms
|
||||||
|
enableWifiRtt?: boolean; // default true
|
||||||
|
enableLogging?: boolean; // default off → bisa on lewat options
|
||||||
|
enableForwardPrediction?: boolean; // default false
|
||||||
|
maxPredictionSeconds?: number; // default 1.0, clamp 0..5
|
||||||
|
emitGnssStatus?: boolean; // default false
|
||||||
|
suppressMockedUpdates?: boolean; // default false
|
||||||
|
keepScreenOn?: boolean; // default false
|
||||||
|
backgroundPollingIntervalMs?: number; // Android: interval polling service (default 5000)
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PermissionStatus {
|
||||||
|
location: 'granted' | 'denied';
|
||||||
|
wifi: 'granted' | 'denied';
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Methods
|
||||||
|
|
||||||
|
```ts
|
||||||
|
startPositioning(): Promise<void>
|
||||||
|
```
|
||||||
|
- Mulai GNSS, IMU, dan Wi‑Fi scanning periodik (RTT opsional). Mengaktifkan loop emisi posisi.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
stopPositioning(): Promise<void>
|
||||||
|
```
|
||||||
|
- Hentikan semua sensor dan emisi update.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
getLatestPosition(): Promise<PositioningData>
|
||||||
|
```
|
||||||
|
- Mencoba mengambil 1x lokasi “fresh” (one‑shot) dari provider (GPS + opsional Network) dengan timeout singkat (~3000 ms). Jika gagal/timeout atau disupress (mock), fallback ke snapshot terakhir yang tersimpan.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
checkAndRequestPermissions(): Promise<PermissionStatus>
|
||||||
|
```
|
||||||
|
- Memeriksa dan (bila perlu) meminta semua izin lokasi/Wi‑Fi yang diperlukan.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
setOptions(options: DumonGeoOptions): Promise<void>
|
||||||
|
```
|
||||||
|
- Mengubah opsi runtime (threshold, interval, logging, prediksi, GNSS status, Wi‑Fi RTT, dll). Sebagian opsi langsung mempengaruhi perilaku GPS polling dan loop emisi.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
setGpsMode(options: { mode: 'normal' | 'driving' }): Promise<void>
|
||||||
|
```
|
||||||
|
- `normal`: polling adaptif (hemat baterai). `driving`: continuous GPS + loop emisi paksa berkala.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
configureEdgeToEdge(options: { bgColor: string; style: 'DARK' | 'LIGHT'; overlay?: boolean }): Promise<void>
|
||||||
|
```
|
||||||
|
- Mengatur status bar/navigation bar agar sesuai tema UI.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
getGnssStatus(): Promise<SatelliteStatus | null>
|
||||||
|
```
|
||||||
|
- Mengambil status satelit terakhir (untuk diagnostik). Listener real‑time tersedia jika `emitGnssStatus` diaktifkan.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>
|
||||||
|
```
|
||||||
|
- Memeriksa apakah provider GNSS dan Network aktif di perangkat.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Android only — background tracking via Foreground Service
|
||||||
|
startBackgroundTracking(options?: { title?: string; text?: string; channelId?: string; channelName?: string; postUrl?: string }): Promise<void>
|
||||||
|
stopBackgroundTracking(): Promise<void>
|
||||||
|
isBackgroundTrackingActive(): Promise<{ active: boolean }>
|
||||||
|
getBackgroundLatestPosition(): Promise<PositioningData | null>
|
||||||
|
setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>
|
||||||
|
clearAuthTokens(): Promise<void>
|
||||||
|
getAuthState(): Promise<{ present: boolean }>
|
||||||
|
setBackgroundPostUrl(options: { url?: string }): Promise<void>
|
||||||
|
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 <token>` dan `refresh-token: <token>`. Di web: no‑op.
|
||||||
|
|
||||||
|
### Events
|
||||||
|
|
||||||
|
```ts
|
||||||
|
addListener('onPositionUpdate', (data: PositioningData) => void)
|
||||||
|
```
|
||||||
|
- Menerima update posisi real‑time dengan debounce dan threshold perubahan. Di mode `driving`, emisi dipaksa periodik (default ~1600 ms) agar UI responsif saat bergerak cepat.
|
||||||
|
|
||||||
|
```ts
|
||||||
|
addListener('onGnssStatus', (data: SatelliteStatus) => void)
|
||||||
|
```
|
||||||
|
- Mengalirkan status GNSS jika `emitGnssStatus: true` diaktifkan.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Perilaku & State Internal (Android)
|
||||||
|
|
||||||
|
Implementasi utama: `android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt`.
|
||||||
|
|
||||||
|
### State penting (ringkas)
|
||||||
|
|
||||||
|
- `latestLatitude/Longitude/Accuracy/Source/Timestamp`: snapshot posisi terakhir dari GNSS.
|
||||||
|
- `latestImu: ImuData?`: speed, acceleration, directionRad dari IMU (heuristik, dismoothing).
|
||||||
|
- `isMockedLocation`: flag hasil deteksi mock dari OS.
|
||||||
|
- `prevLatitude/Longitude`, `prevSpeed`, `prevDirection`: baseline untuk menentukan perubahan signifikan.
|
||||||
|
- Threshold dan interval:
|
||||||
|
- `significantChangeThreshold` (meter), `speedChangeThreshold` (m/s), `directionChangeThreshold` (rad).
|
||||||
|
- `emitIntervalMs`: base debounce; diatur adaptif via kecepatan IMU.
|
||||||
|
- `drivingEmitIntervalMs`: interval emisi paksa di mode `driving`.
|
||||||
|
- Opsi runtime:
|
||||||
|
- `enableWifiRtt`, `enableForwardPrediction`, `maxPredictionSeconds`, `emitGnssStatus`, `suppressMockedUpdates`, `keepScreenOn`.
|
||||||
|
- Mode GPS: `NORMAL` (polling) vs `DRIVING` (continuous + emit paksa).
|
||||||
|
|
||||||
|
### Sumber data
|
||||||
|
|
||||||
|
- GNSS: `GpsStatusManager`
|
||||||
|
- Mode polling adaptif (memicu request single update secara berkala) atau continuous (mode `driving`).
|
||||||
|
- `requestSingleFix(timeoutMs, useNetworkProvider)` untuk one‑shot lokasi dengan timeout.
|
||||||
|
- Meneruskan status satelit via callback.
|
||||||
|
|
||||||
|
- IMU: `ImuSensorManager`
|
||||||
|
- Menggabungkan accelerometer, gyroscope, rotation vector untuk estimasi `speed`, `acceleration`, `directionRad`.
|
||||||
|
- Menyetel delay sensor dan interval emisi adaptif berdasarkan kecepatan.
|
||||||
|
|
||||||
|
- Wi‑Fi: `WifiPositioningManager`
|
||||||
|
- Scan periodik; jika RTT didukung dan diaktifkan, akan mencoba pengukuran jarak AP (diagnostik). Data Wi‑Fi saat ini belum dipakai untuk menghitung posisi final; menjadi sinyal tambahan di update.
|
||||||
|
|
||||||
|
### Emisi update posisi
|
||||||
|
|
||||||
|
- `emitPositionUpdate()` dipicu oleh perubahan GNSS/IMU/Wi‑Fi dengan aturan:
|
||||||
|
- Hanya emit jika lewat debounce dan terjadi perubahan signifikan: jarak, selisih speed, atau selisih arah.
|
||||||
|
- Di mode `driving`, ada loop yang memaksa emisi periodik supaya UI tetap responsif.
|
||||||
|
- Jika `suppressMockedUpdates` aktif dan lokasi terdeteksi mock, emisi dibatalkan.
|
||||||
|
|
||||||
|
### Prediksi maju (opsional)
|
||||||
|
|
||||||
|
- Jika `enableForwardPrediction` aktif dan ada IMU, posisi dapat diproyeksikan pendek (<= `maxPredictionSeconds`) memakai `speed` dan `directionRad` → flag `predicted: true`.
|
||||||
|
- `source` tetap diisi nilai asli (mis. `GNSS`), bukan `FUSED`.
|
||||||
|
|
||||||
|
### getLatestPosition (perilaku terbaru)
|
||||||
|
|
||||||
|
- Memicu `GpsStatusManager.requestSingleFix()` (GPS + opsional Network) dengan timeout default ~3000 ms.
|
||||||
|
- Jika berhasil dan tidak disupress (mock), update state lalu kembalikan hasil `buildPositionData()`.
|
||||||
|
- Jika timeout atau disupress, fallback ke snapshot terakhir.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Driving Mode vs Normal Mode (Android)
|
||||||
|
|
||||||
|
Perbedaan inti antara kedua mode ini ada pada cara GPS diakses dan bagaimana event dikirim ke JS.
|
||||||
|
|
||||||
|
- Pola akses GPS
|
||||||
|
- Driving: continuous updates (GPS selalu aktif dengan requestLocationUpdates 0 ms/0 m). Cocok untuk navigasi real‑time dan pergerakan cepat.
|
||||||
|
- Normal: polling periodik (one‑shot tiap interval) dengan handler internal. Lebih hemat baterai.
|
||||||
|
|
||||||
|
- Emisi ke JS
|
||||||
|
- Driving: ada loop emisi paksa (force emit) menggunakan lokasi yang di‑buffer, pada interval `drivingEmitIntervalMs` (default ~1600 ms) agar UI tetap responsif.
|
||||||
|
- Normal: emisi hanya terjadi bila melewati debounce (`emitIntervalMs`) dan ada perubahan signifikan (jarak/speed/arah) dari baseline sebelumnya.
|
||||||
|
|
||||||
|
- Interval & adaptasi
|
||||||
|
- Driving: diatur oleh `drivingEmitIntervalMs` (ubah via `setOptions`).
|
||||||
|
- Normal: `emitIntervalMs` diatur adaptif berdasarkan estimasi kecepatan IMU, kira‑kira:
|
||||||
|
- > 5 m/s → 3000 ms
|
||||||
|
- > 1.5 m/s → 8000 ms
|
||||||
|
- > 0.3 m/s → 20000 ms
|
||||||
|
- idle → 30000 ms
|
||||||
|
|
||||||
|
- Trade‑off
|
||||||
|
- Driving: latency rendah, konsumsi baterai lebih tinggi.
|
||||||
|
- Normal: hemat baterai, cukup untuk use‑case non‑navigasi.
|
||||||
|
|
||||||
|
- Cara ganti mode
|
||||||
|
- `setGpsMode({ mode: 'driving' })` atau `setGpsMode({ mode: 'normal' })`.
|
||||||
|
|
||||||
|
## Permissions (Android)
|
||||||
|
|
||||||
|
- Wajib: `ACCESS_FINE_LOCATION` (GNSS), `ACCESS_COARSE_LOCATION` (Network provider/Wi‑Fi scan), `ACCESS_WIFI_STATE`, `CHANGE_WIFI_STATE`.
|
||||||
|
- Android 13+: `NEARBY_WIFI_DEVICES` untuk RTT.
|
||||||
|
- Disarankan: aktifkan Location Services (GPS dan Network) agar one‑shot lebih cepat.
|
||||||
|
|
||||||
|
Gunakan `checkAndRequestPermissions()` sebelum memulai.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Contoh Penggunaan
|
||||||
|
|
||||||
|
### Persiapan dasar
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { DumonGeolocation } from 'dumon-geolocation';
|
||||||
|
|
||||||
|
// 1) Minta izin
|
||||||
|
await DumonGeolocation.checkAndRequestPermissions();
|
||||||
|
|
||||||
|
// 2) Opsi (opsional)
|
||||||
|
await DumonGeolocation.setOptions({
|
||||||
|
enableLogging: true,
|
||||||
|
emitDebounceMs: 1000,
|
||||||
|
enableForwardPrediction: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 3) Listener real-time
|
||||||
|
const handle = DumonGeolocation.addListener('onPositionUpdate', data => {
|
||||||
|
console.log('pos:', data);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 4) Mulai
|
||||||
|
await DumonGeolocation.startPositioning();
|
||||||
|
|
||||||
|
// ...
|
||||||
|
|
||||||
|
// 5) Stop bila perlu
|
||||||
|
await DumonGeolocation.stopPositioning();
|
||||||
|
await handle.remove();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Satu kali pembacaan (fresh + fallback)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const data = await DumonGeolocation.getLatestPosition();
|
||||||
|
console.log('latest:', data);
|
||||||
|
// Catatan: bisa menunggu ~3s untuk one-shot fix,
|
||||||
|
// lalu jatuh ke snapshot kalau timeout.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mode GPS
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await DumonGeolocation.setGpsMode({ mode: 'driving' }); // continuous + emit paksa
|
||||||
|
// ...
|
||||||
|
await DumonGeolocation.setGpsMode({ mode: 'normal' }); // polling adaptif
|
||||||
|
```
|
||||||
|
|
||||||
|
### GNSS status dan layanan lokasi
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await DumonGeolocation.setOptions({ emitGnssStatus: true });
|
||||||
|
const gnssHandle = DumonGeolocation.addListener('onGnssStatus', s => console.log('gnss:', s));
|
||||||
|
|
||||||
|
const services = await DumonGeolocation.getLocationServicesStatus();
|
||||||
|
console.log('services:', services);
|
||||||
|
```
|
||||||
|
|
||||||
|
### UI Edge‑to‑Edge
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await DumonGeolocation.configureEdgeToEdge({
|
||||||
|
bgColor: '#ffffff',
|
||||||
|
style: 'DARK',
|
||||||
|
overlay: false,
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
### Background tracking (Android)
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// Minta izin lokasi dan notifikasi (Android 13+) lebih dulu
|
||||||
|
await DumonGeolocation.checkAndRequestPermissions();
|
||||||
|
|
||||||
|
// Mulai Foreground Service untuk tracking di background
|
||||||
|
await DumonGeolocation.startBackgroundTracking({
|
||||||
|
title: 'Dumon tracking active',
|
||||||
|
text: 'Collecting location in background',
|
||||||
|
// opsional: posting otomatis setiap update lokasi (JSON)
|
||||||
|
postUrl: 'https://dumonapp.com/dev-test-cap',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Cek status dan tarik latest fix yang disimpan oleh service
|
||||||
|
const { active } = await DumonGeolocation.isBackgroundTrackingActive();
|
||||||
|
const latestBg = await DumonGeolocation.getBackgroundLatestPosition();
|
||||||
|
|
||||||
|
// Berhenti
|
||||||
|
await DumonGeolocation.stopBackgroundTracking();
|
||||||
|
|
||||||
|
// Opsi: set token autentikasi dan URL endpoint secara terpisah (dapat diubah kapan saja)
|
||||||
|
await DumonGeolocation.setAuthTokens({ accessToken: '<ACCESS>', refreshToken: '<REFRESH>' });
|
||||||
|
await DumonGeolocation.setBackgroundPostUrl({ url: 'https://dumonapp.com/dev-test-cap' });
|
||||||
|
|
||||||
|
// Kirim otomatis hanya jika perpindahan >= 10 meter (default 10 m); atur interval polling 5 detik
|
||||||
|
await DumonGeolocation.setOptions({ backgroundPostMinDistanceMeters: 10, backgroundPollingIntervalMs: 5000 });
|
||||||
|
|
||||||
|
// (Opsional) Tambah pembatasan akurasi & interval minimum antar POST
|
||||||
|
await DumonGeolocation.setOptions({ backgroundPostMinAccuracyMeters: 50, backgroundMinPostIntervalMs: 10000 });
|
||||||
|
|
||||||
|
// Catatan perilaku background:
|
||||||
|
// - Service melakukan polling one‑shot fix periodik dan menyimpan latest fix.
|
||||||
|
// - Posting ke REST API hanya terjadi jika bergerak minimal 'backgroundPostMinDistanceMeters' dari lokasi terakhir yang sukses diposting.
|
||||||
|
// - Jika GPS lemah (mis. indoor), service juga mencoba NETWORK provider saat polling agar tetap ada pembaruan.
|
||||||
|
// - Token Authorization/refresh‑token diambil dari penyimpanan native yang diisi via setAuthTokens().
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Catatan & Batasan
|
||||||
|
|
||||||
|
- Event JS tidak berjalan saat app di‑pause/background. Untuk update background yang andal, gunakan Foreground Service native (rencana peningkatan berikutnya).
|
||||||
|
- Prediksi maju bersifat heuristik (berbasis IMU); aktifkan hanya bila sesuai kebutuhan.
|
||||||
|
- Wi‑Fi RTT dibatasi oleh hardware/OS; di background, kemampuan scan bisa dibatasi OS.
|
||||||
|
- Web: stub (mengembalikan data dummy) sehingga hanya bermanfaat untuk pengembangan UI.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Rencana Peningkatan (arah)
|
||||||
|
|
||||||
|
- Background tracking via Foreground Service (lokasi dan penyimpanan/telemetri saat app di‑pause).
|
||||||
|
- Opsi konfigurasi timeouts/strategi one‑shot getLatestPosition via `setOptions`.
|
||||||
|
- Integrasi Fused Location Provider untuk TTFF/efisiensi lebih baik.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Build & Run (Example App)
|
||||||
|
|
||||||
|
Plugin build
|
||||||
|
```
|
||||||
|
cd dumon-geolocation
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Example app
|
||||||
|
```
|
||||||
|
cd dumon-geolocation/example-app
|
||||||
|
npm install
|
||||||
|
npm run build // <-- untuk update pack ke dist
|
||||||
|
npx cap sync android
|
||||||
|
npx cap run android
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatif: `npx cap open android` lalu Run dari Android Studio.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Referensi File Penting
|
||||||
|
|
||||||
|
- Plugin Android: `dumon-geolocation/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt`
|
||||||
|
- GNSS Manager: `dumon-geolocation/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager.kt`
|
||||||
|
- IMU Manager: `dumon-geolocation/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager.kt`
|
||||||
|
- Wi‑Fi Manager: `dumon-geolocation/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager.kt`
|
||||||
|
- Definisi TS: `dumon-geolocation/src/definitions.ts`
|
||||||
|
- Contoh App: `dumon-geolocation/example-app`
|
||||||
@ -10,3 +10,12 @@ To run the provided example, you can use `npm start` command.
|
|||||||
```bash
|
```bash
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Notes on getLatestPosition()
|
||||||
|
|
||||||
|
- The example now reflects the plugin's updated behavior: `getLatestPosition()` attempts a fresh one-shot location fix and falls back to the last cached snapshot on failure/timeout.
|
||||||
|
- The UI logs additional diagnostics:
|
||||||
|
- `_elapsedMs`: total call duration (includes waiting up to ~3000ms for a fresh fix).
|
||||||
|
- `_ageMs`: `Date.now() - timestamp` of the returned position.
|
||||||
|
- `_guessedFresh`: a simple heuristic based on the two numbers above to hint whether the result is likely fresh.
|
||||||
|
- Make sure to grant location permissions and enable location services for meaningful results.
|
||||||
|
|||||||
@ -13,6 +13,8 @@
|
|||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
margin: 1rem;
|
margin: 1rem;
|
||||||
background-color: #ffffff;
|
background-color: #ffffff;
|
||||||
|
padding-top: 56px;
|
||||||
|
padding-bottom: 56px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
@ -71,6 +73,10 @@
|
|||||||
<label>emitDebounceMs <input type="number" id="optEmitDebounceMs" placeholder="1000" /></label><br />
|
<label>emitDebounceMs <input type="number" id="optEmitDebounceMs" placeholder="1000" /></label><br />
|
||||||
<label>drivingEmitIntervalMs <input type="number" id="optDrivingEmitIntervalMs" placeholder="1600" /></label><br />
|
<label>drivingEmitIntervalMs <input type="number" id="optDrivingEmitIntervalMs" placeholder="1600" /></label><br />
|
||||||
<label>wifiScanIntervalMs <input type="number" id="optWifiScanIntervalMs" placeholder="3000" /></label><br />
|
<label>wifiScanIntervalMs <input type="number" id="optWifiScanIntervalMs" placeholder="3000" /></label><br />
|
||||||
|
<label>backgroundPollingIntervalMs <input type="number" id="optBgIntervalMs" placeholder="5000" /></label><br />
|
||||||
|
<label>backgroundPostMinDistanceMeters <input type="number" step="0.1" id="optBgPostMinDist" placeholder="10.0" /></label><br />
|
||||||
|
<label>backgroundPostMinAccuracyMeters <input type="number" step="0.1" id="optBgPostMinAcc" placeholder="" /></label><br />
|
||||||
|
<label>backgroundMinPostIntervalMs <input type="number" id="optBgMinPostInterval" placeholder="10000" /></label><br />
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label>distanceThresholdMeters <input type="number" step="0.1" id="optDistanceThreshold" placeholder="7.0" /></label><br />
|
<label>distanceThresholdMeters <input type="number" step="0.1" id="optDistanceThreshold" placeholder="7.0" /></label><br />
|
||||||
@ -85,6 +91,27 @@
|
|||||||
<button id="getGnssStatusButton">Get GNSS Status</button>
|
<button id="getGnssStatusButton">Get GNSS Status</button>
|
||||||
<button id="getLocationServicesStatusButton">Get Location Services Status</button>
|
<button id="getLocationServicesStatusButton">Get Location Services Status</button>
|
||||||
|
|
||||||
|
<h1>Background</h1>
|
||||||
|
<button id="startBgBtn">Start Background Tracking</button>
|
||||||
|
<button id="stopBgBtn">Stop Background Tracking</button>
|
||||||
|
<button id="getBgLatestBtn">Get Background Latest Position</button>
|
||||||
|
<button id="quickStartBgBtn" style="background-color:#00695c">Quick Start BG Posting</button>
|
||||||
|
<div style="margin-top:0.5rem">
|
||||||
|
<button id="openBgPermBtn" style="background-color:#795548">Open Background Permission Settings</button>
|
||||||
|
<button id="openNotifPermBtn" style="background-color:#795548">Open Notification Settings</button>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:0.5rem">
|
||||||
|
<label>Background POST URL <input type="text" id="bgPostUrl" placeholder="https://dumonapp.com/dev-test-cap" style="width:420px"/></label>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:0.5rem">
|
||||||
|
<label>Access Token <input type="text" id="bgAccessToken" placeholder="<ACCESS_TOKEN>" style="width:360px"/></label>
|
||||||
|
<label>Refresh Token <input type="text" id="bgRefreshToken" placeholder="<REFRESH_TOKEN>" style="width:360px"/></label>
|
||||||
|
<button id="saveTokensBtn" style="background-color:#8e24aa">Save Tokens</button>
|
||||||
|
<button id="clearTokensBtn" style="background-color:#8e24aa">Clear Tokens</button>
|
||||||
|
<button id="getAuthStateBtn" style="background-color:#8e24aa">Get Auth State</button>
|
||||||
|
<button id="getPostUrlBtn" style="background-color:#8e24aa">Get Post URL</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<pre id="logArea"></pre>
|
<pre id="logArea"></pre>
|
||||||
|
|
||||||
<script type="module" src="./js/example.js"></script>
|
<script type="module" src="./js/example.js"></script>
|
||||||
|
|||||||
@ -49,6 +49,10 @@ async function applyOptionsFromForm() {
|
|||||||
const emitDebounceMs = readNumber('optEmitDebounceMs');
|
const emitDebounceMs = readNumber('optEmitDebounceMs');
|
||||||
const drivingEmitIntervalMs = readNumber('optDrivingEmitIntervalMs');
|
const drivingEmitIntervalMs = readNumber('optDrivingEmitIntervalMs');
|
||||||
const wifiScanIntervalMs = readNumber('optWifiScanIntervalMs');
|
const wifiScanIntervalMs = readNumber('optWifiScanIntervalMs');
|
||||||
|
const backgroundPollingIntervalMs = readNumber('optBgIntervalMs');
|
||||||
|
const backgroundPostMinDistanceMeters = readNumber('optBgPostMinDist');
|
||||||
|
const backgroundPostMinAccuracyMeters = readNumber('optBgPostMinAcc');
|
||||||
|
const backgroundMinPostIntervalMs = readNumber('optBgMinPostInterval');
|
||||||
const distanceThresholdMeters = readNumber('optDistanceThreshold');
|
const distanceThresholdMeters = readNumber('optDistanceThreshold');
|
||||||
const speedChangeThreshold = readNumber('optSpeedThreshold');
|
const speedChangeThreshold = readNumber('optSpeedThreshold');
|
||||||
const directionChangeThreshold = readNumber('optDirectionThreshold');
|
const directionChangeThreshold = readNumber('optDirectionThreshold');
|
||||||
@ -57,6 +61,10 @@ async function applyOptionsFromForm() {
|
|||||||
if (emitDebounceMs !== undefined) options.emitDebounceMs = emitDebounceMs;
|
if (emitDebounceMs !== undefined) options.emitDebounceMs = emitDebounceMs;
|
||||||
if (drivingEmitIntervalMs !== undefined) options.drivingEmitIntervalMs = drivingEmitIntervalMs;
|
if (drivingEmitIntervalMs !== undefined) options.drivingEmitIntervalMs = drivingEmitIntervalMs;
|
||||||
if (wifiScanIntervalMs !== undefined) options.wifiScanIntervalMs = wifiScanIntervalMs;
|
if (wifiScanIntervalMs !== undefined) options.wifiScanIntervalMs = wifiScanIntervalMs;
|
||||||
|
if (backgroundPollingIntervalMs !== undefined) options.backgroundPollingIntervalMs = backgroundPollingIntervalMs;
|
||||||
|
if (backgroundPostMinDistanceMeters !== undefined) options.backgroundPostMinDistanceMeters = backgroundPostMinDistanceMeters;
|
||||||
|
if (backgroundPostMinAccuracyMeters !== undefined) options.backgroundPostMinAccuracyMeters = backgroundPostMinAccuracyMeters;
|
||||||
|
if (backgroundMinPostIntervalMs !== undefined) options.backgroundMinPostIntervalMs = backgroundMinPostIntervalMs;
|
||||||
if (distanceThresholdMeters !== undefined)
|
if (distanceThresholdMeters !== undefined)
|
||||||
options.distanceThresholdMeters = distanceThresholdMeters;
|
options.distanceThresholdMeters = distanceThresholdMeters;
|
||||||
if (speedChangeThreshold !== undefined) options.speedChangeThreshold = speedChangeThreshold;
|
if (speedChangeThreshold !== undefined) options.speedChangeThreshold = speedChangeThreshold;
|
||||||
@ -112,11 +120,33 @@ async function stopGeolocation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getLatestPosition() {
|
async function getLatestPosition() {
|
||||||
|
const btn = document.getElementById('getLatestButton');
|
||||||
|
const originalText = btn ? btn.textContent : '';
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = true;
|
||||||
|
btn.textContent = 'Getting…';
|
||||||
|
}
|
||||||
|
const t0 = performance.now();
|
||||||
try {
|
try {
|
||||||
const data = await DumonGeolocation.getLatestPosition();
|
const result = await DumonGeolocation.getLatestPosition();
|
||||||
appendLog('getLatestPosition', data);
|
const t1 = performance.now();
|
||||||
|
const elapsedMs = Math.round(t1 - t0);
|
||||||
|
const now = Date.now();
|
||||||
|
const ageMs = typeof result?.timestamp === 'number' ? Math.max(0, now - result.timestamp) : undefined;
|
||||||
|
const guessedFresh = typeof ageMs === 'number' ? (elapsedMs < 2900 && ageMs < 2500) : undefined;
|
||||||
|
appendLog('getLatestPosition', {
|
||||||
|
...result,
|
||||||
|
_elapsedMs: elapsedMs, // how long the call took (includes single-fix wait/timeout)
|
||||||
|
_ageMs: ageMs, // now - result.timestamp
|
||||||
|
_guessedFresh: guessedFresh,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
appendLog('getLatestPosition', { error: err.message });
|
appendLog('getLatestPosition', { error: err?.message || String(err) });
|
||||||
|
} finally {
|
||||||
|
if (btn) {
|
||||||
|
btn.disabled = false;
|
||||||
|
btn.textContent = originalText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,4 +204,128 @@ window.addEventListener('DOMContentLoaded', async () => {
|
|||||||
style: 'DARK',
|
style: 'DARK',
|
||||||
overlay: false,
|
overlay: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Background tracking controls
|
||||||
|
document.getElementById('startBgBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const postUrl = document.getElementById('bgPostUrl')?.value?.trim();
|
||||||
|
await DumonGeolocation.startBackgroundTracking({
|
||||||
|
title: 'Dumon tracking active',
|
||||||
|
text: 'Collecting location in background',
|
||||||
|
postUrl: postUrl || undefined,
|
||||||
|
});
|
||||||
|
appendLog('startBackgroundTracking', { success: true });
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('startBackgroundTracking', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('stopBgBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await DumonGeolocation.stopBackgroundTracking();
|
||||||
|
appendLog('stopBackgroundTracking', { success: true });
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('stopBackgroundTracking', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('getBgLatestBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const active = await DumonGeolocation.isBackgroundTrackingActive();
|
||||||
|
const latest = await DumonGeolocation.getBackgroundLatestPosition();
|
||||||
|
appendLog('backgroundStatus', { active });
|
||||||
|
appendLog('getBackgroundLatestPosition', latest);
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('getBackgroundLatestPosition', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('openBgPermBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await DumonGeolocation.openBackgroundPermissionSettings();
|
||||||
|
appendLog('openBackgroundPermissionSettings', { opened: true });
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('openBackgroundPermissionSettings', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('openNotifPermBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await DumonGeolocation.openNotificationPermissionSettings();
|
||||||
|
appendLog('openNotificationPermissionSettings', { opened: true });
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('openNotificationPermissionSettings', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quick Start: set tokens + endpoint + start BG service
|
||||||
|
document.getElementById('quickStartBgBtn').addEventListener('click', async () => {
|
||||||
|
const accessToken = document.getElementById('bgAccessToken')?.value?.trim();
|
||||||
|
const refreshToken = document.getElementById('bgRefreshToken')?.value?.trim();
|
||||||
|
const postUrl = document.getElementById('bgPostUrl')?.value?.trim();
|
||||||
|
if (!accessToken || !refreshToken || !postUrl) {
|
||||||
|
appendLog('quickStartBackground', { error: 'accessToken, refreshToken, and postUrl are required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await DumonGeolocation.setAuthTokens({ accessToken, refreshToken });
|
||||||
|
await DumonGeolocation.setBackgroundPostUrl({ url: postUrl });
|
||||||
|
await DumonGeolocation.startBackgroundTracking({
|
||||||
|
title: 'Dumon tracking active',
|
||||||
|
text: 'Collecting location in background',
|
||||||
|
});
|
||||||
|
const active = await DumonGeolocation.isBackgroundTrackingActive();
|
||||||
|
const auth = await DumonGeolocation.getAuthState();
|
||||||
|
const url = await DumonGeolocation.getBackgroundPostUrl();
|
||||||
|
appendLog('quickStartBackground', { success: true, active, auth, url });
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('quickStartBackground', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Helpers to inspect stored auth and post URL
|
||||||
|
document.getElementById('getAuthStateBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const state = await DumonGeolocation.getAuthState();
|
||||||
|
appendLog('getAuthState', state);
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('getAuthState', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('getPostUrlBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
const url = await DumonGeolocation.getBackgroundPostUrl();
|
||||||
|
appendLog('getBackgroundPostUrl', url);
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('getBackgroundPostUrl', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Tokens management
|
||||||
|
document.getElementById('saveTokensBtn').addEventListener('click', async () => {
|
||||||
|
const accessToken = document.getElementById('bgAccessToken')?.value?.trim();
|
||||||
|
const refreshToken = document.getElementById('bgRefreshToken')?.value?.trim();
|
||||||
|
if (!accessToken || !refreshToken) {
|
||||||
|
appendLog('setAuthTokens', { error: 'Both accessToken and refreshToken are required' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await DumonGeolocation.setAuthTokens({ accessToken, refreshToken });
|
||||||
|
const state = await DumonGeolocation.getAuthState();
|
||||||
|
appendLog('setAuthTokens', { success: true, state });
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('setAuthTokens', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('clearTokensBtn').addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
await DumonGeolocation.clearAuthTokens();
|
||||||
|
const state = await DumonGeolocation.getAuthState();
|
||||||
|
appendLog('clearAuthTokens', { success: true, state });
|
||||||
|
} catch (err) {
|
||||||
|
appendLog('clearAuthTokens', { error: err.message });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "dumon-geolocation",
|
"name": "dumon-geolocation",
|
||||||
"version": "0.0.1",
|
"version": "1.0.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "dumon-geolocation",
|
"name": "dumon-geolocation",
|
||||||
"version": "0.0.1",
|
"version": "1.0.2",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor/android": "^7.0.0",
|
||||||
|
|||||||
@ -84,6 +84,10 @@ export interface DumonGeoOptions {
|
|||||||
emitGnssStatus?: boolean;
|
emitGnssStatus?: boolean;
|
||||||
suppressMockedUpdates?: boolean;
|
suppressMockedUpdates?: boolean;
|
||||||
keepScreenOn?: boolean;
|
keepScreenOn?: boolean;
|
||||||
|
backgroundPollingIntervalMs?: number; // Android background polling interval
|
||||||
|
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
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PermissionStatus {
|
export interface PermissionStatus {
|
||||||
@ -99,6 +103,25 @@ export interface DumonGeolocationPlugin {
|
|||||||
setOptions(options: DumonGeoOptions): Promise<void>;
|
setOptions(options: DumonGeoOptions): Promise<void>;
|
||||||
getGnssStatus(): Promise<SatelliteStatus | null>;
|
getGnssStatus(): Promise<SatelliteStatus | null>;
|
||||||
getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;
|
getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;
|
||||||
|
// Background tracking (Android)
|
||||||
|
startBackgroundTracking(options?: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
channelId?: string;
|
||||||
|
channelName?: string;
|
||||||
|
postUrl?: string; // optional: service will POST latest fixes here as JSON
|
||||||
|
}): Promise<void>;
|
||||||
|
stopBackgroundTracking(): Promise<void>;
|
||||||
|
isBackgroundTrackingActive(): Promise<{ active: boolean }>;
|
||||||
|
getBackgroundLatestPosition(): Promise<PositioningData | null>;
|
||||||
|
openBackgroundPermissionSettings(): Promise<void>;
|
||||||
|
openNotificationPermissionSettings(): Promise<void>;
|
||||||
|
// Auth token management for background posting
|
||||||
|
setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>;
|
||||||
|
clearAuthTokens(): Promise<void>;
|
||||||
|
getAuthState(): Promise<{ present: boolean }>;
|
||||||
|
setBackgroundPostUrl(options: { url?: string }): Promise<void>;
|
||||||
|
getBackgroundPostUrl(): Promise<{ url: string | null }>;
|
||||||
|
|
||||||
configureEdgeToEdge(options: {
|
configureEdgeToEdge(options: {
|
||||||
bgColor: string;
|
bgColor: string;
|
||||||
|
|||||||
51
src/web.ts
51
src/web.ts
@ -57,4 +57,55 @@ export class DumonGeolocationWeb extends WebPlugin {
|
|||||||
// Web stub; assume enabled
|
// Web stub; assume enabled
|
||||||
return { gpsEnabled: true, networkEnabled: true };
|
return { gpsEnabled: true, networkEnabled: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Background tracking stubs (no-op on web)
|
||||||
|
async startBackgroundTracking(_options?: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
channelId?: string;
|
||||||
|
channelName?: string;
|
||||||
|
postUrl?: string;
|
||||||
|
}): Promise<void> {
|
||||||
|
console.info('[dumon-geolocation] startBackgroundTracking is not supported on web.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async stopBackgroundTracking(): Promise<void> {
|
||||||
|
console.info('[dumon-geolocation] stopBackgroundTracking is not supported on web.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async isBackgroundTrackingActive(): Promise<{ active: boolean }> {
|
||||||
|
return { active: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBackgroundLatestPosition(): Promise<PositioningData | null> {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async openBackgroundPermissionSettings(): Promise<void> {
|
||||||
|
console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async openNotificationPermissionSettings(): Promise<void> {
|
||||||
|
console.info('[dumon-geolocation] openNotificationPermissionSettings is not supported on web.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async setAuthTokens(_tokens: { accessToken: string; refreshToken: string }): Promise<void> {
|
||||||
|
console.info('[dumon-geolocation] setAuthTokens is a no-op on web.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async clearAuthTokens(): Promise<void> {
|
||||||
|
console.info('[dumon-geolocation] clearAuthTokens is a no-op on web.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getAuthState(): Promise<{ present: boolean }> {
|
||||||
|
return { present: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
async setBackgroundPostUrl(_options: { url?: string }): Promise<void> {
|
||||||
|
console.info('[dumon-geolocation] setBackgroundPostUrl is not supported on web.');
|
||||||
|
}
|
||||||
|
|
||||||
|
async getBackgroundPostUrl(): Promise<{ url: string | null }> {
|
||||||
|
return { url: null };
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user