2025-06-15 02:07:00 +08:00

264 lines
8.8 KiB
Kotlin

package com.dumon.plugin.geolocation
import android.Manifest
import android.graphics.Color
import android.os.Build
import android.view.View
import android.view.WindowInsetsController
import android.view.WindowManager
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 org.json.JSONArray
import org.json.JSONObject
@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 fusionManager: SensorFusionManager? = 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 val emitIntervalMs: Long = 500L
override fun load() {
gpsManager = GpsStatusManager(
context,
onSatelliteStatusUpdate = { satelliteStatus = it },
onLocationUpdate = { location, isMocked ->
latestLatitude = location.latitude
latestLongitude = location.longitude
latestAccuracy = location.accuracy.toDouble()
latestSource = if (isMocked) "MOCK" else "GNSS"
isMockedLocation = isMocked
latestTimestamp = location.time
fusionManager?.updateGpsPosition(latestLatitude, latestLongitude)
emitPositionUpdate()
}
)
imuManager = ImuSensorManager(
context,
onImuUpdate = {
latestImu = it
emitPositionUpdate()
}
)
wifiManager = WifiPositioningManager(
context,
onWifiPositioningUpdate = {
wifiScanResult = it
emitPositionUpdate()
}
)
fusionManager = SensorFusionManager { lat, lon ->
latestLatitude = lat
latestLongitude = lon
latestAccuracy = 3.0
latestSource = "FUSED"
latestTimestamp = System.currentTimeMillis()
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) {
if (PermissionUtils.hasLocationAndWifiPermissions(context)) {
val result = JSObject().apply {
put("location", "granted")
put("wifi", "granted")
}
call.resolve(result)
} else {
requestAllPermissions(call, "checkAndRequestPermissions")
}
}
@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()
}
private fun emitPositionUpdate() {
val now = System.currentTimeMillis()
if (now - lastEmitTimestamp < emitIntervalMs) return
lastEmitTimestamp = now
notifyListeners("onPositionUpdate", buildPositionData())
}
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)
}
// === Full Detail (commented out for future use) ===
/*
satelliteStatus?.let {
val gnss = JSObject()
gnss.put("satellitesInView", it.satellitesInView)
gnss.put("usedInFix", it.usedInFix)
val constellations = JSObject()
it.constellationCounts.forEach { (k, v) -> constellations.put(k, v) }
gnss.put("constellationCounts", constellations)
obj.put("gnssData", gnss)
}
latestImu?.let {
val imu = JSObject()
imu.put("accelX", it.accelX)
imu.put("accelY", it.accelY)
imu.put("accelZ", it.accelZ)
imu.put("gyroX", it.gyroX)
imu.put("gyroY", it.gyroY)
imu.put("gyroZ", it.gyroZ)
imu.put("speed", it.speed)
imu.put("acceleration", it.acceleration)
imu.put("directionRad", it.directionRad)
obj.put("imuData", imu)
}
wifiScanResult?.let {
val wifi = JSArray()
it.aps.forEach { ap ->
val a = JSObject()
a.put("ssid", ap.ssid)
a.put("bssid", ap.bssid)
a.put("rssi", ap.rssi)
ap.distance?.let { d -> a.put("distance", d) }
wifi.put(a)
}
obj.put("wifiData", wifi)
}
*/
return obj
}
}