20260113-01

This commit is contained in:
wengki81 2026-01-13 08:28:40 +08:00
parent d9292c3fc7
commit 91dbbf8ce7
8 changed files with 229 additions and 3 deletions

View File

@ -125,6 +125,7 @@ setOptions(options: {
emitGnssStatus?: boolean; emitGnssStatus?: boolean;
suppressMockedUpdates?: boolean; suppressMockedUpdates?: boolean;
keepScreenOn?: boolean; keepScreenOn?: boolean;
debuggingMode?: boolean;
backgroundPollingIntervalMs?: number; backgroundPollingIntervalMs?: number;
backgroundPostMinDistanceMeters?: number; backgroundPostMinDistanceMeters?: number;
backgroundPostMinAccuracyMeters?: number; backgroundPostMinAccuracyMeters?: number;
@ -173,11 +174,17 @@ startBackgroundTracking(options?: {
stopBackgroundTracking(): Promise<void> stopBackgroundTracking(): Promise<void>
isBackgroundTrackingActive(): Promise<{ active: boolean }> isBackgroundTrackingActive(): Promise<{ active: boolean }>
getBackgroundLatestPosition(): Promise<PositioningData | null> getBackgroundLatestPosition(): Promise<PositioningData | null>
triggerBackgroundReport(options?: {
postUrl?: string;
timeoutMs?: number;
useNetworkProvider?: boolean;
}): Promise<{ posted: boolean; endpoint?: string; usedCached?: boolean }>
openBackgroundPermissionSettings(): Promise<void> openBackgroundPermissionSettings(): Promise<void>
openNotificationPermissionSettings(): Promise<void> openNotificationPermissionSettings(): Promise<void>
``` ```
Catatan: pada iOS, `title/text/channelId/channelName` diabaikan; `postUrl` tetap didukung. Catatan: pada iOS, `title/text/channelId/channelName` diabaikan; `postUrl` tetap didukung.
`triggerBackgroundReport` hanya tersedia di Android untuk memicu POST lokasi sekali (untuk debugging/dev tool).
### Auth & Post URL (Android + iOS) ### Auth & Post URL (Android + iOS)

View File

@ -6,6 +6,7 @@ import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import android.view.View import android.view.View
import android.view.WindowInsetsController import android.view.WindowInsetsController
import android.view.WindowManager import android.view.WindowManager
@ -32,6 +33,11 @@ import com.dumon.plugin.geolocation.utils.BgPrefs
import kotlin.math.* import kotlin.math.*
import android.location.Location import android.location.Location
import com.dumon.plugin.geolocation.utils.AuthStore import com.dumon.plugin.geolocation.utils.AuthStore
import java.io.BufferedOutputStream
import java.io.BufferedReader
import java.io.InputStreamReader
import java.net.HttpURLConnection
import java.net.URL
@CapacitorPlugin( @CapacitorPlugin(
name = "DumonGeolocation", name = "DumonGeolocation",
@ -107,6 +113,7 @@ class DumonGeolocation : Plugin() {
private var emitGnssStatus = false private var emitGnssStatus = false
private var suppressMockedUpdates = false private var suppressMockedUpdates = false
private var keepScreenOn = false private var keepScreenOn = false
private var debuggingMode = false
private var lastSingleFixRequestTs: Long = 0L private var lastSingleFixRequestTs: Long = 0L
private val minSingleFixIntervalMs: Long = 1000L private val minSingleFixIntervalMs: Long = 1000L
private var pendingPermissionAlias: String? = null private var pendingPermissionAlias: String? = null
@ -121,7 +128,9 @@ class DumonGeolocation : Plugin() {
satelliteStatus = status satelliteStatus = status
if (emitGnssStatus) { if (emitGnssStatus) {
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
notifyListeners("onGnssStatus", buildGnssStatusData(status)) val data = buildGnssStatusData(status)
debugLog("emit onGnssStatus", data)
notifyListeners("onGnssStatus", data)
} }
} }
}, },
@ -401,6 +410,15 @@ class DumonGeolocation : Plugin() {
keepScreenOn = it keepScreenOn = it
applyKeepScreenOn(keepScreenOn) applyKeepScreenOn(keepScreenOn)
} }
call.getBoolean("debuggingMode")?.let {
if (it && !debuggingMode) {
debuggingMode = true
debugLog("debuggingMode enabled")
} else if (!it && debuggingMode) {
debugLog("debuggingMode disabled")
debuggingMode = false
}
}
call.getInt("backgroundPollingIntervalMs")?.let { call.getInt("backgroundPollingIntervalMs")?.let {
val bgInterval = it.toLong().coerceAtLeast(1000L) val bgInterval = it.toLong().coerceAtLeast(1000L)
BgPrefs.setBackgroundIntervalMs(context, bgInterval) BgPrefs.setBackgroundIntervalMs(context, bgInterval)
@ -444,6 +462,80 @@ class DumonGeolocation : Plugin() {
} }
} }
private fun debugLog(message: String, data: JSObject? = null) {
if (!debuggingMode) return
val timestamp = System.currentTimeMillis()
if (data != null) {
Log.d("DUMON_GEO_DEBUG", "[$timestamp] $message payload=$data")
} else {
Log.d("DUMON_GEO_DEBUG", "[$timestamp] $message")
}
}
private fun postBackgroundFix(
endpoint: String,
latitude: Double,
longitude: Double,
accuracy: Double,
speed: Double,
timestamp: Long,
source: String,
isMocked: Boolean,
usedCached: Boolean,
call: PluginCall
) {
Thread {
val json = """{"source":"$source","timestamp":$timestamp,"latitude":$latitude,"longitude":$longitude,"accuracy":$accuracy,"speed":$speed,"isMocked":$isMocked}"""
try {
val url = URL(endpoint)
val conn = (url.openConnection() as HttpURLConnection).apply {
requestMethod = "POST"
connectTimeout = 5000
readTimeout = 5000
doOutput = true
setRequestProperty("Content-Type", "application/json")
val tokens = AuthStore.getTokens(context)
if (tokens != null) {
setRequestProperty("Authorization", "Bearer ${tokens.accessToken}")
setRequestProperty("refresh-token", tokens.refreshToken)
}
}
try {
BufferedOutputStream(conn.outputStream).use { out ->
out.write(json.toByteArray(Charsets.UTF_8))
out.flush()
}
val code = conn.responseCode
if (code in 200..299) {
BgPrefs.setLastPostedFix(context, latitude, longitude, timestamp)
Handler(Looper.getMainLooper()).post {
call.resolve(
JSObject().apply {
put("posted", true)
put("endpoint", endpoint)
put("usedCached", usedCached)
}
)
}
} else {
val err = try {
BufferedReader(InputStreamReader(conn.errorStream ?: conn.inputStream)).readText()
} catch (_: Exception) { "" }
Handler(Looper.getMainLooper()).post {
call.reject("Background report failed (HTTP $code) $err")
}
}
} finally {
conn.disconnect()
}
} catch (e: Exception) {
Handler(Looper.getMainLooper()).post {
call.reject("Background report failed: ${e.message}")
}
}
}.start()
}
@PluginMethod @PluginMethod
fun getGnssStatus(call: PluginCall) { fun getGnssStatus(call: PluginCall) {
val status = satelliteStatus val status = satelliteStatus
@ -543,6 +635,84 @@ class DumonGeolocation : Plugin() {
call.resolve(obj) call.resolve(obj)
} }
@PluginMethod
fun triggerBackgroundReport(call: PluginCall) {
if (!PermissionUtils.hasLocationPermissions(context)) {
call.reject("Location permission not granted")
return
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val bgGranted = ActivityCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_BACKGROUND_LOCATION
) == PackageManager.PERMISSION_GRANTED
if (!bgGranted) {
call.reject("Background location permission not granted")
return
}
}
val endpoint = call.getString("postUrl") ?: BgPrefs.getPostUrl(context)
if (endpoint.isNullOrBlank()) {
call.reject("Background postUrl not set")
return
}
val timeoutMs = call.getInt("timeoutMs")?.toLong()?.coerceAtLeast(1000L) ?: 3000L
val useNetwork = call.getBoolean("useNetworkProvider") ?: true
val manager = gpsManager ?: GpsStatusManager(context)
manager.requestSingleFix(timeoutMs = timeoutMs, useNetworkProvider = useNetwork) { location, isMocked ->
if (location != null) {
BgPrefs.saveLatestFix(
context = context,
latitude = location.latitude,
longitude = location.longitude,
accuracy = location.accuracy.toDouble(),
timestamp = location.time,
source = if (isMocked) "MOCK" else "GNSS",
isMocked = isMocked,
speed = if (location.hasSpeed()) location.speed else 0f,
acceleration = null,
directionRad = null
)
postBackgroundFix(
endpoint = endpoint,
latitude = location.latitude,
longitude = location.longitude,
accuracy = location.accuracy.toDouble(),
speed = if (location.hasSpeed()) location.speed.toDouble() else 0.0,
timestamp = location.time,
source = if (isMocked) "MOCK" else "GNSS",
isMocked = isMocked,
usedCached = false,
call = call
)
return@requestSingleFix
}
val cached = BgPrefs.readLatestFix(context)
if (cached == null) {
Handler(Looper.getMainLooper()).post {
call.reject("No location available for background report")
}
return@requestSingleFix
}
postBackgroundFix(
endpoint = endpoint,
latitude = cached.latitude,
longitude = cached.longitude,
accuracy = cached.accuracy,
speed = cached.speed.toDouble(),
timestamp = cached.timestamp,
source = cached.source,
isMocked = cached.isMocked,
usedCached = true,
call = call
)
}
}
// --- Auth token management for background posting --- // --- Auth token management for background posting ---
@PluginMethod @PluginMethod
fun setAuthTokens(call: PluginCall) { fun setAuthTokens(call: PluginCall) {
@ -650,7 +820,9 @@ class DumonGeolocation : Plugin() {
// Ensure listener notifications run on the main thread for consistency // Ensure listener notifications run on the main thread for consistency
Handler(Looper.getMainLooper()).post { Handler(Looper.getMainLooper()).post {
notifyListeners("onPositionUpdate", buildPositionData()) val data = buildPositionData()
debugLog("emit onPositionUpdate", data)
notifyListeners("onPositionUpdate", data)
} }
} }
} }

View File

@ -58,6 +58,7 @@ export interface DumonGeoOptions {
emitGnssStatus?: boolean; // default false emitGnssStatus?: boolean; // default false
suppressMockedUpdates?: boolean; // default false suppressMockedUpdates?: boolean; // default false
keepScreenOn?: boolean; // default false keepScreenOn?: boolean; // default false
debuggingMode?: boolean; // default false
backgroundPollingIntervalMs?: number; // Android: interval polling service (default 5000) backgroundPollingIntervalMs?: number; // Android: interval polling service (default 5000)
backgroundPostMinDistanceMeters?: number; // Android: minimum perpindahan untuk POST (default 10m) backgroundPostMinDistanceMeters?: number; // Android: minimum perpindahan untuk POST (default 10m)
backgroundPostMinAccuracyMeters?: number; // Android: minimum akurasi fix untuk POST (meter); kosong = tidak dibatasi backgroundPostMinAccuracyMeters?: number; // Android: minimum akurasi fix untuk POST (meter); kosong = tidak dibatasi
@ -124,6 +125,7 @@ startBackgroundTracking(options?: { title?: string; text?: string; channelId?: s
stopBackgroundTracking(): Promise<void> stopBackgroundTracking(): Promise<void>
isBackgroundTrackingActive(): Promise<{ active: boolean }> isBackgroundTrackingActive(): Promise<{ active: boolean }>
getBackgroundLatestPosition(): Promise<PositioningData | null> getBackgroundLatestPosition(): Promise<PositioningData | null>
triggerBackgroundReport(options?: { postUrl?: string; timeoutMs?: number; useNetworkProvider?: boolean }): Promise<{ posted: boolean; endpoint?: string; usedCached?: boolean }>
setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void> setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>
clearAuthTokens(): Promise<void> clearAuthTokens(): Promise<void>
getAuthState(): Promise<{ present: boolean }> getAuthState(): Promise<{ present: boolean }>
@ -350,6 +352,9 @@ await DumonGeolocation.stopBackgroundTracking();
await DumonGeolocation.setAuthTokens({ accessToken: '<ACCESS>', refreshToken: '<REFRESH>' }); await DumonGeolocation.setAuthTokens({ accessToken: '<ACCESS>', refreshToken: '<REFRESH>' });
await DumonGeolocation.setBackgroundPostUrl({ url: 'https://dumonapp.com/dev-test-cap' }); await DumonGeolocation.setBackgroundPostUrl({ url: 'https://dumonapp.com/dev-test-cap' });
// Debug/dev tool: paksa kirim satu report background (tanpa menunggu interval)
await DumonGeolocation.triggerBackgroundReport({ postUrl: 'https://dumonapp.com/dev-test-cap' });
// Kirim otomatis hanya jika perpindahan >= 10 meter (default 10 m); atur interval polling 5 detik // Kirim otomatis hanya jika perpindahan >= 10 meter (default 10 m); atur interval polling 5 detik
await DumonGeolocation.setOptions({ backgroundPostMinDistanceMeters: 10, backgroundPollingIntervalMs: 5000 }); await DumonGeolocation.setOptions({ backgroundPostMinDistanceMeters: 10, backgroundPollingIntervalMs: 5000 });

View File

@ -31,6 +31,8 @@ import UIKit
private var maxPredictionSeconds: Double = 1.0 private var maxPredictionSeconds: Double = 1.0
private var suppressMockedUpdates = false private var suppressMockedUpdates = false
private var keepScreenOn = false private var keepScreenOn = false
private var debuggingMode = false
private let debugDateFormatter = ISO8601DateFormatter()
private var currentMode: String = "normal" private var currentMode: String = "normal"
private var drivingTimer: Timer? private var drivingTimer: Timer?
@ -113,6 +115,15 @@ import UIKit
keepScreenOn = v keepScreenOn = v
applyKeepScreenOn(v) applyKeepScreenOn(v)
} }
if let v = options["debuggingMode"] as? Bool {
if v && !debuggingMode {
debuggingMode = true
debugLog("debuggingMode enabled")
} else if !v && debuggingMode {
debugLog("debuggingMode disabled")
debuggingMode = false
}
}
if let v = options["backgroundPostMinDistanceMeters"] as? Double { if let v = options["backgroundPostMinDistanceMeters"] as? Double {
backgroundPostMinDistanceMeters = max(0.0, v) backgroundPostMinDistanceMeters = max(0.0, v)
} }
@ -260,6 +271,7 @@ import UIKit
let data = buildPositionData(from: location) let data = buildPositionData(from: location)
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.debugLog("emit onPositionUpdate", payload: data)
self?.onPositionUpdate?(data) self?.onPositionUpdate?(data)
} }
} }
@ -415,4 +427,14 @@ import UIKit
} }
} }
private func debugLog(_ message: String, payload: [String: Any]? = nil) {
guard debuggingMode else { return }
let timestamp = debugDateFormatter.string(from: Date())
if let payload = payload {
NSLog("DUMON_GEO_DEBUG [\(timestamp)] \(message) payload=\(payload)")
} else {
NSLog("DUMON_GEO_DEBUG [\(timestamp)] \(message)")
}
}
} }

