package com.dumon.plugin.geolocation import android.Manifest import android.content.pm.PackageManager import android.graphics.Color import android.os.Build import android.util.Log import android.view.View import android.view.WindowInsetsController import android.view.WindowManager import androidx.core.app.ActivityCompat import androidx.core.view.ViewCompat import androidx.core.view.WindowInsetsCompat import com.getcapacitor.* import com.getcapacitor.annotation.CapacitorPlugin import com.getcapacitor.annotation.Permission import com.dumon.plugin.geolocation.gps.GpsStatusManager import com.dumon.plugin.geolocation.gps.SatelliteStatus import com.dumon.plugin.geolocation.imu.ImuData import com.dumon.plugin.geolocation.imu.ImuSensorManager 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.getcapacitor.annotation.PermissionCallback import org.json.JSONArray import org.json.JSONObject import kotlin.math.* @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 ]) ] ) class DumonGeolocation : Plugin() { private var gpsManager: GpsStatusManager? = null private var imuManager: ImuSensorManager? = null private var wifiManager: WifiPositioningManager? = null private var latestLatitude = 0.0 private var latestLongitude = 0.0 private var latestAccuracy = 999.0 private var latestSource = "GNSS" private var latestTimestamp: Long = 0L private var latestImu: ImuData? = null private var satelliteStatus: SatelliteStatus? = null private var wifiScanResult: WifiScanResult? = null private var isMockedLocation = false private var lastEmitTimestamp: Long = 0L private var prevLatitude = 0.0 private var prevLongitude = 0.0 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 val emitIntervalMs: Long = 500L private var emitIntervalMs: Long = 1000L // hard debounce // private val emitIntervalMs: Long = 500L private var motionState: String = "idle" // 'idle', 'driving', 'mocked' override fun load() { gpsManager = GpsStatusManager( context, onSatelliteStatusUpdate = { satelliteStatus = it }, onLocationUpdate = onLocationUpdate@{ location, isMocked -> if (location.latitude == 0.0 && location.longitude == 0.0) { Log.w("GPS_LOCATION", "Ignored location update: (0.0, 0.0)") return@onLocationUpdate } latestLatitude = location.latitude latestLongitude = location.longitude latestAccuracy = location.accuracy.toDouble() latestSource = if (isMocked) "MOCK" else "GNSS" isMockedLocation = isMocked latestTimestamp = location.time emitPositionUpdate() } ) imuManager = ImuSensorManager( context, onImuUpdate = { latestImu = it adjustIntervalAndSensorRate(it.speed) emitPositionUpdate() } ) wifiManager = WifiPositioningManager( context, onWifiPositioningUpdate = { wifiScanResult = it emitPositionUpdate() } ) } @PluginMethod fun startPositioning(call: PluginCall) { if (!PermissionUtils.hasLocationAndWifiPermissions(context)) { call.reject("Required permissions not granted") return } gpsManager?.start() imuManager?.start() wifiManager?.startPeriodicScan(3000L) call.resolve() } @PluginMethod fun stopPositioning(call: PluginCall) { gpsManager?.stop() imuManager?.stop() wifiManager?.stopPeriodicScan() call.resolve() } @PluginMethod fun getLatestPosition(call: PluginCall) { call.resolve(buildPositionData()) } @PluginMethod fun checkAndRequestPermissions(call: PluginCall) { // requestAllPermissions(call, "checkAndRequestPermissions") requestAllPermissions(call, "onPermissionResult") 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) } @PluginMethod fun configureEdgeToEdge(call: PluginCall) { val bgColorHex = call.getString("bgColor") ?: "#FFFFFF" val style = call.getString("style") ?: "DARK" val overlay = call.getBoolean("overlay") ?: false val activity = bridge?.activity val window = activity?.window val view = bridge?.webView if (window == null || view == null) { call.reject("No active window or webView") return } val parsedColor = Color.parseColor(bgColorHex) // Atur overlay (decor fit) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { window.setDecorFitsSystemWindows(!overlay) } else { if (!overlay) { @Suppress("DEPRECATION") window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS) } } // Atur warna status bar & nav bar if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { window.statusBarColor = parsedColor window.navigationBarColor = parsedColor } // Style icon (dark/light) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { val controller = window.insetsController val flags = WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS or WindowInsetsController.APPEARANCE_LIGHT_NAVIGATION_BARS if (style.uppercase() == "DARK") { controller?.setSystemBarsAppearance(flags, flags) } else { controller?.setSystemBarsAppearance(0, flags) } } else { @Suppress("DEPRECATION") view.systemUiVisibility = when (style.uppercase()) { "DARK" -> View.SYSTEM_UI_FLAG_LIGHT_STATUS_BAR or View.SYSTEM_UI_FLAG_LIGHT_NAVIGATION_BAR else -> 0 } } // Handling insets agar konten tidak tertutup nav/status bar ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets -> val sysInsets = insets.getInsets(WindowInsetsCompat.Type.systemBars()) v.setPadding(sysInsets.left, sysInsets.top, sysInsets.right, sysInsets.bottom) WindowInsetsCompat.CONSUMED } call.resolve() } @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) } call.resolve(result) } private fun emitPositionUpdate() { val now = System.currentTimeMillis() if (now - lastEmitTimestamp < emitIntervalMs) return val distance = calculateDistance(latestLatitude, latestLongitude, prevLatitude, prevLongitude) val speedNow = latestImu?.speed ?: 0f val directionNow = latestImu?.directionRad ?: 0f val isSignificantChange = distance >= significantChangeThreshold val speedChanged = abs(speedNow - prevSpeed) > speedChangeThreshold val directionChanged = abs(directionNow - prevDirection) > directionChangeThreshold // Tentukan motion state motionState = when { isMockedLocation -> "mocked" speedNow > 1.0f -> "driving" else -> "idle" } val shouldEmit = isSignificantChange || speedChanged || directionChanged if (shouldEmit) { prevLatitude = latestLatitude prevLongitude = latestLongitude prevSpeed = speedNow prevDirection = directionNow lastEmitTimestamp = now notifyListeners("onPositionUpdate", buildPositionData()) } } private fun adjustIntervalAndSensorRate(speed: Float) { val targetInterval = when { speed > 5f -> 1000L speed > 1.5f -> 5000L speed > 0.3f -> 15000L else -> 30000L } if (emitIntervalMs != targetInterval) { emitIntervalMs = targetInterval Log.d("DUMON_GEOLOCATION", "Auto-set emitIntervalMs = $emitIntervalMs ms") } imuManager?.setSensorDelayBySpeed(speed) } private fun degToRad(deg: Double): Double { return deg * PI / 180.0 } private fun calculateDistance(lat1: Double, lon1: Double, lat2: Double, lon2: Double): Double { val R = 6371000.0 // Radius bumi dalam meter val latDistance = degToRad(lat2 - lat1) val lonDistance = degToRad(lon2 - lon1) val a = sin(latDistance / 2).pow(2) + cos(degToRad(lat1)) * cos(degToRad(lat2)) * sin(lonDistance / 2).pow(2) val c = 2 * atan2(sqrt(a), sqrt(1 - a)) return R * c } private fun buildPositionData(): JSObject { val obj = JSObject() obj.put("source", latestSource) obj.put("timestamp", if (latestTimestamp > 0) latestTimestamp else System.currentTimeMillis()) obj.put("latitude", latestLatitude) obj.put("longitude", latestLongitude) 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) } obj.put("predicted", latestSource == "PREDICTED") return obj } }