From f5be8180f2c6d6530eec10d7fc06ac06867f61e0 Mon Sep 17 00:00:00 2001 From: wengki81 Date: Sun, 28 Sep 2025 19:43:26 +0800 Subject: [PATCH] updated 280925-01 --- README.md | 50 ++++- .../plugin/geolocation/DumonGeolocation.kt | 205 ++++++++++++++++-- .../geolocation/gps/GpsStatusManager.kt | 31 ++- .../geolocation/imu/ImuSensorManager.kt | 11 +- .../plugin/geolocation/utils/LogUtils.kt | 22 ++ .../wifi/WifiPositioningManager.kt | 81 +++++-- dist/docs.json | 194 +++++++++++++++++ dist/esm/definitions.d.ts | 29 +++ dist/esm/definitions.js.map | 2 +- dist/esm/web.d.ts | 8 +- dist/esm/web.js | 10 + dist/esm/web.js.map | 2 +- dist/plugin.cjs.js | 10 + dist/plugin.cjs.js.map | 2 +- dist/plugin.js | 10 + dist/plugin.js.map | 2 +- example-app/package-lock.json | 2 +- example-app/src/index.html | 36 ++- example-app/src/js/example.js | 140 +++++++++++- package.json | 2 +- src/definitions.ts | 32 ++- src/web.ts | 17 +- 22 files changed, 820 insertions(+), 78 deletions(-) create mode 100644 android/src/main/java/com/dumon/plugin/geolocation/utils/LogUtils.kt diff --git a/README.md b/README.md index 7a8be9d..a5ef4eb 100644 --- a/README.md +++ b/README.md @@ -94,6 +94,52 @@ configureEdgeToEdge(options: { Mengatur status bar dan navigasi bar agar transparan, dengan warna dan icon style sesuai UI. +### setOptions() + +```typescript +setOptions(options: { + distanceThresholdMeters?: number; + speedChangeThreshold?: number; + directionChangeThreshold?: number; + emitDebounceMs?: number; + drivingEmitIntervalMs?: number; + wifiScanIntervalMs?: number; + enableWifiRtt?: boolean; + enableLogging?: boolean; + enableForwardPrediction?: boolean; + maxPredictionSeconds?: number; + emitGnssStatus?: boolean; + suppressMockedUpdates?: boolean; + keepScreenOn?: boolean; +}): Promise +``` + +Mengubah parameter runtime tanpa rebuild. Semua default menjaga perilaku saat ini. + +### getGnssStatus() + +```typescript +getGnssStatus(): Promise +``` + +Mengambil status GNSS terakhir untuk debugging. + +### getLocationServicesStatus() + +```typescript +getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }> +``` + +Memeriksa apakah provider lokasi aktif. + +### addListener('onGnssStatus', …) + +```typescript +addListener('onGnssStatus', (data: SatelliteStatus) => void): PluginListenerHandle +``` + +Menerima update status GNSS jika `emitGnssStatus: true` di setOptions. + --- ## Interfaces @@ -102,7 +148,7 @@ PositioningData ```typescript interface PositioningData { - source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK' | 'PREDICTED'; + source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK'; timestamp: number; latitude: number; longitude: number; @@ -189,4 +235,4 @@ Contoh implementasi tersedia di folder /example-app dengan tombol: --- - Lisensi: MIT -- Dibuat oleh: Tim Dumon 🇮🇩 \ No newline at end of file +- Dibuat oleh: Tim Dumon 🇮🇩 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 3f06e26..e3b2672 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt @@ -25,6 +25,7 @@ import com.dumon.plugin.geolocation.wifi.WifiPositioningManager import com.dumon.plugin.geolocation.wifi.WifiScanResult //import com.dumon.plugin.geolocation.fusion.SensorFusionManager import com.dumon.plugin.geolocation.utils.PermissionUtils +import com.dumon.plugin.geolocation.utils.LogUtils import com.getcapacitor.annotation.PermissionCallback import org.json.JSONArray import org.json.JSONObject @@ -67,9 +68,9 @@ class DumonGeolocation : Plugin() { private var prevSpeed = 0f private var prevDirection = 0f // private val significantChangeThreshold = 0.00007 // ~7 meters - private val significantChangeThreshold = 7 // ~7 meters - private val speedChangeThreshold = 0.5f // m/s - private val directionChangeThreshold = 0.17f // ~10 deg + private var significantChangeThreshold = 7.0 // ~7 meters + private var speedChangeThreshold = 0.5f // m/s + private var directionChangeThreshold = 0.17f // ~10 deg // private val emitIntervalMs: Long = 500L private var emitIntervalMs: Long = 1000L // hard debounce @@ -80,17 +81,32 @@ class DumonGeolocation : Plugin() { private var bufferedDrivingLocation: Location? = null private var drivingEmitHandler: Handler? = null private var drivingEmitRunnable: Runnable? = null - private val drivingEmitIntervalMs = 1600L + private var drivingEmitIntervalMs = 1600L + + private var wifiScanIntervalMs = 3000L + private var enableWifiRtt = true + private var enableForwardPrediction = false + private var maxPredictionSeconds = 1.0 + private var emitGnssStatus = false + private var suppressMockedUpdates = false + private var keepScreenOn = false private var currentTrackingMode = GpsTrackingMode.NORMAL override fun load() { gpsManager = GpsStatusManager( context, - onSatelliteStatusUpdate = { satelliteStatus = it }, + onSatelliteStatusUpdate = { status -> + satelliteStatus = status + if (emitGnssStatus) { + Handler(Looper.getMainLooper()).post { + notifyListeners("onGnssStatus", buildGnssStatusData(status)) + } + } + }, onLocationUpdate = onLocationUpdate@{ location, isMocked -> if (location.latitude == 0.0 && location.longitude == 0.0) { - Log.w("GPS_LOCATION", "Ignored location update: (0.0, 0.0)") + LogUtils.w("GPS_LOCATION", "Ignored location update: (0.0, 0.0)") return@onLocationUpdate } @@ -148,7 +164,9 @@ class DumonGeolocation : Plugin() { } private fun stopDrivingEmitLoop() { - drivingEmitHandler?.removeCallbacks(drivingEmitRunnable!!) + drivingEmitRunnable?.let { runnable -> + drivingEmitHandler?.removeCallbacks(runnable) + } drivingEmitHandler = null drivingEmitRunnable = null bufferedDrivingLocation = null @@ -163,7 +181,9 @@ class DumonGeolocation : Plugin() { gpsManager?.start() imuManager?.start() - wifiManager?.startPeriodicScan(3000L) + wifiManager?.setEnableRtt(enableWifiRtt) + wifiManager?.startPeriodicScan(wifiScanIntervalMs) + applyKeepScreenOn(keepScreenOn) call.resolve() } @@ -173,6 +193,7 @@ class DumonGeolocation : Plugin() { imuManager?.stop() wifiManager?.stopPeriodicScan() stopDrivingEmitLoop() + applyKeepScreenOn(false) call.resolve() } @@ -184,7 +205,18 @@ class DumonGeolocation : Plugin() { @PluginMethod fun checkAndRequestPermissions(call: PluginCall) { // requestAllPermissions(call, "checkAndRequestPermissions") - requestAllPermissions(call, "onPermissionResult") + val isLocationGranted = + ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED + val isWifiGranted = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + ActivityCompat.checkSelfPermission(context, Manifest.permission.NEARBY_WIFI_DEVICES) == PackageManager.PERMISSION_GRANTED + } else { + true + } + + if (!isLocationGranted || !isWifiGranted) { + requestAllPermissions(call, "onPermissionResult") + return + } val locationStatus = PermissionUtils.getPermissionStatus( ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) @@ -272,12 +304,12 @@ class DumonGeolocation : Plugin() { gpsManager?.startContinuousMode() currentTrackingMode = GpsTrackingMode.DRIVING startDrivingEmitLoop() - Log.d("DUMON_GEOLOCATION", "Switched to driving mode (continuous GPS)") + LogUtils.d("DUMON_GEOLOCATION", "Switched to driving mode (continuous GPS)") } else { gpsManager?.startPollingMode() currentTrackingMode = GpsTrackingMode.NORMAL stopDrivingEmitLoop() - Log.d("DUMON_GEOLOCATION", "Switched to normal mode (polling GPS)") + LogUtils.d("DUMON_GEOLOCATION", "Switched to normal mode (polling GPS)") } call.resolve() } @@ -302,9 +334,89 @@ class DumonGeolocation : Plugin() { call.resolve(result) } + @PluginMethod + fun setOptions(call: PluginCall) { + call.getDouble("distanceThresholdMeters")?.let { significantChangeThreshold = it } + call.getDouble("speedChangeThreshold")?.let { speedChangeThreshold = it.toFloat() } + call.getDouble("directionChangeThreshold")?.let { directionChangeThreshold = it.toFloat() } + call.getInt("emitDebounceMs")?.let { + emitIntervalMs = it.toLong().coerceAtLeast(0L) + gpsManager?.setPollingInterval(emitIntervalMs) + } + call.getInt("drivingEmitIntervalMs")?.let { + drivingEmitIntervalMs = it.toLong().coerceAtLeast(200L) + if (currentTrackingMode == GpsTrackingMode.DRIVING) { + stopDrivingEmitLoop() + startDrivingEmitLoop() + } + } + call.getInt("wifiScanIntervalMs")?.let { + wifiScanIntervalMs = it.toLong().coerceAtLeast(1000L) + } + call.getBoolean("enableWifiRtt")?.let { + enableWifiRtt = it + wifiManager?.setEnableRtt(it) + } + call.getBoolean("enableLogging")?.let { LogUtils.enabled = it } + call.getBoolean("enableForwardPrediction")?.let { enableForwardPrediction = it } + call.getDouble("maxPredictionSeconds")?.let { maxPredictionSeconds = it.coerceIn(0.0, 5.0) } + call.getBoolean("emitGnssStatus")?.let { emitGnssStatus = it } + call.getBoolean("suppressMockedUpdates")?.let { suppressMockedUpdates = it } + call.getBoolean("keepScreenOn")?.let { + keepScreenOn = it + applyKeepScreenOn(keepScreenOn) + } + call.resolve() + } + + private fun applyKeepScreenOn(enabled: Boolean) { + val webView = bridge?.webView + val activity = bridge?.activity + Handler(Looper.getMainLooper()).post { + webView?.keepScreenOn = enabled + val window = activity?.window + if (window != null) { + if (enabled) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + } + } + + @PluginMethod + fun getGnssStatus(call: PluginCall) { + val status = satelliteStatus + if (status == null) { + call.resolve(JSObject()) + return + } + val obj = JSObject() + obj.put("satellitesInView", status.satellitesInView) + obj.put("usedInFix", status.usedInFix) + val counts = JSObject() + status.constellationCounts.forEach { (k, v) -> counts.put(k, v) } + obj.put("constellationCounts", counts) + call.resolve(obj) + } + + @PluginMethod + fun getLocationServicesStatus(call: PluginCall) { + val lm = context.getSystemService(android.content.Context.LOCATION_SERVICE) as android.location.LocationManager + val gpsEnabled = try { lm.isProviderEnabled(android.location.LocationManager.GPS_PROVIDER) } catch (_: Exception) { false } + val netEnabled = try { lm.isProviderEnabled(android.location.LocationManager.NETWORK_PROVIDER) } catch (_: Exception) { false } + val obj = JSObject().apply { + put("gpsEnabled", gpsEnabled) + put("networkEnabled", netEnabled) + } + call.resolve(obj) + } + private fun emitPositionUpdate(forceEmit: Boolean = false) { val now = System.currentTimeMillis() if (!forceEmit && now - lastEmitTimestamp < emitIntervalMs) return + if (suppressMockedUpdates && isMockedLocation) return val distance = calculateDistance(latestLatitude, latestLongitude, prevLatitude, prevLongitude) val speedNow = latestImu?.speed ?: 0f @@ -330,7 +442,10 @@ class DumonGeolocation : Plugin() { prevDirection = directionNow lastEmitTimestamp = now - notifyListeners("onPositionUpdate", buildPositionData()) + // Ensure listener notifications run on the main thread for consistency + Handler(Looper.getMainLooper()).post { + notifyListeners("onPositionUpdate", buildPositionData()) + } } } @@ -345,7 +460,7 @@ class DumonGeolocation : Plugin() { if (emitIntervalMs != targetInterval) { emitIntervalMs = targetInterval gpsManager?.setPollingInterval(targetInterval) - Log.d("DUMON_GEOLOCATION", "Auto-set emitIntervalMs = $emitIntervalMs ms") + LogUtils.d("DUMON_GEOLOCATION", "Auto-set emitIntervalMs = $emitIntervalMs ms") } imuManager?.setSensorDelayBySpeed(speed) @@ -369,23 +484,67 @@ class DumonGeolocation : Plugin() { return R * c } + private fun buildGnssStatusData(status: SatelliteStatus): JSObject { + val obj = JSObject() + obj.put("satellitesInView", status.satellitesInView) + obj.put("usedInFix", status.usedInFix) + val counts = JSObject() + status.constellationCounts.forEach { (k, v) -> counts.put(k, v) } + obj.put("constellationCounts", counts) + return obj + } + private fun buildPositionData(): JSObject { val obj = JSObject() + val now = System.currentTimeMillis() + + var outLat = latestLatitude + var outLon = latestLongitude + var predicted = false + + val imu = latestImu + val dtSec = ((now - (if (latestTimestamp > 0) latestTimestamp else now)).toDouble() / 1000.0) + if (enableForwardPrediction && imu != null && !isMockedLocation && dtSec > 0) { + val clampedDt = dtSec.coerceAtMost(maxPredictionSeconds) + val speed = imu.speed + val dir = imu.directionRad + val dNorth = speed * clampedDt * kotlin.math.cos(dir) + val dEast = speed * clampedDt * kotlin.math.sin(dir) + val R = 6371000.0 + val latRad = degToRad(latestLatitude) + val dLat = (dNorth / R) * (180.0 / PI) + val dLon = (dEast / (R * kotlin.math.cos(latRad))) * (180.0 / PI) + outLat = latestLatitude + dLat + outLon = latestLongitude + dLon + predicted = true + } + obj.put("source", latestSource) - obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) - obj.put("latitude", latestLatitude) - obj.put("longitude", latestLongitude) + obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else now) + obj.put("latitude", outLat) + obj.put("longitude", outLon) obj.put("accuracy", latestAccuracy) obj.put("isMocked", isMockedLocation) - latestImu?.let { - obj.put("speed", it.speed) - obj.put("acceleration", it.acceleration) - obj.put("directionRad", it.directionRad) - } + // Always provide IMU-related fields to match TS definitions + val speedVal = latestImu?.speed ?: 0f + val accelVal = latestImu?.acceleration ?: 0f + val dirVal = latestImu?.directionRad ?: 0f + obj.put("speed", speedVal) + obj.put("acceleration", accelVal) + obj.put("directionRad", dirVal) - obj.put("predicted", latestSource == "PREDICTED") + obj.put("predicted", predicted) return obj } -} \ No newline at end of file + + override fun handleOnDestroy() { + gpsManager?.stop() + imuManager?.stop() + wifiManager?.stopPeriodicScan() + stopDrivingEmitLoop() + applyKeepScreenOn(false) + super.handleOnDestroy() + } +} 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 42a2427..4d9661f 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 @@ -13,7 +13,9 @@ import android.os.Handler import android.os.Looper import android.text.format.DateFormat import android.util.Log +import com.dumon.plugin.geolocation.utils.LogUtils import androidx.core.app.ActivityCompat +import androidx.core.content.ContextCompat enum class GpsTrackingMode { NORMAL, @@ -73,7 +75,7 @@ class GpsStatusManager( } val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp | Mock=$isMocked" - Log.d("GPS_LOCATION", info) + LogUtils.d("GPS_LOCATION", info) onLocationUpdate(location, isMocked) locationManager.removeUpdates(this) @@ -92,7 +94,7 @@ class GpsStatusManager( oneShotListener ) } else { - Log.e("GPS_STATUS", "Missing location permission") + LogUtils.e("GPS_STATUS", "Missing location permission") } } @@ -106,6 +108,10 @@ class GpsStatusManager( fun setPollingInterval(intervalMs: Long) { this.pollingIntervalMs = intervalMs + if (isPolling) { + handler.removeCallbacks(pollingRunnable) + handler.postDelayed(pollingRunnable, pollingIntervalMs) + } } @SuppressLint("MissingPermission") @@ -114,11 +120,18 @@ class GpsStatusManager( currentMode = mode if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - Log.e("GPS_STATUS", "Missing location permissions") + LogUtils.e("GPS_STATUS", "Missing location permissions") return } - locationManager.registerGnssStatusCallback(gnssStatusCallback, null) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + locationManager.registerGnssStatusCallback( + ContextCompat.getMainExecutor(context), + gnssStatusCallback + ) + } else { + locationManager.registerGnssStatusCallback(gnssStatusCallback, handler) + } if (mode == GpsTrackingMode.DRIVING) { startContinuousUpdates() @@ -139,12 +152,12 @@ class GpsStatusManager( } } - Log.d("GPS_STATUS", "GPS started with mode: $mode") + LogUtils.d("GPS_STATUS", "GPS started with mode: $mode") } private fun startContinuousUpdates() { if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) { - Log.e("GPS_STATUS", "Missing ACCESS_FINE_LOCATION permission") + LogUtils.e("GPS_STATUS", "Missing ACCESS_FINE_LOCATION permission") return } @@ -166,7 +179,7 @@ class GpsStatusManager( continuousListener!! ) - Log.d("GPS_STATUS", "Started continuous location updates") + LogUtils.d("GPS_STATUS", "Started continuous location updates") } fun startContinuousMode() { @@ -188,7 +201,7 @@ class GpsStatusManager( locationManager.removeUpdates(it) continuousListener = null } - Log.d("GPS_STATUS", "GPS stopped for mode: $currentMode") + LogUtils.d("GPS_STATUS", "GPS stopped for mode: $currentMode") } private fun getConstellationName(type: Int): String { @@ -209,4 +222,4 @@ data class SatelliteStatus( val satellitesInView: Int, val usedInFix: Int, val constellationCounts: Map -) \ No newline at end of file +) diff --git a/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager.kt b/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager.kt index ce5759f..ae47971 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/imu/ImuSensorManager.kt @@ -3,6 +3,7 @@ package com.dumon.plugin.geolocation.imu import android.content.Context import android.hardware.* import android.util.Log +import com.dumon.plugin.geolocation.utils.LogUtils import kotlin.math.* class ImuSensorManager( @@ -35,12 +36,12 @@ class ImuSensorManager( accelerometer?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } gyroscope?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } rotationVector?.let { sensorManager.registerListener(this, it, SensorManager.SENSOR_DELAY_GAME) } - Log.d("IMU_SENSOR", "IMU sensor tracking started") + LogUtils.d("IMU_SENSOR", "IMU sensor tracking started") } fun stop() { sensorManager.unregisterListener(this) - Log.d("IMU_SENSOR", "IMU sensor tracking stopped") + LogUtils.d("IMU_SENSOR", "IMU sensor tracking stopped") } fun setSensorDelayBySpeed(speed: Float) { @@ -54,7 +55,7 @@ class ImuSensorManager( if (desiredDelay != currentDelay) { currentDelay = desiredDelay restartSensorsWithDelay(currentDelay) - Log.d("IMU_SENSOR", "Sensor delay changed to $currentDelay") + LogUtils.d("IMU_SENSOR", "Sensor delay changed to $currentDelay") } } @@ -110,7 +111,7 @@ class ImuSensorManager( emitCombinedImuData(speed, acceleration, latestDirectionRad) - Log.d("IMU_SENSOR", "Accel x: %.3f y: %.3f z: %.3f | Speed: %.3f | AccelMag: %.3f | Dir: %.2f rad".format( + LogUtils.d("IMU_SENSOR", "Accel x: %.3f y: %.3f z: %.3f | Speed: %.3f | AccelMag: %.3f | Dir: %.2f rad".format( lastAccel[0], lastAccel[1], lastAccel[2], speed, acceleration, latestDirectionRad )) } @@ -165,4 +166,4 @@ data class ImuData( val speed: Float, val acceleration: Float, val directionRad: Float -) \ No newline at end of file +) diff --git a/android/src/main/java/com/dumon/plugin/geolocation/utils/LogUtils.kt b/android/src/main/java/com/dumon/plugin/geolocation/utils/LogUtils.kt new file mode 100644 index 0000000..8c0bb7c --- /dev/null +++ b/android/src/main/java/com/dumon/plugin/geolocation/utils/LogUtils.kt @@ -0,0 +1,22 @@ +package com.dumon.plugin.geolocation.utils + +import android.util.Log + +object LogUtils { + @Volatile + var enabled: Boolean = true + + fun d(tag: String, msg: String) { + if (enabled) Log.d(tag, msg) + } + + fun w(tag: String, msg: String) { + if (enabled) Log.w(tag, msg) + } + + fun e(tag: String, msg: String, tr: Throwable? = null) { + if (!enabled) return + if (tr != null) Log.e(tag, msg, tr) else Log.e(tag, msg) + } +} + diff --git a/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager.kt b/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager.kt index bb0b261..8928274 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/wifi/WifiPositioningManager.kt @@ -2,6 +2,9 @@ package com.dumon.plugin.geolocation.wifi import android.Manifest import android.content.Context +import android.content.BroadcastReceiver +import android.content.Intent +import android.content.IntentFilter import android.content.pm.PackageManager import android.net.wifi.ScanResult import android.net.wifi.WifiManager @@ -10,8 +13,9 @@ import android.net.wifi.rtt.RangingResultCallback import android.net.wifi.rtt.WifiRttManager import android.os.* import android.util.Log +import com.dumon.plugin.geolocation.utils.LogUtils import androidx.core.app.ActivityCompat -import java.util.concurrent.Executors +import androidx.core.content.ContextCompat class WifiPositioningManager( private val context: Context, @@ -30,6 +34,9 @@ class WifiPositioningManager( private var isScanning = false private var lastWifiScanAps: MutableList = mutableListOf() + private var scanResultsReceiver: BroadcastReceiver? = null + private var scanReceiverRegistered: Boolean = false + private var enableRtt: Boolean = true fun isRttSupported(): Boolean { return Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && wifiRttManager != null && wifiManager.isWifiEnabled @@ -37,6 +44,7 @@ class WifiPositioningManager( fun startPeriodicScan(intervalMs: Long = 3000L) { isScanning = true + registerScanResultsReceiverIfNeeded() handler.post(object : Runnable { override fun run() { if (isScanning) { @@ -50,6 +58,7 @@ class WifiPositioningManager( fun stopPeriodicScan() { isScanning = false handler.removeCallbacksAndMessages(null) + unregisterScanResultsReceiverIfNeeded() } fun startWifiScan() { @@ -59,22 +68,50 @@ class WifiPositioningManager( ) == PackageManager.PERMISSION_GRANTED ) { val success = wifiManager.startScan() - if (success) { - val results = wifiManager.scanResults - processScanResults(results) - if (isRttSupported()) { - startRttRanging(results) - } - } else { - Log.e("WIFI_POSITION", "Wi-Fi scan failed") + if (!success) { + LogUtils.e("WIFI_POSITION", "Wi-Fi scan failed") onWifiPositioningUpdate(WifiScanResult(0, emptyList())) } } else { - Log.e("WIFI_POSITION", "Missing ACCESS_FINE_LOCATION permission") + LogUtils.e("WIFI_POSITION", "Missing ACCESS_FINE_LOCATION permission") onWifiPositioningUpdate(WifiScanResult(0, emptyList())) } } + private fun registerScanResultsReceiverIfNeeded() { + if (scanReceiverRegistered) return + scanResultsReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) { + val results = wifiManager.scanResults + processScanResults(results) + if (enableRtt && isRttSupported()) { + startRttRanging(results) + } + } + } + } + val filter = IntentFilter(WifiManager.SCAN_RESULTS_AVAILABLE_ACTION) + try { + context.registerReceiver(scanResultsReceiver, filter) + scanReceiverRegistered = true + } catch (e: Exception) { + LogUtils.e("WIFI_POSITION", "Failed to register scan receiver", e) + } + } + + private fun unregisterScanResultsReceiverIfNeeded() { + if (!scanReceiverRegistered) return + try { + context.unregisterReceiver(scanResultsReceiver) + } catch (e: IllegalArgumentException) { + // Receiver already unregistered + } finally { + scanReceiverRegistered = false + scanResultsReceiver = null + } + } + private fun processScanResults(results: List) { lastWifiScanAps = results.map { result -> WifiAp( @@ -84,9 +121,9 @@ class WifiPositioningManager( ) }.toMutableList() - Log.d("WIFI_POSITION", "Wi-Fi scan → AP count: ${lastWifiScanAps.size}") + LogUtils.d("WIFI_POSITION", "Wi-Fi scan → AP count: ${lastWifiScanAps.size}") lastWifiScanAps.forEach { - Log.d("WIFI_POSITION", "SSID: ${it.ssid}, BSSID: ${it.bssid}, RSSI: ${it.rssi} dBm") + LogUtils.d("WIFI_POSITION", "SSID: ${it.ssid}, BSSID: ${it.bssid}, RSSI: ${it.rssi} dBm") } onWifiPositioningUpdate(WifiScanResult(lastWifiScanAps.size, lastWifiScanAps)) @@ -94,13 +131,13 @@ class WifiPositioningManager( private fun startRttRanging(results: List) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.P || wifiRttManager == null) { - Log.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.") + LogUtils.d("WIFI_POSITION", "RTT not supported or WifiRttManager is null.") return } val rttCapableAps = results.filter { it.is80211mcResponder } if (rttCapableAps.isEmpty()) { - Log.d("WIFI_POSITION", "No RTT-capable AP found.") + LogUtils.d("WIFI_POSITION", "No RTT-capable AP found.") return } @@ -111,10 +148,10 @@ class WifiPositioningManager( try { wifiRttManager.startRanging( rangingRequest, - Executors.newSingleThreadExecutor(), + ContextCompat.getMainExecutor(context), object : RangingResultCallback() { override fun onRangingFailure(code: Int) { - Log.e("WIFI_POSITION", "RTT Ranging failed: $code") + LogUtils.e("WIFI_POSITION", "RTT Ranging failed: $code") } override fun onRangingResults(results: List) { @@ -123,7 +160,7 @@ class WifiPositioningManager( val distance = if (result.status == android.net.wifi.rtt.RangingResult.STATUS_SUCCESS) { result.distanceMm / 1000.0 } else { - Log.w("WIFI_POSITION", "RTT distance unavailable for ${result.macAddress}") + LogUtils.w("WIFI_POSITION", "RTT distance unavailable for ${result.macAddress}") null } @@ -131,7 +168,7 @@ class WifiPositioningManager( lastWifiScanAps[idx] = lastWifiScanAps[idx].copy(distance = distance) } - Log.d( + LogUtils.d( "WIFI_POSITION", if (distance != null) "RTT → ${mac}, Distance: ${distance} m" @@ -145,9 +182,13 @@ class WifiPositioningManager( } ) } catch (e: SecurityException) { - Log.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission", e) + LogUtils.e("WIFI_POSITION", "SecurityException: Missing NEARBY_WIFI_DEVICES permission", e) } } + + fun setEnableRtt(enable: Boolean) { + enableRtt = enable + } } data class WifiAp( @@ -160,4 +201,4 @@ data class WifiAp( data class WifiScanResult( val apCount: Int, val aps: List -) \ No newline at end of file +) diff --git a/dist/docs.json b/dist/docs.json index 321edee..a543450 100644 --- a/dist/docs.json +++ b/dist/docs.json @@ -49,6 +49,46 @@ ], "slug": "checkandrequestpermissions" }, + { + "name": "setOptions", + "signature": "(options: DumonGeoOptions) => Promise", + "parameters": [ + { + "name": "options", + "docs": "", + "type": "DumonGeoOptions" + } + ], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [ + "DumonGeoOptions" + ], + "slug": "setoptions" + }, + { + "name": "getGnssStatus", + "signature": "() => Promise", + "parameters": [], + "returns": "Promise", + "tags": [], + "docs": "", + "complexTypes": [ + "SatelliteStatus" + ], + "slug": "getgnssstatus" + }, + { + "name": "getLocationServicesStatus", + "signature": "() => Promise<{ gpsEnabled: boolean; networkEnabled: boolean; }>", + "parameters": [], + "returns": "Promise<{ gpsEnabled: boolean; networkEnabled: boolean; }>", + "tags": [], + "docs": "", + "complexTypes": [], + "slug": "getlocationservicesstatus" + }, { "name": "configureEdgeToEdge", "signature": "(options: { bgColor: string; style: 'DARK' | 'LIGHT'; overlay?: boolean; }) => Promise", @@ -104,6 +144,30 @@ "PositioningData" ], "slug": "addlisteneronpositionupdate-" + }, + { + "name": "addListener", + "signature": "(eventName: 'onGnssStatus', listenerFunc: (data: SatelliteStatus) => void) => PluginListenerHandle", + "parameters": [ + { + "name": "eventName", + "docs": "", + "type": "'onGnssStatus'" + }, + { + "name": "listenerFunc", + "docs": "", + "type": "(data: SatelliteStatus) => void" + } + ], + "returns": "PluginListenerHandle", + "tags": [], + "docs": "", + "complexTypes": [ + "PluginListenerHandle", + "SatelliteStatus" + ], + "slug": "addlistenerongnssstatus-" } ], "properties": [] @@ -211,6 +275,136 @@ } ] }, + { + "name": "DumonGeoOptions", + "slug": "dumongeooptions", + "docs": "", + "tags": [], + "methods": [], + "properties": [ + { + "name": "distanceThresholdMeters", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "speedChangeThreshold", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "directionChangeThreshold", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "emitDebounceMs", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "drivingEmitIntervalMs", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "wifiScanIntervalMs", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "enableWifiRtt", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "enableLogging", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "enableForwardPrediction", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "maxPredictionSeconds", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number | undefined" + }, + { + "name": "emitGnssStatus", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "suppressMockedUpdates", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "boolean | undefined" + }, + { + "name": "keepScreenOn", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "boolean | undefined" + } + ] + }, + { + "name": "SatelliteStatus", + "slug": "satellitestatus", + "docs": "", + "tags": [], + "methods": [], + "properties": [ + { + "name": "satellitesInView", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number" + }, + { + "name": "usedInFix", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "number" + }, + { + "name": "constellationCounts", + "tags": [], + "docs": "", + "complexTypes": [], + "type": "{ [key: string]: number; }" + } + ] + }, { "name": "PluginListenerHandle", "slug": "pluginlistenerhandle", diff --git a/dist/esm/definitions.d.ts b/dist/esm/definitions.d.ts index 60dad03..422d2a6 100644 --- a/dist/esm/definitions.d.ts +++ b/dist/esm/definitions.d.ts @@ -11,6 +11,28 @@ export interface PositioningData { isMocked: boolean; predicted?: boolean; } +export interface SatelliteStatus { + satellitesInView: number; + usedInFix: number; + constellationCounts: { + [key: string]: number; + }; +} +export interface DumonGeoOptions { + distanceThresholdMeters?: number; + speedChangeThreshold?: number; + directionChangeThreshold?: number; + emitDebounceMs?: number; + drivingEmitIntervalMs?: number; + wifiScanIntervalMs?: number; + enableWifiRtt?: boolean; + enableLogging?: boolean; + enableForwardPrediction?: boolean; + maxPredictionSeconds?: number; + emitGnssStatus?: boolean; + suppressMockedUpdates?: boolean; + keepScreenOn?: boolean; +} export interface PermissionStatus { location: 'granted' | 'denied'; wifi: 'granted' | 'denied'; @@ -20,6 +42,12 @@ export interface DumonGeolocationPlugin { stopPositioning(): Promise; getLatestPosition(): Promise; checkAndRequestPermissions(): Promise; + setOptions(options: DumonGeoOptions): Promise; + getGnssStatus(): Promise; + getLocationServicesStatus(): Promise<{ + gpsEnabled: boolean; + networkEnabled: boolean; + }>; configureEdgeToEdge(options: { bgColor: string; style: 'DARK' | 'LIGHT'; @@ -29,4 +57,5 @@ export interface DumonGeolocationPlugin { mode: 'normal' | 'driving'; }): Promise; addListener(eventName: 'onPositionUpdate', listenerFunc: (data: PositioningData) => void): PluginListenerHandle; + addListener(eventName: 'onGnssStatus', listenerFunc: (data: SatelliteStatus) => void): PluginListenerHandle; } diff --git a/dist/esm/definitions.js.map b/dist/esm/definitions.js.map index b75245f..bd6647c 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 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\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}"]} \ 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}\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 diff --git a/dist/esm/web.d.ts b/dist/esm/web.d.ts index ce73fe6..47b94d4 100644 --- a/dist/esm/web.d.ts +++ b/dist/esm/web.d.ts @@ -1,5 +1,5 @@ import { WebPlugin } from '@capacitor/core'; -import type { PositioningData } from './definitions'; +import type { PositioningData, DumonGeoOptions, SatelliteStatus } from './definitions'; export declare class DumonGeolocationWeb extends WebPlugin { startPositioning(): Promise; stopPositioning(): Promise; @@ -13,4 +13,10 @@ export declare class DumonGeolocationWeb extends WebPlugin { style: 'DARK' | 'LIGHT'; overlay?: boolean; }): Promise; + setOptions(_options: DumonGeoOptions): Promise; + getGnssStatus(): Promise; + getLocationServicesStatus(): Promise<{ + gpsEnabled: boolean; + networkEnabled: boolean; + }>; } diff --git a/dist/esm/web.js b/dist/esm/web.js index feee146..bddc6b2 100644 --- a/dist/esm/web.js +++ b/dist/esm/web.js @@ -31,5 +31,15 @@ export class DumonGeolocationWeb extends WebPlugin { console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options); // No-op } + async setOptions(_options) { + // No-op on web + } + async getGnssStatus() { + return null; + } + async getLocationServicesStatus() { + // Web stub; assume enabled + return { gpsEnabled: true, networkEnabled: true }; + } } //# 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 96d5ce0..9d1d4c6 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;CACF","sourcesContent":["import { WebPlugin } from '@capacitor/core';\nimport type { PositioningData } 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}"]} \ 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;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 diff --git a/dist/plugin.cjs.js b/dist/plugin.cjs.js index 776d759..0d01963 100644 --- a/dist/plugin.cjs.js +++ b/dist/plugin.cjs.js @@ -38,6 +38,16 @@ class DumonGeolocationWeb extends core.WebPlugin { console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options); // No-op } + async setOptions(_options) { + // No-op on web + } + async getGnssStatus() { + return null; + } + async getLocationServicesStatus() { + // Web stub; assume enabled + return { gpsEnabled: true, networkEnabled: true }; + } } var web = /*#__PURE__*/Object.freeze({ diff --git a/dist/plugin.cjs.js.map b/dist/plugin.cjs.js.map index 9e649df..d947eca 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}\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;;;;;;;;;"} \ 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}\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 diff --git a/dist/plugin.js b/dist/plugin.js index ec55d7f..71abd54 100644 --- a/dist/plugin.js +++ b/dist/plugin.js @@ -37,6 +37,16 @@ var capacitorDumonGeolocation = (function (exports, core) { console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options); // No-op } + async setOptions(_options) { + // No-op on web + } + async getGnssStatus() { + return null; + } + async getLocationServicesStatus() { + // Web stub; assume enabled + return { gpsEnabled: true, networkEnabled: true }; + } } var web = /*#__PURE__*/Object.freeze({ diff --git a/dist/plugin.js.map b/dist/plugin.js.map index c9865b0..0b11cda 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}\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;;;;;;;;;;;;;;;"} \ 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}\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 diff --git a/example-app/package-lock.json b/example-app/package-lock.json index 84ab05b..23d8028 100644 --- a/example-app/package-lock.json +++ b/example-app/package-lock.json @@ -20,7 +20,7 @@ } }, "..": { - "version": "0.0.1", + "version": "1.0.2", "license": "MIT", "devDependencies": { "@capacitor/android": "^7.0.0", diff --git a/example-app/src/index.html b/example-app/src/index.html index bf83e55..cefd414 100644 --- a/example-app/src/index.html +++ b/example-app/src/index.html @@ -50,9 +50,43 @@ + + + +

Modes

+ + + +

Options

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

Diagnostics

+ +

 
     
   
-
\ No newline at end of file
+
diff --git a/example-app/src/js/example.js b/example-app/src/js/example.js
index c85d223..fe81ddb 100644
--- a/example-app/src/js/example.js
+++ b/example-app/src/js/example.js
@@ -1,18 +1,99 @@
 import { DumonGeolocation } from 'dumon-geolocation';
 
 const logArea = document.getElementById('logArea');
+let posListenerHandle = null;
+let gnssListenerHandle = null;
 
 function appendLog(title, data) {
   const timestamp = new Date().toLocaleTimeString();
   const formatted = `[${timestamp}] ${title}\n${JSON.stringify(data, null, 2)}\n\n`;
-  logArea.textContent = formatted; // + logArea.textContent;
+  logArea.textContent = formatted + logArea.textContent;
+}
+
+function clearLog() {
+  logArea.textContent = '';
+}
+
+function readNumber(id) {
+  const el = document.getElementById(id);
+  if (!el) return undefined;
+  const v = el.value.trim();
+  if (v === '') return undefined;
+  const n = Number(v);
+  return Number.isFinite(n) ? n : undefined;
+}
+
+function readBool(id) {
+  const el = document.getElementById(id);
+  return !!el?.checked;
+}
+
+async function applyOptionsFromForm() {
+  const options = {};
+
+  // Booleans
+  if (document.getElementById('optEnableForwardPrediction'))
+    options.enableForwardPrediction = readBool('optEnableForwardPrediction');
+  if (document.getElementById('optEmitGnssStatus'))
+    options.emitGnssStatus = readBool('optEmitGnssStatus');
+  if (document.getElementById('optEnableWifiRtt'))
+    options.enableWifiRtt = readBool('optEnableWifiRtt');
+  if (document.getElementById('optEnableLogging'))
+    options.enableLogging = readBool('optEnableLogging');
+  if (document.getElementById('optSuppressMocked'))
+    options.suppressMockedUpdates = readBool('optSuppressMocked');
+  if (document.getElementById('optKeepScreenOn'))
+    options.keepScreenOn = readBool('optKeepScreenOn');
+
+  // Numbers
+  const emitDebounceMs = readNumber('optEmitDebounceMs');
+  const drivingEmitIntervalMs = readNumber('optDrivingEmitIntervalMs');
+  const wifiScanIntervalMs = readNumber('optWifiScanIntervalMs');
+  const distanceThresholdMeters = readNumber('optDistanceThreshold');
+  const speedChangeThreshold = readNumber('optSpeedThreshold');
+  const directionChangeThreshold = readNumber('optDirectionThreshold');
+  const maxPredictionSeconds = readNumber('optMaxPrediction');
+
+  if (emitDebounceMs !== undefined) options.emitDebounceMs = emitDebounceMs;
+  if (drivingEmitIntervalMs !== undefined) options.drivingEmitIntervalMs = drivingEmitIntervalMs;
+  if (wifiScanIntervalMs !== undefined) options.wifiScanIntervalMs = wifiScanIntervalMs;
+  if (distanceThresholdMeters !== undefined)
+    options.distanceThresholdMeters = distanceThresholdMeters;
+  if (speedChangeThreshold !== undefined) options.speedChangeThreshold = speedChangeThreshold;
+  if (directionChangeThreshold !== undefined)
+    options.directionChangeThreshold = directionChangeThreshold;
+  if (maxPredictionSeconds !== undefined) options.maxPredictionSeconds = maxPredictionSeconds;
+
+  await DumonGeolocation.setOptions(options);
+  appendLog('setOptions', options);
+
+  // Manage GNSS status listener dynamically
+  const wantGnss = !!options.emitGnssStatus;
+  if (wantGnss && !gnssListenerHandle) {
+    gnssListenerHandle = DumonGeolocation.addListener('onGnssStatus', (data) => {
+      appendLog('onGnssStatus', data);
+    });
+  } else if (!wantGnss && gnssListenerHandle) {
+    await gnssListenerHandle.remove();
+    gnssListenerHandle = null;
+  }
+}
+
+async function requestPermissions() {
+  try {
+    const perm = await DumonGeolocation.checkAndRequestPermissions();
+    appendLog('permissions', perm);
+  } catch (err) {
+    appendLog('permissions', { error: err.message });
+  }
 }
 
 async function startGeolocation() {
-  DumonGeolocation.addListener('onPositionUpdate', (data) => {
-    appendLog('onPositionUpdate', data);
-  });
-
+  if (!posListenerHandle) {
+    posListenerHandle = DumonGeolocation.addListener('onPositionUpdate', (data) => {
+      appendLog('onPositionUpdate', data);
+    });
+  }
   try {
     await DumonGeolocation.startPositioning();
     appendLog('startPositioning', { success: true });
@@ -39,15 +120,58 @@ async function getLatestPosition() {
   }
 }
 
+async function setDrivingMode() {
+  try {
+    await DumonGeolocation.setGpsMode({ mode: 'driving' });
+    appendLog('setGpsMode', { mode: 'driving' });
+  } catch (err) {
+    appendLog('setGpsMode', { error: err.message });
+  }
+}
+
+async function setNormalMode() {
+  try {
+    await DumonGeolocation.setGpsMode({ mode: 'normal' });
+    appendLog('setGpsMode', { mode: 'normal' });
+  } catch (err) {
+    appendLog('setGpsMode', { error: err.message });
+  }
+}
+
+async function getGnssStatus() {
+  try {
+    const data = await DumonGeolocation.getGnssStatus();
+    appendLog('getGnssStatus', data);
+  } catch (err) {
+    appendLog('getGnssStatus', { error: err.message });
+  }
+}
+
+async function getLocationServicesStatus() {
+  try {
+    const data = await DumonGeolocation.getLocationServicesStatus();
+    appendLog('getLocationServicesStatus', data);
+  } catch (err) {
+    appendLog('getLocationServicesStatus', { error: err.message });
+  }
+}
+
 window.addEventListener('DOMContentLoaded', async () => {
   document.getElementById('startButton').addEventListener('click', startGeolocation);
   document.getElementById('stopButton').addEventListener('click', stopGeolocation);
   document.getElementById('getLatestButton').addEventListener('click', getLatestPosition);
+  document.getElementById('permButton').addEventListener('click', requestPermissions);
+  document.getElementById('drivingBtn').addEventListener('click', setDrivingMode);
+  document.getElementById('normalBtn').addEventListener('click', setNormalMode);
+  document.getElementById('applyOptionsButton').addEventListener('click', applyOptionsFromForm);
+  document.getElementById('getGnssStatusButton').addEventListener('click', getGnssStatus);
+  document.getElementById('getLocationServicesStatusButton').addEventListener('click', getLocationServicesStatus);
+  document.getElementById('clearLogButton').addEventListener('click', clearLog);
 
-
+  // Apply a reasonable default UI config for E2E
   await DumonGeolocation.configureEdgeToEdge({
     bgColor: '#ffffff',
     style: 'DARK',
-    overlay: false, // atau true jika kamu ingin konten masuk ke area statusbar/navbar
+    overlay: false,
   });
-});
\ No newline at end of file
+});
diff --git a/package.json b/package.json
index 3c66c6d..cda3261 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "dumon-geolocation",
-  "version": "0.0.1",
+  "version": "1.0.2",
   "description": "Implement manager GNSS, Wi‑Fi RTT, IMU, Kalman fusion, event emitter",
   "main": "dist/plugin.cjs.js",
   "module": "dist/esm/index.js",
diff --git a/src/definitions.ts b/src/definitions.ts
index f9107e6..b1bd6a7 100644
--- a/src/definitions.ts
+++ b/src/definitions.ts
@@ -64,6 +64,28 @@ export interface PositioningData {
   predicted?: boolean;
 }
 
+export interface SatelliteStatus {
+  satellitesInView: number;
+  usedInFix: number;
+  constellationCounts: { [key: string]: number };
+}
+
+export interface DumonGeoOptions {
+  distanceThresholdMeters?: number;
+  speedChangeThreshold?: number;
+  directionChangeThreshold?: number;
+  emitDebounceMs?: number;
+  drivingEmitIntervalMs?: number;
+  wifiScanIntervalMs?: number;
+  enableWifiRtt?: boolean;
+  enableLogging?: boolean;
+  enableForwardPrediction?: boolean;
+  maxPredictionSeconds?: number;
+  emitGnssStatus?: boolean;
+  suppressMockedUpdates?: boolean;
+  keepScreenOn?: boolean;
+}
+
 export interface PermissionStatus {
   location: 'granted' | 'denied';
   wifi: 'granted' | 'denied';
@@ -74,6 +96,9 @@ export interface DumonGeolocationPlugin {
   stopPositioning(): Promise;
   getLatestPosition(): Promise;
   checkAndRequestPermissions(): Promise;
+  setOptions(options: DumonGeoOptions): Promise;
+  getGnssStatus(): Promise;
+  getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;
 
   configureEdgeToEdge(options: {
     bgColor: string;
@@ -87,4 +112,9 @@ export interface DumonGeolocationPlugin {
     eventName: 'onPositionUpdate',
     listenerFunc: (data: PositioningData) => void
   ): PluginListenerHandle;
-}
\ No newline at end of file
+
+  addListener(
+    eventName: 'onGnssStatus',
+    listenerFunc: (data: SatelliteStatus) => void
+  ): PluginListenerHandle;
+}
diff --git a/src/web.ts b/src/web.ts
index 29423ff..1b0e5a6 100644
--- a/src/web.ts
+++ b/src/web.ts
@@ -1,5 +1,5 @@
 import { WebPlugin } from '@capacitor/core';
-import type { PositioningData } from './definitions';
+import type { PositioningData, DumonGeoOptions, SatelliteStatus } from './definitions';
 
 export class DumonGeolocationWeb extends WebPlugin {
   async startPositioning(): Promise {
@@ -44,4 +44,17 @@ export class DumonGeolocationWeb extends WebPlugin {
     console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);
     // No-op
   }
-}
\ No newline at end of file
+
+  async setOptions(_options: DumonGeoOptions): Promise {
+    // No-op on web
+  }
+
+  async getGnssStatus(): Promise {
+    return null;
+  }
+
+  async getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }> {
+    // Web stub; assume enabled
+    return { gpsEnabled: true, networkEnabled: true };
+  }
+}