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)
|
||||
- 📶 **Wi-Fi RTT / RSSI**
|
||||
- 🎯 **IMU Sensor** (Accelerometer, Gyroscope, Rotation)
|
||||
- ⚙️ **Kalman Filter Fusion** dan **Forward Prediction**
|
||||
- ⏩ **Forward Prediction (opsional)**
|
||||
- 🛡️ **Deteksi Lokasi Palsu (Mock Location Detection)**
|
||||
|
||||
---
|
||||
@ -64,7 +64,7 @@ Menghentikan semua sensor dan proses positioning.
|
||||
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’, …)
|
||||
|
||||
@ -94,6 +94,16 @@ configureEdgeToEdge(options: {
|
||||
|
||||
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()
|
||||
|
||||
```typescript
|
||||
@ -174,8 +184,9 @@ interface PermissionStatus {
|
||||
|
||||
## Fusion dan Prediksi
|
||||
|
||||
- Fusion posisi dilakukan menggunakan Kalman Filter sederhana untuk latitude & longitude.
|
||||
- Saat GNSS tidak tersedia (signal hilang), plugin akan melakukan forward prediction berbasis speed + heading dari IMU selama 1 detik ke depan.
|
||||
- Tidak ada Kalman/Fusion saat ini. Data yang dipancarkan berasal dari GNSS, dilengkapi IMU untuk heuristik interval dan estimasi arah/kecepatan.
|
||||
- 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)
|
||||
|
||||
Contoh implementasi tersedia di folder /example-app dengan tombol:
|
||||
Contoh implementasi tersedia di folder `/example-app` dengan kontrol:
|
||||
|
||||
- Start Positioning
|
||||
- Stop Positioning
|
||||
- Get Latest Position
|
||||
- Realtime log via onPositionUpdate
|
||||
- Start/Stop Positioning, Get Latest Position
|
||||
- Request Permissions, Clear Log
|
||||
- Mode: Driving / Normal
|
||||
- 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">
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<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_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />
|
||||
<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>
|
||||
|
||||
@ -27,10 +27,15 @@ import com.dumon.plugin.geolocation.wifi.WifiScanResult
|
||||
import com.dumon.plugin.geolocation.utils.PermissionUtils
|
||||
import com.dumon.plugin.geolocation.utils.LogUtils
|
||||
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.JSONObject
|
||||
import kotlin.math.*
|
||||
import android.location.Location
|
||||
import com.dumon.plugin.geolocation.utils.AuthStore
|
||||
|
||||
@CapacitorPlugin(
|
||||
name = "DumonGeolocation",
|
||||
@ -199,7 +204,38 @@ class DumonGeolocation : Plugin() {
|
||||
|
||||
@PluginMethod
|
||||
fun getLatestPosition(call: PluginCall) {
|
||||
call.resolve(buildPositionData())
|
||||
// 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())
|
||||
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
|
||||
@ -366,6 +402,26 @@ class DumonGeolocation : Plugin() {
|
||||
keepScreenOn = it
|
||||
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()
|
||||
}
|
||||
|
||||
@ -413,6 +469,162 @@ class DumonGeolocation : Plugin() {
|
||||
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) {
|
||||
val now = System.currentTimeMillis()
|
||||
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() {
|
||||
val oneShotListener = object : LocationListener {
|
||||
override fun onLocationChanged(location: Location) {
|
||||
@ -85,16 +180,40 @@ class GpsStatusManager(
|
||||
override fun onProviderEnabled(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) {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
0L,
|
||||
0f,
|
||||
oneShotListener
|
||||
)
|
||||
} else {
|
||||
LogUtils.e("GPS_STATUS", "Missing location permission")
|
||||
var requested = false
|
||||
if (hasFine) {
|
||||
try {
|
||||
locationManager.requestLocationUpdates(
|
||||
LocationManager.GPS_PROVIDER,
|
||||
0L,
|
||||
0f,
|
||||
oneShotListener
|
||||
)
|
||||
requested = true
|
||||
} 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": [],
|
||||
"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",
|
||||
"signature": "(options: { bgColor: string; style: 'DARK' | 'LIGHT'; overlay?: boolean; }) => Promise<void>",
|
||||
@ -372,6 +502,34 @@
|
||||
"docs": "",
|
||||
"complexTypes": [],
|
||||
"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;
|
||||
suppressMockedUpdates?: boolean;
|
||||
keepScreenOn?: boolean;
|
||||
backgroundPollingIntervalMs?: number;
|
||||
backgroundPostMinDistanceMeters?: number;
|
||||
backgroundPostMinAccuracyMeters?: number;
|
||||
backgroundMinPostIntervalMs?: number;
|
||||
}
|
||||
export interface PermissionStatus {
|
||||
location: 'granted' | 'denied';
|
||||
@ -48,6 +52,34 @@ export interface DumonGeolocationPlugin {
|
||||
gpsEnabled: 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: {
|
||||
bgColor: string;
|
||||
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;
|
||||
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
|
||||
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
|
||||
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
|
||||
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({
|
||||
|
||||
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
|
||||
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({
|
||||
|
||||
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
|
||||
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;
|
||||
margin: 1rem;
|
||||
background-color: #ffffff;
|
||||
padding-top: 56px;
|
||||
padding-bottom: 56px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@ -71,6 +73,10 @@
|
||||
<label>emitDebounceMs <input type="number" id="optEmitDebounceMs" placeholder="1000" /></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>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>
|
||||
<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="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>
|
||||
|
||||
<script type="module" src="./js/example.js"></script>
|
||||
|
||||
@ -49,6 +49,10 @@ async function applyOptionsFromForm() {
|
||||
const emitDebounceMs = readNumber('optEmitDebounceMs');
|
||||
const drivingEmitIntervalMs = readNumber('optDrivingEmitIntervalMs');
|
||||
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 speedChangeThreshold = readNumber('optSpeedThreshold');
|
||||
const directionChangeThreshold = readNumber('optDirectionThreshold');
|
||||
@ -57,6 +61,10 @@ async function applyOptionsFromForm() {
|
||||
if (emitDebounceMs !== undefined) options.emitDebounceMs = emitDebounceMs;
|
||||
if (drivingEmitIntervalMs !== undefined) options.drivingEmitIntervalMs = drivingEmitIntervalMs;
|
||||
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)
|
||||
options.distanceThresholdMeters = distanceThresholdMeters;
|
||||
if (speedChangeThreshold !== undefined) options.speedChangeThreshold = speedChangeThreshold;
|
||||
@ -112,11 +120,33 @@ async function stopGeolocation() {
|
||||
}
|
||||
|
||||
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 {
|
||||
const data = await DumonGeolocation.getLatestPosition();
|
||||
appendLog('getLatestPosition', data);
|
||||
const result = await DumonGeolocation.getLatestPosition();
|
||||
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) {
|
||||
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',
|
||||
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",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "dumon-geolocation",
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.2",
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@capacitor/android": "^7.0.0",
|
||||
|
||||
@ -84,6 +84,10 @@ export interface DumonGeoOptions {
|
||||
emitGnssStatus?: boolean;
|
||||
suppressMockedUpdates?: 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 {
|
||||
@ -99,6 +103,25 @@ export interface DumonGeolocationPlugin {
|
||||
setOptions(options: DumonGeoOptions): Promise<void>;
|
||||
getGnssStatus(): Promise<SatelliteStatus | null>;
|
||||
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: {
|
||||
bgColor: string;
|
||||
|
||||
51
src/web.ts
51
src/web.ts
@ -57,4 +57,55 @@ export class DumonGeolocationWeb extends WebPlugin {
|
||||
// Web stub; assume enabled
|
||||
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