From 511d9038904d415191f27451f6cf67ba39dc8f93 Mon Sep 17 00:00:00 2001 From: wengki81 Date: Mon, 13 Oct 2025 00:21:31 +0800 Subject: [PATCH] updated 121025-01 --- README.md | 30 +- android/src/main/AndroidManifest.xml | 16 + .../plugin/geolocation/DumonGeolocation.kt | 214 +++++++++- .../bg/BackgroundLocationService.kt | 260 ++++++++++++ .../geolocation/gps/GpsStatusManager.kt | 137 ++++++- .../plugin/geolocation/utils/AuthStore.kt | 42 ++ .../dumon/plugin/geolocation/utils/BgPrefs.kt | 182 +++++++++ dist/docs.json | 158 +++++++ dist/esm/definitions.d.ts | 32 ++ dist/esm/definitions.js.map | 2 +- dist/esm/web.d.ts | 28 ++ dist/esm/web.js | 34 ++ dist/esm/web.js.map | 2 +- dist/plugin.cjs.js | 34 ++ dist/plugin.cjs.js.map | 2 +- dist/plugin.js | 34 ++ dist/plugin.js.map | 2 +- documentation/PLUGIN_REFERENCE.md | 386 ++++++++++++++++++ example-app/README.md | 9 + example-app/src/index.html | 27 ++ example-app/src/js/example.js | 160 +++++++- package-lock.json | 4 +- src/definitions.ts | 23 ++ src/web.ts | 51 +++ 24 files changed, 1841 insertions(+), 28 deletions(-) create mode 100644 android/src/main/java/com/dumon/plugin/geolocation/bg/BackgroundLocationService.kt create mode 100644 android/src/main/java/com/dumon/plugin/geolocation/utils/AuthStore.kt create mode 100644 android/src/main/java/com/dumon/plugin/geolocation/utils/BgPrefs.kt create mode 100644 documentation/PLUGIN_REFERENCE.md diff --git a/README.md b/README.md index a5ef4eb..62c3882 100644 --- a/README.md +++ b/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 ``` -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 +``` + +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 --- diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index 44080fe..4cc900d 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,7 +1,23 @@ + + + + + + + + + + + + + diff --git a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt index e3b2672..6525f9b 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt @@ -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 diff --git a/android/src/main/java/com/dumon/plugin/geolocation/bg/BackgroundLocationService.kt b/android/src/main/java/com/dumon/plugin/geolocation/bg/BackgroundLocationService.kt new file mode 100644 index 0000000..dfc0093 --- /dev/null +++ b/android/src/main/java/com/dumon/plugin/geolocation/bg/BackgroundLocationService.kt @@ -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 + } +} diff --git a/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager.kt b/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager.kt index 4d9661f..199f09d 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/gps/GpsStatusManager.kt @@ -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)") } } diff --git a/android/src/main/java/com/dumon/plugin/geolocation/utils/AuthStore.kt b/android/src/main/java/com/dumon/plugin/geolocation/utils/AuthStore.kt new file mode 100644 index 0000000..ab4b5c9 --- /dev/null +++ b/android/src/main/java/com/dumon/plugin/geolocation/utils/AuthStore.kt @@ -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) + } +} + diff --git a/android/src/main/java/com/dumon/plugin/geolocation/utils/BgPrefs.kt b/android/src/main/java/com/dumon/plugin/geolocation/utils/BgPrefs.kt new file mode 100644 index 0000000..ddd406d --- /dev/null +++ b/android/src/main/java/com/dumon/plugin/geolocation/utils/BgPrefs.kt @@ -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) + } +} diff --git a/dist/docs.json b/dist/docs.json index a543450..bcbeefe 100644 --- a/dist/docs.json +++ b/dist/docs.json @@ -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", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ title?: string | undefined; text?: string | undefined; channelId?: string | undefined; channelName?: string | undefined; postUrl?: string | undefined; } | undefined" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "startbackgroundtracking" + }, + { + "name": "stopBackgroundTracking", + "signature": "() => Promise", + "parameters": [], + "returns": "Promise", + "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", + "parameters": [], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [ + "PositioningData" + ], + "slug": "getbackgroundlatestposition" + }, + { + "name": "openBackgroundPermissionSettings", + "signature": "() => Promise", + "parameters": [], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "openbackgroundpermissionsettings" + }, + { + "name": "openNotificationPermissionSettings", + "signature": "() => Promise", + "parameters": [], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "opennotificationpermissionsettings" + }, + { + "name": "setAuthTokens", + "signature": "(tokens: { accessToken: string; refreshToken: string; }) => Promise", + "parameters": [ + { + "name": "tokens", + "docs": "", + "type": "{ accessToken: string; refreshToken: string; }" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "setauthtokens" + }, + { + "name": "clearAuthTokens", + "signature": "() => Promise", + "parameters": [], + "returns": "Promise", + "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", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "{ url?: string | undefined; }" + } + ], + "returns": "Promise", + "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", @@ -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" } ] }, diff --git a/dist/esm/definitions.d.ts b/dist/esm/definitions.d.ts index 422d2a6..3ac4f13 100644 --- a/dist/esm/definitions.d.ts +++ b/dist/esm/definitions.d.ts @@ -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; + stopBackgroundTracking(): Promise; + isBackgroundTrackingActive(): Promise<{ + active: boolean; + }>; + getBackgroundLatestPosition(): Promise; + openBackgroundPermissionSettings(): Promise; + openNotificationPermissionSettings(): Promise; + setAuthTokens(tokens: { + accessToken: string; + refreshToken: string; + }): Promise; + clearAuthTokens(): Promise; + getAuthState(): Promise<{ + present: boolean; + }>; + setBackgroundPostUrl(options: { + url?: string; + }): Promise; + getBackgroundPostUrl(): Promise<{ + url: string | null; + }>; configureEdgeToEdge(options: { bgColor: string; style: 'DARK' | 'LIGHT'; diff --git a/dist/esm/definitions.js.map b/dist/esm/definitions.js.map index bd6647c..51678cf 100644 --- a/dist/esm/definitions.js.map +++ b/dist/esm/definitions.js.map @@ -1 +1 @@ -{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// export interface SatelliteStatus {\n// satellitesInView: number;\n// usedInFix: number;\n// constellationCounts: { [key: string]: number };\n// }\n\n// export interface WifiAp {\n// ssid: string;\n// bssid: string;\n// rssi: number;\n// distance?: number;\n// }\n\n// export interface WifiScanResult {\n// apCount: number;\n// aps: WifiAp[];\n// }\n\n// export interface ImuData {\n// accelX: number;\n// accelY: number;\n// accelZ: number;\n// gyroX: number;\n// gyroY: number;\n// gyroZ: number;\n// speed?: number;\n// acceleration?: number;\n// directionRad?: number;\n// }\n\n// export interface GpsData {\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n// satellitesInView?: number;\n// usedInFix?: number;\n// constellationCounts?: { [key: string]: number };\n// }\n\n// export interface PositioningData {\n// source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n// timestamp: number;\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n\n// gnssData?: SatelliteStatus;\n// wifiData?: WifiAp[];\n// imuData?: ImuData;\n// }\n\nexport interface PositioningData {\n source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n timestamp: number;\n latitude: number;\n longitude: number;\n accuracy: number;\n speed: number;\n acceleration: number;\n directionRad: number;\n isMocked: boolean;\n 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;\n stopPositioning(): Promise;\n getLatestPosition(): Promise;\n checkAndRequestPermissions(): Promise;\n setOptions(options: DumonGeoOptions): Promise;\n getGnssStatus(): Promise;\n getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise;\n\n addListener(\n eventName: 'onPositionUpdate',\n listenerFunc: (data: PositioningData) => void\n ): PluginListenerHandle;\n\n addListener(\n eventName: 'onGnssStatus',\n listenerFunc: (data: SatelliteStatus) => void\n ): PluginListenerHandle;\n}\n"]} \ No newline at end of file +{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// export interface SatelliteStatus {\n// satellitesInView: number;\n// usedInFix: number;\n// constellationCounts: { [key: string]: number };\n// }\n\n// export interface WifiAp {\n// ssid: string;\n// bssid: string;\n// rssi: number;\n// distance?: number;\n// }\n\n// export interface WifiScanResult {\n// apCount: number;\n// aps: WifiAp[];\n// }\n\n// export interface ImuData {\n// accelX: number;\n// accelY: number;\n// accelZ: number;\n// gyroX: number;\n// gyroY: number;\n// gyroZ: number;\n// speed?: number;\n// acceleration?: number;\n// directionRad?: number;\n// }\n\n// export interface GpsData {\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n// satellitesInView?: number;\n// usedInFix?: number;\n// constellationCounts?: { [key: string]: number };\n// }\n\n// export interface PositioningData {\n// source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n// timestamp: number;\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n\n// gnssData?: SatelliteStatus;\n// wifiData?: WifiAp[];\n// imuData?: ImuData;\n// }\n\nexport interface PositioningData {\n source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n timestamp: number;\n latitude: number;\n longitude: number;\n accuracy: number;\n speed: number;\n acceleration: number;\n directionRad: number;\n isMocked: boolean;\n 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;\n stopPositioning(): Promise;\n getLatestPosition(): Promise;\n checkAndRequestPermissions(): Promise;\n setOptions(options: DumonGeoOptions): Promise;\n getGnssStatus(): Promise;\n getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;\n // Background tracking (Android)\n startBackgroundTracking(options?: {\n title?: string;\n text?: string;\n channelId?: string;\n channelName?: string;\n postUrl?: string; // optional: service will POST latest fixes here as JSON\n }): Promise;\n stopBackgroundTracking(): Promise;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise;\n openBackgroundPermissionSettings(): Promise;\n openNotificationPermissionSettings(): Promise;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise;\n clearAuthTokens(): Promise;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise;\n\n addListener(\n eventName: 'onPositionUpdate',\n listenerFunc: (data: PositioningData) => void\n ): PluginListenerHandle;\n\n addListener(\n eventName: 'onGnssStatus',\n listenerFunc: (data: SatelliteStatus) => void\n ): PluginListenerHandle;\n}\n"]} \ No newline at end of file diff --git a/dist/esm/web.d.ts b/dist/esm/web.d.ts index 47b94d4..ce042a6 100644 --- a/dist/esm/web.d.ts +++ b/dist/esm/web.d.ts @@ -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; + stopBackgroundTracking(): Promise; + isBackgroundTrackingActive(): Promise<{ + active: boolean; + }>; + getBackgroundLatestPosition(): Promise; + openBackgroundPermissionSettings(): Promise; + openNotificationPermissionSettings(): Promise; + setAuthTokens(_tokens: { + accessToken: string; + refreshToken: string; + }): Promise; + clearAuthTokens(): Promise; + getAuthState(): Promise<{ + present: boolean; + }>; + setBackgroundPostUrl(_options: { + url?: string; + }): Promise; + getBackgroundPostUrl(): Promise<{ + url: string | null; + }>; } diff --git a/dist/esm/web.js b/dist/esm/web.js index bddc6b2..b304f36 100644 --- a/dist/esm/web.js +++ b/dist/esm/web.js @@ -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 \ No newline at end of file diff --git a/dist/esm/web.js.map b/dist/esm/web.js.map index 9d1d4c6..a1c4d08 100644 --- a/dist/esm/web.js.map +++ b/dist/esm/web.js.map @@ -1 +1 @@ -{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,MAAM,OAAO,mBAAoB,SAAQ,SAAS;IAChD,KAAK,CAAC,gBAAgB;QACpB,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC,CAAC;QACtF,OAAO;YACL,MAAM,EAAE,MAAM;YACd,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,QAAQ,EAAE,CAAC;YACX,SAAS,EAAE,CAAC;YACZ,QAAQ,EAAE,GAAG;YACb,KAAK,EAAE,CAAC;YACR,YAAY,EAAE,CAAC;YACf,YAAY,EAAE,CAAC;YACf,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,0BAA0B;QAI9B,OAAO,CAAC,IAAI,CAAC,gEAAgE,CAAC,CAAC;QAC/E,OAAO;YACL,QAAQ,EAAE,SAAS;YACnB,IAAI,EAAE,SAAS;SAChB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,OAIzB;QACC,OAAO,CAAC,IAAI,CAAC,6DAA6D,EAAE,OAAO,CAAC,CAAC;QACrF,QAAQ;IACV,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAyB;QACxC,eAAe;IACjB,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,yBAAyB;QAC7B,2BAA2B;QAC3B,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;IACpD,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport type { PositioningData, DumonGeoOptions, SatelliteStatus } from './definitions';\n\nexport class DumonGeolocationWeb extends WebPlugin {\n async startPositioning(): Promise {\n console.log('DumonGeolocationWeb: startPositioning() called (no-op)');\n }\n\n async stopPositioning(): Promise {\n console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');\n }\n\n async getLatestPosition(): Promise {\n console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');\n return {\n source: 'GNSS',\n timestamp: Date.now(),\n latitude: 0,\n longitude: 0,\n accuracy: 999,\n speed: 0,\n acceleration: 0,\n directionRad: 0,\n isMocked: false,\n };\n }\n\n async checkAndRequestPermissions(): Promise<{\n location: 'granted' | 'denied';\n wifi: 'granted' | 'denied';\n }> {\n console.info('[dumon-geolocation] checkAndRequestPermissions mocked for web.');\n return {\n location: 'granted',\n wifi: 'granted',\n };\n }\n\n async configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise {\n console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);\n // No-op\n }\n\n async setOptions(_options: DumonGeoOptions): Promise {\n // No-op on web\n }\n\n async getGnssStatus(): Promise {\n return null;\n }\n\n async getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }> {\n // Web stub; assume enabled\n return { gpsEnabled: true, networkEnabled: true };\n }\n}\n"]} \ No newline at end of file +{"version":3,"file":"web.js","sourceRoot":"","sources":["../../src/web.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AAG5C,MAAM,OAAO,mBAAoB,SAAQ,SAAS;IAChD,KAAK,CAAC,gBAAgB;QACpB,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC,CAAC;IACxE,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,iBAAiB;QACrB,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC,CAAC;QACtF,OAAO;YACL,MAAM,EAAE,MAAM;YACd,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,QAAQ,EAAE,CAAC;YACX,SAAS,EAAE,CAAC;YACZ,QAAQ,EAAE,GAAG;YACb,KAAK,EAAE,CAAC;YACR,YAAY,EAAE,CAAC;YACf,YAAY,EAAE,CAAC;YACf,QAAQ,EAAE,KAAK;SAChB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,0BAA0B;QAI9B,OAAO,CAAC,IAAI,CAAC,gEAAgE,CAAC,CAAC;QAC/E,OAAO;YACL,QAAQ,EAAE,SAAS;YACnB,IAAI,EAAE,SAAS;SAChB,CAAC;IACJ,CAAC;IAED,KAAK,CAAC,mBAAmB,CAAC,OAIzB;QACC,OAAO,CAAC,IAAI,CAAC,6DAA6D,EAAE,OAAO,CAAC,CAAC;QACrF,QAAQ;IACV,CAAC;IAED,KAAK,CAAC,UAAU,CAAC,QAAyB;QACxC,eAAe;IACjB,CAAC;IAED,KAAK,CAAC,aAAa;QACjB,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,yBAAyB;QAC7B,2BAA2B;QAC3B,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,CAAC;IACpD,CAAC;IAED,2CAA2C;IAC3C,KAAK,CAAC,uBAAuB,CAAC,QAM7B;QACC,OAAO,CAAC,IAAI,CAAC,sEAAsE,CAAC,CAAC;IACvF,CAAC;IAED,KAAK,CAAC,sBAAsB;QAC1B,OAAO,CAAC,IAAI,CAAC,qEAAqE,CAAC,CAAC;IACtF,CAAC;IAED,KAAK,CAAC,0BAA0B;QAC9B,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAC3B,CAAC;IAED,KAAK,CAAC,2BAA2B;QAC/B,OAAO,IAAI,CAAC;IACd,CAAC;IAED,KAAK,CAAC,gCAAgC;QACpC,OAAO,CAAC,IAAI,CAAC,+EAA+E,CAAC,CAAC;IAChG,CAAC;IAED,KAAK,CAAC,kCAAkC;QACtC,OAAO,CAAC,IAAI,CAAC,iFAAiF,CAAC,CAAC;IAClG,CAAC;IAED,KAAK,CAAC,aAAa,CAAC,OAAsD;QACxE,OAAO,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAC;IACvE,CAAC;IAED,KAAK,CAAC,eAAe;QACnB,OAAO,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAC;IACzE,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;IAC5B,CAAC;IAED,KAAK,CAAC,oBAAoB,CAAC,QAA0B;QACnD,OAAO,CAAC,IAAI,CAAC,mEAAmE,CAAC,CAAC;IACpF,CAAC;IAED,KAAK,CAAC,oBAAoB;QACxB,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE,CAAC;IACvB,CAAC;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport type { PositioningData, DumonGeoOptions, SatelliteStatus } from './definitions';\n\nexport class DumonGeolocationWeb extends WebPlugin {\n async startPositioning(): Promise {\n console.log('DumonGeolocationWeb: startPositioning() called (no-op)');\n }\n\n async stopPositioning(): Promise {\n console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');\n }\n\n async getLatestPosition(): Promise {\n console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');\n return {\n source: 'GNSS',\n timestamp: Date.now(),\n latitude: 0,\n longitude: 0,\n accuracy: 999,\n speed: 0,\n acceleration: 0,\n directionRad: 0,\n isMocked: false,\n };\n }\n\n async checkAndRequestPermissions(): Promise<{\n location: 'granted' | 'denied';\n wifi: 'granted' | 'denied';\n }> {\n console.info('[dumon-geolocation] checkAndRequestPermissions mocked for web.');\n return {\n location: 'granted',\n wifi: 'granted',\n };\n }\n\n async configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise {\n console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);\n // No-op\n }\n\n async setOptions(_options: DumonGeoOptions): Promise {\n // No-op on web\n }\n\n async getGnssStatus(): Promise {\n return null;\n }\n\n async getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }> {\n // Web stub; assume enabled\n return { gpsEnabled: true, networkEnabled: true };\n }\n\n // Background tracking stubs (no-op on web)\n async startBackgroundTracking(_options?: {\n title?: string;\n text?: string;\n channelId?: string;\n channelName?: string;\n postUrl?: string;\n }): Promise {\n console.info('[dumon-geolocation] startBackgroundTracking is not supported on web.');\n }\n\n async stopBackgroundTracking(): Promise {\n console.info('[dumon-geolocation] stopBackgroundTracking is not supported on web.');\n }\n\n async isBackgroundTrackingActive(): Promise<{ active: boolean }> {\n return { active: false };\n }\n\n async getBackgroundLatestPosition(): Promise {\n return null;\n }\n\n async openBackgroundPermissionSettings(): Promise {\n console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');\n }\n\n async openNotificationPermissionSettings(): Promise {\n console.info('[dumon-geolocation] openNotificationPermissionSettings is not supported on web.');\n }\n\n async setAuthTokens(_tokens: { accessToken: string; refreshToken: string }): Promise {\n console.info('[dumon-geolocation] setAuthTokens is a no-op on web.');\n }\n\n async clearAuthTokens(): Promise {\n console.info('[dumon-geolocation] clearAuthTokens is a no-op on web.');\n }\n\n async getAuthState(): Promise<{ present: boolean }> {\n return { present: false };\n }\n\n async setBackgroundPostUrl(_options: { url?: string }): Promise {\n console.info('[dumon-geolocation] setBackgroundPostUrl is not supported on web.');\n }\n\n async getBackgroundPostUrl(): Promise<{ url: string | null }> {\n return { url: null };\n }\n}\n"]} \ No newline at end of file diff --git a/dist/plugin.cjs.js b/dist/plugin.cjs.js index 0d01963..42b225d 100644 --- a/dist/plugin.cjs.js +++ b/dist/plugin.cjs.js @@ -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({ diff --git a/dist/plugin.cjs.js.map b/dist/plugin.cjs.js.map index d947eca..9b2c816 100644 --- a/dist/plugin.cjs.js.map +++ b/dist/plugin.cjs.js.map @@ -1 +1 @@ -{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst DumonGeolocation = registerPlugin('DumonGeolocation', {\n web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),\n});\nexport * from './definitions';\nexport { DumonGeolocation };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class DumonGeolocationWeb extends WebPlugin {\n async startPositioning() {\n console.log('DumonGeolocationWeb: startPositioning() called (no-op)');\n }\n async stopPositioning() {\n console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');\n }\n async getLatestPosition() {\n console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');\n return {\n source: 'GNSS',\n timestamp: Date.now(),\n latitude: 0,\n longitude: 0,\n accuracy: 999,\n speed: 0,\n acceleration: 0,\n directionRad: 0,\n isMocked: false,\n };\n }\n async checkAndRequestPermissions() {\n console.info('[dumon-geolocation] checkAndRequestPermissions mocked for web.');\n return {\n location: 'granted',\n wifi: 'granted',\n };\n }\n async configureEdgeToEdge(options) {\n console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);\n // No-op\n }\n async setOptions(_options) {\n // No-op on web\n }\n async getGnssStatus() {\n return null;\n }\n async getLocationServicesStatus() {\n // Web stub; assume enabled\n return { gpsEnabled: true, networkEnabled: true };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,gBAAgB,GAAGA,mBAAc,CAAC,kBAAkB,EAAE;AAC5D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;AACvE,CAAC;;ACFM,MAAM,mBAAmB,SAASC,cAAS,CAAC;AACnD,IAAI,MAAM,gBAAgB,GAAG;AAC7B,QAAQ,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC;AAC7E;AACA,IAAI,MAAM,eAAe,GAAG;AAC5B,QAAQ,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC;AAC5E;AACA,IAAI,MAAM,iBAAiB,GAAG;AAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC;AAC7F,QAAQ,OAAO;AACf,YAAY,MAAM,EAAE,MAAM;AAC1B,YAAY,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;AACjC,YAAY,QAAQ,EAAE,CAAC;AACvB,YAAY,SAAS,EAAE,CAAC;AACxB,YAAY,QAAQ,EAAE,GAAG;AACzB,YAAY,KAAK,EAAE,CAAC;AACpB,YAAY,YAAY,EAAE,CAAC;AAC3B,YAAY,YAAY,EAAE,CAAC;AAC3B,YAAY,QAAQ,EAAE,KAAK;AAC3B,SAAS;AACT;AACA,IAAI,MAAM,0BAA0B,GAAG;AACvC,QAAQ,OAAO,CAAC,IAAI,CAAC,gEAAgE,CAAC;AACtF,QAAQ,OAAO;AACf,YAAY,QAAQ,EAAE,SAAS;AAC/B,YAAY,IAAI,EAAE,SAAS;AAC3B,SAAS;AACT;AACA,IAAI,MAAM,mBAAmB,CAAC,OAAO,EAAE;AACvC,QAAQ,OAAO,CAAC,IAAI,CAAC,6DAA6D,EAAE,OAAO,CAAC;AAC5F;AACA;AACA,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B;AACA;AACA,IAAI,MAAM,aAAa,GAAG;AAC1B,QAAQ,OAAO,IAAI;AACnB;AACA,IAAI,MAAM,yBAAyB,GAAG;AACtC;AACA,QAAQ,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;AACzD;AACA;;;;;;;;;"} \ No newline at end of file +{"version":3,"file":"plugin.cjs.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst DumonGeolocation = registerPlugin('DumonGeolocation', {\n web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),\n});\nexport * from './definitions';\nexport { DumonGeolocation };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class DumonGeolocationWeb extends WebPlugin {\n async startPositioning() {\n console.log('DumonGeolocationWeb: startPositioning() called (no-op)');\n }\n async stopPositioning() {\n console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');\n }\n async getLatestPosition() {\n console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');\n return {\n source: 'GNSS',\n timestamp: Date.now(),\n latitude: 0,\n longitude: 0,\n accuracy: 999,\n speed: 0,\n acceleration: 0,\n directionRad: 0,\n isMocked: false,\n };\n }\n async checkAndRequestPermissions() {\n console.info('[dumon-geolocation] checkAndRequestPermissions mocked for web.');\n return {\n location: 'granted',\n wifi: 'granted',\n };\n }\n async configureEdgeToEdge(options) {\n console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);\n // No-op\n }\n async setOptions(_options) {\n // No-op on web\n }\n async getGnssStatus() {\n return null;\n }\n async getLocationServicesStatus() {\n // Web stub; assume enabled\n return { gpsEnabled: true, networkEnabled: true };\n }\n // Background tracking stubs (no-op on web)\n async startBackgroundTracking(_options) {\n console.info('[dumon-geolocation] startBackgroundTracking is not supported on web.');\n }\n async stopBackgroundTracking() {\n console.info('[dumon-geolocation] stopBackgroundTracking is not supported on web.');\n }\n async isBackgroundTrackingActive() {\n return { active: false };\n }\n async getBackgroundLatestPosition() {\n return null;\n }\n async openBackgroundPermissionSettings() {\n console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');\n }\n async openNotificationPermissionSettings() {\n console.info('[dumon-geolocation] openNotificationPermissionSettings is not supported on web.');\n }\n async setAuthTokens(_tokens) {\n console.info('[dumon-geolocation] setAuthTokens is a no-op on web.');\n }\n async clearAuthTokens() {\n console.info('[dumon-geolocation] clearAuthTokens is a no-op on web.');\n }\n async getAuthState() {\n return { present: false };\n }\n async setBackgroundPostUrl(_options) {\n console.info('[dumon-geolocation] setBackgroundPostUrl is not supported on web.');\n }\n async getBackgroundPostUrl() {\n return { url: null };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;;AACK,MAAC,gBAAgB,GAAGA,mBAAc,CAAC,kBAAkB,EAAE;AAC5D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;AACvE,CAAC;;ACFM,MAAM,mBAAmB,SAASC,cAAS,CAAC;AACnD,IAAI,MAAM,gBAAgB,GAAG;AAC7B,QAAQ,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC;AAC7E;AACA,IAAI,MAAM,eAAe,GAAG;AAC5B,QAAQ,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC;AAC5E;AACA,IAAI,MAAM,iBAAiB,GAAG;AAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC;AAC7F,QAAQ,OAAO;AACf,YAAY,MAAM,EAAE,MAAM;AAC1B,YAAY,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;AACjC,YAAY,QAAQ,EAAE,CAAC;AACvB,YAAY,SAAS,EAAE,CAAC;AACxB,YAAY,QAAQ,EAAE,GAAG;AACzB,YAAY,KAAK,EAAE,CAAC;AACpB,YAAY,YAAY,EAAE,CAAC;AAC3B,YAAY,YAAY,EAAE,CAAC;AAC3B,YAAY,QAAQ,EAAE,KAAK;AAC3B,SAAS;AACT;AACA,IAAI,MAAM,0BAA0B,GAAG;AACvC,QAAQ,OAAO,CAAC,IAAI,CAAC,gEAAgE,CAAC;AACtF,QAAQ,OAAO;AACf,YAAY,QAAQ,EAAE,SAAS;AAC/B,YAAY,IAAI,EAAE,SAAS;AAC3B,SAAS;AACT;AACA,IAAI,MAAM,mBAAmB,CAAC,OAAO,EAAE;AACvC,QAAQ,OAAO,CAAC,IAAI,CAAC,6DAA6D,EAAE,OAAO,CAAC;AAC5F;AACA;AACA,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;AAC/B;AACA;AACA,IAAI,MAAM,aAAa,GAAG;AAC1B,QAAQ,OAAO,IAAI;AACnB;AACA,IAAI,MAAM,yBAAyB,GAAG;AACtC;AACA,QAAQ,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;AACzD;AACA;AACA,IAAI,MAAM,uBAAuB,CAAC,QAAQ,EAAE;AAC5C,QAAQ,OAAO,CAAC,IAAI,CAAC,sEAAsE,CAAC;AAC5F;AACA,IAAI,MAAM,sBAAsB,GAAG;AACnC,QAAQ,OAAO,CAAC,IAAI,CAAC,qEAAqE,CAAC;AAC3F;AACA,IAAI,MAAM,0BAA0B,GAAG;AACvC,QAAQ,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE;AAChC;AACA,IAAI,MAAM,2BAA2B,GAAG;AACxC,QAAQ,OAAO,IAAI;AACnB;AACA,IAAI,MAAM,gCAAgC,GAAG;AAC7C,QAAQ,OAAO,CAAC,IAAI,CAAC,+EAA+E,CAAC;AACrG;AACA,IAAI,MAAM,kCAAkC,GAAG;AAC/C,QAAQ,OAAO,CAAC,IAAI,CAAC,iFAAiF,CAAC;AACvG;AACA,IAAI,MAAM,aAAa,CAAC,OAAO,EAAE;AACjC,QAAQ,OAAO,CAAC,IAAI,CAAC,sDAAsD,CAAC;AAC5E;AACA,IAAI,MAAM,eAAe,GAAG;AAC5B,QAAQ,OAAO,CAAC,IAAI,CAAC,wDAAwD,CAAC;AAC9E;AACA,IAAI,MAAM,YAAY,GAAG;AACzB,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;AACjC;AACA,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;AACzC,QAAQ,OAAO,CAAC,IAAI,CAAC,mEAAmE,CAAC;AACzF;AACA,IAAI,MAAM,oBAAoB,GAAG;AACjC,QAAQ,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE;AAC5B;AACA;;;;;;;;;"} \ No newline at end of file diff --git a/dist/plugin.js b/dist/plugin.js index 71abd54..a82f63a 100644 --- a/dist/plugin.js +++ b/dist/plugin.js @@ -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({ diff --git a/dist/plugin.js.map b/dist/plugin.js.map index 0b11cda..bf2d4bf 100644 --- a/dist/plugin.js.map +++ b/dist/plugin.js.map @@ -1 +1 @@ -{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst DumonGeolocation = registerPlugin('DumonGeolocation', {\n web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),\n});\nexport * from './definitions';\nexport { DumonGeolocation };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class DumonGeolocationWeb extends WebPlugin {\n async startPositioning() {\n console.log('DumonGeolocationWeb: startPositioning() called (no-op)');\n }\n async stopPositioning() {\n console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');\n }\n async getLatestPosition() {\n console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');\n return {\n source: 'GNSS',\n timestamp: Date.now(),\n latitude: 0,\n longitude: 0,\n accuracy: 999,\n speed: 0,\n acceleration: 0,\n directionRad: 0,\n isMocked: false,\n };\n }\n async checkAndRequestPermissions() {\n console.info('[dumon-geolocation] checkAndRequestPermissions mocked for web.');\n return {\n location: 'granted',\n wifi: 'granted',\n };\n }\n async configureEdgeToEdge(options) {\n console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);\n // No-op\n }\n async setOptions(_options) {\n // No-op on web\n }\n async getGnssStatus() {\n return null;\n }\n async getLocationServicesStatus() {\n // Web stub; assume enabled\n return { gpsEnabled: true, networkEnabled: true };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,gBAAgB,GAAGA,mBAAc,CAAC,kBAAkB,EAAE;IAC5D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;IACvE,CAAC;;ICFM,MAAM,mBAAmB,SAASC,cAAS,CAAC;IACnD,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC;IAC7E;IACA,IAAI,MAAM,eAAe,GAAG;IAC5B,QAAQ,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC;IAC5E;IACA,IAAI,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC;IAC7F,QAAQ,OAAO;IACf,YAAY,MAAM,EAAE,MAAM;IAC1B,YAAY,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;IACjC,YAAY,QAAQ,EAAE,CAAC;IACvB,YAAY,SAAS,EAAE,CAAC;IACxB,YAAY,QAAQ,EAAE,GAAG;IACzB,YAAY,KAAK,EAAE,CAAC;IACpB,YAAY,YAAY,EAAE,CAAC;IAC3B,YAAY,YAAY,EAAE,CAAC;IAC3B,YAAY,QAAQ,EAAE,KAAK;IAC3B,SAAS;IACT;IACA,IAAI,MAAM,0BAA0B,GAAG;IACvC,QAAQ,OAAO,CAAC,IAAI,CAAC,gEAAgE,CAAC;IACtF,QAAQ,OAAO;IACf,YAAY,QAAQ,EAAE,SAAS;IAC/B,YAAY,IAAI,EAAE,SAAS;IAC3B,SAAS;IACT;IACA,IAAI,MAAM,mBAAmB,CAAC,OAAO,EAAE;IACvC,QAAQ,OAAO,CAAC,IAAI,CAAC,6DAA6D,EAAE,OAAO,CAAC;IAC5F;IACA;IACA,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B;IACA;IACA,IAAI,MAAM,aAAa,GAAG;IAC1B,QAAQ,OAAO,IAAI;IACnB;IACA,IAAI,MAAM,yBAAyB,GAAG;IACtC;IACA,QAAQ,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;IACzD;IACA;;;;;;;;;;;;;;;"} \ No newline at end of file +{"version":3,"file":"plugin.js","sources":["esm/index.js","esm/web.js"],"sourcesContent":["import { registerPlugin } from '@capacitor/core';\nconst DumonGeolocation = registerPlugin('DumonGeolocation', {\n web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),\n});\nexport * from './definitions';\nexport { DumonGeolocation };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nexport class DumonGeolocationWeb extends WebPlugin {\n async startPositioning() {\n console.log('DumonGeolocationWeb: startPositioning() called (no-op)');\n }\n async stopPositioning() {\n console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');\n }\n async getLatestPosition() {\n console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');\n return {\n source: 'GNSS',\n timestamp: Date.now(),\n latitude: 0,\n longitude: 0,\n accuracy: 999,\n speed: 0,\n acceleration: 0,\n directionRad: 0,\n isMocked: false,\n };\n }\n async checkAndRequestPermissions() {\n console.info('[dumon-geolocation] checkAndRequestPermissions mocked for web.');\n return {\n location: 'granted',\n wifi: 'granted',\n };\n }\n async configureEdgeToEdge(options) {\n console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);\n // No-op\n }\n async setOptions(_options) {\n // No-op on web\n }\n async getGnssStatus() {\n return null;\n }\n async getLocationServicesStatus() {\n // Web stub; assume enabled\n return { gpsEnabled: true, networkEnabled: true };\n }\n // Background tracking stubs (no-op on web)\n async startBackgroundTracking(_options) {\n console.info('[dumon-geolocation] startBackgroundTracking is not supported on web.');\n }\n async stopBackgroundTracking() {\n console.info('[dumon-geolocation] stopBackgroundTracking is not supported on web.');\n }\n async isBackgroundTrackingActive() {\n return { active: false };\n }\n async getBackgroundLatestPosition() {\n return null;\n }\n async openBackgroundPermissionSettings() {\n console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');\n }\n async openNotificationPermissionSettings() {\n console.info('[dumon-geolocation] openNotificationPermissionSettings is not supported on web.');\n }\n async setAuthTokens(_tokens) {\n console.info('[dumon-geolocation] setAuthTokens is a no-op on web.');\n }\n async clearAuthTokens() {\n console.info('[dumon-geolocation] clearAuthTokens is a no-op on web.');\n }\n async getAuthState() {\n return { present: false };\n }\n async setBackgroundPostUrl(_options) {\n console.info('[dumon-geolocation] setBackgroundPostUrl is not supported on web.');\n }\n async getBackgroundPostUrl() {\n return { url: null };\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":";;;AACK,UAAC,gBAAgB,GAAGA,mBAAc,CAAC,kBAAkB,EAAE;IAC5D,IAAI,GAAG,EAAE,MAAM,mDAAe,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;IACvE,CAAC;;ICFM,MAAM,mBAAmB,SAASC,cAAS,CAAC;IACnD,IAAI,MAAM,gBAAgB,GAAG;IAC7B,QAAQ,OAAO,CAAC,GAAG,CAAC,wDAAwD,CAAC;IAC7E;IACA,IAAI,MAAM,eAAe,GAAG;IAC5B,QAAQ,OAAO,CAAC,GAAG,CAAC,uDAAuD,CAAC;IAC5E;IACA,IAAI,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,OAAO,CAAC,GAAG,CAAC,wEAAwE,CAAC;IAC7F,QAAQ,OAAO;IACf,YAAY,MAAM,EAAE,MAAM;IAC1B,YAAY,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;IACjC,YAAY,QAAQ,EAAE,CAAC;IACvB,YAAY,SAAS,EAAE,CAAC;IACxB,YAAY,QAAQ,EAAE,GAAG;IACzB,YAAY,KAAK,EAAE,CAAC;IACpB,YAAY,YAAY,EAAE,CAAC;IAC3B,YAAY,YAAY,EAAE,CAAC;IAC3B,YAAY,QAAQ,EAAE,KAAK;IAC3B,SAAS;IACT;IACA,IAAI,MAAM,0BAA0B,GAAG;IACvC,QAAQ,OAAO,CAAC,IAAI,CAAC,gEAAgE,CAAC;IACtF,QAAQ,OAAO;IACf,YAAY,QAAQ,EAAE,SAAS;IAC/B,YAAY,IAAI,EAAE,SAAS;IAC3B,SAAS;IACT;IACA,IAAI,MAAM,mBAAmB,CAAC,OAAO,EAAE;IACvC,QAAQ,OAAO,CAAC,IAAI,CAAC,6DAA6D,EAAE,OAAO,CAAC;IAC5F;IACA;IACA,IAAI,MAAM,UAAU,CAAC,QAAQ,EAAE;IAC/B;IACA;IACA,IAAI,MAAM,aAAa,GAAG;IAC1B,QAAQ,OAAO,IAAI;IACnB;IACA,IAAI,MAAM,yBAAyB,GAAG;IACtC;IACA,QAAQ,OAAO,EAAE,UAAU,EAAE,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE;IACzD;IACA;IACA,IAAI,MAAM,uBAAuB,CAAC,QAAQ,EAAE;IAC5C,QAAQ,OAAO,CAAC,IAAI,CAAC,sEAAsE,CAAC;IAC5F;IACA,IAAI,MAAM,sBAAsB,GAAG;IACnC,QAAQ,OAAO,CAAC,IAAI,CAAC,qEAAqE,CAAC;IAC3F;IACA,IAAI,MAAM,0BAA0B,GAAG;IACvC,QAAQ,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE;IAChC;IACA,IAAI,MAAM,2BAA2B,GAAG;IACxC,QAAQ,OAAO,IAAI;IACnB;IACA,IAAI,MAAM,gCAAgC,GAAG;IAC7C,QAAQ,OAAO,CAAC,IAAI,CAAC,+EAA+E,CAAC;IACrG;IACA,IAAI,MAAM,kCAAkC,GAAG;IAC/C,QAAQ,OAAO,CAAC,IAAI,CAAC,iFAAiF,CAAC;IACvG;IACA,IAAI,MAAM,aAAa,CAAC,OAAO,EAAE;IACjC,QAAQ,OAAO,CAAC,IAAI,CAAC,sDAAsD,CAAC;IAC5E;IACA,IAAI,MAAM,eAAe,GAAG;IAC5B,QAAQ,OAAO,CAAC,IAAI,CAAC,wDAAwD,CAAC;IAC9E;IACA,IAAI,MAAM,YAAY,GAAG;IACzB,QAAQ,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE;IACjC;IACA,IAAI,MAAM,oBAAoB,CAAC,QAAQ,EAAE;IACzC,QAAQ,OAAO,CAAC,IAAI,CAAC,mEAAmE,CAAC;IACzF;IACA,IAAI,MAAM,oBAAoB,GAAG;IACjC,QAAQ,OAAO,EAAE,GAAG,EAAE,IAAI,EAAE;IAC5B;IACA;;;;;;;;;;;;;;;"} \ No newline at end of file diff --git a/documentation/PLUGIN_REFERENCE.md b/documentation/PLUGIN_REFERENCE.md new file mode 100644 index 0000000..04b4744 --- /dev/null +++ b/documentation/PLUGIN_REFERENCE.md @@ -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 +``` +- Mulai GNSS, IMU, dan Wi‑Fi scanning periodik (RTT opsional). Mengaktifkan loop emisi posisi. + +```ts +stopPositioning(): Promise +``` +- Hentikan semua sensor dan emisi update. + +```ts +getLatestPosition(): Promise +``` +- 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 +``` +- Memeriksa dan (bila perlu) meminta semua izin lokasi/Wi‑Fi yang diperlukan. + +```ts +setOptions(options: DumonGeoOptions): Promise +``` +- 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 +``` +- `normal`: polling adaptif (hemat baterai). `driving`: continuous GPS + loop emisi paksa berkala. + +```ts +configureEdgeToEdge(options: { bgColor: string; style: 'DARK' | 'LIGHT'; overlay?: boolean }): Promise +``` +- Mengatur status bar/navigation bar agar sesuai tema UI. + +```ts +getGnssStatus(): Promise +``` +- 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 +stopBackgroundTracking(): Promise +isBackgroundTrackingActive(): Promise<{ active: boolean }> +getBackgroundLatestPosition(): Promise +setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise +clearAuthTokens(): Promise +getAuthState(): Promise<{ present: boolean }> +setBackgroundPostUrl(options: { url?: string }): Promise +getBackgroundPostUrl(): Promise<{ url: string | null }> +``` +- Menjalankan tracking lokasi saat app di‑pause menggunakan Foreground Service. Menyimpan latest fix ke penyimpanan lokal agar bisa diambil saat kembali ke foreground. Opsi `postUrl` (opsional) akan mengirim setiap pembaruan lokasi via HTTP POST (JSON) ke endpoint tersebut. Untuk autentikasi header, panggil `setAuthTokens` (menyimpan token secara native) agar setiap POST menyertakan header `Authorization: Bearer ` dan `refresh-token: `. Di web: no‑op. + +### 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: '', refreshToken: '' }); +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` diff --git a/example-app/README.md b/example-app/README.md index 486ed63..8bf9ec1 100644 --- a/example-app/README.md +++ b/example-app/README.md @@ -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. diff --git a/example-app/src/index.html b/example-app/src/index.html index cefd414..a16eae1 100644 --- a/example-app/src/index.html +++ b/example-app/src/index.html @@ -13,6 +13,8 @@ font-family: sans-serif; margin: 1rem; background-color: #ffffff; + padding-top: 56px; + padding-bottom: 56px; } h1 { @@ -71,6 +73,10 @@


+
+
+
+

@@ -85,6 +91,27 @@ +

Background

+ + + + +
+ + +
+
+ +
+
+ + + + + + +
+

 
     
diff --git a/example-app/src/js/example.js b/example-app/src/js/example.js
index fe81ddb..0331451 100644
--- a/example-app/src/js/example.js
+++ b/example-app/src/js/example.js
@@ -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 });
+    }
+  });
 });
