From 91dbbf8ce7a92a7c8a196e7546136854d7b626a4 Mon Sep 17 00:00:00 2001 From: wengki81 Date: Tue, 13 Jan 2026 08:28:40 +0800 Subject: [PATCH] 20260113-01 --- README.md | 7 + .../plugin/geolocation/DumonGeolocation.kt | 176 +++++++++++++++++- documentation/PLUGIN_REFERENCE.md | 5 + .../DumonGeolocation.swift | 22 +++ .../DumonGeolocationPlugin.swift | 5 + package.json | 2 +- src/definitions.ts | 6 + src/web.ts | 9 + 8 files changed, 229 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 662b3c4..51665e2 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,7 @@ setOptions(options: { emitGnssStatus?: boolean; suppressMockedUpdates?: boolean; keepScreenOn?: boolean; + debuggingMode?: boolean; backgroundPollingIntervalMs?: number; backgroundPostMinDistanceMeters?: number; backgroundPostMinAccuracyMeters?: number; @@ -173,11 +174,17 @@ startBackgroundTracking(options?: { stopBackgroundTracking(): Promise isBackgroundTrackingActive(): Promise<{ active: boolean }> getBackgroundLatestPosition(): Promise +triggerBackgroundReport(options?: { + postUrl?: string; + timeoutMs?: number; + useNetworkProvider?: boolean; +}): Promise<{ posted: boolean; endpoint?: string; usedCached?: boolean }> openBackgroundPermissionSettings(): Promise openNotificationPermissionSettings(): Promise ``` 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) diff --git a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt index eef5ac6..751d061 100644 --- a/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt +++ b/android/src/main/java/com/dumon/plugin/geolocation/DumonGeolocation.kt @@ -6,6 +6,7 @@ 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 @@ -32,6 +33,11 @@ import com.dumon.plugin.geolocation.utils.BgPrefs import kotlin.math.* import android.location.Location 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( name = "DumonGeolocation", @@ -107,6 +113,7 @@ class DumonGeolocation : Plugin() { private var emitGnssStatus = false private var suppressMockedUpdates = false private var keepScreenOn = false + private var debuggingMode = false private var lastSingleFixRequestTs: Long = 0L private val minSingleFixIntervalMs: Long = 1000L private var pendingPermissionAlias: String? = null @@ -121,7 +128,9 @@ class DumonGeolocation : Plugin() { satelliteStatus = status if (emitGnssStatus) { 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 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 { val bgInterval = it.toLong().coerceAtLeast(1000L) 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 fun getGnssStatus(call: PluginCall) { val status = satelliteStatus @@ -543,6 +635,84 @@ class DumonGeolocation : Plugin() { 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 --- @PluginMethod fun setAuthTokens(call: PluginCall) { @@ -650,7 +820,9 @@ class DumonGeolocation : Plugin() { // Ensure listener notifications run on the main thread for consistency Handler(Looper.getMainLooper()).post { - notifyListeners("onPositionUpdate", buildPositionData()) + val data = buildPositionData() + debugLog("emit onPositionUpdate", data) + notifyListeners("onPositionUpdate", data) } } } diff --git a/documentation/PLUGIN_REFERENCE.md b/documentation/PLUGIN_REFERENCE.md index 4f0de59..39123fd 100644 --- a/documentation/PLUGIN_REFERENCE.md +++ b/documentation/PLUGIN_REFERENCE.md @@ -58,6 +58,7 @@ export interface DumonGeoOptions { emitGnssStatus?: boolean; // default false suppressMockedUpdates?: boolean; // default false keepScreenOn?: boolean; // default false + debuggingMode?: boolean; // default false backgroundPollingIntervalMs?: number; // Android: interval polling service (default 5000) backgroundPostMinDistanceMeters?: number; // Android: minimum perpindahan untuk POST (default 10m) 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 isBackgroundTrackingActive(): Promise<{ active: boolean }> getBackgroundLatestPosition(): Promise +triggerBackgroundReport(options?: { postUrl?: string; timeoutMs?: number; useNetworkProvider?: boolean }): Promise<{ posted: boolean; endpoint?: string; usedCached?: boolean }> setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise clearAuthTokens(): Promise getAuthState(): Promise<{ present: boolean }> @@ -350,6 +352,9 @@ await DumonGeolocation.stopBackgroundTracking(); await DumonGeolocation.setAuthTokens({ accessToken: '', refreshToken: '' }); 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 await DumonGeolocation.setOptions({ backgroundPostMinDistanceMeters: 10, backgroundPollingIntervalMs: 5000 }); diff --git a/ios/Sources/DumonGeolocationPlugin/DumonGeolocation.swift b/ios/Sources/DumonGeolocationPlugin/DumonGeolocation.swift index 2d62bf8..ed3ee87 100644 --- a/ios/Sources/DumonGeolocationPlugin/DumonGeolocation.swift +++ b/ios/Sources/DumonGeolocationPlugin/DumonGeolocation.swift @@ -31,6 +31,8 @@ import UIKit private var maxPredictionSeconds: Double = 1.0 private var suppressMockedUpdates = false private var keepScreenOn = false + private var debuggingMode = false + private let debugDateFormatter = ISO8601DateFormatter() private var currentMode: String = "normal" private var drivingTimer: Timer? @@ -113,6 +115,15 @@ import UIKit keepScreenOn = 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 { backgroundPostMinDistanceMeters = max(0.0, v) } @@ -260,6 +271,7 @@ import UIKit let data = buildPositionData(from: location) DispatchQueue.main.async { [weak self] in + self?.debugLog("emit onPositionUpdate", payload: 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)") + } + } + } diff --git a/ios/Sources/DumonGeolocationPlugin/DumonGeolocationPlugin.swift b/ios/Sources/DumonGeolocationPlugin/DumonGeolocationPlugin.swift index 59b3948..80eeaae 100644 --- a/ios/Sources/DumonGeolocationPlugin/DumonGeolocationPlugin.swift +++ b/ios/Sources/DumonGeolocationPlugin/DumonGeolocationPlugin.swift @@ -19,6 +19,7 @@ public class DumonGeolocationPlugin: CAPPlugin, CAPBridgedPlugin { CAPPluginMethod(name: "stopBackgroundTracking", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "isBackgroundTrackingActive", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "getBackgroundLatestPosition", returnType: CAPPluginReturnPromise), + CAPPluginMethod(name: "triggerBackgroundReport", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "openBackgroundPermissionSettings", returnType: CAPPluginReturnPromise), CAPPluginMethod(name: "openNotificationPermissionSettings", 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) { openAppSettings(call) } diff --git a/package.json b/package.json index 9c8e188..ebcd7d0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "dumon-geolocation", - "version": "1.1.2", + "version": "1.1.3", "description": "Implement manager GNSS, Wi‑Fi RTT, IMU, Kalman fusion, event emitter", "main": "dist/plugin.cjs.js", "module": "dist/esm/index.js", diff --git a/src/definitions.ts b/src/definitions.ts index 14601b1..baaf2ee 100644 --- a/src/definitions.ts +++ b/src/definitions.ts @@ -90,6 +90,7 @@ export interface DumonGeoOptions { emitGnssStatus?: boolean; suppressMockedUpdates?: boolean; keepScreenOn?: boolean; + debuggingMode?: boolean; backgroundPollingIntervalMs?: number; // Android background polling interval backgroundPostMinDistanceMeters?: number; // Android background min distance to post backgroundPostMinAccuracyMeters?: number; // Android background min acceptable accuracy for POST (meters) @@ -121,6 +122,11 @@ export interface DumonGeolocationPlugin { stopBackgroundTracking(): Promise; isBackgroundTrackingActive(): Promise<{ active: boolean }>; getBackgroundLatestPosition(): Promise; + triggerBackgroundReport(options?: { + postUrl?: string; + timeoutMs?: number; + useNetworkProvider?: boolean; + }): Promise<{ posted: boolean; endpoint?: string; usedCached?: boolean }>; openBackgroundPermissionSettings(): Promise; openNotificationPermissionSettings(): Promise; // Auth token management for background posting diff --git a/src/web.ts b/src/web.ts index f097f4c..3febef8 100644 --- a/src/web.ts +++ b/src/web.ts @@ -91,6 +91,15 @@ export class DumonGeolocationWeb extends WebPlugin { 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 { console.info('[dumon-geolocation] openBackgroundPermissionSettings is not supported on web.'); }