20260108-01
This commit is contained in:
parent
0c141c88ad
commit
2575acb553
2
.gitignore
vendored
2
.gitignore
vendored
@ -14,6 +14,8 @@ xcuserdata/
|
|||||||
DerivedData/
|
DerivedData/
|
||||||
.swiftpm/configuration/registries.json
|
.swiftpm/configuration/registries.json
|
||||||
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
example-app/ios/App/App/public
|
||||||
|
example-app/ios/App/App/capacitor.config.json
|
||||||
.netrc
|
.netrc
|
||||||
.history
|
.history
|
||||||
|
|
||||||
|
|||||||
@ -13,12 +13,44 @@ This guide provides instructions for contributing to this Capacitor plugin.
|
|||||||
npm install
|
npm install
|
||||||
```
|
```
|
||||||
|
|
||||||
|
1. Install iOS dependencies (macOS only).
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd example-app/ios/App
|
||||||
|
pod install
|
||||||
|
```
|
||||||
|
|
||||||
1. Install SwiftLint if you're on macOS.
|
1. Install SwiftLint if you're on macOS.
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
brew install swiftlint
|
brew install swiftlint
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Example App (iOS Simulator)
|
||||||
|
|
||||||
|
1. Sync native projects.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd example-app
|
||||||
|
npx cap sync ios
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Build for a simulator device.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
xcodebuild -workspace ios/App/App.xcworkspace -scheme App -destination "id=<SIMULATOR_UDID>" build
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Install and launch on the simulator.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
xcrun simctl boot <SIMULATOR_UDID>
|
||||||
|
xcrun simctl install <SIMULATOR_UDID> <PATH_TO_App.app>
|
||||||
|
xcrun simctl launch <SIMULATOR_UDID> com.example.plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
> **Note**: The example app already includes the required location permission keys in its Info.plist. If you add a new app target, ensure those keys exist.
|
||||||
|
|
||||||
### Scripts
|
### Scripts
|
||||||
|
|
||||||
#### `npm run build`
|
#### `npm run build`
|
||||||
|
|||||||
60
README.md
60
README.md
@ -1,6 +1,6 @@
|
|||||||
# dumon-geolocation
|
# dumon-geolocation
|
||||||
|
|
||||||
Capacitor Plugin untuk Android yang menyediakan **real-time high-accuracy positioning** menggunakan kombinasi:
|
Capacitor plugin dengan target utama Android dan dukungan iOS (dengan keterbatasan), menyediakan **real-time high-accuracy positioning** menggunakan kombinasi:
|
||||||
- 📡 **GNSS multi-konstelasi** (GPS, GLONASS, BeiDou, Galileo)
|
- 📡 **GNSS multi-konstelasi** (GPS, GLONASS, BeiDou, Galileo)
|
||||||
- 📶 **Wi-Fi RTT / RSSI**
|
- 📶 **Wi-Fi RTT / RSSI**
|
||||||
- 🎯 **IMU Sensor** (Accelerometer, Gyroscope, Rotation)
|
- 🎯 **IMU Sensor** (Accelerometer, Gyroscope, Rotation)
|
||||||
@ -29,6 +29,7 @@ npx cap sync
|
|||||||
|
|
||||||
## Permissions
|
## Permissions
|
||||||
|
|
||||||
|
### Android
|
||||||
Plugin ini memerlukan izin berikut:
|
Plugin ini memerlukan izin berikut:
|
||||||
- ACCESS_FINE_LOCATION
|
- ACCESS_FINE_LOCATION
|
||||||
- ACCESS_COARSE_LOCATION
|
- ACCESS_COARSE_LOCATION
|
||||||
@ -38,6 +39,9 @@ Plugin ini memerlukan izin berikut:
|
|||||||
|
|
||||||
Gunakan API checkAndRequestPermissions() sebelum memulai positioning.
|
Gunakan API checkAndRequestPermissions() sebelum memulai positioning.
|
||||||
|
|
||||||
|
### iOS
|
||||||
|
Tambahkan entri Info.plist (lihat bagian iOS Requirements).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API
|
## API
|
||||||
@ -92,7 +96,7 @@ configureEdgeToEdge(options: {
|
|||||||
}): Promise<void>
|
}): Promise<void>
|
||||||
```
|
```
|
||||||
|
|
||||||
Mengatur status bar dan navigasi bar agar transparan, dengan warna dan icon style sesuai UI.
|
Mengatur status bar dan navigasi bar agar transparan, dengan warna dan icon style sesuai UI. (Android-only, no-op di iOS)
|
||||||
|
|
||||||
### setGpsMode()
|
### setGpsMode()
|
||||||
|
|
||||||
@ -121,6 +125,11 @@ setOptions(options: {
|
|||||||
emitGnssStatus?: boolean;
|
emitGnssStatus?: boolean;
|
||||||
suppressMockedUpdates?: boolean;
|
suppressMockedUpdates?: boolean;
|
||||||
keepScreenOn?: boolean;
|
keepScreenOn?: boolean;
|
||||||
|
backgroundPollingIntervalMs?: number;
|
||||||
|
backgroundPostMinDistanceMeters?: number;
|
||||||
|
backgroundPostMinAccuracyMeters?: number;
|
||||||
|
backgroundMinPostIntervalMs?: number;
|
||||||
|
backgroundUseImuFallback?: boolean;
|
||||||
}): Promise<void>
|
}): Promise<void>
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -150,6 +159,36 @@ addListener('onGnssStatus', (data: SatelliteStatus) => void): PluginListenerHand
|
|||||||
|
|
||||||
Menerima update status GNSS jika `emitGnssStatus: true` di setOptions.
|
Menerima update status GNSS jika `emitGnssStatus: true` di setOptions.
|
||||||
|
|
||||||
|
### Background Tracking (Android + iOS)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
startBackgroundTracking(options?: {
|
||||||
|
title?: string;
|
||||||
|
text?: string;
|
||||||
|
channelId?: string;
|
||||||
|
channelName?: string;
|
||||||
|
postUrl?: string;
|
||||||
|
}): Promise<void>
|
||||||
|
|
||||||
|
stopBackgroundTracking(): Promise<void>
|
||||||
|
isBackgroundTrackingActive(): Promise<{ active: boolean }>
|
||||||
|
getBackgroundLatestPosition(): Promise<PositioningData | null>
|
||||||
|
openBackgroundPermissionSettings(): Promise<void>
|
||||||
|
openNotificationPermissionSettings(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
Catatan: pada iOS, `title/text/channelId/channelName` diabaikan; `postUrl` tetap didukung.
|
||||||
|
|
||||||
|
### Auth & Post URL (Android + iOS)
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
setAuthTokens(tokens: { accessToken: string; refreshToken: string }): Promise<void>
|
||||||
|
clearAuthTokens(): Promise<void>
|
||||||
|
getAuthState(): Promise<{ present: boolean }>
|
||||||
|
setBackgroundPostUrl(options: { url?: string }): Promise<void>
|
||||||
|
getBackgroundPostUrl(): Promise<{ url: string | null }>
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Interfaces
|
## Interfaces
|
||||||
@ -240,12 +279,27 @@ interface WifiAp {
|
|||||||
|
|
||||||
## Catatan
|
## Catatan
|
||||||
|
|
||||||
- Hanya mendukung platform Android.
|
- Android sebagai target utama, iOS tersedia dengan keterbatasan (lihat bagian iOS).
|
||||||
- Gunakan bersama @capacitor/geolocation jika ingin membandingkan hasil native vs Dumon.
|
- Gunakan bersama @capacitor/geolocation jika ingin membandingkan hasil native vs Dumon.
|
||||||
- Posisi dipancarkan setiap 1000ms atau saat terjadi perubahan signifikan (jarak, kecepatan, arah).
|
- Posisi dipancarkan setiap 1000ms atau saat terjadi perubahan signifikan (jarak, kecepatan, arah).
|
||||||
- directionRad dalam radian relatif terhadap utara.
|
- directionRad dalam radian relatif terhadap utara.
|
||||||
- Fitur Mock Location Detection aktif secara default.
|
- Fitur Mock Location Detection aktif secara default.
|
||||||
|
|
||||||
|
## iOS (Status & Keterbatasan)
|
||||||
|
|
||||||
|
Implementasi iOS tersedia dengan API yang sama, tetapi ada keterbatasan platform:
|
||||||
|
- Wi-Fi RTT/RSSI scan tidak tersedia untuk app iOS biasa, sehingga sumber posisi hanya GNSS.
|
||||||
|
- GNSS status (satellite info) tidak tersedia di iOS; `getGnssStatus()` akan mengembalikan objek kosong.
|
||||||
|
- Mock location detection terbatas; hanya tersedia pada iOS 15+ jika sistem menandai simulasi.
|
||||||
|
- Background posting dijalankan best-effort saat background location aktif.
|
||||||
|
|
||||||
|
### iOS Requirements
|
||||||
|
|
||||||
|
Tambahkan entri Info.plist di aplikasi yang memakai plugin:
|
||||||
|
- `NSLocationWhenInUseUsageDescription`
|
||||||
|
- `NSLocationAlwaysAndWhenInUseUsageDescription`
|
||||||
|
- `UIBackgroundModes` dengan value `location` (jika memakai background tracking)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Testing (example-app)
|
## Testing (example-app)
|
||||||
|
|||||||
@ -6,7 +6,6 @@ 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
|
||||||
@ -22,7 +21,6 @@ import com.dumon.plugin.geolocation.gps.SatelliteStatus
|
|||||||
import com.dumon.plugin.geolocation.imu.ImuData
|
import com.dumon.plugin.geolocation.imu.ImuData
|
||||||
import com.dumon.plugin.geolocation.imu.ImuSensorManager
|
import com.dumon.plugin.geolocation.imu.ImuSensorManager
|
||||||
import com.dumon.plugin.geolocation.wifi.WifiPositioningManager
|
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.fusion.SensorFusionManager
|
||||||
import com.dumon.plugin.geolocation.utils.PermissionUtils
|
import com.dumon.plugin.geolocation.utils.PermissionUtils
|
||||||
import com.dumon.plugin.geolocation.utils.LogUtils
|
import com.dumon.plugin.geolocation.utils.LogUtils
|
||||||
@ -31,8 +29,6 @@ import android.content.Intent
|
|||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import com.dumon.plugin.geolocation.bg.BackgroundLocationService
|
import com.dumon.plugin.geolocation.bg.BackgroundLocationService
|
||||||
import com.dumon.plugin.geolocation.utils.BgPrefs
|
import com.dumon.plugin.geolocation.utils.BgPrefs
|
||||||
import org.json.JSONArray
|
|
||||||
import org.json.JSONObject
|
|
||||||
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
|
||||||
@ -80,7 +76,6 @@ class DumonGeolocation : Plugin() {
|
|||||||
|
|
||||||
private var latestImu: ImuData? = null
|
private var latestImu: ImuData? = null
|
||||||
private var satelliteStatus: SatelliteStatus? = null
|
private var satelliteStatus: SatelliteStatus? = null
|
||||||
private var wifiScanResult: WifiScanResult? = null
|
|
||||||
|
|
||||||
private var isMockedLocation = false
|
private var isMockedLocation = false
|
||||||
private var lastEmitTimestamp: Long = 0L
|
private var lastEmitTimestamp: Long = 0L
|
||||||
@ -100,8 +95,6 @@ class DumonGeolocation : Plugin() {
|
|||||||
private var emitIntervalMs: Long = 1000L // hard debounce
|
private var emitIntervalMs: Long = 1000L // hard debounce
|
||||||
// private val emitIntervalMs: Long = 500L
|
// private val emitIntervalMs: Long = 500L
|
||||||
|
|
||||||
private var motionState: String = "idle" // 'idle', 'driving', 'mocked'
|
|
||||||
|
|
||||||
private var bufferedDrivingLocation: Location? = null
|
private var bufferedDrivingLocation: Location? = null
|
||||||
private var drivingEmitHandler: Handler? = null
|
private var drivingEmitHandler: Handler? = null
|
||||||
private var drivingEmitRunnable: Runnable? = null
|
private var drivingEmitRunnable: Runnable? = null
|
||||||
@ -166,7 +159,6 @@ class DumonGeolocation : Plugin() {
|
|||||||
wifiManager = WifiPositioningManager(
|
wifiManager = WifiPositioningManager(
|
||||||
context,
|
context,
|
||||||
onWifiPositioningUpdate = {
|
onWifiPositioningUpdate = {
|
||||||
wifiScanResult = it
|
|
||||||
emitPositionUpdate()
|
emitPositionUpdate()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -646,13 +638,6 @@ class DumonGeolocation : Plugin() {
|
|||||||
val speedChanged = abs(speedNow - prevSpeed) > speedChangeThreshold
|
val speedChanged = abs(speedNow - prevSpeed) > speedChangeThreshold
|
||||||
val directionChanged = abs(directionNow - prevDirection) > directionChangeThreshold
|
val directionChanged = abs(directionNow - prevDirection) > directionChangeThreshold
|
||||||
|
|
||||||
// Tentukan motion state
|
|
||||||
motionState = when {
|
|
||||||
isMockedLocation -> "mocked"
|
|
||||||
speedNow > 1.0f -> "driving"
|
|
||||||
else -> "idle"
|
|
||||||
}
|
|
||||||
|
|
||||||
val shouldEmit = isSignificantChange || speedChanged || directionChanged
|
val shouldEmit = isSignificantChange || speedChanged || directionChanged
|
||||||
|
|
||||||
if (forceEmit || shouldEmit) {
|
if (forceEmit || shouldEmit) {
|
||||||
|
|||||||
@ -27,11 +27,4 @@ object PermissionUtils {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun hasLocationAndWifiPermissions(context: Context): Boolean {
|
|
||||||
return hasLocationPermissions(context) && hasWifiScanPermissions(context)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getPermissionStatus(granted: Int): String {
|
|
||||||
return if (granted == PackageManager.PERMISSION_GRANTED) "granted" else "denied"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -45,5 +45,13 @@
|
|||||||
</array>
|
</array>
|
||||||
<key>UIViewControllerBasedStatusBarAppearance</key>
|
<key>UIViewControllerBasedStatusBarAppearance</key>
|
||||||
<true/>
|
<true/>
|
||||||
|
<key>NSLocationWhenInUseUsageDescription</key>
|
||||||
|
<string>We use your location to provide real-time positioning.</string>
|
||||||
|
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
|
||||||
|
<string>We use your location to provide real-time positioning, even in the background.</string>
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>location</string>
|
||||||
|
</array>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
|
|||||||
@ -1,8 +1,459 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
import CoreLocation
|
||||||
|
import CoreMotion
|
||||||
|
import UIKit
|
||||||
|
|
||||||
@objc public class DumonGeolocation: NSObject {
|
@objc public class DumonGeolocation: NSObject, CLLocationManagerDelegate {
|
||||||
@objc public func echo(_ value: String) -> String {
|
public var onPositionUpdate: (([String: Any]) -> Void)?
|
||||||
print(value)
|
public var onGnssStatus: (([String: Any]) -> Void)?
|
||||||
return value
|
public var onAuthorizationChange: ((CLAuthorizationStatus) -> Void)?
|
||||||
|
|
||||||
|
private let locationManager = CLLocationManager()
|
||||||
|
private let motionManager = CMMotionManager()
|
||||||
|
|
||||||
|
private var latestLocation: CLLocation?
|
||||||
|
private var latestTimestampMs: Double = 0
|
||||||
|
private var latestAcceleration: Double = 0
|
||||||
|
private var latestDirectionRad: Double = 0
|
||||||
|
|
||||||
|
private var prevLocation: CLLocation?
|
||||||
|
private var prevSpeed: Double = 0
|
||||||
|
private var prevDirection: Double = 0
|
||||||
|
private var lastEmitTimestampMs: Double = 0
|
||||||
|
|
||||||
|
private var distanceThresholdMeters: Double = 7.0
|
||||||
|
private var speedChangeThreshold: Double = 0.5
|
||||||
|
private var directionChangeThreshold: Double = 0.17
|
||||||
|
private var emitDebounceMs: Double = 1000
|
||||||
|
private var drivingEmitIntervalMs: Double = 1600
|
||||||
|
|
||||||
|
private var enableForwardPrediction = false
|
||||||
|
private var maxPredictionSeconds: Double = 1.0
|
||||||
|
private var suppressMockedUpdates = false
|
||||||
|
private var keepScreenOn = false
|
||||||
|
|
||||||
|
private var currentMode: String = "normal"
|
||||||
|
private var drivingTimer: Timer?
|
||||||
|
|
||||||
|
private var backgroundTrackingActive = false
|
||||||
|
private var foregroundTrackingActive = false
|
||||||
|
private var backgroundLatestLocation: CLLocation?
|
||||||
|
|
||||||
|
private var backgroundPostMinDistanceMeters: Double = 10.0
|
||||||
|
private var backgroundPostMinAccuracyMeters: Double = Double.greatestFiniteMagnitude
|
||||||
|
private var backgroundMinPostIntervalMs: Double = 10_000
|
||||||
|
private var lastPostAttemptMs: Double = 0
|
||||||
|
private var lastPostedLocation: CLLocation?
|
||||||
|
|
||||||
|
private let defaults = UserDefaults.standard
|
||||||
|
private let accessTokenKey = "dumon_geo_access_token"
|
||||||
|
private let refreshTokenKey = "dumon_geo_refresh_token"
|
||||||
|
private let backgroundPostUrlKey = "dumon_geo_post_url"
|
||||||
|
|
||||||
|
public override init() {
|
||||||
|
super.init()
|
||||||
|
locationManager.delegate = self
|
||||||
|
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||||
|
locationManager.distanceFilter = distanceThresholdMeters
|
||||||
|
locationManager.activityType = .other
|
||||||
|
locationManager.pausesLocationUpdatesAutomatically = true
|
||||||
|
}
|
||||||
|
|
||||||
|
public func authorizationStatus() -> CLAuthorizationStatus {
|
||||||
|
if #available(iOS 14.0, *) {
|
||||||
|
return locationManager.authorizationStatus
|
||||||
|
}
|
||||||
|
return CLLocationManager.authorizationStatus()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func requestWhenInUsePermission() {
|
||||||
|
locationManager.requestWhenInUseAuthorization()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func requestAlwaysPermission() {
|
||||||
|
locationManager.requestAlwaysAuthorization()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startPositioning() {
|
||||||
|
foregroundTrackingActive = true
|
||||||
|
applyKeepScreenOn(keepScreenOn)
|
||||||
|
startMotionUpdates()
|
||||||
|
locationManager.startUpdatingLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopPositioning() {
|
||||||
|
foregroundTrackingActive = false
|
||||||
|
locationManager.stopUpdatingLocation()
|
||||||
|
stopMotionUpdates()
|
||||||
|
stopDrivingEmitLoop()
|
||||||
|
applyKeepScreenOn(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setOptions(_ options: [String: Any]) {
|
||||||
|
if let v = options["distanceThresholdMeters"] as? Double {
|
||||||
|
distanceThresholdMeters = v
|
||||||
|
if currentMode == "normal" {
|
||||||
|
locationManager.distanceFilter = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let v = options["speedChangeThreshold"] as? Double { speedChangeThreshold = v }
|
||||||
|
if let v = options["directionChangeThreshold"] as? Double { directionChangeThreshold = v }
|
||||||
|
if let v = options["emitDebounceMs"] as? Double { emitDebounceMs = max(0, v) }
|
||||||
|
if let v = options["drivingEmitIntervalMs"] as? Double {
|
||||||
|
drivingEmitIntervalMs = max(200, v)
|
||||||
|
if currentMode == "driving" {
|
||||||
|
stopDrivingEmitLoop()
|
||||||
|
startDrivingEmitLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let v = options["enableForwardPrediction"] as? Bool { enableForwardPrediction = v }
|
||||||
|
if let v = options["maxPredictionSeconds"] as? Double { maxPredictionSeconds = min(max(v, 0.0), 5.0) }
|
||||||
|
if let v = options["suppressMockedUpdates"] as? Bool { suppressMockedUpdates = v }
|
||||||
|
if let v = options["keepScreenOn"] as? Bool {
|
||||||
|
keepScreenOn = v
|
||||||
|
applyKeepScreenOn(v)
|
||||||
|
}
|
||||||
|
if let v = options["backgroundPostMinDistanceMeters"] as? Double {
|
||||||
|
backgroundPostMinDistanceMeters = max(0.0, v)
|
||||||
|
}
|
||||||
|
if let v = options["backgroundPostMinAccuracyMeters"] as? Double {
|
||||||
|
backgroundPostMinAccuracyMeters = max(0.0, v)
|
||||||
|
}
|
||||||
|
if let v = options["backgroundMinPostIntervalMs"] as? Double {
|
||||||
|
backgroundMinPostIntervalMs = max(0.0, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setGpsMode(_ mode: String) {
|
||||||
|
currentMode = mode == "driving" ? "driving" : "normal"
|
||||||
|
if currentMode == "driving" {
|
||||||
|
locationManager.desiredAccuracy = kCLLocationAccuracyBestForNavigation
|
||||||
|
locationManager.distanceFilter = kCLDistanceFilterNone
|
||||||
|
locationManager.activityType = .automotiveNavigation
|
||||||
|
startDrivingEmitLoop()
|
||||||
|
} else {
|
||||||
|
locationManager.desiredAccuracy = kCLLocationAccuracyBest
|
||||||
|
locationManager.distanceFilter = distanceThresholdMeters
|
||||||
|
locationManager.activityType = .other
|
||||||
|
stopDrivingEmitLoop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getLatestPosition() -> [String: Any] {
|
||||||
|
if let location = latestLocation {
|
||||||
|
return buildPositionData(from: location)
|
||||||
|
}
|
||||||
|
return buildDefaultPosition()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getBackgroundLatestPosition() -> [String: Any]? {
|
||||||
|
guard let location = backgroundLatestLocation else { return nil }
|
||||||
|
return buildPositionData(from: location)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getGnssStatus() -> [String: Any]? {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getLocationServicesStatus() -> [String: Any] {
|
||||||
|
let enabled = CLLocationManager.locationServicesEnabled()
|
||||||
|
return ["gpsEnabled": enabled, "networkEnabled": enabled]
|
||||||
|
}
|
||||||
|
|
||||||
|
public func startBackgroundTracking() {
|
||||||
|
backgroundTrackingActive = true
|
||||||
|
if #available(iOS 9.0, *) {
|
||||||
|
locationManager.allowsBackgroundLocationUpdates = true
|
||||||
|
}
|
||||||
|
if #available(iOS 11.0, *) {
|
||||||
|
locationManager.showsBackgroundLocationIndicator = true
|
||||||
|
}
|
||||||
|
locationManager.pausesLocationUpdatesAutomatically = false
|
||||||
|
locationManager.startUpdatingLocation()
|
||||||
|
}
|
||||||
|
|
||||||
|
public func stopBackgroundTracking() {
|
||||||
|
backgroundTrackingActive = false
|
||||||
|
if #available(iOS 9.0, *) {
|
||||||
|
locationManager.allowsBackgroundLocationUpdates = false
|
||||||
|
}
|
||||||
|
if !foregroundTrackingActive {
|
||||||
|
locationManager.pausesLocationUpdatesAutomatically = true
|
||||||
|
locationManager.stopUpdatingLocation()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func isBackgroundTrackingActive() -> Bool {
|
||||||
|
return backgroundTrackingActive
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setAuthTokens(accessToken: String, refreshToken: String) {
|
||||||
|
defaults.set(accessToken, forKey: accessTokenKey)
|
||||||
|
defaults.set(refreshToken, forKey: refreshTokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func clearAuthTokens() {
|
||||||
|
defaults.removeObject(forKey: accessTokenKey)
|
||||||
|
defaults.removeObject(forKey: refreshTokenKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getAuthState() -> Bool {
|
||||||
|
return defaults.string(forKey: accessTokenKey) != nil
|
||||||
|
}
|
||||||
|
|
||||||
|
public func setBackgroundPostUrl(_ url: String?) {
|
||||||
|
if let url = url, !url.isEmpty {
|
||||||
|
defaults.set(url, forKey: backgroundPostUrlKey)
|
||||||
|
} else {
|
||||||
|
defaults.removeObject(forKey: backgroundPostUrlKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func getBackgroundPostUrl() -> String? {
|
||||||
|
return defaults.string(forKey: backgroundPostUrlKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - CLLocationManagerDelegate
|
||||||
|
|
||||||
|
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
|
||||||
|
onAuthorizationChange?(status)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
||||||
|
guard let location = locations.last else { return }
|
||||||
|
latestLocation = location
|
||||||
|
latestTimestampMs = location.timestamp.timeIntervalSince1970 * 1000.0
|
||||||
|
backgroundLatestLocation = location
|
||||||
|
|
||||||
|
if backgroundTrackingActive {
|
||||||
|
postFixIfNeeded(location)
|
||||||
|
}
|
||||||
|
|
||||||
|
emitPositionUpdate(from: location, forceEmit: false)
|
||||||
|
}
|
||||||
|
|
||||||
|
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
||||||
|
// No-op: keep last known fix
|
||||||
|
_ = error
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Internal
|
||||||
|
|
||||||
|
private func emitPositionUpdate(from location: CLLocation, forceEmit: Bool) {
|
||||||
|
let nowMs = Date().timeIntervalSince1970 * 1000.0
|
||||||
|
if !forceEmit && (nowMs - lastEmitTimestampMs) < emitDebounceMs {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let isMocked = isSimulated(location)
|
||||||
|
if suppressMockedUpdates && isMocked {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
let speedNow = resolvedSpeed(from: location).speed
|
||||||
|
let directionNow = resolvedDirection(from: location)
|
||||||
|
|
||||||
|
let distance = prevLocation.map { location.distance(from: $0) } ?? Double.greatestFiniteMagnitude
|
||||||
|
let isSignificantChange = distance >= distanceThresholdMeters
|
||||||
|
let speedChanged = abs(speedNow - prevSpeed) > speedChangeThreshold
|
||||||
|
let directionChanged = abs(directionNow - prevDirection) > directionChangeThreshold
|
||||||
|
|
||||||
|
if forceEmit || isSignificantChange || speedChanged || directionChanged {
|
||||||
|
prevLocation = location
|
||||||
|
prevSpeed = speedNow
|
||||||
|
prevDirection = directionNow
|
||||||
|
lastEmitTimestampMs = nowMs
|
||||||
|
|
||||||
|
let data = buildPositionData(from: location)
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.onPositionUpdate?(data)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildDefaultPosition() -> [String: Any] {
|
||||||
|
let nowMs = Date().timeIntervalSince1970 * 1000.0
|
||||||
|
return [
|
||||||
|
"source": "GNSS",
|
||||||
|
"timestamp": nowMs,
|
||||||
|
"latitude": 0.0,
|
||||||
|
"longitude": 0.0,
|
||||||
|
"accuracy": 999.0,
|
||||||
|
"speed": 0.0,
|
||||||
|
"acceleration": latestAcceleration,
|
||||||
|
"directionRad": latestDirectionRad,
|
||||||
|
"isMocked": false,
|
||||||
|
"mode": currentMode,
|
||||||
|
"predicted": false,
|
||||||
|
"speedSource": "NONE"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func buildPositionData(from location: CLLocation) -> [String: Any] {
|
||||||
|
let nowMs = Date().timeIntervalSince1970 * 1000.0
|
||||||
|
let timestampMs = location.timestamp.timeIntervalSince1970 * 1000.0
|
||||||
|
let fixAgeMs = max(0.0, nowMs - timestampMs)
|
||||||
|
|
||||||
|
let speedInfo = resolvedSpeed(from: location)
|
||||||
|
let directionRad = resolvedDirection(from: location)
|
||||||
|
let accel = latestAcceleration
|
||||||
|
|
||||||
|
var outLat = location.coordinate.latitude
|
||||||
|
var outLon = location.coordinate.longitude
|
||||||
|
var predicted = false
|
||||||
|
|
||||||
|
if enableForwardPrediction && fixAgeMs > 0 {
|
||||||
|
let dtSec = min(fixAgeMs / 1000.0, maxPredictionSeconds)
|
||||||
|
if dtSec > 0 {
|
||||||
|
let speed = speedInfo.speed
|
||||||
|
let dNorth = speed * dtSec * cos(directionRad)
|
||||||
|
let dEast = speed * dtSec * sin(directionRad)
|
||||||
|
let earthRadius = 6371000.0
|
||||||
|
let latRad = outLat * Double.pi / 180.0
|
||||||
|
let dLat = (dNorth / earthRadius) * (180.0 / Double.pi)
|
||||||
|
let dLon = (dEast / (earthRadius * cos(latRad))) * (180.0 / Double.pi)
|
||||||
|
outLat += dLat
|
||||||
|
outLon += dLon
|
||||||
|
predicted = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var data: [String: Any] = [
|
||||||
|
"source": isSimulated(location) ? "MOCK" : "GNSS",
|
||||||
|
"timestamp": timestampMs,
|
||||||
|
"latitude": outLat,
|
||||||
|
"longitude": outLon,
|
||||||
|
"accuracy": location.horizontalAccuracy,
|
||||||
|
"speed": speedInfo.speed,
|
||||||
|
"acceleration": accel,
|
||||||
|
"directionRad": directionRad,
|
||||||
|
"isMocked": isSimulated(location),
|
||||||
|
"mode": currentMode,
|
||||||
|
"predicted": predicted,
|
||||||
|
"speedSource": speedInfo.source
|
||||||
|
]
|
||||||
|
|
||||||
|
if let gnss = speedInfo.speedGnss {
|
||||||
|
data["speedGnss"] = gnss
|
||||||
|
}
|
||||||
|
if let derived = speedInfo.speedDerived {
|
||||||
|
data["speedDerived"] = derived
|
||||||
|
}
|
||||||
|
|
||||||
|
latestDirectionRad = directionRad
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedSpeed(from location: CLLocation) -> (speed: Double, source: String, speedGnss: Double?, speedDerived: Double?) {
|
||||||
|
let nowMs = Date().timeIntervalSince1970 * 1000.0
|
||||||
|
let timestampMs = location.timestamp.timeIntervalSince1970 * 1000.0
|
||||||
|
let fixAgeMs = max(0.0, nowMs - timestampMs)
|
||||||
|
|
||||||
|
let gnssSpeed = location.speed >= 0 ? location.speed : -1
|
||||||
|
let validGnss = gnssSpeed >= 0 && fixAgeMs <= 3000
|
||||||
|
|
||||||
|
let derivedSpeed: Double? = {
|
||||||
|
guard let prev = prevLocation else { return nil }
|
||||||
|
let dtMs = timestampMs - prev.timestamp.timeIntervalSince1970 * 1000.0
|
||||||
|
let dtSec = dtMs / 1000.0
|
||||||
|
if dtSec >= 3.0 && dtSec <= 30.0 {
|
||||||
|
let dist = location.distance(from: prev)
|
||||||
|
return dist / dtSec
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}()
|
||||||
|
|
||||||
|
if validGnss {
|
||||||
|
return (gnssSpeed, "GNSS", gnssSpeed, nil)
|
||||||
|
}
|
||||||
|
if let derived = derivedSpeed {
|
||||||
|
return (derived, "DELTA", nil, derived)
|
||||||
|
}
|
||||||
|
return (0.0, "NONE", nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
private func resolvedDirection(from location: CLLocation) -> Double {
|
||||||
|
if location.course >= 0 {
|
||||||
|
return location.course * Double.pi / 180.0
|
||||||
|
}
|
||||||
|
return latestDirectionRad
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isSimulated(_ location: CLLocation) -> Bool {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
return location.sourceInformation?.isSimulatedBySoftware ?? false
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startMotionUpdates() {
|
||||||
|
guard motionManager.isDeviceMotionAvailable else { return }
|
||||||
|
motionManager.deviceMotionUpdateInterval = 0.2
|
||||||
|
motionManager.startDeviceMotionUpdates(to: OperationQueue()) { [weak self] motion, _ in
|
||||||
|
guard let motion = motion else { return }
|
||||||
|
let a = motion.userAcceleration
|
||||||
|
let magnitude = sqrt(a.x * a.x + a.y * a.y + a.z * a.z)
|
||||||
|
self?.latestAcceleration = magnitude
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopMotionUpdates() {
|
||||||
|
motionManager.stopDeviceMotionUpdates()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func startDrivingEmitLoop() {
|
||||||
|
if drivingTimer != nil { return }
|
||||||
|
let interval = drivingEmitIntervalMs / 1000.0
|
||||||
|
drivingTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true) { [weak self] _ in
|
||||||
|
guard let self = self, let location = self.latestLocation else { return }
|
||||||
|
self.emitPositionUpdate(from: location, forceEmit: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func stopDrivingEmitLoop() {
|
||||||
|
drivingTimer?.invalidate()
|
||||||
|
drivingTimer = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applyKeepScreenOn(_ enabled: Bool) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIApplication.shared.isIdleTimerDisabled = enabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func postFixIfNeeded(_ location: CLLocation) {
|
||||||
|
guard let urlString = getBackgroundPostUrl(), let url = URL(string: urlString) else { return }
|
||||||
|
|
||||||
|
let nowMs = Date().timeIntervalSince1970 * 1000.0
|
||||||
|
if nowMs - lastPostAttemptMs < backgroundMinPostIntervalMs {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if location.horizontalAccuracy > backgroundPostMinAccuracyMeters {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if let lastPosted = lastPostedLocation {
|
||||||
|
let dist = location.distance(from: lastPosted)
|
||||||
|
if dist < backgroundPostMinDistanceMeters {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lastPostAttemptMs = nowMs
|
||||||
|
let payload = buildPositionData(from: location)
|
||||||
|
|
||||||
|
var request = URLRequest(url: url)
|
||||||
|
request.httpMethod = "POST"
|
||||||
|
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
||||||
|
if let token = defaults.string(forKey: accessTokenKey) {
|
||||||
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
||||||
|
}
|
||||||
|
|
||||||
|
request.httpBody = try? JSONSerialization.data(withJSONObject: payload, options: [])
|
||||||
|
|
||||||
|
URLSession.shared.dataTask(with: request) { [weak self] _, _, _ in
|
||||||
|
self?.lastPostedLocation = location
|
||||||
|
}.resume()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,23 +1,223 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
import Capacitor
|
import Capacitor
|
||||||
|
import CoreLocation
|
||||||
|
import UIKit
|
||||||
|
|
||||||
/**
|
|
||||||
* Please read the Capacitor iOS Plugin Development Guide
|
|
||||||
* here: https://capacitorjs.com/docs/plugins/ios
|
|
||||||
*/
|
|
||||||
@objc(DumonGeolocationPlugin)
|
@objc(DumonGeolocationPlugin)
|
||||||
public class DumonGeolocationPlugin: CAPPlugin, CAPBridgedPlugin {
|
public class DumonGeolocationPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||||
public let identifier = "DumonGeolocationPlugin"
|
public let identifier = "DumonGeolocationPlugin"
|
||||||
public let jsName = "DumonGeolocation"
|
public let jsName = "DumonGeolocation"
|
||||||
public let pluginMethods: [CAPPluginMethod] = [
|
public let pluginMethods: [CAPPluginMethod] = [
|
||||||
CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise)
|
CAPPluginMethod(name: "startPositioning", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "stopPositioning", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getLatestPosition", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "checkAndRequestPermissions", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "setOptions", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getGnssStatus", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getLocationServicesStatus", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "startBackgroundTracking", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "stopBackgroundTracking", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "isBackgroundTrackingActive", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getBackgroundLatestPosition", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "openBackgroundPermissionSettings", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "openNotificationPermissionSettings", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "setAuthTokens", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "clearAuthTokens", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getAuthState", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "setBackgroundPostUrl", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "getBackgroundPostUrl", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "configureEdgeToEdge", returnType: CAPPluginReturnPromise),
|
||||||
|
CAPPluginMethod(name: "setGpsMode", returnType: CAPPluginReturnPromise)
|
||||||
]
|
]
|
||||||
private let implementation = DumonGeolocation()
|
|
||||||
|
|
||||||
@objc func echo(_ call: CAPPluginCall) {
|
private let implementation = DumonGeolocation()
|
||||||
let value = call.getString("value") ?? ""
|
private var pendingPermissionCall: CAPPluginCall?
|
||||||
call.resolve([
|
private var pendingBackgroundCall: CAPPluginCall?
|
||||||
"value": implementation.echo(value)
|
|
||||||
])
|
public override func load() {
|
||||||
|
implementation.onPositionUpdate = { [weak self] data in
|
||||||
|
self?.notifyListeners("onPositionUpdate", data: data)
|
||||||
|
}
|
||||||
|
implementation.onGnssStatus = { [weak self] data in
|
||||||
|
self?.notifyListeners("onGnssStatus", data: data)
|
||||||
|
}
|
||||||
|
implementation.onAuthorizationChange = { [weak self] status in
|
||||||
|
self?.handleAuthorizationChange(status)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func startPositioning(_ call: CAPPluginCall) {
|
||||||
|
let status = implementation.authorizationStatus()
|
||||||
|
if !isLocationAuthorized(status) {
|
||||||
|
call.reject("Location permission not granted")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
implementation.startPositioning()
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func stopPositioning(_ call: CAPPluginCall) {
|
||||||
|
implementation.stopPositioning()
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getLatestPosition(_ call: CAPPluginCall) {
|
||||||
|
call.resolve(implementation.getLatestPosition())
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func checkAndRequestPermissions(_ call: CAPPluginCall) {
|
||||||
|
let status = implementation.authorizationStatus()
|
||||||
|
if status == .notDetermined {
|
||||||
|
pendingPermissionCall = call
|
||||||
|
implementation.requestWhenInUsePermission()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
call.resolve(permissionStatusDict(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setOptions(_ call: CAPPluginCall) {
|
||||||
|
var options: [String: Any] = [:]
|
||||||
|
for (key, value) in call.options {
|
||||||
|
if let key = key as? String {
|
||||||
|
options[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
implementation.setOptions(options)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getGnssStatus(_ call: CAPPluginCall) {
|
||||||
|
if let data = implementation.getGnssStatus() {
|
||||||
|
call.resolve(data)
|
||||||
|
} else {
|
||||||
|
call.resolve([:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getLocationServicesStatus(_ call: CAPPluginCall) {
|
||||||
|
call.resolve(implementation.getLocationServicesStatus())
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func startBackgroundTracking(_ call: CAPPluginCall) {
|
||||||
|
if let postUrl = call.getString("postUrl") {
|
||||||
|
implementation.setBackgroundPostUrl(postUrl)
|
||||||
|
}
|
||||||
|
let status = implementation.authorizationStatus()
|
||||||
|
if status == .authorizedAlways {
|
||||||
|
implementation.startBackgroundTracking()
|
||||||
|
call.resolve()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pendingBackgroundCall = call
|
||||||
|
implementation.requestAlwaysPermission()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func stopBackgroundTracking(_ call: CAPPluginCall) {
|
||||||
|
implementation.stopBackgroundTracking()
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func isBackgroundTrackingActive(_ call: CAPPluginCall) {
|
||||||
|
call.resolve(["active": implementation.isBackgroundTrackingActive()])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getBackgroundLatestPosition(_ call: CAPPluginCall) {
|
||||||
|
if let data = implementation.getBackgroundLatestPosition() {
|
||||||
|
call.resolve(data)
|
||||||
|
} else {
|
||||||
|
call.resolve([:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openBackgroundPermissionSettings(_ call: CAPPluginCall) {
|
||||||
|
openAppSettings(call)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func openNotificationPermissionSettings(_ call: CAPPluginCall) {
|
||||||
|
openAppSettings(call)
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setAuthTokens(_ call: CAPPluginCall) {
|
||||||
|
guard let access = call.getString("accessToken"), !access.isEmpty else {
|
||||||
|
call.reject("accessToken is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
guard let refresh = call.getString("refreshToken"), !refresh.isEmpty else {
|
||||||
|
call.reject("refreshToken is required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
implementation.setAuthTokens(accessToken: access, refreshToken: refresh)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func clearAuthTokens(_ call: CAPPluginCall) {
|
||||||
|
implementation.clearAuthTokens()
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getAuthState(_ call: CAPPluginCall) {
|
||||||
|
call.resolve(["present": implementation.getAuthState()])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setBackgroundPostUrl(_ call: CAPPluginCall) {
|
||||||
|
let url = call.getString("url")
|
||||||
|
implementation.setBackgroundPostUrl(url)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func getBackgroundPostUrl(_ call: CAPPluginCall) {
|
||||||
|
let url = implementation.getBackgroundPostUrl()
|
||||||
|
call.resolve(["url": url ?? NSNull()])
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func configureEdgeToEdge(_ call: CAPPluginCall) {
|
||||||
|
// No-op on iOS; UI handled by app.
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc func setGpsMode(_ call: CAPPluginCall) {
|
||||||
|
let mode = call.getString("mode") ?? "normal"
|
||||||
|
implementation.setGpsMode(mode)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func handleAuthorizationChange(_ status: CLAuthorizationStatus) {
|
||||||
|
if let call = pendingPermissionCall {
|
||||||
|
pendingPermissionCall = nil
|
||||||
|
call.resolve(permissionStatusDict(status))
|
||||||
|
}
|
||||||
|
|
||||||
|
if let call = pendingBackgroundCall {
|
||||||
|
pendingBackgroundCall = nil
|
||||||
|
if status == .authorizedAlways {
|
||||||
|
implementation.startBackgroundTracking()
|
||||||
|
call.resolve()
|
||||||
|
} else {
|
||||||
|
call.reject("Background location permission not granted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func permissionStatusDict(_ status: CLAuthorizationStatus) -> [String: String] {
|
||||||
|
let granted = isLocationAuthorized(status)
|
||||||
|
return [
|
||||||
|
"location": granted ? "granted" : "denied",
|
||||||
|
"wifi": "granted"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
private func isLocationAuthorized(_ status: CLAuthorizationStatus) -> Bool {
|
||||||
|
return status == .authorizedAlways || status == .authorizedWhenInUse
|
||||||
|
}
|
||||||
|
|
||||||
|
private func openAppSettings(_ call: CAPPluginCall) {
|
||||||
|
guard let url = URL(string: UIApplication.openSettingsURLString) else {
|
||||||
|
call.reject("Unable to open settings")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||||
|
call.resolve()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
package-lock.json
generated
4
package-lock.json
generated
@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "dumon-geolocation",
|
"name": "dumon-geolocation",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "dumon-geolocation",
|
"name": "dumon-geolocation",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@capacitor/android": "^7.0.0",
|
"@capacitor/android": "^7.0.0",
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dumon-geolocation",
|
"name": "dumon-geolocation",
|
||||||
"version": "1.0.5",
|
"version": "1.1.0",
|
||||||
"description": "Implement manager GNSS, Wi‑Fi RTT, IMU, Kalman fusion, event emitter",
|
"description": "Implement manager GNSS, Wi‑Fi 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",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user