321 lines
11 KiB
Kotlin
321 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
|
||
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()
|
||
}
|
||
|
||
@PluginMethod
|
||
fun setEmitInterval(call: PluginCall) {
|
||
val intervalMs = call.getInt("intervalMs") ?: 1000
|
||
emitIntervalMs = intervalMs.toLong().coerceIn(250L, 30000L) // batas antara 0.25s – 30s
|
||
Log.d("DUMON_GEOLOCATION", "Emit interval set to $emitIntervalMs ms")
|
||
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 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
|
||
}
|
||
} |