@bricks-soft/capacitor-location-tracking 8.0.1 → 8.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/BricksSoftCapacitorLocationTracking.podspec +18 -0
- package/Package.swift +13 -0
- package/README.md +476 -147
- package/android/build.gradle +3 -1
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/OptionParsers.kt +4 -1
- package/android/src/main/java/com/brickssoft/locationtracking/bridge/PluginHandlers.kt +14 -5
- package/android/src/main/java/com/brickssoft/locationtracking/config/Config.kt +10 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigJson.kt +70 -15
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigStore.kt +6 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/ConfigValidator.kt +8 -3
- package/android/src/main/java/com/brickssoft/locationtracking/config/RuntimeStateJson.kt +10 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SecretStore.kt +188 -0
- package/android/src/main/java/com/brickssoft/locationtracking/config/SharedPrefsConfigStore.kt +188 -17
- package/android/src/main/java/com/brickssoft/locationtracking/config/State.kt +11 -0
- package/android/src/main/java/com/brickssoft/locationtracking/core/Components.kt +21 -5
- package/android/src/main/java/com/brickssoft/locationtracking/core/ForceStopProbe.kt +20 -9
- package/android/src/main/java/com/brickssoft/locationtracking/core/TrackingEvent.kt +24 -2
- package/android/src/main/java/com/brickssoft/locationtracking/data/TrackingDatabase.kt +43 -1
- package/android/src/main/java/com/brickssoft/locationtracking/device/DefaultDeviceInfoProvider.kt +17 -0
- package/android/src/main/java/com/brickssoft/locationtracking/device/DeviceDetails.kt +167 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/DefaultTrackingEngine.kt +191 -34
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingEngine.kt +6 -0
- package/android/src/main/java/com/brickssoft/locationtracking/engine/TrackingTimers.kt +3 -0
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/DefaultHeartbeatScheduler.kt +52 -8
- package/android/src/main/java/com/brickssoft/locationtracking/heartbeat/HeartbeatAlarmReceiver.kt +22 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/AuthorizationManager.kt +78 -27
- package/android/src/main/java/com/brickssoft/locationtracking/http/ConfigChecks.kt +140 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSupport.kt +50 -12
- package/android/src/main/java/com/brickssoft/locationtracking/http/HttpSyncer.kt +17 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/OkHttpSyncer.kt +507 -92
- package/android/src/main/java/com/brickssoft/locationtracking/http/SyncPolicy.kt +6 -6
- package/android/src/main/java/com/brickssoft/locationtracking/http/TemplateRenderer.kt +23 -1
- package/android/src/main/java/com/brickssoft/locationtracking/http/UploadJobs.kt +217 -0
- package/android/src/main/java/com/brickssoft/locationtracking/http/UploadWait.kt +184 -0
- package/android/src/main/java/com/brickssoft/locationtracking/logging/FileLogger.kt +66 -7
- package/android/src/main/java/com/brickssoft/locationtracking/logging/LogRedaction.kt +31 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/DeviceModels.kt +44 -1
- package/android/src/main/java/com/brickssoft/locationtracking/model/ModelJson.kt +79 -8
- package/android/src/main/java/com/brickssoft/locationtracking/model/Record.kt +9 -0
- package/android/src/main/java/com/brickssoft/locationtracking/model/TrackedLocation.kt +12 -2
- package/android/src/main/java/com/brickssoft/locationtracking/position/DefaultPositionService.kt +10 -2
- package/android/src/main/java/com/brickssoft/locationtracking/position/PositionWatch.kt +24 -1
- package/android/src/main/java/com/brickssoft/locationtracking/processing/DefaultRecordFactory.kt +31 -10
- package/android/src/main/java/com/brickssoft/locationtracking/provider/Backends.kt +48 -3
- package/android/src/main/java/com/brickssoft/locationtracking/provider/DefaultProviderFactory.kt +60 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/LocationSubscriptions.kt +180 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/AndroidLocationBackend.kt +200 -126
- package/android/src/main/java/com/brickssoft/locationtracking/provider/android/LocationProviders.kt +20 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsLocationBackend.kt +39 -51
- package/android/src/main/java/com/brickssoft/locationtracking/provider/gms/GmsProviderBundle.kt +8 -0
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsLocationBackend.kt +47 -53
- package/android/src/main/java/com/brickssoft/locationtracking/provider/hms/HmsProviderBundle.kt +34 -2
- package/android/src/main/java/com/brickssoft/locationtracking/record/RecordSink.kt +19 -1
- package/android/src/main/java/com/brickssoft/locationtracking/service/BootCountStore.kt +7 -0
- package/android/src/main/res/xml/lt_file_paths.xml +1 -1
- package/dist/esm/definitions.d.ts +117 -8
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/events.d.ts +8 -2
- package/dist/esm/events.js +7 -1
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/index.d.ts +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web/config.js +10 -0
- package/dist/esm/web/config.js.map +1 -1
- package/dist/esm/web/device.d.ts +1 -1
- package/dist/esm/web/device.js +8 -1
- package/dist/esm/web/device.js.map +1 -1
- package/dist/esm/web.d.ts +0 -8
- package/dist/esm/web.js +12 -3
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +39 -5
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +39 -5
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/LocationTrackingPlugin/Api/BackgroundUpload.swift +24 -0
- package/ios/Sources/LocationTrackingPlugin/Api/LocationTrackingNative.swift +183 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigDefaults.swift +96 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigSchema.swift +74 -0
- package/ios/Sources/LocationTrackingPlugin/Config/ConfigValidator.swift +40 -0
- package/ios/Sources/LocationTrackingPlugin/Config/SecretStore.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Config/TrackingConfig.swift +104 -0
- package/ios/Sources/LocationTrackingPlugin/Core/JSON.swift +50 -0
- package/ios/Sources/LocationTrackingPlugin/Core/Options.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Core/PluginInfo.swift +6 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingClock.swift +11 -0
- package/ios/Sources/LocationTrackingPlugin/Core/TrackingCore.swift +298 -0
- package/ios/Sources/LocationTrackingPlugin/Data/TrackingStore.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Device/DeviceReadings.swift +43 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/CheckInNotifier.swift +92 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/MotionStateMachine.swift +122 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/SessionDeadline.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingEngine.swift +344 -0
- package/ios/Sources/LocationTrackingPlugin/Engine/TrackingTimers.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/DwellTracker.swift +35 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceEventAdapter.swift +88 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceManager.swift +453 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceMonitor.swift +207 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceSpec.swift +103 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/GeofenceStore.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Geofence/PolygonMath.swift +176 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/ExecutionAssertion.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Heartbeat/HeartbeatWindow.swift +29 -0
- package/ios/Sources/LocationTrackingPlugin/Http/BodyBuilder.swift +52 -0
- package/ios/Sources/LocationTrackingPlugin/Http/HTTPTransport.swift +87 -0
- package/ios/Sources/LocationTrackingPlugin/Http/StrictJSON.swift +98 -0
- package/ios/Sources/LocationTrackingPlugin/Http/Syncer.swift +313 -0
- package/ios/Sources/LocationTrackingPlugin/Http/UploadBackoff.swift +26 -0
- package/ios/Sources/LocationTrackingPlugin/Location/RouteSource.swift +124 -0
- package/ios/Sources/LocationTrackingPlugin/LocationTrackingPlugin.swift +155 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/LogExport.swift +73 -0
- package/ios/Sources/LocationTrackingPlugin/Logging/TrackingLog.swift +47 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/LocationServices.swift +32 -0
- package/ios/Sources/LocationTrackingPlugin/Permission/Permissions.swift +102 -0
- package/ios/Sources/LocationTrackingPlugin/Position/PositionSource.swift +79 -0
- package/ios/Sources/LocationTrackingPlugin/Position/Positions.swift +33 -0
- package/ios/Sources/LocationTrackingPlugin/Position/SampleCollector.swift +31 -0
- package/ios/Sources/LocationTrackingPlugin/PrivacyInfo.xcprivacy +10 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/LocationProcessor.swift +83 -0
- package/ios/Sources/LocationTrackingPlugin/Processing/TrackedLocation.swift +64 -0
- package/ios/Sources/LocationTrackingPlugin/Record/ExternalRecord.swift +54 -0
- package/ios/Sources/LocationTrackingPlugin/Record/RecordFactory.swift +27 -0
- package/package.json +10 -4
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
final class Syncer {
|
|
4
|
+
let transport = HTTPTransport()
|
|
5
|
+
private let queue: DispatchQueue
|
|
6
|
+
private let store: TrackingStore
|
|
7
|
+
private let config: TrackingConfig
|
|
8
|
+
private let emit: (String, TrackingJSON) -> Void
|
|
9
|
+
private var callbacks: [(Result<[TrackingJSON], Error>) -> Void] = []
|
|
10
|
+
private var followUp: [(Result<[TrackingJSON], Error>) -> Void] = []
|
|
11
|
+
private var connectivityWaiters: [() -> Void] = []
|
|
12
|
+
private var uploaded: [TrackingJSON] = []
|
|
13
|
+
private var reducedBatch: Int?
|
|
14
|
+
private var busy = false
|
|
15
|
+
private var closed = false
|
|
16
|
+
private var pendingAutomatic = false
|
|
17
|
+
private var pendingRecovery = false
|
|
18
|
+
private var changedUntil: Date?
|
|
19
|
+
private var rejectedPriority = Set<String>()
|
|
20
|
+
private var passFailure: (Error, TrackingJSON, Int, Double?)?
|
|
21
|
+
private enum Renewal { case renewed(TrackingJSON), changed, failed(Error) }
|
|
22
|
+
var connectivityKnown = false
|
|
23
|
+
private var priorityOnly = false
|
|
24
|
+
private var generation = 0
|
|
25
|
+
private var shortLivedToken: String?
|
|
26
|
+
private var failures = 0
|
|
27
|
+
private var retryTimer: DispatchWorkItem?
|
|
28
|
+
var connected = true
|
|
29
|
+
var cellular = false
|
|
30
|
+
var tracking = false
|
|
31
|
+
private let priority = Set(["heartbeat", "tracking_start", "tracking_stop", "providerchange"])
|
|
32
|
+
init(queue: DispatchQueue, store: TrackingStore, config: TrackingConfig, emit: @escaping (String, TrackingJSON) -> Void) {
|
|
33
|
+
self.queue = queue; self.store = store; self.config = config; self.emit = emit
|
|
34
|
+
failures = Int((try? store.load("uploadWait"))?.number("failures") ?? 0)
|
|
35
|
+
}
|
|
36
|
+
static func needsRefresh(_ auth: TrackingJSON, now: Double, shortLivedToken: String? = nil) -> Bool {
|
|
37
|
+
let token = auth.text("accessToken")
|
|
38
|
+
if token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { return true }
|
|
39
|
+
let expires = auth.number("expires", -1)
|
|
40
|
+
if expires <= 0 { return false }
|
|
41
|
+
return now >= expires || (now >= expires - 60000 && token != shortLivedToken)
|
|
42
|
+
}
|
|
43
|
+
func close() {
|
|
44
|
+
guard !closed else { return }
|
|
45
|
+
closed = true
|
|
46
|
+
retryTimer?.cancel()
|
|
47
|
+
cancel()
|
|
48
|
+
transport.close()
|
|
49
|
+
let waiters = connectivityWaiters; connectivityWaiters = []
|
|
50
|
+
waiters.forEach { $0() }
|
|
51
|
+
}
|
|
52
|
+
func cancel() {
|
|
53
|
+
generation += 1
|
|
54
|
+
pendingAutomatic = false
|
|
55
|
+
transport.cancel()
|
|
56
|
+
if busy { finish(.failure(TrackingError("NETWORK_ERROR", "Upload cancelled"))) }
|
|
57
|
+
}
|
|
58
|
+
func connectivity(connected: Bool, cellular: Bool) {
|
|
59
|
+
let recovered = connectivityKnown && !self.connected && connected
|
|
60
|
+
connectivityKnown = true
|
|
61
|
+
self.connected = connected
|
|
62
|
+
self.cellular = cellular
|
|
63
|
+
if recovered && busy { pendingRecovery = true }
|
|
64
|
+
if recovered, let wait = try? store.load("uploadWait"), wait.text("reason") == "server" {
|
|
65
|
+
try? store.save("uploadWait", [:])
|
|
66
|
+
failures = 0
|
|
67
|
+
retryTimer?.cancel()
|
|
68
|
+
}
|
|
69
|
+
let waiters = connectivityWaiters; connectivityWaiters = []
|
|
70
|
+
waiters.forEach { $0() }
|
|
71
|
+
automatic()
|
|
72
|
+
}
|
|
73
|
+
func whenConnectivityKnown(_ body: @escaping () -> Void) {
|
|
74
|
+
if connectivityKnown || closed { body() } else { connectivityWaiters.append(body) }
|
|
75
|
+
}
|
|
76
|
+
func automatic(completion: ((Result<[TrackingJSON], Error>) -> Void)? = nil) {
|
|
77
|
+
guard !closed else { completion?(.failure(TrackingError("NETWORK_ERROR", "Syncer is closed"))); return }
|
|
78
|
+
if busy {
|
|
79
|
+
pendingAutomatic = true
|
|
80
|
+
if let completion { followUp.append(completion) }
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
guard connectivityKnown && connected else { completion?(.success([])); return }
|
|
84
|
+
if let until = changedUntil, until > Date() { schedule(until.timeIntervalSince1970 * 1000); completion?(.success([])); return }
|
|
85
|
+
changedUntil = nil
|
|
86
|
+
let total: Int, hasPriority: Bool
|
|
87
|
+
do {
|
|
88
|
+
total = try store.count
|
|
89
|
+
hasPriority = try total > 0 && store.contains(events: priority)
|
|
90
|
+
} catch { completion?(.failure(error)); return }
|
|
91
|
+
guard total > 0 else { completion?(.success([])); return }
|
|
92
|
+
let http = config.value.object("http")
|
|
93
|
+
guard !http.text("url").isEmpty else { completion?(.success([])); return }
|
|
94
|
+
if hasPriority { run(priorityOnly: cellular && http.flag("disableAutoSyncOnCellular"), completion: completion ?? { _ in }); return }
|
|
95
|
+
guard http.flag("autoSync", true), !(cellular && http.flag("disableAutoSyncOnCellular")) else { completion?(.success([])); return }
|
|
96
|
+
if let wait = try? store.load("uploadWait"), wait.number("at") > Date().timeIntervalSince1970 * 1000,
|
|
97
|
+
wait.text("reason") != "authorization" || wait.text("authKey") == UploadBackoff.authKey(http) { schedule(wait.number("at")); completion?(.success([])); return }
|
|
98
|
+
let interval = tracking ? http.number("syncInterval") : 0
|
|
99
|
+
let threshold = http.number("autoSyncThreshold")
|
|
100
|
+
if interval > 0, let oldest = try? store.locations(limit: 1).first {
|
|
101
|
+
let age = Date().timeIntervalSince(isoDate(oldest.text("recorded_at")) ?? .distantPast)
|
|
102
|
+
if age >= 0 && age < interval && !(threshold > 0 && Double(total) >= threshold) { schedule(Date().timeIntervalSince1970 * 1000 + (interval - age) * 1000); completion?(.success([])); return }
|
|
103
|
+
} else if Double(total) < max(1, threshold) { completion?(.success([])); return }
|
|
104
|
+
run(completion: completion ?? { _ in })
|
|
105
|
+
}
|
|
106
|
+
func run(priorityOnly: Bool = false, completion: @escaping (Result<[TrackingJSON], Error>) -> Void) {
|
|
107
|
+
guard !closed else { completion(.failure(TrackingError("NETWORK_ERROR", "Syncer is closed"))); return }
|
|
108
|
+
guard !config.value.object("http").text("url").isEmpty else { completion(.failure(TrackingError("NO_URL", "No http.url configured"))); return }
|
|
109
|
+
callbacks.append(completion)
|
|
110
|
+
if busy { if !priorityOnly { self.priorityOnly = false }; return }
|
|
111
|
+
self.priorityOnly = priorityOnly
|
|
112
|
+
busy = true; uploaded = []; reducedBatch = nil; rejectedPriority = []; passFailure = nil
|
|
113
|
+
next()
|
|
114
|
+
}
|
|
115
|
+
private func next() {
|
|
116
|
+
do {
|
|
117
|
+
let http = config.value.object("http")
|
|
118
|
+
let limit = http.flag("batchSync") ? max(1, Int(http.number("maxBatchSize", 100))) : 1
|
|
119
|
+
let size = reducedBatch.map { min(limit, $0) } ?? limit
|
|
120
|
+
let chunk = priorityOnly ? Array(try store.locations(events: priority).filter { !rejectedPriority.contains($0.text("uuid")) }.prefix(size)) : try store.locations(limit: size)
|
|
121
|
+
if chunk.isEmpty {
|
|
122
|
+
if let failure = passFailure { fail(failure.0, http: failure.1, status: failure.2, retryAfter: failure.3, authorizationFailure: false) }
|
|
123
|
+
else {
|
|
124
|
+
if !priorityOnly { failures = 0; try store.save("uploadWait", [:]) }
|
|
125
|
+
finish(.success(uploaded))
|
|
126
|
+
}
|
|
127
|
+
return
|
|
128
|
+
}
|
|
129
|
+
upload(chunk, http: http, attempt: 0, refreshed: false)
|
|
130
|
+
} catch { finish(.failure(error)) }
|
|
131
|
+
}
|
|
132
|
+
private func upload(_ records: [TrackingJSON], http: TrackingJSON, attempt: Int, refreshed: Bool) {
|
|
133
|
+
let captured = generation
|
|
134
|
+
let explicitAuthorization = http.object("headers").keys.contains { $0.lowercased() == "authorization" }
|
|
135
|
+
if config.secretsUnavailable && !explicitAuthorization {
|
|
136
|
+
emit("authorization", ["success": false, "status": 0, "error": "Stored authorization is unavailable; set http.authorization again"])
|
|
137
|
+
fail(TrackingError("HTTP_ERROR", "Stored authorization is unavailable; set http.authorization again"), http: http, status: 401, retryAfter: nil)
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
guard UploadBackoff.authKey(http) == UploadBackoff.authKey(config.value.object("http")) else { credentialsChanged(); return }
|
|
141
|
+
let auth = explicitAuthorization ? [:] : http.object("authorization")
|
|
142
|
+
if !auth.isEmpty && !auth.text("refreshUrl").isEmpty && !refreshed && Self.needsRefresh(auth, now: Date().timeIntervalSince1970 * 1000, shortLivedToken: shortLivedToken) {
|
|
143
|
+
refresh(http) { result in
|
|
144
|
+
guard self.generation == captured else { return }
|
|
145
|
+
switch result {
|
|
146
|
+
case .renewed(let fresh): self.upload(records, http: fresh, attempt: attempt, refreshed: true)
|
|
147
|
+
case .changed: self.credentialsChanged()
|
|
148
|
+
case .failed(let error):
|
|
149
|
+
if !auth.text("accessToken").isEmpty { self.upload(records, http: http, attempt: attempt, refreshed: true) }
|
|
150
|
+
else { self.fail(error, http: http, status: 401, retryAfter: nil) }
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
do {
|
|
156
|
+
guard let url = URL(string: http.text("url")), ["http", "https"].contains(url.scheme ?? "") else { throw TrackingError("INVALID_ARGUMENT", "Invalid upload URL") }
|
|
157
|
+
var headers = http["headers"] as? [String: String] ?? [:]
|
|
158
|
+
if !auth.text("accessToken").trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { headers = headers.filter { $0.key.lowercased() != "authorization" }; headers["Authorization"] = "Bearer " + auth.text("accessToken") }
|
|
159
|
+
let body = try BodyBuilder.build(records, http: http)
|
|
160
|
+
transport.send(url: url, method: http.text("method", "POST"), headers: headers, body: body, timeout: http.number("timeout", 60000)) { result in
|
|
161
|
+
self.queue.async {
|
|
162
|
+
guard self.generation == captured, self.busy else { return }
|
|
163
|
+
let response = try? result.get()
|
|
164
|
+
let status = response?.status ?? 0
|
|
165
|
+
self.emit("http", ["success": response?.success ?? false, "status": status, "responseText": response.map { String(decoding: $0.body, as: UTF8.self) } ?? "Network request failed", "uuids": records.map { $0.text("uuid") }])
|
|
166
|
+
if response?.success == true {
|
|
167
|
+
do {
|
|
168
|
+
try self.store.transaction { for record in records { try self.store.delete(record.text("uuid")) } }
|
|
169
|
+
self.uploaded += records
|
|
170
|
+
self.next()
|
|
171
|
+
} catch { self.finish(.failure(error)) }
|
|
172
|
+
return
|
|
173
|
+
}
|
|
174
|
+
if UploadBackoff.authKey(http) != UploadBackoff.authKey(self.config.value.object("http")) {
|
|
175
|
+
self.credentialsChanged(); return
|
|
176
|
+
}
|
|
177
|
+
if status == 401 && !refreshed && !auth.isEmpty {
|
|
178
|
+
self.refresh(http) { renewal in
|
|
179
|
+
guard self.generation == captured else { return }
|
|
180
|
+
switch renewal {
|
|
181
|
+
case .renewed(let fresh): self.upload(records, http: fresh, attempt: attempt, refreshed: true)
|
|
182
|
+
case .changed: self.credentialsChanged()
|
|
183
|
+
case .failed(let error): self.fail(error, http: http, status: 401, retryAfter: nil)
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
if status == 413 && records.count > 1 { self.reducedBatch = max(1, records.count / 2); self.next(); return }
|
|
189
|
+
let retryAfter = (status == 429 || status == 503) ? UploadBackoff.retryAfter(response?.retryAfter, maximum: http.number("maxRetryAfter", 3600)) : nil
|
|
190
|
+
if (status == 0 || status == 429 || status >= 500), attempt < 3 {
|
|
191
|
+
let delay = UploadBackoff.retryDelay(attempt, random: Double.random(in: 0..<1))
|
|
192
|
+
if (retryAfter ?? 0) <= delay {
|
|
193
|
+
self.queue.asyncAfter(deadline: .now() + delay / 1000) {
|
|
194
|
+
guard self.generation == captured, self.busy else { return }
|
|
195
|
+
self.upload(records, http: http, attempt: attempt + 1, refreshed: refreshed)
|
|
196
|
+
}
|
|
197
|
+
return
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
self.reject(records, error: TrackingError(status == 0 ? "NETWORK_ERROR" : "HTTP_ERROR", status == 0 ? "Network request failed" : "Upload failed: HTTP \(status)"), http: http, status: status, retryAfter: retryAfter)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
} catch { reject(records, error: error, http: http, status: -1, retryAfter: nil) }
|
|
204
|
+
}
|
|
205
|
+
private func refresh(_ http: TrackingJSON, completion: @escaping (Renewal) -> Void) {
|
|
206
|
+
let auth = http.object("authorization")
|
|
207
|
+
guard let url = URL(string: auth.text("refreshUrl")), ["http", "https"].contains(url.scheme ?? "") else { emit("authorization", ["success": false, "status": 0, "error": "Token refresh is not configured"]); completion(.failed(TrackingError("HTTP_ERROR", "Token refresh is not configured"))); return }
|
|
208
|
+
var payload = auth.object("refreshPayload")
|
|
209
|
+
for (key, value) in payload { if let text = value as? String { payload[key] = text.replacingOccurrences(of: "{refreshToken}", with: auth.text("refreshToken")) } }
|
|
210
|
+
do {
|
|
211
|
+
var headers = auth["refreshHeaders"] as? [String: String] ?? [:]
|
|
212
|
+
let body: Data
|
|
213
|
+
if auth.text("refreshPayloadEncoding", "json") == "form" {
|
|
214
|
+
var components = URLComponents()
|
|
215
|
+
components.queryItems = payload.map { URLQueryItem(name: $0.key, value: String(describing: $0.value)) }
|
|
216
|
+
body = Data((components.percentEncodedQuery ?? "").replacingOccurrences(of: "+", with: "%2B").utf8)
|
|
217
|
+
headers["Content-Type"] = "application/x-www-form-urlencoded"
|
|
218
|
+
} else { body = try jsonData(payload) }
|
|
219
|
+
let captured = generation
|
|
220
|
+
transport.send(url: url, method: "POST", headers: headers, body: body, timeout: http.number("timeout", 60000), responseLimit: nil) { result in
|
|
221
|
+
self.queue.async {
|
|
222
|
+
guard captured == self.generation else { return }
|
|
223
|
+
guard UploadBackoff.authKey(http) == UploadBackoff.authKey(self.config.value.object("http")) else { completion(.changed); return }
|
|
224
|
+
let response = try? result.get()
|
|
225
|
+
let json = response.flatMap { try? jsonObject($0.body) }
|
|
226
|
+
var event: TrackingJSON = ["success": false, "status": response?.status ?? 0]
|
|
227
|
+
if let json { event["response"] = json }
|
|
228
|
+
do {
|
|
229
|
+
guard let response, let json else { throw TrackingError("HTTP_ERROR", "Token refresh failed") }
|
|
230
|
+
let access = json.text("accessToken", json.text("access_token"))
|
|
231
|
+
guard response.success, !access.isEmpty else { throw TrackingError("HTTP_ERROR", "Token refresh failed") }
|
|
232
|
+
var next = auth
|
|
233
|
+
next["accessToken"] = access
|
|
234
|
+
next["refreshToken"] = json.text("refreshToken", json.text("refresh_token", auth.text("refreshToken")))
|
|
235
|
+
var expires = json.number("expires", json.number("expires_at", -1))
|
|
236
|
+
if expires > 0 && expires < 1e12 { expires *= 1000 }
|
|
237
|
+
if expires <= 0 && json.number("expires_in") > 0 { expires = Date().timeIntervalSince1970 * 1000 + json.number("expires_in") * 1000 }
|
|
238
|
+
next["expires"] = expires
|
|
239
|
+
try self.config.apply(["http": ["authorization": next]], reset: false)
|
|
240
|
+
if expires > 0 && expires - Date().timeIntervalSince1970 * 1000 <= 60000 { self.shortLivedToken = access }
|
|
241
|
+
event["success"] = true
|
|
242
|
+
self.emit("authorization", event)
|
|
243
|
+
completion(.renewed(self.config.value.object("http")))
|
|
244
|
+
} catch {
|
|
245
|
+
event["error"] = "Token refresh failed"
|
|
246
|
+
self.emit("authorization", event)
|
|
247
|
+
completion(.failed(error))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
emit("authorization", ["success": false, "status": 0, "error": "Token refresh failed"])
|
|
253
|
+
completion(.failed(error))
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
private func credentialsChanged() {
|
|
257
|
+
retryTimer?.cancel()
|
|
258
|
+
let current = config.value.object("http")
|
|
259
|
+
let hasReplacement = current["authorization"] is TrackingJSON || current.object("headers").keys.contains { $0.lowercased() == "authorization" }
|
|
260
|
+
if hasReplacement {
|
|
261
|
+
changedUntil = Date().addingTimeInterval(10)
|
|
262
|
+
schedule(changedUntil!.timeIntervalSince1970 * 1000)
|
|
263
|
+
}
|
|
264
|
+
pendingAutomatic = false
|
|
265
|
+
finish(.failure(TrackingError("HTTP_ERROR", "Authorization changed; records retained")))
|
|
266
|
+
}
|
|
267
|
+
private func reject(_ records: [TrackingJSON], error: Error, http: TrackingJSON, status: Int, retryAfter: Double?) {
|
|
268
|
+
let usesJWT = !http.object("headers").keys.contains { $0.lowercased() == "authorization" } && !http.object("authorization").isEmpty
|
|
269
|
+
if status != 0 && !(status == 401 && usesJWT) && (!priorityOnly || passFailure != nil) {
|
|
270
|
+
if passFailure == nil { passFailure = (error, http, status, retryAfter) }
|
|
271
|
+
if records.allSatisfy({ priority.contains($0.text("event")) }) { rejectedPriority.formUnion(records.map { $0.text("uuid") }) }
|
|
272
|
+
priorityOnly = true
|
|
273
|
+
next()
|
|
274
|
+
} else { fail(error, http: http, status: status, retryAfter: retryAfter, authorizationFailure: status == 401 && usesJWT) }
|
|
275
|
+
}
|
|
276
|
+
private func fail(_ error: Error, http: TrackingJSON, status: Int, retryAfter: Double?, authorizationFailure: Bool? = nil) {
|
|
277
|
+
let authFailure = authorizationFailure ?? (status == 401)
|
|
278
|
+
failures += 1
|
|
279
|
+
let delay = authFailure ? 900000 : UploadBackoff.serverWait(failures, random: Double.random(in: 0..<1), retryAfter: retryAfter, minimum: tracking ? http.number("syncInterval") * 1000 : 0)
|
|
280
|
+
let at = Date().timeIntervalSince1970 * 1000 + delay
|
|
281
|
+
do { try store.save("uploadWait", ["at": at, "reason": authFailure ? "authorization" : "server", "failures": failures, "authKey": UploadBackoff.authKey(http)]) }
|
|
282
|
+
catch { finish(.failure(error)); return }
|
|
283
|
+
schedule(at)
|
|
284
|
+
finish(.failure(error))
|
|
285
|
+
}
|
|
286
|
+
private func schedule(_ at: Double) {
|
|
287
|
+
retryTimer?.cancel()
|
|
288
|
+
let item = DispatchWorkItem { [weak self] in self?.automatic() }
|
|
289
|
+
retryTimer = item
|
|
290
|
+
queue.asyncAfter(deadline: .now() + max(0.1, at / 1000 - Date().timeIntervalSince1970), execute: item)
|
|
291
|
+
}
|
|
292
|
+
private func finish(_ result: Result<[TrackingJSON], Error>) {
|
|
293
|
+
busy = false
|
|
294
|
+
let waiting = callbacks; callbacks = []
|
|
295
|
+
waiting.forEach { $0(result) }
|
|
296
|
+
if pendingRecovery {
|
|
297
|
+
if let wait = try? store.load("uploadWait"), wait.text("reason") == "server" { try? store.save("uploadWait", [:]); failures = 0 }
|
|
298
|
+
pendingRecovery = false
|
|
299
|
+
}
|
|
300
|
+
let requested = pendingAutomatic
|
|
301
|
+
pendingAutomatic = false
|
|
302
|
+
let later = followUp; followUp = []
|
|
303
|
+
if requested {
|
|
304
|
+
let completion: ((Result<[TrackingJSON], Error>) -> Void)? = later.isEmpty ? nil : { value in later.forEach { $0(value) } }
|
|
305
|
+
queue.async { [weak self] in
|
|
306
|
+
if let self { self.automatic(completion: completion) } else { completion?(.failure(TrackingError("NETWORK_ERROR", "Syncer is closed"))) }
|
|
307
|
+
}
|
|
308
|
+
} else {
|
|
309
|
+
later.forEach { $0(result) }
|
|
310
|
+
if case .success = result { queue.async { [weak self] in self?.automatic() } }
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CryptoKit
|
|
3
|
+
|
|
4
|
+
struct UploadBackoff {
|
|
5
|
+
static func jittered(_ base: Double, random: Double) -> Double { floor(base * (0.8 + 0.4 * random)) }
|
|
6
|
+
static func retryDelay(_ index: Int, random: Double) -> Double { jittered([2000, 4000, 8000][index], random: random) }
|
|
7
|
+
static func serverWait(_ failures: Int, random: Double, retryAfter: Double?, minimum: Double) -> Double {
|
|
8
|
+
max(jittered(min(30000 * pow(2, Double(max(0, min(20, failures - 1)))), 900000), random: random), retryAfter ?? 0, minimum)
|
|
9
|
+
}
|
|
10
|
+
static func retryAfter(_ text: String?, maximum: Double, now: Date = Date()) -> Double? {
|
|
11
|
+
guard let text else { return nil }
|
|
12
|
+
let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
13
|
+
if let seconds = Double(cleaned), seconds >= 0 { return min(seconds, maximum) * 1000 }
|
|
14
|
+
let format = DateFormatter()
|
|
15
|
+
format.locale = Locale(identifier: "en_US_POSIX")
|
|
16
|
+
format.timeZone = TimeZone(secondsFromGMT: 0)
|
|
17
|
+
format.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
|
|
18
|
+
guard let date = format.date(from: cleaned) else { return nil }
|
|
19
|
+
return min(max(0, date.timeIntervalSince(now)), maximum) * 1000
|
|
20
|
+
}
|
|
21
|
+
static func authKey(_ http: TrackingJSON) -> String {
|
|
22
|
+
if http.object("headers").keys.contains(where: { $0.lowercased() == "authorization" }) { return "explicit-header" }
|
|
23
|
+
let auth = http.object("authorization")
|
|
24
|
+
return SHA256.hash(data: Data((auth.text("accessToken") + "\0" + auth.text("refreshToken")).utf8)).prefix(12).map { String(format: "%02x", $0) }.joined()
|
|
25
|
+
}
|
|
26
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import CoreLocation
|
|
3
|
+
import CoreMotion
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
struct RouteProfile: Equatable {
|
|
7
|
+
var enabled: Bool
|
|
8
|
+
var moving: Bool
|
|
9
|
+
var accuracy: String
|
|
10
|
+
var activityEnabled: Bool
|
|
11
|
+
var sessionID = ""
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
final class RouteSource: NSObject, CLLocationManagerDelegate {
|
|
15
|
+
private let manager: CLLocationManager
|
|
16
|
+
private let relaunch: CLLocationManager
|
|
17
|
+
private let motion = CMMotionActivityManager()
|
|
18
|
+
private var profile = RouteProfile(enabled: false, moving: false, accuracy: "high", activityEnabled: false)
|
|
19
|
+
private var activityRunning = false
|
|
20
|
+
private var cachedLocation: TrackedLocation?
|
|
21
|
+
private let clock: () -> TrackingInstant
|
|
22
|
+
private var timebase: Double?
|
|
23
|
+
var locations: (([TrackedLocation], String) -> Void)?
|
|
24
|
+
var error: ((TrackingError, String) -> Void)?
|
|
25
|
+
var activity: ((ActivitySample, String) -> Void)?
|
|
26
|
+
var authorizationChanged: (() -> Void)?
|
|
27
|
+
var lastLocation: TrackedLocation? {
|
|
28
|
+
precondition(Thread.isMainThread)
|
|
29
|
+
return cachedLocation
|
|
30
|
+
}
|
|
31
|
+
init(manager: CLLocationManager = CLLocationManager(), relaunch: CLLocationManager = CLLocationManager(), clock: @escaping () -> TrackingInstant = TrackingClock.now) {
|
|
32
|
+
self.manager = manager; self.relaunch = relaunch; self.clock = clock
|
|
33
|
+
super.init()
|
|
34
|
+
manager.delegate = self; relaunch.delegate = self
|
|
35
|
+
manager.distanceFilter = kCLDistanceFilterNone
|
|
36
|
+
manager.pausesLocationUpdatesAutomatically = false
|
|
37
|
+
manager.showsBackgroundLocationIndicator = true
|
|
38
|
+
}
|
|
39
|
+
func apply(_ new: RouteProfile, force: Bool = false) {
|
|
40
|
+
precondition(Thread.isMainThread)
|
|
41
|
+
guard new != profile || force else { return }
|
|
42
|
+
if new.sessionID != profile.sessionID { stopMotion() }
|
|
43
|
+
profile = new
|
|
44
|
+
guard new.enabled else { manager.stopUpdatingLocation(); relaunch.stopMonitoringSignificantLocationChanges(); stopMotion(); return }
|
|
45
|
+
guard manager.authorizationStatus == .authorizedAlways || manager.authorizationStatus == .authorizedWhenInUse else {
|
|
46
|
+
manager.stopUpdatingLocation(); relaunch.stopMonitoringSignificantLocationChanges(); stopMotion(); return
|
|
47
|
+
}
|
|
48
|
+
let modes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String] ?? []
|
|
49
|
+
manager.allowsBackgroundLocationUpdates = modes.contains("location")
|
|
50
|
+
manager.desiredAccuracy = new.moving ? Self.accuracy(new.accuracy) : kCLLocationAccuracyHundredMeters
|
|
51
|
+
if force { manager.stopUpdatingLocation() }
|
|
52
|
+
manager.startUpdatingLocation()
|
|
53
|
+
if manager.authorizationStatus == .authorizedAlways && CLLocationManager.significantLocationChangeMonitoringAvailable() { relaunch.startMonitoringSignificantLocationChanges() }
|
|
54
|
+
else { relaunch.stopMonitoringSignificantLocationChanges() }
|
|
55
|
+
if new.activityEnabled && CMMotionActivityManager.isActivityAvailable() && CMMotionActivityManager.authorizationStatus() == .authorized {
|
|
56
|
+
if !activityRunning {
|
|
57
|
+
activityRunning = true
|
|
58
|
+
let session = new.sessionID
|
|
59
|
+
motion.startActivityUpdates(to: .main) { [weak self] sample in
|
|
60
|
+
guard let self, self.profile.enabled, self.profile.activityEnabled, self.activityRunning, self.profile.sessionID == session, let sample else { return }
|
|
61
|
+
let confidence: Int
|
|
62
|
+
switch sample.confidence { case .low: confidence = 33; case .medium: confidence = 66; case .high: confidence = 100; @unknown default: confidence = 0 }
|
|
63
|
+
self.activity?(ActivitySample.map(automotive: sample.automotive, cycling: sample.cycling, running: sample.running, walking: sample.walking, stationary: sample.stationary, confidence: confidence), session)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
} else { stopMotion() }
|
|
67
|
+
}
|
|
68
|
+
private func stopMotion() { if activityRunning { activityRunning = false; motion.stopActivityUpdates() } }
|
|
69
|
+
static func accuracy(_ desired: String) -> CLLocationAccuracy {
|
|
70
|
+
switch desired { case "balanced": return kCLLocationAccuracyHundredMeters; case "low": return kCLLocationAccuracyKilometer; case "passive": return kCLLocationAccuracyThreeKilometers; default: return kCLLocationAccuracyBest }
|
|
71
|
+
}
|
|
72
|
+
func locationManager(_ source: CLLocationManager, didUpdateLocations fixes: [CLLocation]) {
|
|
73
|
+
guard source === manager else { return }
|
|
74
|
+
let valid = ordered(fixes.filter { $0.horizontalAccuracy >= 0 && CLLocationCoordinate2DIsValid($0.coordinate) })
|
|
75
|
+
if let newest = valid.reversed().max(by: { $0.isOlder(than: $1) }), cachedLocation == nil || !newest.isOlder(than: cachedLocation!) { cachedLocation = newest }
|
|
76
|
+
guard profile.enabled else { return }
|
|
77
|
+
locations?(valid, profile.sessionID)
|
|
78
|
+
}
|
|
79
|
+
private func ordered(_ fixes: [CLLocation]) -> [TrackedLocation] {
|
|
80
|
+
let now = clock()
|
|
81
|
+
let offset = now.wall - now.elapsed
|
|
82
|
+
let previous = timebase
|
|
83
|
+
timebase = offset
|
|
84
|
+
var mapped: [TrackedLocation] = []
|
|
85
|
+
for fix in fixes {
|
|
86
|
+
var location = TrackedLocation(fix)
|
|
87
|
+
var elapsed = location.time - offset
|
|
88
|
+
if elapsed > now.elapsed + 1000 {
|
|
89
|
+
guard let previous, abs(previous - offset) >= 2000, location.time - previous <= now.elapsed + 1000 else { continue }
|
|
90
|
+
elapsed = location.time - previous
|
|
91
|
+
}
|
|
92
|
+
location.elapsed = min(elapsed, now.elapsed) > 0 ? min(elapsed, now.elapsed) : nil
|
|
93
|
+
mapped.append(location)
|
|
94
|
+
}
|
|
95
|
+
for index in mapped.indices.reversed().dropFirst() {
|
|
96
|
+
if let current = mapped[index].elapsed, let next = mapped[index + 1].elapsed, current > next { mapped[index].elapsed = next }
|
|
97
|
+
}
|
|
98
|
+
return mapped
|
|
99
|
+
}
|
|
100
|
+
func locationManager(_ source: CLLocationManager, didFailWithError failure: Error) {
|
|
101
|
+
guard source === manager, profile.enabled, (failure as? CLError)?.code != .locationUnknown else { return }
|
|
102
|
+
let session = profile.sessionID
|
|
103
|
+
LocationServices.shared.refresh { [weak self] in
|
|
104
|
+
guard let self, self.profile.enabled, self.profile.sessionID == session else { return }
|
|
105
|
+
self.authorizationChanged?()
|
|
106
|
+
if LocationServices.shared.enabled { self.error?(TrackingError((failure as? CLError)?.code == .denied ? "PERMISSION_DENIED" : "UNAVAILABLE", "Core Location route updates failed"), session) }
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) { authorizationChanged?() }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
extension TrackedLocation {
|
|
113
|
+
init(_ fix: CLLocation) {
|
|
114
|
+
self.init(latitude: fix.coordinate.latitude, longitude: fix.coordinate.longitude, accuracy: fix.horizontalAccuracy, altitude: fix.verticalAccuracy >= 0 ? fix.altitude : nil, altitudeAccuracy: fix.verticalAccuracy >= 0 ? fix.verticalAccuracy : nil, speed: fix.speed >= 0 ? fix.speed : nil, speedAccuracy: fix.speedAccuracy >= 0 ? fix.speedAccuracy : nil, heading: fix.course >= 0 ? fix.course : nil, headingAccuracy: fix.courseAccuracy >= 0 ? fix.courseAccuracy : nil, time: fix.timestamp.timeIntervalSince1970 * 1000, isMock: fix.sourceInformation?.isSimulatedBySoftware ?? false)
|
|
115
|
+
}
|
|
116
|
+
init(_ fix: CLLocation, receivedAt now: TrackingInstant) {
|
|
117
|
+
self.init(fix)
|
|
118
|
+
let mapped = now.elapsed - (now.wall - time)
|
|
119
|
+
elapsed = time <= now.wall + 1000 && min(mapped, now.elapsed) > 0 ? min(mapped, now.elapsed) : nil
|
|
120
|
+
}
|
|
121
|
+
var location: CLLocation {
|
|
122
|
+
CLLocation(coordinate: CLLocationCoordinate2D(latitude: latitude, longitude: longitude), altitude: altitude ?? 0, horizontalAccuracy: accuracy, verticalAccuracy: altitudeAccuracy ?? -1, course: heading ?? -1, courseAccuracy: headingAccuracy ?? -1, speed: speed ?? -1, speedAccuracy: speedAccuracy ?? -1, timestamp: Date(timeIntervalSince1970: time / 1000))
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import Capacitor
|
|
3
|
+
|
|
4
|
+
@objc(LocationTrackingPlugin)
|
|
5
|
+
public final class LocationTrackingPlugin: CAPPlugin, CAPBridgedPlugin {
|
|
6
|
+
public let identifier = "LocationTrackingPlugin"
|
|
7
|
+
public let jsName = "LocationTracking"
|
|
8
|
+
public let pluginMethods: [CAPPluginMethod] = [
|
|
9
|
+
CAPPluginMethod(name: "ready", returnType: CAPPluginReturnPromise),
|
|
10
|
+
CAPPluginMethod(name: "setConfig", returnType: CAPPluginReturnPromise),
|
|
11
|
+
CAPPluginMethod(name: "reset", returnType: CAPPluginReturnPromise),
|
|
12
|
+
CAPPluginMethod(name: "getState", returnType: CAPPluginReturnPromise),
|
|
13
|
+
CAPPluginMethod(name: "start", returnType: CAPPluginReturnPromise),
|
|
14
|
+
CAPPluginMethod(name: "startGeofences", returnType: CAPPluginReturnPromise),
|
|
15
|
+
CAPPluginMethod(name: "stop", returnType: CAPPluginReturnPromise),
|
|
16
|
+
CAPPluginMethod(name: "changePace", returnType: CAPPluginReturnPromise),
|
|
17
|
+
CAPPluginMethod(name: "getCurrentPosition", returnType: CAPPluginReturnPromise),
|
|
18
|
+
CAPPluginMethod(name: "clearWatch", returnType: CAPPluginReturnPromise),
|
|
19
|
+
CAPPluginMethod(name: "getOdometer", returnType: CAPPluginReturnPromise),
|
|
20
|
+
CAPPluginMethod(name: "setOdometer", returnType: CAPPluginReturnPromise),
|
|
21
|
+
CAPPluginMethod(name: "resetOdometer", returnType: CAPPluginReturnPromise),
|
|
22
|
+
CAPPluginMethod(name: "getLocations", returnType: CAPPluginReturnPromise),
|
|
23
|
+
CAPPluginMethod(name: "getCount", returnType: CAPPluginReturnPromise),
|
|
24
|
+
CAPPluginMethod(name: "insertLocation", returnType: CAPPluginReturnPromise),
|
|
25
|
+
CAPPluginMethod(name: "destroyLocations", returnType: CAPPluginReturnPromise),
|
|
26
|
+
CAPPluginMethod(name: "destroyLocation", returnType: CAPPluginReturnPromise),
|
|
27
|
+
CAPPluginMethod(name: "sync", returnType: CAPPluginReturnPromise),
|
|
28
|
+
CAPPluginMethod(name: "addGeofence", returnType: CAPPluginReturnPromise),
|
|
29
|
+
CAPPluginMethod(name: "addGeofences", returnType: CAPPluginReturnPromise),
|
|
30
|
+
CAPPluginMethod(name: "removeGeofence", returnType: CAPPluginReturnPromise),
|
|
31
|
+
CAPPluginMethod(name: "removeGeofences", returnType: CAPPluginReturnPromise),
|
|
32
|
+
CAPPluginMethod(name: "getGeofences", returnType: CAPPluginReturnPromise),
|
|
33
|
+
CAPPluginMethod(name: "getGeofence", returnType: CAPPluginReturnPromise),
|
|
34
|
+
CAPPluginMethod(name: "geofenceExists", returnType: CAPPluginReturnPromise),
|
|
35
|
+
CAPPluginMethod(name: "getHeartbeatStatus", returnType: CAPPluginReturnPromise),
|
|
36
|
+
CAPPluginMethod(name: "getProviderState", returnType: CAPPluginReturnPromise),
|
|
37
|
+
CAPPluginMethod(name: "isPowerSaveMode", returnType: CAPPluginReturnPromise),
|
|
38
|
+
CAPPluginMethod(name: "getBatteryOptimizationStatus", returnType: CAPPluginReturnPromise),
|
|
39
|
+
CAPPluginMethod(name: "openBatteryOptimizationSettings", returnType: CAPPluginReturnPromise),
|
|
40
|
+
CAPPluginMethod(name: "getPowerManagerInfo", returnType: CAPPluginReturnPromise),
|
|
41
|
+
CAPPluginMethod(name: "openPowerManagerSettings", returnType: CAPPluginReturnPromise),
|
|
42
|
+
CAPPluginMethod(name: "openLocationSettings", returnType: CAPPluginReturnPromise),
|
|
43
|
+
CAPPluginMethod(name: "openAppSettings", returnType: CAPPluginReturnPromise),
|
|
44
|
+
CAPPluginMethod(name: "getDeviceInfo", returnType: CAPPluginReturnPromise),
|
|
45
|
+
CAPPluginMethod(name: "getSensors", returnType: CAPPluginReturnPromise),
|
|
46
|
+
CAPPluginMethod(name: "checkPermissions", returnType: CAPPluginReturnPromise),
|
|
47
|
+
CAPPluginMethod(name: "requestPermissions", returnType: CAPPluginReturnPromise),
|
|
48
|
+
CAPPluginMethod(name: "log", returnType: CAPPluginReturnPromise),
|
|
49
|
+
CAPPluginMethod(name: "getLog", returnType: CAPPluginReturnPromise),
|
|
50
|
+
CAPPluginMethod(name: "destroyLog", returnType: CAPPluginReturnPromise),
|
|
51
|
+
CAPPluginMethod(name: "uploadLog", returnType: CAPPluginReturnPromise),
|
|
52
|
+
CAPPluginMethod(name: "emailLog", returnType: CAPPluginReturnPromise),
|
|
53
|
+
CAPPluginMethod(name: "watchPosition", returnType: CAPPluginReturnCallback),
|
|
54
|
+
]
|
|
55
|
+
private var subscription: NativeSubscription?
|
|
56
|
+
private var watches: [String: CAPPluginCall] = [:]
|
|
57
|
+
public override func load() {
|
|
58
|
+
subscription = LocationTrackingNative.addListener(PluginEventForwarder(self))
|
|
59
|
+
LocationTrackingNative.bootstrap(registerBackgroundTasks: false)
|
|
60
|
+
}
|
|
61
|
+
deinit {
|
|
62
|
+
subscription?.remove()
|
|
63
|
+
for id in watches.keys { LocationTrackingNative.call("clearWatch", options: ["id": id]) { _ in } }
|
|
64
|
+
}
|
|
65
|
+
public func onEvent(_ name: String, payload: TrackingJSON) {
|
|
66
|
+
DispatchQueue.main.async { [weak self] in self?.notifyListeners(name, data: payload) }
|
|
67
|
+
}
|
|
68
|
+
private func invoke(_ method: String, _ call: CAPPluginCall) {
|
|
69
|
+
LocationTrackingNative.perform(method, options: call.options as? TrackingJSON ?? [:], native: false) { result in
|
|
70
|
+
DispatchQueue.main.async {
|
|
71
|
+
switch result {
|
|
72
|
+
case .success(let value): call.resolve(value)
|
|
73
|
+
case .failure(let error): call.reject(error.localizedDescription, (error as? TrackingError)?.code ?? "INTERNAL")
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
@objc public func watchPosition(_ call: CAPPluginCall) {
|
|
79
|
+
call.keepAlive = true
|
|
80
|
+
DispatchQueue.main.async { self.beginWatch(call) }
|
|
81
|
+
}
|
|
82
|
+
private func beginWatch(_ call: CAPPluginCall) {
|
|
83
|
+
guard let id = call.callbackId else { call.reject("Missing callback id", "INTERNAL"); return }
|
|
84
|
+
watches[id] = call
|
|
85
|
+
var options: TrackingJSON = call.options as? TrackingJSON ?? [:]
|
|
86
|
+
options["_watchId"] = id
|
|
87
|
+
LocationTrackingNative.perform("watchPosition", options: options, native: false) { [weak self] result in
|
|
88
|
+
DispatchQueue.main.async {
|
|
89
|
+
guard let self, self.watches[id] != nil else { return }
|
|
90
|
+
switch result {
|
|
91
|
+
case .success(let wire): call.resolve(wire)
|
|
92
|
+
case .failure(let error): call.reject(error.localizedDescription, (error as? TrackingError)?.code ?? "INTERNAL")
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
@objc public func ready(_ call: CAPPluginCall) { invoke("ready", call) }
|
|
98
|
+
@objc public func setConfig(_ call: CAPPluginCall) { invoke("setConfig", call) }
|
|
99
|
+
@objc public func reset(_ call: CAPPluginCall) { invoke("reset", call) }
|
|
100
|
+
@objc public func getState(_ call: CAPPluginCall) { invoke("getState", call) }
|
|
101
|
+
@objc public func start(_ call: CAPPluginCall) { invoke("start", call) }
|
|
102
|
+
@objc public func startGeofences(_ call: CAPPluginCall) { invoke("startGeofences", call) }
|
|
103
|
+
@objc public func stop(_ call: CAPPluginCall) { invoke("stop", call) }
|
|
104
|
+
@objc public func changePace(_ call: CAPPluginCall) { invoke("changePace", call) }
|
|
105
|
+
@objc public func getCurrentPosition(_ call: CAPPluginCall) { invoke("getCurrentPosition", call) }
|
|
106
|
+
@objc public func clearWatch(_ call: CAPPluginCall) {
|
|
107
|
+
DispatchQueue.main.async {
|
|
108
|
+
if let id = call.getString("id"), let watch = self.watches.removeValue(forKey: id) {
|
|
109
|
+
watch.keepAlive = false
|
|
110
|
+
self.bridge?.releaseCall(watch)
|
|
111
|
+
}
|
|
112
|
+
self.invoke("clearWatch", call)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
@objc public func getOdometer(_ call: CAPPluginCall) { invoke("getOdometer", call) }
|
|
116
|
+
@objc public func setOdometer(_ call: CAPPluginCall) { invoke("setOdometer", call) }
|
|
117
|
+
@objc public func resetOdometer(_ call: CAPPluginCall) { invoke("resetOdometer", call) }
|
|
118
|
+
@objc public func getLocations(_ call: CAPPluginCall) { invoke("getLocations", call) }
|
|
119
|
+
@objc public func getCount(_ call: CAPPluginCall) { invoke("getCount", call) }
|
|
120
|
+
@objc public func insertLocation(_ call: CAPPluginCall) { invoke("insertLocation", call) }
|
|
121
|
+
@objc public func destroyLocations(_ call: CAPPluginCall) { invoke("destroyLocations", call) }
|
|
122
|
+
@objc public func destroyLocation(_ call: CAPPluginCall) { invoke("destroyLocation", call) }
|
|
123
|
+
@objc public func sync(_ call: CAPPluginCall) { invoke("sync", call) }
|
|
124
|
+
@objc public func addGeofence(_ call: CAPPluginCall) { invoke("addGeofence", call) }
|
|
125
|
+
@objc public func addGeofences(_ call: CAPPluginCall) { invoke("addGeofences", call) }
|
|
126
|
+
@objc public func removeGeofence(_ call: CAPPluginCall) { invoke("removeGeofence", call) }
|
|
127
|
+
@objc public func removeGeofences(_ call: CAPPluginCall) { invoke("removeGeofences", call) }
|
|
128
|
+
@objc public func getGeofences(_ call: CAPPluginCall) { invoke("getGeofences", call) }
|
|
129
|
+
@objc public func getGeofence(_ call: CAPPluginCall) { invoke("getGeofence", call) }
|
|
130
|
+
@objc public func geofenceExists(_ call: CAPPluginCall) { invoke("geofenceExists", call) }
|
|
131
|
+
@objc public func getHeartbeatStatus(_ call: CAPPluginCall) { invoke("getHeartbeatStatus", call) }
|
|
132
|
+
@objc public func getProviderState(_ call: CAPPluginCall) { invoke("getProviderState", call) }
|
|
133
|
+
@objc public func isPowerSaveMode(_ call: CAPPluginCall) { invoke("isPowerSaveMode", call) }
|
|
134
|
+
@objc public func getBatteryOptimizationStatus(_ call: CAPPluginCall) { invoke("getBatteryOptimizationStatus", call) }
|
|
135
|
+
@objc public func openBatteryOptimizationSettings(_ call: CAPPluginCall) { invoke("openBatteryOptimizationSettings", call) }
|
|
136
|
+
@objc public func getPowerManagerInfo(_ call: CAPPluginCall) { invoke("getPowerManagerInfo", call) }
|
|
137
|
+
@objc public func openPowerManagerSettings(_ call: CAPPluginCall) { invoke("openPowerManagerSettings", call) }
|
|
138
|
+
@objc public func openLocationSettings(_ call: CAPPluginCall) { invoke("openLocationSettings", call) }
|
|
139
|
+
@objc public func openAppSettings(_ call: CAPPluginCall) { invoke("openAppSettings", call) }
|
|
140
|
+
@objc public func getDeviceInfo(_ call: CAPPluginCall) { invoke("getDeviceInfo", call) }
|
|
141
|
+
@objc public func getSensors(_ call: CAPPluginCall) { invoke("getSensors", call) }
|
|
142
|
+
@objc public override func checkPermissions(_ call: CAPPluginCall) { invoke("checkPermissions", call) }
|
|
143
|
+
@objc public override func requestPermissions(_ call: CAPPluginCall) { invoke("requestPermissions", call) }
|
|
144
|
+
@objc public func log(_ call: CAPPluginCall) { invoke("log", call) }
|
|
145
|
+
@objc public func getLog(_ call: CAPPluginCall) { invoke("getLog", call) }
|
|
146
|
+
@objc public func destroyLog(_ call: CAPPluginCall) { invoke("destroyLog", call) }
|
|
147
|
+
@objc public func uploadLog(_ call: CAPPluginCall) { invoke("uploadLog", call) }
|
|
148
|
+
@objc public func emailLog(_ call: CAPPluginCall) { invoke("emailLog", call) }
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private final class PluginEventForwarder: LocationTrackingListener {
|
|
152
|
+
weak var plugin: LocationTrackingPlugin?
|
|
153
|
+
init(_ plugin: LocationTrackingPlugin) { self.plugin = plugin }
|
|
154
|
+
func onEvent(_ name: String, payload: TrackingJSON) { plugin?.onEvent(name, payload: payload) }
|
|
155
|
+
}
|