View File

@ -19,6 +19,7 @@ public class DumonGeolocationPlugin: CAPPlugin, CAPBridgedPlugin {
CAPPluginMethod(name: "stopBackgroundTracking", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "stopBackgroundTracking", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "isBackgroundTrackingActive", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "isBackgroundTrackingActive", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "getBackgroundLatestPosition", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getBackgroundLatestPosition", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "triggerBackgroundReport", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "openBackgroundPermissionSettings", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "openBackgroundPermissionSettings", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "openNotificationPermissionSettings", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "openNotificationPermissionSettings", returnType: CAPPluginReturnPromise),
CAPPluginMethod(name: "setAuthTokens", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "setAuthTokens", returnType: CAPPluginReturnPromise),
@ -135,6 +136,10 @@ public class DumonGeolocationPlugin: CAPPlugin, CAPBridgedPlugin {
} }
} }
@objc func triggerBackgroundReport(_ call: CAPPluginCall) {
call.reject("triggerBackgroundReport is Android-only")
}
@objc func openBackgroundPermissionSettings(_ call: CAPPluginCall) { @objc func openBackgroundPermissionSettings(_ call: CAPPluginCall) {
openAppSettings(call) openAppSettings(call)
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "dumon-geolocation", "name": "dumon-geolocation",
"version": "1.1.2", "version": "1.1.3",
"description": "Implement manager GNSS, WiFi RTT, IMU, Kalman fusion, event emitter", "description": "Implement manager GNSS, WiFi RTT, IMU, Kalman fusion, event emitter",
"main": "dist/plugin.cjs.js", "main": "dist/plugin.cjs.js",
"module": "dist/esm/index.js", "module": "dist/esm/index.js",

View File

@ -90,6 +90,7 @@ export interface DumonGeoOptions {
emitGnssStatus?: boolean; emitGnssStatus?: boolean;
suppressMockedUpdates?: boolean; suppressMockedUpdates?: boolean;
keepScreenOn?: boolean; keepScreenOn?: boolean;
debuggingMode?: boolean;
backgroundPollingIntervalMs?: number; // Android background polling interval backgroundPollingIntervalMs?: number; // Android background polling interval
backgroundPostMinDistanceMeters?: number; // Android background min distance to post backgroundPostMinDistanceMeters?: number; // Android background min distance to post
backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters) backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters)
@ -121,6 +122,11 @@ export interface DumonGeolocationPlugin {
stopBackgroundTracking(): Promise<void>; stopBackgroundTracking(): Promise<void>;
isBackgroundTrackingActive(): Promise<{ active: boolean }>; isBackgroundTrackingActive(): Promise<{ active: boolean }>;
getBackgroundLatestPosition(): Promise<PositioningData | null>; getBackgroundLatestPosition(): Promise<PositioningData | null>;
triggerBackgroundReport(options?: {
postUrl?: string;
timeoutMs?: number;
useNetworkProvider?: boolean;
}): Promise<{ posted: boolean; endpoint?: string; usedCached?: boolean }>;
openBackgroundPermissionSettings(): Promise<void>; openBackgroundPermissionSettings(): Promise<void>;
openNotificationPermissionSettings(): Promise<void>; openNotificationPermissionSettings(): Promise<void>;
// Auth token management for background posting // Auth token management for background posting

View File

@ -91,6 +91,15 @@ export class DumonGeolocationWeb extends WebPlugin {
return null; return null;
} }
async triggerBackgroundReport(_options?: {
postUrl?: string;
timeoutMs?: number;
useNetworkProvider?: boolean;
}): Promise<{ posted: boolean; endpoint?: string; usedCached?: boolean }> {
console.info('[dumon-geolocation] triggerBackgroundReport is not supported on web.');
return { posted: false };
}
async openBackgroundPermissionSettings(): Promise<void> { async openBackgroundPermissionSettings(): Promise<void> {
console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.'); console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.');
} }