330 lines
11 KiB
Kotlin

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
}
}