392 lines
14 KiB
Kotlin
392 lines
14 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.os.Handler
|
|
import android.os.Looper
|
|
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.GpsTrackingMode
|
|
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.*
|
|
import android.location.Location
|
|
|
|
@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'
|
|
|
|
private var bufferedDrivingLocation: Location? = null
|
|
private var drivingEmitHandler: Handler? = null
|
|
private var drivingEmitRunnable: Runnable? = null
|
|
private val drivingEmitIntervalMs = 1050L
|
|
|
|
private var currentTrackingMode = GpsTrackingMode.NORMAL
|
|
|
|
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
|
|
|
|
if (currentTrackingMode == GpsTrackingMode.DRIVING) {
|
|
bufferedDrivingLocation = location
|
|
} else {
|
|
emitPositionUpdate() // langsung emit di mode normal
|
|
}
|
|
}
|
|
)
|
|
|
|
imuManager = ImuSensorManager(
|
|
context,
|
|
onImuUpdate = {
|
|
latestImu = it
|
|
adjustIntervalAndSensorRate(it.speed)
|
|
emitPositionUpdate()
|
|
}
|
|
)
|
|
|
|
wifiManager = WifiPositioningManager(
|
|
context,
|
|
onWifiPositioningUpdate = {
|
|
wifiScanResult = it
|
|
emitPositionUpdate()
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun startDrivingEmitLoop() {
|
|
if (drivingEmitHandler != null) return // already running
|
|
|
|
drivingEmitHandler = Handler(Looper.getMainLooper())
|
|
drivingEmitRunnable = object : Runnable {
|
|
override fun run() {
|
|
bufferedDrivingLocation?.let { location ->
|
|
latestLatitude = location.latitude
|
|
latestLongitude = location.longitude
|
|
latestAccuracy = location.accuracy.toDouble()
|
|
latestTimestamp = location.time
|
|
latestSource = if (isMockedLocation) "MOCK" else "GNSS"
|
|
emitPositionUpdate()
|
|
}
|
|
drivingEmitHandler?.postDelayed(this, drivingEmitIntervalMs)
|
|
}
|
|
}
|
|
drivingEmitHandler?.postDelayed(drivingEmitRunnable!!, drivingEmitIntervalMs)
|
|
}
|
|
|
|
private fun stopDrivingEmitLoop() {
|
|
drivingEmitHandler?.removeCallbacks(drivingEmitRunnable!!)
|
|
drivingEmitHandler = null
|
|
drivingEmitRunnable = null
|
|
bufferedDrivingLocation = null
|
|
}
|
|
|
|
@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()
|
|
stopDrivingEmitLoop()
|
|
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 setGpsMode(call: PluginCall) {
|
|
val mode = call.getString("mode") ?: "normal"
|
|
if (mode == "driving") {
|
|
gpsManager?.startContinuousMode()
|
|
currentTrackingMode = GpsTrackingMode.DRIVING
|
|
startDrivingEmitLoop()
|
|
Log.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)")
|
|
}
|
|
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 -> 3000L
|
|
speed > 1.5f -> 8000L
|
|
speed > 0.3f -> 20000L
|
|
else -> 30000L
|
|
}
|
|
|
|
if (emitIntervalMs != targetInterval) {
|
|
emitIntervalMs = targetInterval
|
|
gpsManager?.setPollingInterval(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
|
|
}
|
|
} |