20260106-01

This commit is contained in:
wengki81 2026-01-06 11:08:16 +08:00
parent a392b97e50
commit 244f70b567
15 changed files with 326 additions and 411 deletions

View File

@ -40,13 +40,29 @@ import com.dumon.plugin.geolocation.utils.AuthStore
@CapacitorPlugin(
name = "DumonGeolocation",
permissions = [
Permission(strings = [
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION,
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.NEARBY_WIFI_DEVICES
])
Permission(
alias = "location",
strings = [
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.ACCESS_COARSE_LOCATION
]
),
Permission(
alias = "wifi",
strings = [
Manifest.permission.ACCESS_WIFI_STATE,
Manifest.permission.CHANGE_WIFI_STATE,
Manifest.permission.NEARBY_WIFI_DEVICES
]
),
Permission(
alias = "backgroundLocation",
strings = [Manifest.permission.ACCESS_BACKGROUND_LOCATION]
),
Permission(
alias = "notifications",
strings = [Manifest.permission.POST_NOTIFICATIONS]
)
]
)
class DumonGeolocation : Plugin() {
@ -76,6 +92,7 @@ class DumonGeolocation : Plugin() {
private var prevTimestamp: Long = 0L
// private val significantChangeThreshold = 0.00007 // ~7 meters
private var significantChangeThreshold = 7.0 // ~7 meters
private var gpsMinDistanceMeters = 7.0
private var speedChangeThreshold = 0.5f // m/s
private var directionChangeThreshold = 0.17f // ~10 deg
@ -97,6 +114,10 @@ class DumonGeolocation : Plugin() {
private var emitGnssStatus = false
private var suppressMockedUpdates = false
private var keepScreenOn = false
private var lastSingleFixRequestTs: Long = 0L
private val minSingleFixIntervalMs: Long = 1000L
private var pendingPermissionAlias: String? = null
private var pendingBackgroundPermissionAlias: String? = null
private var currentTrackingMode = GpsTrackingMode.NORMAL
@ -183,15 +204,19 @@ class DumonGeolocation : Plugin() {
@PluginMethod
fun startPositioning(call: PluginCall) {
if (!PermissionUtils.hasLocationAndWifiPermissions(context)) {
call.reject("Required permissions not granted")
if (!PermissionUtils.hasLocationPermissions(context)) {
call.reject("Location permission not granted")
return
}
gpsManager?.start()
imuManager?.start()
wifiManager?.setEnableRtt(enableWifiRtt)
wifiManager?.startPeriodicScan(wifiScanIntervalMs)
if (PermissionUtils.hasWifiScanPermissions(context)) {
wifiManager?.setEnableRtt(enableWifiRtt)
wifiManager?.startPeriodicScan(wifiScanIntervalMs)
} else {
LogUtils.w("DUMON_GEOLOCATION", "Wi-Fi scan permissions not granted; skipping Wi-Fi scans")
}
applyKeepScreenOn(keepScreenOn)
call.resolve()
}
@ -212,6 +237,13 @@ class DumonGeolocation : Plugin() {
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 now = System.currentTimeMillis()
if (now - lastSingleFixRequestTs < minSingleFixIntervalMs) {
call.resolve(buildPositionData())
return
}
lastSingleFixRequestTs = now
val manager = gpsManager
if (manager == null || (!hasFine && !hasCoarse)) {
// No manager or no permissions — fallback immediately
@ -245,36 +277,10 @@ class DumonGeolocation : Plugin() {
@PluginMethod
fun checkAndRequestPermissions(call: PluginCall) {
// requestAllPermissions(call, "checkAndRequestPermissions")
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")
if (!requestNextForegroundPermission(call)) {
return
}
val locationStatus = PermissionUtils.getPermissionStatus(
ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
)
val wifiStatus = PermissionUtils.getPermissionStatus(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
ActivityCompat.checkSelfPermission(context, Manifest.permission.NEARBY_WIFI_DEVICES)
else
PackageManager.PERMISSION_GRANTED
)
val result = JSObject().apply {
put("location", locationStatus)
put("wifi", wifiStatus)
}
call.resolve(result)
call.resolve(buildForegroundPermissionResult())
}
@PluginMethod
@ -343,11 +349,13 @@ class DumonGeolocation : Plugin() {
val mode = call.getString("mode") ?: "normal"
if (mode == "driving") {
gpsManager?.startContinuousMode()
gpsManager?.setMinDistanceMeters(0.0)
currentTrackingMode = GpsTrackingMode.DRIVING
startDrivingEmitLoop()
LogUtils.d("DUMON_GEOLOCATION", "Switched to driving mode (continuous GPS)")
} else {
gpsManager?.startPollingMode()
gpsManager?.setMinDistanceMeters(gpsMinDistanceMeters)
currentTrackingMode = GpsTrackingMode.NORMAL
stopDrivingEmitLoop()
LogUtils.d("DUMON_GEOLOCATION", "Switched to normal mode (polling GPS)")
@ -357,27 +365,21 @@ class DumonGeolocation : Plugin() {
@PermissionCallback
private fun onPermissionResult(call: PluginCall) {
val locationStatus = PermissionUtils.getPermissionStatus(
ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
)
val wifiStatus = PermissionUtils.getPermissionStatus(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
ActivityCompat.checkSelfPermission(context, Manifest.permission.NEARBY_WIFI_DEVICES)
else
PackageManager.PERMISSION_GRANTED
)
val result = JSObject().apply {
put("location", locationStatus)
put("wifi", wifiStatus)
if (!requestNextForegroundPermission(call)) {
return
}
call.resolve(result)
call.resolve(buildForegroundPermissionResult())
}
@PluginMethod
fun setOptions(call: PluginCall) {
call.getDouble("distanceThresholdMeters")?.let { significantChangeThreshold = it }
call.getDouble("distanceThresholdMeters")?.let {
significantChangeThreshold = it
gpsMinDistanceMeters = it
if (currentTrackingMode == GpsTrackingMode.NORMAL) {
gpsManager?.setMinDistanceMeters(it)
}
}
call.getDouble("speedChangeThreshold")?.let { speedChangeThreshold = it.toFloat() }
call.getDouble("directionChangeThreshold")?.let { directionChangeThreshold = it.toFloat() }
call.getInt("emitDebounceMs")?.let {
@ -480,19 +482,15 @@ class DumonGeolocation : Plugin() {
@PluginMethod
fun startBackgroundTracking(call: PluginCall) {
if (!ensureBackgroundPermissions(call)) {
return
}
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)
@ -800,4 +798,88 @@ class DumonGeolocation : Plugin() {
applyKeepScreenOn(false)
super.handleOnDestroy()
}
private fun buildForegroundPermissionResult(): JSObject {
val locationStatus = if (PermissionUtils.hasLocationPermissions(context)) "granted" else "denied"
val wifiStatus = if (PermissionUtils.hasWifiScanPermissions(context)) "granted" else "denied"
return JSObject().apply {
put("location", locationStatus)
put("wifi", wifiStatus)
}
}
private fun getMissingForegroundPermissionAlias(): String? {
return when {
!PermissionUtils.hasLocationPermissions(context) -> "location"
!PermissionUtils.hasWifiScanPermissions(context) -> "wifi"
else -> null
}
}
private fun requestNextForegroundPermission(call: PluginCall): Boolean {
val missing = getMissingForegroundPermissionAlias()
if (missing == null) {
pendingPermissionAlias = null
return true
}
if (pendingPermissionAlias == missing) {
pendingPermissionAlias = null
call.resolve(buildForegroundPermissionResult())
return false
}
pendingPermissionAlias = missing
call.save()
requestPermissionForAlias(missing, call, "onPermissionResult")
return false
}
private fun getMissingBackgroundPermissionAlias(): String? {
if (!PermissionUtils.hasLocationPermissions(context)) {
return "location"
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val bgGranted = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_BACKGROUND_LOCATION) == PackageManager.PERMISSION_GRANTED
if (!bgGranted) return "backgroundLocation"
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
val notifGranted = ActivityCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) == PackageManager.PERMISSION_GRANTED
if (!notifGranted) return "notifications"
}
return null
}
private fun ensureBackgroundPermissions(call: PluginCall): Boolean {
val missing = getMissingBackgroundPermissionAlias()
if (missing == null) {
pendingBackgroundPermissionAlias = null
return true
}
if (pendingBackgroundPermissionAlias == missing) {
pendingBackgroundPermissionAlias = null
val msg = when (missing) {
"location" -> "Location permission not granted"
"backgroundLocation" -> "Background location permission not granted"
"notifications" -> "Notification permission not granted"
else -> "Required permission not granted"
}
call.reject(msg)
return false
}
pendingBackgroundPermissionAlias = missing
call.save()
requestPermissionForAlias(missing, call, "onBackgroundPermissionResult")
return false
}
@PermissionCallback
private fun onBackgroundPermissionResult(call: PluginCall) {
if (!ensureBackgroundPermissions(call)) {
return
}
startBackgroundTracking(call)
}
}

View File

@ -32,7 +32,7 @@ class GpsStatusManager(
private val handler = Handler(Looper.getMainLooper())
private var pollingIntervalMs: Long = 1000L // Default NORMAL mode
private var isPolling = false
private var minDistanceMeters: Float = 0f
private var currentMode = GpsTrackingMode.NORMAL
private var continuousListener: LocationListener? = null
@ -153,83 +153,17 @@ class GpsStatusManager(
mainHandler.postDelayed(timeoutRunnable, timeoutMs)
}
private fun pollOnceAndEmit() {
val oneShotListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
val providerTag = when (location.provider) {
LocationManager.GPS_PROVIDER -> "[GPS]"
LocationManager.NETWORK_PROVIDER -> "[NET]"
else -> "[OTHER]"
}
val timestamp = DateFormat.format("HH:mm:ss", System.currentTimeMillis())
val isMocked = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
location.isMock
} else {
location.isFromMockProvider
}
val info = "$providerTag Lat: ${location.latitude}, Lon: ${location.longitude}, Acc: ${location.accuracy} m @ $timestamp | Mock=$isMocked"
LogUtils.d("GPS_LOCATION", info)
onLocationUpdate(location, isMocked)
locationManager.removeUpdates(this)
}
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) {}
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
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)")
}
}
private val pollingRunnable = object : Runnable {
override fun run() {
if (!isPolling) return
pollOnceAndEmit()
handler.postDelayed(this, pollingIntervalMs)
}
}
fun setPollingInterval(intervalMs: Long) {
this.pollingIntervalMs = intervalMs
if (isPolling) {
handler.removeCallbacks(pollingRunnable)
handler.postDelayed(pollingRunnable, pollingIntervalMs)
if (currentMode == GpsTrackingMode.NORMAL) {
startContinuousUpdates(pollingIntervalMs, minDistanceMeters)
}
}
fun setMinDistanceMeters(distanceMeters: Double) {
minDistanceMeters = distanceMeters.toFloat().coerceAtLeast(0f)
if (currentMode == GpsTrackingMode.NORMAL) {
startContinuousUpdates(pollingIntervalMs, minDistanceMeters)
}
}
@ -238,25 +172,30 @@ class GpsStatusManager(
stop() // Reset dulu
currentMode = mode
if (ActivityCompat.checkSelfPermission(context, android.Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
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", "Missing location permissions")
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
locationManager.registerGnssStatusCallback(
ContextCompat.getMainExecutor(context),
gnssStatusCallback
)
} else {
locationManager.registerGnssStatusCallback(gnssStatusCallback, handler)
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
locationManager.registerGnssStatusCallback(
ContextCompat.getMainExecutor(context),
gnssStatusCallback
)
} else {
locationManager.registerGnssStatusCallback(gnssStatusCallback, handler)
}
} catch (e: Exception) {
LogUtils.e("GPS_STATUS", "Failed to register GNSS status callback: ${e.message}")
}
if (mode == GpsTrackingMode.DRIVING) {
startContinuousUpdates()
startContinuousUpdates(0L, 0f)
} else {
isPolling = true
handler.post(pollingRunnable)
startContinuousUpdates(pollingIntervalMs, minDistanceMeters)
}
// Fallback lokasi terakhir
@ -274,12 +213,25 @@ class GpsStatusManager(
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) {
LogUtils.e("GPS_STATUS", "Missing ACCESS_FINE_LOCATION permission")
private fun startContinuousUpdates(minTimeMs: Long, minDistanceMeters: Float) {
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", "Missing location permissions")
return
}
continuousListener?.let {
try { locationManager.removeUpdates(it) } catch (_: Exception) {}
}
continuousListener = object : LocationListener {
override fun onLocationChanged(location: Location) {
val isMocked = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) location.isMock else location.isFromMockProvider
@ -291,31 +243,77 @@ class GpsStatusManager(
override fun onProviderDisabled(provider: String) {}
}
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
0L,
0f,
continuousListener!!
)
val gpsEnabled = try {
locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)
} catch (_: Exception) {
false
}
val netEnabled = try {
locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)
} catch (_: Exception) {
false
}
LogUtils.d("GPS_STATUS", "Started continuous location updates")
if (!gpsEnabled && !netEnabled) {
LogUtils.w("GPS_STATUS", "No provider enabled; skip location requests")
}
var requested = false
if (hasFine && gpsEnabled) {
try {
locationManager.requestLocationUpdates(
LocationManager.GPS_PROVIDER,
minTimeMs,
minDistanceMeters,
continuousListener!!
)
requested = true
} catch (e: Exception) {
LogUtils.e("GPS_STATUS", "Failed GPS request: ${e.message}")
}
} else if (hasFine) {
LogUtils.w("GPS_STATUS", "GPS provider disabled; skip GPS request")
}
if (hasCoarse && netEnabled) {
try {
locationManager.requestLocationUpdates(
LocationManager.NETWORK_PROVIDER,
minTimeMs,
minDistanceMeters,
continuousListener!!
)
requested = true
} catch (e: Exception) {
LogUtils.e("GPS_STATUS", "Failed NETWORK request: ${e.message}")
}
} else if (hasCoarse) {
LogUtils.w("GPS_STATUS", "Network provider disabled; skip NETWORK request")
}
if (requested) {
LogUtils.d("GPS_STATUS", "Started continuous updates (minTimeMs=$minTimeMs, minDistanceMeters=$minDistanceMeters)")
} else {
LogUtils.e("GPS_STATUS", "No provider requested (permissions/providers)")
}
}
fun startContinuousMode() {
stop()
startContinuousUpdates()
currentMode = GpsTrackingMode.DRIVING
startContinuousUpdates(0L, 0f)
}
fun startPollingMode() {
stop()
isPolling = true
handler.post(pollingRunnable)
currentMode = GpsTrackingMode.NORMAL
startContinuousUpdates(pollingIntervalMs, minDistanceMeters)
}
fun stop() {
isPolling = false
handler.removeCallbacks(pollingRunnable)
locationManager.unregisterGnssStatusCallback(gnssStatusCallback)
try {
locationManager.unregisterGnssStatusCallback(gnssStatusCallback)
} catch (_: Exception) {}
continuousListener?.let {
locationManager.removeUpdates(it)
continuousListener = null

View File

@ -29,6 +29,7 @@ class ImuSensorManager(
private var latestDirectionRad = 0f
private var latestSpeed = 0f
private var lastLogTimestampMs: Long = 0L
private var currentDelay: Int = SensorManager.SENSOR_DELAY_GAME
@ -111,9 +112,16 @@ class ImuSensorManager(
emitCombinedImuData(speed, acceleration, latestDirectionRad)
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
))
val nowMs = System.currentTimeMillis()
if (nowMs - lastLogTimestampMs >= 1000L) {
lastLogTimestampMs = nowMs
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
)
)
}
}
private fun handleGyroscope(event: SensorEvent) {

View File

@ -57,11 +57,6 @@ object BgPrefs {
.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)

View File

@ -7,21 +7,31 @@ import android.os.Build
import androidx.core.app.ActivityCompat
object PermissionUtils {
fun hasLocationAndWifiPermissions(context: Context): Boolean {
fun hasLocationPermissions(context: Context): Boolean {
val fineLocation = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION)
val coarseLocation = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION)
val nearbyWifi = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.checkSelfPermission(context, Manifest.permission.NEARBY_WIFI_DEVICES)
} else {
PackageManager.PERMISSION_GRANTED
return fineLocation == PackageManager.PERMISSION_GRANTED || coarseLocation == PackageManager.PERMISSION_GRANTED
}
fun hasWifiScanPermissions(context: Context): Boolean {
val wifiState = ActivityCompat.checkSelfPermission(context, Manifest.permission.ACCESS_WIFI_STATE)
val changeWifi = ActivityCompat.checkSelfPermission(context, Manifest.permission.CHANGE_WIFI_STATE)
if (wifiState != PackageManager.PERMISSION_GRANTED || changeWifi != PackageManager.PERMISSION_GRANTED) {
return false
}
return fineLocation == PackageManager.PERMISSION_GRANTED &&
coarseLocation == PackageManager.PERMISSION_GRANTED &&
nearbyWifi == PackageManager.PERMISSION_GRANTED
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
ActivityCompat.checkSelfPermission(context, Manifest.permission.NEARBY_WIFI_DEVICES) == PackageManager.PERMISSION_GRANTED
} else {
hasLocationPermissions(context)
}
}
fun hasLocationAndWifiPermissions(context: Context): Boolean {
return hasLocationPermissions(context) && hasWifiScanPermissions(context)
}
fun getPermissionStatus(granted: Int): String {
return if (granted == PackageManager.PERMISSION_GRANTED) "granted" else "denied"
}
}
}

View File

@ -14,8 +14,8 @@ 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 androidx.core.content.ContextCompat
import com.dumon.plugin.geolocation.utils.PermissionUtils
class WifiPositioningManager(
private val context: Context,
@ -62,18 +62,20 @@ class WifiPositioningManager(
}
fun startWifiScan() {
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
) {
if (!PermissionUtils.hasWifiScanPermissions(context)) {
LogUtils.e("WIFI_POSITION", "Missing Wi-Fi scan permission")
onWifiPositioningUpdate(WifiScanResult(0, emptyList()))
return
}
try {
val success = wifiManager.startScan()
if (!success) {
LogUtils.e("WIFI_POSITION", "Wi-Fi scan failed")
onWifiPositioningUpdate(WifiScanResult(0, emptyList()))
}
} else {
LogUtils.e("WIFI_POSITION", "Missing ACCESS_FINE_LOCATION permission")
} catch (e: SecurityException) {
LogUtils.e("WIFI_POSITION", "SecurityException on Wi-Fi scan: ${e.message}")
onWifiPositioningUpdate(WifiScanResult(0, emptyList()))
}
}

View File

@ -1 +1 @@
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// export interface SatelliteStatus {\n// satellitesInView: number;\n// usedInFix: number;\n// constellationCounts: { [key: string]: number };\n// }\n\n// export interface WifiAp {\n// ssid: string;\n// bssid: string;\n// rssi: number;\n// distance?: number;\n// }\n\n// export interface WifiScanResult {\n// apCount: number;\n// aps: WifiAp[];\n// }\n\n// export interface ImuData {\n// accelX: number;\n// accelY: number;\n// accelZ: number;\n// gyroX: number;\n// gyroY: number;\n// gyroZ: number;\n// speed?: number;\n// acceleration?: number;\n// directionRad?: number;\n// }\n\n// export interface GpsData {\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n// satellitesInView?: number;\n// usedInFix?: number;\n// constellationCounts?: { [key: string]: number };\n// }\n\n// export interface PositioningData {\n// source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n// timestamp: number;\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n\n// gnssData?: SatelliteStatus;\n// wifiData?: WifiAp[];\n// imuData?: ImuData;\n// }\n\nexport interface PositioningData {\n source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n timestamp: number;\n latitude: number;\n longitude: number;\n accuracy: number;\n speed: number;\n acceleration: number;\n directionRad: number;\n isMocked: boolean;\n mode: 'normal' | 'driving';\n predicted?: boolean;\n // Optional detailed speed fields and provenance\n speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';\n speedGnss?: number; // m/s from Location.getSpeed when available and fresh\n speedImu?: number; // m/s from IMU fusion (internal heuristic)\n speedDerived?: number; // m/s from delta-position / delta-time (coarse)\n}\n\nexport interface SatelliteStatus {\n satellitesInView: number;\n usedInFix: number;\n constellationCounts: { [key: string]: number };\n}\n\nexport interface DumonGeoOptions {\n distanceThresholdMeters?: number;\n speedChangeThreshold?: number;\n directionChangeThreshold?: number;\n emitDebounceMs?: number;\n drivingEmitIntervalMs?: number;\n wifiScanIntervalMs?: number;\n enableWifiRtt?: boolean;\n enableLogging?: boolean;\n enableForwardPrediction?: boolean;\n maxPredictionSeconds?: number;\n emitGnssStatus?: boolean;\n suppressMockedUpdates?: boolean;\n keepScreenOn?: boolean;\n backgroundPollingIntervalMs?: number; // Android background polling interval\n backgroundPostMinDistanceMeters?: number; // Android background min distance to post\n backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters)\n backgroundMinPostIntervalMs?: number; // Android background minimum interval between POST attempts\n backgroundUseImuFallback?: boolean; // Android background: enable IMU-based speed fallback (default true)\n}\n\nexport interface PermissionStatus {\n location: 'granted' | 'denied';\n wifi: 'granted' | 'denied';\n}\n\nexport interface DumonGeolocationPlugin {\n startPositioning(): Promise<void>;\n stopPositioning(): Promise<void>;\n getLatestPosition(): Promise<PositioningData>;\n checkAndRequestPermissions(): Promise<PermissionStatus>;\n setOptions(options: DumonGeoOptions): Promise<void>;\n getGnssStatus(): Promise<SatelliteStatus | null>;\n getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;\n // Background tracking (Android)\n startBackgroundTracking(options?: {\n title?: string;\n text?: string;\n channelId?: string;\n channelName?: string;\n postUrl?: string; // optional: service will POST latest fixes here as JSON\n }): Promise<void>;\n stopBackgroundTracking(): Promise<void>;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise<PositioningData | null>;\n openBackgroundPermissionSettings(): Promise<void>;\n openNotificationPermissionSettings(): Promise<void>;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>;\n clearAuthTokens(): Promise<void>;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise<void>;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise<void>;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise<void>;\n\n addListener(\n eventName: 'onPositionUpdate',\n listenerFunc: (data: PositioningData) => void\n ): PluginListenerHandle;\n\n addListener(\n eventName: 'onGnssStatus',\n listenerFunc: (data: SatelliteStatus) => void\n ): PluginListenerHandle;\n}\n"]}
{"version":3,"file":"definitions.js","sourceRoot":"","sources":["../../src/definitions.ts"],"names":[],"mappings":"","sourcesContent":["import type { PluginListenerHandle } from '@capacitor/core';\n\n// export interface SatelliteStatus {\n// satellitesInView: number;\n// usedInFix: number;\n// constellationCounts: { [key: string]: number };\n// }\n\n// export interface WifiAp {\n// ssid: string;\n// bssid: string;\n// rssi: number;\n// distance?: number;\n// }\n\n// export interface WifiScanResult {\n// apCount: number;\n// aps: WifiAp[];\n// }\n\n// export interface ImuData {\n// accelX: number;\n// accelY: number;\n// accelZ: number;\n// gyroX: number;\n// gyroY: number;\n// gyroZ: number;\n// speed?: number;\n// acceleration?: number;\n// directionRad?: number;\n// }\n\n// export interface GpsData {\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n// satellitesInView?: number;\n// usedInFix?: number;\n// constellationCounts?: { [key: string]: number };\n// }\n\n// export interface PositioningData {\n// source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n// timestamp: number;\n// latitude: number;\n// longitude: number;\n// accuracy: number;\n\n// gnssData?: SatelliteStatus;\n// wifiData?: WifiAp[];\n// imuData?: ImuData;\n// }\n\nexport interface PositioningData {\n source: 'GNSS' | 'WIFI' | 'FUSED' | 'MOCK';\n timestamp: number;\n latitude: number;\n longitude: number;\n accuracy: number;\n speed: number;\n acceleration: number;\n directionRad: number;\n isMocked: boolean;\n mode: 'normal' | 'driving';\n predicted?: boolean;\n // Optional detailed speed fields and provenance\n speedSource?: 'GNSS' | 'IMU' | 'DELTA' | 'NONE';\n speedGnss?: number; // m/s from Location.getSpeed when available and fresh\n speedImu?: number; // m/s from IMU fusion (internal heuristic)\n speedDerived?: number; // m/s from delta-position / delta-time (coarse)\n}\n\nexport interface SatelliteStatus {\n satellitesInView: number;\n usedInFix: number;\n constellationCounts: { [key: string]: number };\n}\n\nexport interface DumonGeoOptions {\n distanceThresholdMeters?: number;\n speedChangeThreshold?: number;\n directionChangeThreshold?: number;\n emitDebounceMs?: number;\n drivingEmitIntervalMs?: number;\n wifiScanIntervalMs?: number;\n enableWifiRtt?: boolean;\n enableLogging?: boolean;\n enableForwardPrediction?: boolean;\n maxPredictionSeconds?: number;\n emitGnssStatus?: boolean;\n suppressMockedUpdates?: boolean;\n keepScreenOn?: boolean;\n backgroundPollingIntervalMs?: number; // Android background polling interval\n backgroundPostMinDistanceMeters?: number; // Android background min distance to post\n backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters)\n backgroundMinPostIntervalMs?: number; // Android background minimum interval between POST attempts\n backgroundUseImuFallback?: boolean; // Android background: enable IMU-based speed fallback (default false)\n}\n\nexport interface PermissionStatus {\n location: 'granted' | 'denied';\n wifi: 'granted' | 'denied';\n}\n\nexport interface DumonGeolocationPlugin {\n startPositioning(): Promise<void>;\n stopPositioning(): Promise<void>;\n getLatestPosition(): Promise<PositioningData>;\n checkAndRequestPermissions(): Promise<PermissionStatus>;\n setOptions(options: DumonGeoOptions): Promise<void>;\n getGnssStatus(): Promise<SatelliteStatus | null>;\n getLocationServicesStatus(): Promise<{ gpsEnabled: boolean; networkEnabled: boolean }>;\n // Background tracking (Android)\n startBackgroundTracking(options?: {\n title?: string;\n text?: string;\n channelId?: string;\n channelName?: string;\n postUrl?: string; // optional: service will POST latest fixes here as JSON\n }): Promise<void>;\n stopBackgroundTracking(): Promise<void>;\n isBackgroundTrackingActive(): Promise<{ active: boolean }>;\n getBackgroundLatestPosition(): Promise<PositioningData | null>;\n openBackgroundPermissionSettings(): Promise<void>;\n openNotificationPermissionSettings(): Promise<void>;\n // Auth token management for background posting\n setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>;\n clearAuthTokens(): Promise<void>;\n getAuthState(): Promise<{ present: boolean }>;\n setBackgroundPostUrl(options: { url?: string }): Promise<void>;\n getBackgroundPostUrl(): Promise<{ url: string | null }>;\n\n configureEdgeToEdge(options: {\n bgColor: string;\n style: 'DARK' | 'LIGHT';\n overlay?: boolean;\n }): Promise<void>;\n\n setGpsMode(options: { mode: 'normal' | 'driving' }): Promise<void>;\n\n addListener(\n eventName: 'onPositionUpdate',\n listenerFunc: (data: PositioningData) => void\n ): PluginListenerHandle;\n\n addListener(\n eventName: 'onGnssStatus',\n listenerFunc: (data: SatelliteStatus) => void\n ): PluginListenerHandle;\n}\n"]}

6
dist/esm/index.d.ts vendored
View File

@ -1,4 +1,8 @@
import type { DumonGeolocationPlugin } from './definitions';
declare const DumonGeolocation: DumonGeolocationPlugin;
declare const DumonGeolocationPlugin: DumonGeolocationPlugin;
declare const DumonGeolocation: DumonGeolocationPlugin & {
getGnssStatus(): Promise<import("./definitions").SatelliteStatus | null>;
getBackgroundLatestPosition(): Promise<import("./definitions").PositioningData | null>;
};
export * from './definitions';
export { DumonGeolocation };

17
dist/esm/index.js vendored
View File

@ -1,7 +1,22 @@
import { registerPlugin } from '@capacitor/core';
const DumonGeolocation = registerPlugin('DumonGeolocation', {
const DumonGeolocationPlugin = registerPlugin('DumonGeolocation', {
web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),
});
const isEmptyObject = (value) => {
if (!value || typeof value !== 'object' || Array.isArray(value))
return false;
return Object.keys(value).length === 0;
};
const DumonGeolocation = Object.assign(DumonGeolocationPlugin, {
async getGnssStatus() {
const result = await DumonGeolocationPlugin.getGnssStatus();
return isEmptyObject(result) ? null : result;
},
async getBackgroundLatestPosition() {
const result = await DumonGeolocationPlugin.getBackgroundLatestPosition();
return isEmptyObject(result) ? null : result;
},
});
export * from './definitions';
export { DumonGeolocation };
//# sourceMappingURL=index.js.map

View File

@ -1 +1 @@
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,gBAAgB,GAAG,cAAc,CAAyB,kBAAkB,EAAE;IAClF,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;CACpE,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { DumonGeolocationPlugin } from './definitions';\n\nconst DumonGeolocation = registerPlugin<DumonGeolocationPlugin>('DumonGeolocation', {\n web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),\n});\n\nexport * from './definitions';\nexport { DumonGeolocation };"]}
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAIjD,MAAM,sBAAsB,GAAG,cAAc,CAAyB,kBAAkB,EAAE;IACxF,GAAG,EAAE,GAAG,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,CAAC,mBAAmB,EAAE,CAAC;CACpE,CAAC,CAAC;AAEH,MAAM,aAAa,GAAG,CAAC,KAAc,EAAW,EAAE;IAChD,IAAI,CAAC,KAAK,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IAC9E,OAAO,MAAM,CAAC,IAAI,CAAC,KAAgC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC;AACpE,CAAC,CAAC;AAEF,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,CAAC,sBAAsB,EAAE;IAC7D,KAAK,CAAC,aAAa;QACjB,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,aAAa,EAAE,CAAC;QAC5D,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/C,CAAC;IACD,KAAK,CAAC,2BAA2B;QAC/B,MAAM,MAAM,GAAG,MAAM,sBAAsB,CAAC,2BAA2B,EAAE,CAAC;QAC1E,OAAO,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC;IAC/C,CAAC;CACF,CAAC,CAAC;AAEH,cAAc,eAAe,CAAC;AAC9B,OAAO,EAAE,gBAAgB,EAAE,CAAC","sourcesContent":["import { registerPlugin } from '@capacitor/core';\n\nimport type { DumonGeolocationPlugin } from './definitions';\n\nconst DumonGeolocationPlugin = registerPlugin<DumonGeolocationPlugin>('DumonGeolocation', {\n web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),\n});\n\nconst isEmptyObject = (value: unknown): boolean => {\n if (!value || typeof value !== 'object' || Array.isArray(value)) return false;\n return Object.keys(value as Record<string, unknown>).length === 0;\n};\n\nconst DumonGeolocation = Object.assign(DumonGeolocationPlugin, {\n async getGnssStatus() {\n const result = await DumonGeolocationPlugin.getGnssStatus();\n return isEmptyObject(result) ? null : result;\n },\n async getBackgroundLatestPosition() {\n const result = await DumonGeolocationPlugin.getBackgroundLatestPosition();\n return isEmptyObject(result) ? null : result;\n },\n});\n\nexport * from './definitions';\nexport { DumonGeolocation };\n"]}

105
dist/plugin.cjs.js vendored
View File

@ -1,105 +0,0 @@
'use strict';
var core = require('@capacitor/core');
const DumonGeolocation = core.registerPlugin('DumonGeolocation', {
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.DumonGeolocationWeb()),
});
class DumonGeolocationWeb extends core.WebPlugin {
constructor() {
super(...arguments);
this._mode = 'normal';
}
async startPositioning() {
console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
}
async stopPositioning() {
console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
}
async getLatestPosition() {
console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');
return {
source: 'GNSS',
timestamp: Date.now(),
latitude: 0,
longitude: 0,
accuracy: 999,
speed: 0,
acceleration: 0,
directionRad: 0,
isMocked: false,
mode: this._mode,
speedSource: 'NONE',
speedImu: 0,
speedGnss: 0,
speedDerived: 0,
};
}
async checkAndRequestPermissions() {
console.info('[dumon-geolocation] checkAndRequestPermissions mocked for web.');
return {
location: 'granted',
wifi: 'granted',
};
}
async configureEdgeToEdge(options) {
console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);
// No-op
}
async setOptions(_options) {
// No-op on web
}
async setGpsMode(options) {
this._mode = (options === null || options === void 0 ? void 0 : options.mode) === 'driving' ? 'driving' : 'normal';
}
async getGnssStatus() {
return null;
}
async getLocationServicesStatus() {
// 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({
__proto__: null,
DumonGeolocationWeb: DumonGeolocationWeb
});
exports.DumonGeolocation = DumonGeolocation;
//# sourceMappingURL=plugin.cjs.js.map

File diff suppressed because one or more lines are too long

108
dist/plugin.js vendored
View File

@ -1,108 +0,0 @@
var capacitorDumonGeolocation = (function (exports, core) {
'use strict';
const DumonGeolocation = core.registerPlugin('DumonGeolocation', {
web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.DumonGeolocationWeb()),
});
class DumonGeolocationWeb extends core.WebPlugin {
constructor() {
super(...arguments);
this._mode = 'normal';
}
async startPositioning() {
console.log('DumonGeolocationWeb: startPositioning() called (no-op)');
}
async stopPositioning() {
console.log('DumonGeolocationWeb: stopPositioning() called (no-op)');
}
async getLatestPosition() {
console.log('DumonGeolocationWeb: getLatestPosition() called (returning dummy data)');
return {
source: 'GNSS',
timestamp: Date.now(),
latitude: 0,
longitude: 0,
accuracy: 999,
speed: 0,
acceleration: 0,
directionRad: 0,
isMocked: false,
mode: this._mode,
speedSource: 'NONE',
speedImu: 0,
speedGnss: 0,
speedDerived: 0,
};
}
async checkAndRequestPermissions() {
console.info('[dumon-geolocation] checkAndRequestPermissions mocked for web.');
return {
location: 'granted',
wifi: 'granted',
};
}
async configureEdgeToEdge(options) {
console.info('[dumon-geolocation] configureEdgeToEdge called on web with:', options);
// No-op
}
async setOptions(_options) {
// No-op on web
}
async setGpsMode(options) {
this._mode = (options === null || options === void 0 ? void 0 : options.mode) === 'driving' ? 'driving' : 'normal';
}
async getGnssStatus() {
return null;
}
async getLocationServicesStatus() {
// 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({
__proto__: null,
DumonGeolocationWeb: DumonGeolocationWeb
});
exports.DumonGeolocation = DumonGeolocation;
return exports;
})({}, capacitorExports);
//# sourceMappingURL=plugin.js.map

1
dist/plugin.js.map vendored

File diff suppressed because one or more lines are too long

View File

@ -2,9 +2,25 @@ import { registerPlugin } from '@capacitor/core';
import type { DumonGeolocationPlugin } from './definitions';
const DumonGeolocation = registerPlugin<DumonGeolocationPlugin>('DumonGeolocation', {
const DumonGeolocationPlugin = registerPlugin<DumonGeolocationPlugin>('DumonGeolocation', {
web: () => import('./web').then((m) => new m.DumonGeolocationWeb()),
});
const isEmptyObject = (value: unknown): boolean => {
if (!value || typeof value !== 'object' || Array.isArray(value)) return false;
return Object.keys(value as Record<string, unknown>).length === 0;
};
const DumonGeolocation = Object.assign(DumonGeolocationPlugin, {
async getGnssStatus() {
const result = await DumonGeolocationPlugin.getGnssStatus();
return isEmptyObject(result) ? null : result;
},
async getBackgroundLatestPosition() {
const result = await DumonGeolocationPlugin.getBackgroundLatestPosition();
return isEmptyObject(result) ? null : result;
},
});
export * from './definitions';
export { DumonGeolocation };
export { DumonGeolocation };