@bglocation/capacitor 1.1.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/CapacitorBackgroundLocation.podspec +19 -0
- package/LICENSE.md +97 -0
- package/Package.swift +44 -0
- package/README.md +264 -0
- package/android/build.gradle +74 -0
- package/android/src/main/AndroidManifest.xml +37 -0
- package/android/src/main/kotlin/dev/bglocation/BackgroundLocationPlugin.kt +684 -0
- package/android/src/main/kotlin/dev/bglocation/core/Models.kt +76 -0
- package/android/src/main/kotlin/dev/bglocation/core/battery/BGLBatteryHelper.kt +127 -0
- package/android/src/main/kotlin/dev/bglocation/core/boot/BGLBootCompletedReceiver.kt +32 -0
- package/android/src/main/kotlin/dev/bglocation/core/config/BGLConfigParser.kt +114 -0
- package/android/src/main/kotlin/dev/bglocation/core/config/BGLVersion.kt +6 -0
- package/android/src/main/kotlin/dev/bglocation/core/debug/BGLDebugLogger.kt +174 -0
- package/android/src/main/kotlin/dev/bglocation/core/geofence/BGLGeofenceBroadcastReceiver.kt +93 -0
- package/android/src/main/kotlin/dev/bglocation/core/geofence/BGLGeofenceManager.kt +310 -0
- package/android/src/main/kotlin/dev/bglocation/core/http/BGLHttpSender.kt +187 -0
- package/android/src/main/kotlin/dev/bglocation/core/http/BGLLocationBuffer.kt +152 -0
- package/android/src/main/kotlin/dev/bglocation/core/license/BGLBuildConfig.kt +16 -0
- package/android/src/main/kotlin/dev/bglocation/core/license/BGLLicenseEnforcer.kt +137 -0
- package/android/src/main/kotlin/dev/bglocation/core/license/BGLLicenseValidator.kt +134 -0
- package/android/src/main/kotlin/dev/bglocation/core/license/BGLTrialTimer.kt +176 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLAdaptiveFilter.kt +94 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLHeartbeatTimer.kt +38 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLLocationForegroundService.kt +289 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLLocationHelpers.kt +72 -0
- package/android/src/main/kotlin/dev/bglocation/core/location/BGLPermissionManager.kt +99 -0
- package/android/src/main/kotlin/dev/bglocation/core/notification/BGLNotificationHelper.kt +77 -0
- package/dist/esm/definitions.d.ts +390 -0
- package/dist/esm/definitions.js +3 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +26 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +47 -0
- package/dist/esm/web.js +231 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/esm/web.test.d.ts +1 -0
- package/dist/esm/web.test.js +940 -0
- package/dist/esm/web.test.js.map +1 -0
- package/dist/plugin.cjs.js +267 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +270 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/BGLocationCore/Config/BGLConfigParser.swift +88 -0
- package/ios/Sources/BGLocationCore/Config/BGLVersion.swift +6 -0
- package/ios/Sources/BGLocationCore/Debug/BGLDebugLogger.swift +201 -0
- package/ios/Sources/BGLocationCore/Geofence/BGLGeofenceManager.swift +538 -0
- package/ios/Sources/BGLocationCore/Http/BGLHttpSender.swift +227 -0
- package/ios/Sources/BGLocationCore/Http/BGLLocationBuffer.swift +198 -0
- package/ios/Sources/BGLocationCore/License/BGLBuildConfig.swift +11 -0
- package/ios/Sources/BGLocationCore/License/BGLLicenseEnforcer.swift +134 -0
- package/ios/Sources/BGLocationCore/License/BGLLicenseValidator.swift +163 -0
- package/ios/Sources/BGLocationCore/License/BGLTrialTimer.swift +168 -0
- package/ios/Sources/BGLocationCore/Location/BGLAdaptiveFilter.swift +91 -0
- package/ios/Sources/BGLocationCore/Location/BGLHeartbeatTimer.swift +50 -0
- package/ios/Sources/BGLocationCore/Location/BGLLocationData.swift +48 -0
- package/ios/Sources/BGLocationCore/Location/BGLLocationHelpers.swift +42 -0
- package/ios/Sources/BGLocationCore/Location/BGLLocationManager.swift +268 -0
- package/ios/Sources/BGLocationCore/Location/BGLPermissionManager.swift +33 -0
- package/ios/Sources/BackgroundLocationPlugin/BackgroundLocationPlugin.swift +657 -0
- package/package.json +75 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public struct BGLHttpResult {
|
|
4
|
+
public let statusCode: Int
|
|
5
|
+
public let success: Bool
|
|
6
|
+
public let responseText: String
|
|
7
|
+
public let error: String?
|
|
8
|
+
public let bufferedCount: Int
|
|
9
|
+
|
|
10
|
+
public init(
|
|
11
|
+
statusCode: Int, success: Bool, responseText: String, error: String?, bufferedCount: Int = 0
|
|
12
|
+
) {
|
|
13
|
+
self.statusCode = statusCode
|
|
14
|
+
self.success = success
|
|
15
|
+
self.responseText = responseText
|
|
16
|
+
self.error = error
|
|
17
|
+
self.bufferedCount = bufferedCount
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public class BGLHttpSender {
|
|
22
|
+
|
|
23
|
+
private var url: URL?
|
|
24
|
+
private var headers: [String: String] = [:]
|
|
25
|
+
private let session: URLSession
|
|
26
|
+
private var buffer: BGLLocationBuffer?
|
|
27
|
+
private var isFlushing = false
|
|
28
|
+
private let flushLock = NSLock()
|
|
29
|
+
private static let flushBatchSize = 50
|
|
30
|
+
|
|
31
|
+
/// Called for each HTTP result during buffer flush. Set this to receive onHttp events for flushed items.
|
|
32
|
+
public var onFlushProgress: ((BGLHttpResult) -> Void)?
|
|
33
|
+
|
|
34
|
+
public init(session: URLSession = .shared) {
|
|
35
|
+
self.session = session
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public func configure(url: String?, headers: [String: String]) {
|
|
39
|
+
if let url = url {
|
|
40
|
+
self.url = URL(string: url)
|
|
41
|
+
} else {
|
|
42
|
+
self.url = nil
|
|
43
|
+
}
|
|
44
|
+
self.headers = headers
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public func setBuffer(_ locationBuffer: BGLLocationBuffer?) {
|
|
48
|
+
buffer = locationBuffer
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public var isConfigured: Bool {
|
|
52
|
+
url != nil
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
public func sendLocation(
|
|
56
|
+
_ location: BGLLocationData, completion: @escaping (BGLHttpResult) -> Void
|
|
57
|
+
) {
|
|
58
|
+
guard let url = url else { return }
|
|
59
|
+
|
|
60
|
+
let locationDict = location.toDict()
|
|
61
|
+
executePost(url: url, locationDict: locationDict) { [weak self] result in
|
|
62
|
+
guard let self = self else { return }
|
|
63
|
+
|
|
64
|
+
if !result.success, let buf = self.buffer {
|
|
65
|
+
buf.add(location)
|
|
66
|
+
let count = buf.count()
|
|
67
|
+
print("BGLocation: HTTP failed, location buffered (buffer=\(count))")
|
|
68
|
+
completion(
|
|
69
|
+
BGLHttpResult(
|
|
70
|
+
statusCode: result.statusCode,
|
|
71
|
+
success: false,
|
|
72
|
+
responseText: result.responseText,
|
|
73
|
+
error: result.error,
|
|
74
|
+
bufferedCount: count
|
|
75
|
+
))
|
|
76
|
+
} else {
|
|
77
|
+
let count = self.buffer?.count() ?? 0
|
|
78
|
+
completion(
|
|
79
|
+
BGLHttpResult(
|
|
80
|
+
statusCode: result.statusCode,
|
|
81
|
+
success: result.success,
|
|
82
|
+
responseText: result.responseText,
|
|
83
|
+
error: result.error,
|
|
84
|
+
bufferedCount: count
|
|
85
|
+
))
|
|
86
|
+
// After a successful send, try to flush buffered items
|
|
87
|
+
if result.success && count > 0 {
|
|
88
|
+
self.flushBuffer(onProgress: self.onFlushProgress)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/// Flush buffered locations to the server.
|
|
95
|
+
public func flushBuffer(onProgress: ((BGLHttpResult) -> Void)?) {
|
|
96
|
+
guard let url = url, let buf = buffer else { return }
|
|
97
|
+
flushLock.lock()
|
|
98
|
+
guard !isFlushing else {
|
|
99
|
+
flushLock.unlock()
|
|
100
|
+
return
|
|
101
|
+
}
|
|
102
|
+
isFlushing = true
|
|
103
|
+
flushLock.unlock()
|
|
104
|
+
|
|
105
|
+
DispatchQueue.global(qos: .utility).async { [weak self] in
|
|
106
|
+
guard let self = self else { return }
|
|
107
|
+
defer {
|
|
108
|
+
self.flushLock.lock()
|
|
109
|
+
self.isFlushing = false
|
|
110
|
+
self.flushLock.unlock()
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
while true {
|
|
114
|
+
let batch = buf.peek(limit: BGLHttpSender.flushBatchSize)
|
|
115
|
+
if batch.isEmpty { break }
|
|
116
|
+
|
|
117
|
+
var successIds: [Int64] = []
|
|
118
|
+
let group = DispatchGroup()
|
|
119
|
+
|
|
120
|
+
for (id, location) in batch {
|
|
121
|
+
var sendResult: BGLHttpResult?
|
|
122
|
+
group.enter()
|
|
123
|
+
self.executePost(url: url, locationDict: location.toDict()) { result in
|
|
124
|
+
sendResult = result
|
|
125
|
+
group.leave()
|
|
126
|
+
}
|
|
127
|
+
group.wait()
|
|
128
|
+
|
|
129
|
+
if let result = sendResult, result.success {
|
|
130
|
+
successIds.append(id)
|
|
131
|
+
} else {
|
|
132
|
+
// Stop flushing on first failure
|
|
133
|
+
if !successIds.isEmpty {
|
|
134
|
+
buf.remove(ids: successIds)
|
|
135
|
+
}
|
|
136
|
+
let count = buf.count()
|
|
137
|
+
if let progress = onProgress {
|
|
138
|
+
DispatchQueue.main.async {
|
|
139
|
+
progress(
|
|
140
|
+
BGLHttpResult(
|
|
141
|
+
statusCode: sendResult?.statusCode ?? 0,
|
|
142
|
+
success: false,
|
|
143
|
+
responseText: sendResult?.responseText ?? "",
|
|
144
|
+
error: sendResult?.error,
|
|
145
|
+
bufferedCount: count
|
|
146
|
+
))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if !successIds.isEmpty {
|
|
153
|
+
buf.remove(ids: successIds)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let count = buf.count()
|
|
158
|
+
if count == 0 {
|
|
159
|
+
print("BGLocation: Buffer flush complete — all locations sent")
|
|
160
|
+
}
|
|
161
|
+
if let progress = onProgress {
|
|
162
|
+
DispatchQueue.main.async {
|
|
163
|
+
progress(
|
|
164
|
+
BGLHttpResult(
|
|
165
|
+
statusCode: 200,
|
|
166
|
+
success: true,
|
|
167
|
+
responseText: "",
|
|
168
|
+
error: nil,
|
|
169
|
+
bufferedCount: count
|
|
170
|
+
))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// MARK: - Private
|
|
177
|
+
|
|
178
|
+
private func executePost(
|
|
179
|
+
url: URL, locationDict: [String: Any], completion: @escaping (BGLHttpResult) -> Void
|
|
180
|
+
) {
|
|
181
|
+
var request = URLRequest(url: url)
|
|
182
|
+
request.httpMethod = "POST"
|
|
183
|
+
request.setValue("application/json", forHTTPHeaderField: "Content-Type")
|
|
184
|
+
request.timeoutInterval = 10
|
|
185
|
+
|
|
186
|
+
for (key, value) in headers {
|
|
187
|
+
request.setValue(value, forHTTPHeaderField: key)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
let body: [String: Any] = ["location": locationDict]
|
|
191
|
+
|
|
192
|
+
do {
|
|
193
|
+
request.httpBody = try JSONSerialization.data(withJSONObject: body)
|
|
194
|
+
} catch {
|
|
195
|
+
completion(
|
|
196
|
+
BGLHttpResult(
|
|
197
|
+
statusCode: 0, success: false, responseText: "",
|
|
198
|
+
error: error.localizedDescription))
|
|
199
|
+
return
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
session.dataTask(with: request) { data, response, error in
|
|
203
|
+
if let error = error {
|
|
204
|
+
print("BGLocation: HTTP POST failed — \(error.localizedDescription)")
|
|
205
|
+
completion(
|
|
206
|
+
BGLHttpResult(
|
|
207
|
+
statusCode: 0, success: false, responseText: "",
|
|
208
|
+
error: error.localizedDescription))
|
|
209
|
+
return
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
let httpResponse = response as? HTTPURLResponse
|
|
213
|
+
let statusCode = httpResponse?.statusCode ?? 0
|
|
214
|
+
let responseText = data.flatMap { String(data: $0, encoding: .utf8) } ?? ""
|
|
215
|
+
|
|
216
|
+
print("BGLocation: HTTP POST → \(url) → \(statusCode)")
|
|
217
|
+
|
|
218
|
+
completion(
|
|
219
|
+
BGLHttpResult(
|
|
220
|
+
statusCode: statusCode,
|
|
221
|
+
success: (200...299).contains(statusCode),
|
|
222
|
+
responseText: responseText,
|
|
223
|
+
error: nil
|
|
224
|
+
))
|
|
225
|
+
}.resume()
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import SQLite3
|
|
3
|
+
|
|
4
|
+
/// SQLite-backed offline buffer for location data.
|
|
5
|
+
///
|
|
6
|
+
/// When HTTP upload fails, locations are stored locally and retried later.
|
|
7
|
+
/// The buffer enforces a configurable maximum size — oldest entries are dropped
|
|
8
|
+
/// when the limit is exceeded.
|
|
9
|
+
public class BGLLocationBuffer {
|
|
10
|
+
|
|
11
|
+
public static let dbName = "bgl_location_buffer.db"
|
|
12
|
+
|
|
13
|
+
private var db: OpaquePointer?
|
|
14
|
+
private var maxSize: Int = 1000
|
|
15
|
+
private let dbPath: String
|
|
16
|
+
|
|
17
|
+
/// Called when buffer overflow triggers auto-trim. Parameter is the number of dropped entries.
|
|
18
|
+
public var onOverflow: ((Int) -> Void)?
|
|
19
|
+
|
|
20
|
+
public init(dbPath: String? = nil, maxSize: Int = 1000) {
|
|
21
|
+
if let dbPath = dbPath {
|
|
22
|
+
self.dbPath = dbPath
|
|
23
|
+
} else {
|
|
24
|
+
let docsUrl = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)
|
|
25
|
+
.first!
|
|
26
|
+
self.dbPath = docsUrl.appendingPathComponent(BGLLocationBuffer.dbName).path
|
|
27
|
+
}
|
|
28
|
+
self.maxSize = maxSize
|
|
29
|
+
openDatabase()
|
|
30
|
+
createTable()
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
deinit {
|
|
34
|
+
close()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public func close() {
|
|
38
|
+
if let db = db {
|
|
39
|
+
sqlite3_close(db)
|
|
40
|
+
self.db = nil
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public func configure(maxSize: Int) {
|
|
45
|
+
self.maxSize = maxSize
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// MARK: - Public API
|
|
49
|
+
|
|
50
|
+
/// Store a failed location in the buffer.
|
|
51
|
+
public func add(_ location: BGLLocationData) {
|
|
52
|
+
guard let db = db else { return }
|
|
53
|
+
|
|
54
|
+
let sql = """
|
|
55
|
+
INSERT INTO buffered_locations
|
|
56
|
+
(latitude, longitude, accuracy, speed, heading, altitude, timestamp, is_moving, is_mock)
|
|
57
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
var stmt: OpaquePointer?
|
|
61
|
+
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return }
|
|
62
|
+
defer { sqlite3_finalize(stmt) }
|
|
63
|
+
|
|
64
|
+
sqlite3_bind_double(stmt, 1, location.latitude)
|
|
65
|
+
sqlite3_bind_double(stmt, 2, location.longitude)
|
|
66
|
+
sqlite3_bind_double(stmt, 3, location.accuracy)
|
|
67
|
+
sqlite3_bind_double(stmt, 4, location.speed)
|
|
68
|
+
sqlite3_bind_double(stmt, 5, location.heading)
|
|
69
|
+
sqlite3_bind_double(stmt, 6, location.altitude)
|
|
70
|
+
sqlite3_bind_int64(stmt, 7, Int64(location.timestamp))
|
|
71
|
+
sqlite3_bind_int(stmt, 8, location.isMoving ? 1 : 0)
|
|
72
|
+
sqlite3_bind_int(stmt, 9, location.isMock ? 1 : 0)
|
|
73
|
+
|
|
74
|
+
sqlite3_step(stmt)
|
|
75
|
+
trimBuffer()
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/// Retrieve the oldest `limit` buffered locations (FIFO order).
|
|
79
|
+
/// Returns pairs of (rowId, BGLLocationData) for deletion after successful send.
|
|
80
|
+
public func peek(limit: Int = 50) -> [(Int64, BGLLocationData)] {
|
|
81
|
+
guard let db = db else { return [] }
|
|
82
|
+
|
|
83
|
+
let sql =
|
|
84
|
+
"SELECT id, latitude, longitude, accuracy, speed, heading, altitude, timestamp, is_moving, is_mock FROM buffered_locations ORDER BY id ASC LIMIT ?"
|
|
85
|
+
|
|
86
|
+
var stmt: OpaquePointer?
|
|
87
|
+
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return [] }
|
|
88
|
+
defer { sqlite3_finalize(stmt) }
|
|
89
|
+
|
|
90
|
+
sqlite3_bind_int(stmt, 1, Int32(limit))
|
|
91
|
+
|
|
92
|
+
var results: [(Int64, BGLLocationData)] = []
|
|
93
|
+
while sqlite3_step(stmt) == SQLITE_ROW {
|
|
94
|
+
let id = sqlite3_column_int64(stmt, 0)
|
|
95
|
+
let data = BGLLocationData(
|
|
96
|
+
latitude: sqlite3_column_double(stmt, 1),
|
|
97
|
+
longitude: sqlite3_column_double(stmt, 2),
|
|
98
|
+
accuracy: sqlite3_column_double(stmt, 3),
|
|
99
|
+
speed: sqlite3_column_double(stmt, 4),
|
|
100
|
+
heading: sqlite3_column_double(stmt, 5),
|
|
101
|
+
altitude: sqlite3_column_double(stmt, 6),
|
|
102
|
+
timestamp: Double(sqlite3_column_int64(stmt, 7)),
|
|
103
|
+
isMoving: sqlite3_column_int(stmt, 8) == 1,
|
|
104
|
+
isMock: sqlite3_column_int(stmt, 9) == 1
|
|
105
|
+
)
|
|
106
|
+
results.append((id, data))
|
|
107
|
+
}
|
|
108
|
+
return results
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// Remove specific entries by their row IDs after successful send.
|
|
112
|
+
public func remove(ids: [Int64]) {
|
|
113
|
+
guard let db = db, !ids.isEmpty else { return }
|
|
114
|
+
|
|
115
|
+
let placeholders = ids.map { _ in "?" }.joined(separator: ",")
|
|
116
|
+
let sql = "DELETE FROM buffered_locations WHERE id IN (\(placeholders))"
|
|
117
|
+
|
|
118
|
+
var stmt: OpaquePointer?
|
|
119
|
+
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return }
|
|
120
|
+
defer { sqlite3_finalize(stmt) }
|
|
121
|
+
|
|
122
|
+
for (index, id) in ids.enumerated() {
|
|
123
|
+
sqlite3_bind_int64(stmt, Int32(index + 1), id)
|
|
124
|
+
}
|
|
125
|
+
sqlite3_step(stmt)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Current number of buffered locations.
|
|
129
|
+
public func count() -> Int {
|
|
130
|
+
guard let db = db else { return 0 }
|
|
131
|
+
|
|
132
|
+
let sql = "SELECT COUNT(*) FROM buffered_locations"
|
|
133
|
+
var stmt: OpaquePointer?
|
|
134
|
+
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return 0 }
|
|
135
|
+
defer { sqlite3_finalize(stmt) }
|
|
136
|
+
|
|
137
|
+
if sqlite3_step(stmt) == SQLITE_ROW {
|
|
138
|
+
return Int(sqlite3_column_int(stmt, 0))
|
|
139
|
+
}
|
|
140
|
+
return 0
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/// Remove all buffered locations.
|
|
144
|
+
public func clear() {
|
|
145
|
+
guard let db = db else { return }
|
|
146
|
+
sqlite3_exec(db, "DELETE FROM buffered_locations", nil, nil, nil)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// MARK: - Private
|
|
150
|
+
|
|
151
|
+
private func openDatabase() {
|
|
152
|
+
let flags = SQLITE_OPEN_CREATE | SQLITE_OPEN_READWRITE | SQLITE_OPEN_FULLMUTEX
|
|
153
|
+
if sqlite3_open_v2(dbPath, &db, flags, nil) != SQLITE_OK {
|
|
154
|
+
print("BGLocation: Failed to open BGLLocationBuffer database")
|
|
155
|
+
db = nil
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
private func createTable() {
|
|
160
|
+
guard let db = db else { return }
|
|
161
|
+
|
|
162
|
+
let sql = """
|
|
163
|
+
CREATE TABLE IF NOT EXISTS buffered_locations (
|
|
164
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
165
|
+
latitude REAL NOT NULL,
|
|
166
|
+
longitude REAL NOT NULL,
|
|
167
|
+
accuracy REAL NOT NULL,
|
|
168
|
+
speed REAL NOT NULL,
|
|
169
|
+
heading REAL NOT NULL,
|
|
170
|
+
altitude REAL NOT NULL,
|
|
171
|
+
timestamp INTEGER NOT NULL,
|
|
172
|
+
is_moving INTEGER NOT NULL,
|
|
173
|
+
is_mock INTEGER NOT NULL DEFAULT 0,
|
|
174
|
+
created_at INTEGER NOT NULL DEFAULT (strftime('%s','now') * 1000)
|
|
175
|
+
)
|
|
176
|
+
"""
|
|
177
|
+
sqlite3_exec(db, sql, nil, nil, nil)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
private func trimBuffer() {
|
|
181
|
+
let currentCount = count()
|
|
182
|
+
let overflow = currentCount - maxSize
|
|
183
|
+
if overflow <= 0 { return }
|
|
184
|
+
|
|
185
|
+
guard let db = db else { return }
|
|
186
|
+
let sql =
|
|
187
|
+
"DELETE FROM buffered_locations WHERE id IN (SELECT id FROM buffered_locations ORDER BY id ASC LIMIT ?)"
|
|
188
|
+
var stmt: OpaquePointer?
|
|
189
|
+
guard sqlite3_prepare_v2(db, sql, -1, &stmt, nil) == SQLITE_OK else { return }
|
|
190
|
+
defer { sqlite3_finalize(stmt) }
|
|
191
|
+
|
|
192
|
+
sqlite3_bind_int(stmt, 1, Int32(overflow))
|
|
193
|
+
sqlite3_step(stmt)
|
|
194
|
+
print(
|
|
195
|
+
"BGLocation: BGLLocationBuffer trimmed \(overflow) oldest entries (maxSize=\(maxSize))")
|
|
196
|
+
onOverflow?(overflow)
|
|
197
|
+
}
|
|
198
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Build-time configuration injected by the consuming plugin bridge.
|
|
4
|
+
/// The bridge sets `buildEpoch` during plugin initialization.
|
|
5
|
+
/// Used for update gating: licenses with `exp < buildEpoch` degrade to trial mode.
|
|
6
|
+
public enum BGLBuildConfig {
|
|
7
|
+
/// Unix epoch seconds when the consuming plugin was built.
|
|
8
|
+
/// Set by the bridge during initialization (e.g. from Xcode build-phase script).
|
|
9
|
+
/// Default 0 means update gating is disabled.
|
|
10
|
+
public static var buildEpoch: Int = 0
|
|
11
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// Manages license validation, trial timer lifecycle, and cooldown enforcement.
|
|
4
|
+
/// Extracted from BackgroundLocationPlugin to reduce bridge complexity.
|
|
5
|
+
public class BGLLicenseEnforcer {
|
|
6
|
+
|
|
7
|
+
private let licenseDefaults: UserDefaults
|
|
8
|
+
public let licenseValidator: BGLLicenseValidator
|
|
9
|
+
|
|
10
|
+
public var licenseMode: LicenseMode = .trial
|
|
11
|
+
public var trialTimer: BGLTrialTimer?
|
|
12
|
+
|
|
13
|
+
public init(defaults: UserDefaults) {
|
|
14
|
+
self.licenseDefaults = defaults
|
|
15
|
+
self.licenseValidator = BGLLicenseValidator(defaults: defaults)
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/// Validate license key and populate configure result.
|
|
19
|
+
/// - Parameters:
|
|
20
|
+
/// - licenseKey: the license key from plugin config
|
|
21
|
+
/// - isDebug: user's original debug setting (restored in full mode)
|
|
22
|
+
/// - debugSounds: user's original debugSounds setting
|
|
23
|
+
/// - debug: the debug logger to reconfigure
|
|
24
|
+
/// - configureResult: dictionary to populate with license status
|
|
25
|
+
/// - onTrialExpired: callback for when trial expires
|
|
26
|
+
public func validateLicense(
|
|
27
|
+
licenseKey: String?,
|
|
28
|
+
isDebug: Bool,
|
|
29
|
+
debugSounds: Bool,
|
|
30
|
+
debug: BGLDebugLogger,
|
|
31
|
+
configureResult: inout [String: Any],
|
|
32
|
+
onTrialExpired: @escaping () -> Void,
|
|
33
|
+
bundleId: String = Bundle.main.bundleIdentifier ?? ""
|
|
34
|
+
) {
|
|
35
|
+
let licenseResult = licenseValidator.validate(
|
|
36
|
+
licenseKey: licenseKey, bundleId: bundleId, buildEpoch: BGLBuildConfig.buildEpoch)
|
|
37
|
+
|
|
38
|
+
switch licenseResult {
|
|
39
|
+
case .valid(let updatesUntil):
|
|
40
|
+
licenseMode = .full
|
|
41
|
+
trialTimer?.stop()
|
|
42
|
+
trialTimer = nil
|
|
43
|
+
debug.configure(debug: isDebug, debugSounds: debugSounds)
|
|
44
|
+
configureResult["licenseMode"] = "full"
|
|
45
|
+
if let updatesUntil = updatesUntil {
|
|
46
|
+
let updatesDate = ISO8601DateFormatter().string(
|
|
47
|
+
from: Date(timeIntervalSince1970: TimeInterval(updatesUntil)))
|
|
48
|
+
configureResult["licenseUpdatesUntil"] = updatesDate
|
|
49
|
+
print("BGLocation: License valid — full mode, updates until \(updatesDate)")
|
|
50
|
+
} else {
|
|
51
|
+
print("BGLocation: License valid — full mode, no update expiry")
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
case .updatesExpired(let updatesUntil):
|
|
55
|
+
licenseMode = .trial
|
|
56
|
+
debug.configure(debug: true, debugSounds: debugSounds)
|
|
57
|
+
let updatesDate = ISO8601DateFormatter().string(
|
|
58
|
+
from: Date(timeIntervalSince1970: TimeInterval(updatesUntil)))
|
|
59
|
+
let buildDate = ISO8601DateFormatter().string(
|
|
60
|
+
from: Date(timeIntervalSince1970: TimeInterval(BGLBuildConfig.buildEpoch)))
|
|
61
|
+
configureResult["licenseMode"] = "trial"
|
|
62
|
+
configureResult["licenseUpdateExpired"] = true
|
|
63
|
+
configureResult["licenseUpdatesUntil"] = updatesDate
|
|
64
|
+
configureResult["licenseError"] =
|
|
65
|
+
"Updates expired. This plugin version requires a license with updates valid until \(buildDate). Please renew at bglocation.dev/portal"
|
|
66
|
+
|
|
67
|
+
if trialTimer == nil {
|
|
68
|
+
trialTimer = BGLTrialTimer(defaults: licenseDefaults, onExpired: onTrialExpired)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
print(
|
|
72
|
+
"BGLocation: License updates expired (until \(updatesDate)). Running in TRIAL mode."
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
case .invalid(let reason):
|
|
76
|
+
licenseMode = .trial
|
|
77
|
+
debug.configure(debug: true, debugSounds: debugSounds)
|
|
78
|
+
configureResult["licenseMode"] = "trial"
|
|
79
|
+
configureResult["licenseError"] = reason
|
|
80
|
+
|
|
81
|
+
if trialTimer == nil {
|
|
82
|
+
trialTimer = BGLTrialTimer(defaults: licenseDefaults, onExpired: onTrialExpired)
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
print(
|
|
86
|
+
"BGLocation: License: \(reason). Running in TRIAL mode (30 min limit, 1h cooldown)."
|
|
87
|
+
)
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Result of a cooldown check.
|
|
92
|
+
public struct CooldownResult {
|
|
93
|
+
public let remainingSeconds: Int
|
|
94
|
+
public let message: String
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/// Check if trial cooldown is active.
|
|
98
|
+
/// Returns CooldownResult if cooldown is active, or nil if the operation can proceed.
|
|
99
|
+
public func checkCooldown() -> CooldownResult? {
|
|
100
|
+
guard licenseMode == .trial,
|
|
101
|
+
let timer = trialTimer,
|
|
102
|
+
timer.isInCooldown()
|
|
103
|
+
else {
|
|
104
|
+
return nil
|
|
105
|
+
}
|
|
106
|
+
let remaining = timer.remainingCooldownSeconds()
|
|
107
|
+
return CooldownResult(
|
|
108
|
+
remainingSeconds: remaining,
|
|
109
|
+
message:
|
|
110
|
+
"Trial cooldown active. Please wait \(remaining) seconds or provide a license key."
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/// Start trial timer if in trial mode.
|
|
115
|
+
public func startTrialIfNeeded() {
|
|
116
|
+
if licenseMode == .trial {
|
|
117
|
+
trialTimer?.start()
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/// Stop trial timer if no geofences are active and tracking is stopped.
|
|
122
|
+
public func stopTrialIfIdle(hasGeofences: Bool, isTracking: Bool) {
|
|
123
|
+
if licenseMode == .trial && !hasGeofences && !isTracking {
|
|
124
|
+
trialTimer?.stop()
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Stop trial timer if no geofences are active.
|
|
129
|
+
public func stopTrialIfNoGeofences(hasGeofences: Bool) {
|
|
130
|
+
if !hasGeofences {
|
|
131
|
+
trialTimer?.stop()
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|