diff --git a/package-lock.json b/package-lock.json
index e92962b..3b9c61c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/src/definitions.ts b/src/definitions.ts
index b1bd6a7..d8b96ca 100644
--- a/src/definitions.ts
+++ b/src/definitions.ts
@@ -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;
   getGnssStatus(): Promise;
   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;
+  stopBackgroundTracking(): Promise;
+  isBackgroundTrackingActive(): Promise<{ active: boolean }>;
+  getBackgroundLatestPosition(): Promise;
+  openBackgroundPermissionSettings(): Promise;
+  openNotificationPermissionSettings(): Promise;
+  // Auth token management for background posting
+  setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise;
+  clearAuthTokens(): Promise;
+  getAuthState(): Promise<{ present: boolean }>;
+  setBackgroundPostUrl(options: { url?: string }): Promise;
+  getBackgroundPostUrl(): Promise<{ url: string | null }>;
 
   configureEdgeToEdge(options: {
     bgColor: string;
diff --git a/src/web.ts b/src/web.ts
index 1b0e5a6..13ad4ba 100644
--- a/src/web.ts
+++ b/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 {
+    console.info('[dumon-geolocation] startBackgroundTracking is not supported on web.');
+  }
+
+  async stopBackgroundTracking(): Promise {
+    console.info('[dumon-geolocation] stopBackgroundTracking is not supported on web.');
+  }
+
+  async isBackgroundTrackingActive(): Promise<{ active: boolean }> {
+    return { active: false };
+  }
+
+  async getBackgroundLatestPosition(): Promise {
+    return null;
+  }
+
+  async openBackgroundPermissionSettings(): Promise {
+    console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');
+  }
+
+  async openNotificationPermissionSettings(): Promise {
+    console.info('[dumon-geolocation] openNotificationPermissionSettings is not supported on web.');
+  }
+
+  async setAuthTokens(_tokens: { accessToken: string; refreshToken: string }): Promise {
+    console.info('[dumon-geolocation] setAuthTokens is a no-op on web.');
+  }
+
+  async clearAuthTokens(): Promise {
+    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 {
+    console.info('[dumon-geolocation] setBackgroundPostUrl is not supported on web.');
+  }
+
+  async getBackgroundPostUrl(): Promise<{ url: string | null }> {
+    return { url: null };
+  }
 }