@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.
Files changed (61) hide show
  1. package/CapacitorBackgroundLocation.podspec +19 -0
  2. package/LICENSE.md +97 -0
  3. package/Package.swift +44 -0
  4. package/README.md +264 -0
  5. package/android/build.gradle +74 -0
  6. package/android/src/main/AndroidManifest.xml +37 -0
  7. package/android/src/main/kotlin/dev/bglocation/BackgroundLocationPlugin.kt +684 -0
  8. package/android/src/main/kotlin/dev/bglocation/core/Models.kt +76 -0
  9. package/android/src/main/kotlin/dev/bglocation/core/battery/BGLBatteryHelper.kt +127 -0
  10. package/android/src/main/kotlin/dev/bglocation/core/boot/BGLBootCompletedReceiver.kt +32 -0
  11. package/android/src/main/kotlin/dev/bglocation/core/config/BGLConfigParser.kt +114 -0
  12. package/android/src/main/kotlin/dev/bglocation/core/config/BGLVersion.kt +6 -0
  13. package/android/src/main/kotlin/dev/bglocation/core/debug/BGLDebugLogger.kt +174 -0
  14. package/android/src/main/kotlin/dev/bglocation/core/geofence/BGLGeofenceBroadcastReceiver.kt +93 -0
  15. package/android/src/main/kotlin/dev/bglocation/core/geofence/BGLGeofenceManager.kt +310 -0
  16. package/android/src/main/kotlin/dev/bglocation/core/http/BGLHttpSender.kt +187 -0
  17. package/android/src/main/kotlin/dev/bglocation/core/http/BGLLocationBuffer.kt +152 -0
  18. package/android/src/main/kotlin/dev/bglocation/core/license/BGLBuildConfig.kt +16 -0
  19. package/android/src/main/kotlin/dev/bglocation/core/license/BGLLicenseEnforcer.kt +137 -0
  20. package/android/src/main/kotlin/dev/bglocation/core/license/BGLLicenseValidator.kt +134 -0
  21. package/android/src/main/kotlin/dev/bglocation/core/license/BGLTrialTimer.kt +176 -0
  22. package/android/src/main/kotlin/dev/bglocation/core/location/BGLAdaptiveFilter.kt +94 -0
  23. package/android/src/main/kotlin/dev/bglocation/core/location/BGLHeartbeatTimer.kt +38 -0
  24. package/android/src/main/kotlin/dev/bglocation/core/location/BGLLocationForegroundService.kt +289 -0
  25. package/android/src/main/kotlin/dev/bglocation/core/location/BGLLocationHelpers.kt +72 -0
  26. package/android/src/main/kotlin/dev/bglocation/core/location/BGLPermissionManager.kt +99 -0
  27. package/android/src/main/kotlin/dev/bglocation/core/notification/BGLNotificationHelper.kt +77 -0
  28. package/dist/esm/definitions.d.ts +390 -0
  29. package/dist/esm/definitions.js +3 -0
  30. package/dist/esm/definitions.js.map +1 -0
  31. package/dist/esm/index.d.ts +4 -0
  32. package/dist/esm/index.js +26 -0
  33. package/dist/esm/index.js.map +1 -0
  34. package/dist/esm/web.d.ts +47 -0
  35. package/dist/esm/web.js +231 -0
  36. package/dist/esm/web.js.map +1 -0
  37. package/dist/esm/web.test.d.ts +1 -0
  38. package/dist/esm/web.test.js +940 -0
  39. package/dist/esm/web.test.js.map +1 -0
  40. package/dist/plugin.cjs.js +267 -0
  41. package/dist/plugin.cjs.js.map +1 -0
  42. package/dist/plugin.js +270 -0
  43. package/dist/plugin.js.map +1 -0
  44. package/ios/Sources/BGLocationCore/Config/BGLConfigParser.swift +88 -0
  45. package/ios/Sources/BGLocationCore/Config/BGLVersion.swift +6 -0
  46. package/ios/Sources/BGLocationCore/Debug/BGLDebugLogger.swift +201 -0
  47. package/ios/Sources/BGLocationCore/Geofence/BGLGeofenceManager.swift +538 -0
  48. package/ios/Sources/BGLocationCore/Http/BGLHttpSender.swift +227 -0
  49. package/ios/Sources/BGLocationCore/Http/BGLLocationBuffer.swift +198 -0
  50. package/ios/Sources/BGLocationCore/License/BGLBuildConfig.swift +11 -0
  51. package/ios/Sources/BGLocationCore/License/BGLLicenseEnforcer.swift +134 -0
  52. package/ios/Sources/BGLocationCore/License/BGLLicenseValidator.swift +163 -0
  53. package/ios/Sources/BGLocationCore/License/BGLTrialTimer.swift +168 -0
  54. package/ios/Sources/BGLocationCore/Location/BGLAdaptiveFilter.swift +91 -0
  55. package/ios/Sources/BGLocationCore/Location/BGLHeartbeatTimer.swift +50 -0
  56. package/ios/Sources/BGLocationCore/Location/BGLLocationData.swift +48 -0
  57. package/ios/Sources/BGLocationCore/Location/BGLLocationHelpers.swift +42 -0
  58. package/ios/Sources/BGLocationCore/Location/BGLLocationManager.swift +268 -0
  59. package/ios/Sources/BGLocationCore/Location/BGLPermissionManager.swift +33 -0
  60. package/ios/Sources/BackgroundLocationPlugin/BackgroundLocationPlugin.swift +657 -0
  61. package/package.json +75 -0
package/dist/plugin.js ADDED
@@ -0,0 +1,270 @@
1
+ var capacitorBackgroundLocation = (function (exports, core) {
2
+ 'use strict';
3
+
4
+ /** Maximum number of geofences that can be registered simultaneously (iOS CLLocationManager limit). */
5
+ const GEOFENCE_MAX_COUNT = 20;
6
+
7
+ const RawPlugin = core.registerPlugin('BackgroundLocation', {
8
+ web: () => Promise.resolve().then(function () { return web; }).then((m) => new m.BackgroundLocationWeb()),
9
+ });
10
+ /**
11
+ * Thin wrapper that merges partial `configure()` calls with the last-applied
12
+ * config so callers don't lose previously set fields (e.g. http endpoint)
13
+ * when reconfiguring a single field (e.g. distanceFilter).
14
+ */
15
+ let lastConfig;
16
+ const BackgroundLocation = new Proxy(RawPlugin, {
17
+ get(target, prop, receiver) {
18
+ if (prop === 'configure') {
19
+ return async (options) => {
20
+ const merged = lastConfig ? { ...lastConfig, ...options } : options;
21
+ const result = await target.configure(merged);
22
+ lastConfig = merged;
23
+ return result;
24
+ };
25
+ }
26
+ return Reflect.get(target, prop, receiver);
27
+ },
28
+ });
29
+
30
+ /**
31
+ * Web fallback using navigator.geolocation.
32
+ * Used for development/testing in the browser only.
33
+ */
34
+ class BackgroundLocationWeb extends core.WebPlugin {
35
+ watchId = null;
36
+ heartbeatTimer = null;
37
+ config = null;
38
+ httpConfig = null;
39
+ isTracking = false;
40
+ debug = false;
41
+ locationCount = 0;
42
+ heartbeatCount = 0;
43
+ httpSuccessCount = 0;
44
+ httpErrorCount = 0;
45
+ geofenceStore = new Map();
46
+ async getVersion() {
47
+ return { pluginVersion: '1.1.0', coreVersion: 'web' };
48
+ }
49
+ async checkPermissions() {
50
+ const result = await navigator.permissions.query({ name: 'geolocation' });
51
+ const state = result.state === 'granted' ? 'granted' : result.state === 'denied' ? 'denied' : 'prompt';
52
+ // Web has no separate background permission — mirror foreground state
53
+ return { location: state, backgroundLocation: state };
54
+ }
55
+ async requestPermissions() {
56
+ // Web API doesn't have explicit requestPermissions — permission is requested on first use.
57
+ return this.checkPermissions();
58
+ }
59
+ async configure(options) {
60
+ this.config = options;
61
+ this.httpConfig = options.http ?? null;
62
+ this.debug = options.debug ?? false;
63
+ this.resetCounters();
64
+ const distanceFilter = options.distanceFilter ?? 15;
65
+ const heartbeatInterval = options.heartbeatInterval ?? 15;
66
+ const distanceFilterMode = distanceFilter === 'auto' ? 'auto' : 'fixed';
67
+ if (distanceFilter === 'auto') {
68
+ this.emitDebug('CONFIGURE distanceFilter=auto ignored on Web — browser does not support native distance filtering');
69
+ }
70
+ this.emitDebug(`CONFIGURE distance=${distanceFilter}m heartbeat=${heartbeatInterval}s http=${options.http?.url ?? 'disabled'}`);
71
+ console.info('[BackgroundLocation:web] configured', options);
72
+ return { licenseMode: 'full', distanceFilterMode };
73
+ }
74
+ async start() {
75
+ if (!this.config) {
76
+ throw new Error('Plugin not configured. Call configure() first.');
77
+ }
78
+ this.watchId = navigator.geolocation.watchPosition((position) => {
79
+ const location = this.geolocationToLocation(position);
80
+ this.notifyListeners('onLocation', location);
81
+ this.locationCount++;
82
+ this.emitDebug(`LOCATION #${this.locationCount} (${location.latitude.toFixed(5)}, ${location.longitude.toFixed(5)}) acc=${location.accuracy.toFixed(1)}m`);
83
+ this.sendHttp(location);
84
+ }, (error) => {
85
+ console.error('[BackgroundLocation:web] watch error:', error.message);
86
+ }, {
87
+ enableHighAccuracy: (this.config.desiredAccuracy ?? 'high') === 'high',
88
+ maximumAge: 1000,
89
+ timeout: 20000,
90
+ });
91
+ this.startHeartbeat();
92
+ this.isTracking = true;
93
+ this.resetCounters();
94
+ this.emitDebug('START tracking');
95
+ return { enabled: true, tracking: true };
96
+ }
97
+ async stop() {
98
+ if (this.watchId !== null) {
99
+ navigator.geolocation.clearWatch(this.watchId);
100
+ this.watchId = null;
101
+ }
102
+ this.stopHeartbeat();
103
+ this.isTracking = false;
104
+ this.emitDebug(`STOP tracking — locations=${this.locationCount} heartbeats=${this.heartbeatCount} http_ok=${this.httpSuccessCount} http_err=${this.httpErrorCount}`);
105
+ const permissions = await this.checkPermissions();
106
+ return { enabled: permissions.location === 'granted', tracking: false };
107
+ }
108
+ async getState() {
109
+ const permissions = await this.checkPermissions();
110
+ return { enabled: permissions.location === 'granted', tracking: this.isTracking };
111
+ }
112
+ async removeAllListeners() {
113
+ if (this.watchId !== null) {
114
+ navigator.geolocation.clearWatch(this.watchId);
115
+ this.watchId = null;
116
+ }
117
+ this.stopHeartbeat();
118
+ this.isTracking = false;
119
+ await super.removeAllListeners();
120
+ }
121
+ async getCurrentPosition(options) {
122
+ return new Promise((resolve, reject) => {
123
+ navigator.geolocation.getCurrentPosition((position) => resolve(this.geolocationToLocation(position)), (error) => reject(new Error(error.message)), { enableHighAccuracy: true, timeout: options?.timeout ?? 20000 });
124
+ });
125
+ }
126
+ // B.1: Battery optimization — no-op on Web (not applicable to browsers)
127
+ async checkBatteryOptimization() {
128
+ return {
129
+ isIgnoringOptimizations: true,
130
+ manufacturer: '',
131
+ helpUrl: '',
132
+ message: 'Battery optimization is not applicable on Web platform.',
133
+ };
134
+ }
135
+ async requestBatteryOptimization() {
136
+ return this.checkBatteryOptimization();
137
+ }
138
+ // -----------------------------------------------------------------------
139
+ // Geofencing — in-memory stub for development/testing
140
+ // -----------------------------------------------------------------------
141
+ async addGeofence(geofence) {
142
+ if (this.geofenceStore.size >= GEOFENCE_MAX_COUNT && !this.geofenceStore.has(geofence.identifier)) {
143
+ throw new Error(`Geofence limit reached (max ${GEOFENCE_MAX_COUNT})`);
144
+ }
145
+ this.geofenceStore.set(geofence.identifier, { ...geofence });
146
+ this.emitDebug(`[GEOFENCE ADD] "${geofence.identifier}" (${this.geofenceStore.size}/${GEOFENCE_MAX_COUNT})`);
147
+ }
148
+ async addGeofences(options) {
149
+ for (const geofence of options.geofences) {
150
+ await this.addGeofence(geofence);
151
+ }
152
+ }
153
+ async removeGeofence(options) {
154
+ if (!this.geofenceStore.has(options.identifier)) {
155
+ throw new Error(`Geofence "${options.identifier}" not found`);
156
+ }
157
+ this.geofenceStore.delete(options.identifier);
158
+ this.emitDebug(`[GEOFENCE REMOVE] "${options.identifier}" (${this.geofenceStore.size}/${GEOFENCE_MAX_COUNT})`);
159
+ }
160
+ async removeAllGeofences() {
161
+ const count = this.geofenceStore.size;
162
+ this.geofenceStore.clear();
163
+ this.emitDebug(`[GEOFENCE REMOVE_ALL] removed ${count}`);
164
+ }
165
+ async getGeofences() {
166
+ return { geofences: Array.from(this.geofenceStore.values()) };
167
+ }
168
+ startHeartbeat() {
169
+ if (!this.config)
170
+ return;
171
+ this.heartbeatTimer = setInterval(() => {
172
+ navigator.geolocation.getCurrentPosition((position) => {
173
+ const location = this.geolocationToLocation(position);
174
+ this.notifyListeners('onHeartbeat', { location, timestamp: Date.now() });
175
+ this.heartbeatCount++;
176
+ this.emitDebug(`HEARTBEAT #${this.heartbeatCount} with location`);
177
+ }, (_error) => {
178
+ // Emit heartbeat with null location to match native platform behavior
179
+ this.notifyListeners('onHeartbeat', { location: null, timestamp: Date.now() });
180
+ this.heartbeatCount++;
181
+ this.emitDebug(`HEARTBEAT #${this.heartbeatCount} no location`);
182
+ });
183
+ }, (this.config.heartbeatInterval ?? 15) * 1000);
184
+ }
185
+ stopHeartbeat() {
186
+ if (this.heartbeatTimer !== null) {
187
+ clearInterval(this.heartbeatTimer);
188
+ this.heartbeatTimer = null;
189
+ }
190
+ }
191
+ geolocationToLocation(position) {
192
+ return {
193
+ latitude: position.coords.latitude,
194
+ longitude: position.coords.longitude,
195
+ accuracy: position.coords.accuracy,
196
+ speed: position.coords.speed ?? 0,
197
+ heading: position.coords.heading ?? -1,
198
+ altitude: position.coords.altitude ?? 0,
199
+ timestamp: position.timestamp,
200
+ isMoving: (position.coords.speed ?? 0) > 0.5,
201
+ isMock: false,
202
+ };
203
+ }
204
+ sendHttp(location) {
205
+ if (!this.httpConfig)
206
+ return;
207
+ const headers = {
208
+ 'Content-Type': 'application/json',
209
+ ...(this.httpConfig.headers ?? {}),
210
+ };
211
+ fetch(this.httpConfig.url, {
212
+ method: 'POST',
213
+ headers,
214
+ body: JSON.stringify({ location }),
215
+ })
216
+ .then(async (response) => {
217
+ const responseText = await response.text().catch(() => '');
218
+ if (response.ok) {
219
+ this.httpSuccessCount++;
220
+ this.emitDebug(`HTTP OK #${this.httpSuccessCount} status=${response.status}`);
221
+ }
222
+ else {
223
+ this.httpErrorCount++;
224
+ this.emitDebug(`HTTP ERROR #${this.httpErrorCount} status=${response.status}`);
225
+ }
226
+ this.notifyListeners('onHttp', {
227
+ statusCode: response.status,
228
+ success: response.ok,
229
+ responseText,
230
+ bufferedCount: 0,
231
+ });
232
+ })
233
+ .catch((error) => {
234
+ this.httpErrorCount++;
235
+ this.emitDebug(`HTTP ERROR #${this.httpErrorCount} status=0 error=${error.message}`);
236
+ this.notifyListeners('onHttp', {
237
+ statusCode: 0,
238
+ success: false,
239
+ responseText: '',
240
+ error: error.message,
241
+ bufferedCount: 0,
242
+ });
243
+ });
244
+ }
245
+ emitDebug(message) {
246
+ if (!this.debug)
247
+ return;
248
+ console.debug(`[BGL_DEBUG] ${message}`);
249
+ this.notifyListeners('onDebug', { message, timestamp: Date.now() });
250
+ }
251
+ resetCounters() {
252
+ this.locationCount = 0;
253
+ this.heartbeatCount = 0;
254
+ this.httpSuccessCount = 0;
255
+ this.httpErrorCount = 0;
256
+ }
257
+ }
258
+
259
+ var web = /*#__PURE__*/Object.freeze({
260
+ __proto__: null,
261
+ BackgroundLocationWeb: BackgroundLocationWeb
262
+ });
263
+
264
+ exports.BackgroundLocation = BackgroundLocation;
265
+ exports.GEOFENCE_MAX_COUNT = GEOFENCE_MAX_COUNT;
266
+
267
+ return exports;
268
+
269
+ })({}, capacitorExports);
270
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sources":["esm/definitions.js","esm/index.js","esm/web.js"],"sourcesContent":["/** Maximum number of geofences that can be registered simultaneously (iOS CLLocationManager limit). */\nexport const GEOFENCE_MAX_COUNT = 20;\n//# sourceMappingURL=definitions.js.map","import { registerPlugin } from '@capacitor/core';\nconst RawPlugin = registerPlugin('BackgroundLocation', {\n web: () => import('./web').then((m) => new m.BackgroundLocationWeb()),\n});\n/**\n * Thin wrapper that merges partial `configure()` calls with the last-applied\n * config so callers don't lose previously set fields (e.g. http endpoint)\n * when reconfiguring a single field (e.g. distanceFilter).\n */\nlet lastConfig;\nconst BackgroundLocation = new Proxy(RawPlugin, {\n get(target, prop, receiver) {\n if (prop === 'configure') {\n return async (options) => {\n const merged = lastConfig ? { ...lastConfig, ...options } : options;\n const result = await target.configure(merged);\n lastConfig = merged;\n return result;\n };\n }\n return Reflect.get(target, prop, receiver);\n },\n});\nexport * from './definitions';\nexport { BackgroundLocation };\n//# sourceMappingURL=index.js.map","import { WebPlugin } from '@capacitor/core';\nimport { GEOFENCE_MAX_COUNT } from './definitions';\n/**\n * Web fallback using navigator.geolocation.\n * Used for development/testing in the browser only.\n */\nexport class BackgroundLocationWeb extends WebPlugin {\n watchId = null;\n heartbeatTimer = null;\n config = null;\n httpConfig = null;\n isTracking = false;\n debug = false;\n locationCount = 0;\n heartbeatCount = 0;\n httpSuccessCount = 0;\n httpErrorCount = 0;\n geofenceStore = new Map();\n async getVersion() {\n return { pluginVersion: '1.1.0', coreVersion: 'web' };\n }\n async checkPermissions() {\n const result = await navigator.permissions.query({ name: 'geolocation' });\n const state = result.state === 'granted' ? 'granted' : result.state === 'denied' ? 'denied' : 'prompt';\n // Web has no separate background permission — mirror foreground state\n return { location: state, backgroundLocation: state };\n }\n async requestPermissions() {\n // Web API doesn't have explicit requestPermissions — permission is requested on first use.\n return this.checkPermissions();\n }\n async configure(options) {\n this.config = options;\n this.httpConfig = options.http ?? null;\n this.debug = options.debug ?? false;\n this.resetCounters();\n const distanceFilter = options.distanceFilter ?? 15;\n const heartbeatInterval = options.heartbeatInterval ?? 15;\n const distanceFilterMode = distanceFilter === 'auto' ? 'auto' : 'fixed';\n if (distanceFilter === 'auto') {\n this.emitDebug('CONFIGURE distanceFilter=auto ignored on Web — browser does not support native distance filtering');\n }\n this.emitDebug(`CONFIGURE distance=${distanceFilter}m heartbeat=${heartbeatInterval}s http=${options.http?.url ?? 'disabled'}`);\n console.info('[BackgroundLocation:web] configured', options);\n return { licenseMode: 'full', distanceFilterMode };\n }\n async start() {\n if (!this.config) {\n throw new Error('Plugin not configured. Call configure() first.');\n }\n this.watchId = navigator.geolocation.watchPosition((position) => {\n const location = this.geolocationToLocation(position);\n this.notifyListeners('onLocation', location);\n this.locationCount++;\n this.emitDebug(`LOCATION #${this.locationCount} (${location.latitude.toFixed(5)}, ${location.longitude.toFixed(5)}) acc=${location.accuracy.toFixed(1)}m`);\n this.sendHttp(location);\n }, (error) => {\n console.error('[BackgroundLocation:web] watch error:', error.message);\n }, {\n enableHighAccuracy: (this.config.desiredAccuracy ?? 'high') === 'high',\n maximumAge: 1000,\n timeout: 20000,\n });\n this.startHeartbeat();\n this.isTracking = true;\n this.resetCounters();\n this.emitDebug('START tracking');\n return { enabled: true, tracking: true };\n }\n async stop() {\n if (this.watchId !== null) {\n navigator.geolocation.clearWatch(this.watchId);\n this.watchId = null;\n }\n this.stopHeartbeat();\n this.isTracking = false;\n this.emitDebug(`STOP tracking — locations=${this.locationCount} heartbeats=${this.heartbeatCount} http_ok=${this.httpSuccessCount} http_err=${this.httpErrorCount}`);\n const permissions = await this.checkPermissions();\n return { enabled: permissions.location === 'granted', tracking: false };\n }\n async getState() {\n const permissions = await this.checkPermissions();\n return { enabled: permissions.location === 'granted', tracking: this.isTracking };\n }\n async removeAllListeners() {\n if (this.watchId !== null) {\n navigator.geolocation.clearWatch(this.watchId);\n this.watchId = null;\n }\n this.stopHeartbeat();\n this.isTracking = false;\n await super.removeAllListeners();\n }\n async getCurrentPosition(options) {\n return new Promise((resolve, reject) => {\n navigator.geolocation.getCurrentPosition((position) => resolve(this.geolocationToLocation(position)), (error) => reject(new Error(error.message)), { enableHighAccuracy: true, timeout: options?.timeout ?? 20000 });\n });\n }\n // B.1: Battery optimization — no-op on Web (not applicable to browsers)\n async checkBatteryOptimization() {\n return {\n isIgnoringOptimizations: true,\n manufacturer: '',\n helpUrl: '',\n message: 'Battery optimization is not applicable on Web platform.',\n };\n }\n async requestBatteryOptimization() {\n return this.checkBatteryOptimization();\n }\n // -----------------------------------------------------------------------\n // Geofencing — in-memory stub for development/testing\n // -----------------------------------------------------------------------\n async addGeofence(geofence) {\n if (this.geofenceStore.size >= GEOFENCE_MAX_COUNT && !this.geofenceStore.has(geofence.identifier)) {\n throw new Error(`Geofence limit reached (max ${GEOFENCE_MAX_COUNT})`);\n }\n this.geofenceStore.set(geofence.identifier, { ...geofence });\n this.emitDebug(`[GEOFENCE ADD] \"${geofence.identifier}\" (${this.geofenceStore.size}/${GEOFENCE_MAX_COUNT})`);\n }\n async addGeofences(options) {\n for (const geofence of options.geofences) {\n await this.addGeofence(geofence);\n }\n }\n async removeGeofence(options) {\n if (!this.geofenceStore.has(options.identifier)) {\n throw new Error(`Geofence \"${options.identifier}\" not found`);\n }\n this.geofenceStore.delete(options.identifier);\n this.emitDebug(`[GEOFENCE REMOVE] \"${options.identifier}\" (${this.geofenceStore.size}/${GEOFENCE_MAX_COUNT})`);\n }\n async removeAllGeofences() {\n const count = this.geofenceStore.size;\n this.geofenceStore.clear();\n this.emitDebug(`[GEOFENCE REMOVE_ALL] removed ${count}`);\n }\n async getGeofences() {\n return { geofences: Array.from(this.geofenceStore.values()) };\n }\n startHeartbeat() {\n if (!this.config)\n return;\n this.heartbeatTimer = setInterval(() => {\n navigator.geolocation.getCurrentPosition((position) => {\n const location = this.geolocationToLocation(position);\n this.notifyListeners('onHeartbeat', { location, timestamp: Date.now() });\n this.heartbeatCount++;\n this.emitDebug(`HEARTBEAT #${this.heartbeatCount} with location`);\n }, (_error) => {\n // Emit heartbeat with null location to match native platform behavior\n this.notifyListeners('onHeartbeat', { location: null, timestamp: Date.now() });\n this.heartbeatCount++;\n this.emitDebug(`HEARTBEAT #${this.heartbeatCount} no location`);\n });\n }, (this.config.heartbeatInterval ?? 15) * 1000);\n }\n stopHeartbeat() {\n if (this.heartbeatTimer !== null) {\n clearInterval(this.heartbeatTimer);\n this.heartbeatTimer = null;\n }\n }\n geolocationToLocation(position) {\n return {\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n accuracy: position.coords.accuracy,\n speed: position.coords.speed ?? 0,\n heading: position.coords.heading ?? -1,\n altitude: position.coords.altitude ?? 0,\n timestamp: position.timestamp,\n isMoving: (position.coords.speed ?? 0) > 0.5,\n isMock: false,\n };\n }\n sendHttp(location) {\n if (!this.httpConfig)\n return;\n const headers = {\n 'Content-Type': 'application/json',\n ...(this.httpConfig.headers ?? {}),\n };\n fetch(this.httpConfig.url, {\n method: 'POST',\n headers,\n body: JSON.stringify({ location }),\n })\n .then(async (response) => {\n const responseText = await response.text().catch(() => '');\n if (response.ok) {\n this.httpSuccessCount++;\n this.emitDebug(`HTTP OK #${this.httpSuccessCount} status=${response.status}`);\n }\n else {\n this.httpErrorCount++;\n this.emitDebug(`HTTP ERROR #${this.httpErrorCount} status=${response.status}`);\n }\n this.notifyListeners('onHttp', {\n statusCode: response.status,\n success: response.ok,\n responseText,\n bufferedCount: 0,\n });\n })\n .catch((error) => {\n this.httpErrorCount++;\n this.emitDebug(`HTTP ERROR #${this.httpErrorCount} status=0 error=${error.message}`);\n this.notifyListeners('onHttp', {\n statusCode: 0,\n success: false,\n responseText: '',\n error: error.message,\n bufferedCount: 0,\n });\n });\n }\n emitDebug(message) {\n if (!this.debug)\n return;\n console.debug(`[BGL_DEBUG] ${message}`);\n this.notifyListeners('onDebug', { message, timestamp: Date.now() });\n }\n resetCounters() {\n this.locationCount = 0;\n this.heartbeatCount = 0;\n this.httpSuccessCount = 0;\n this.httpErrorCount = 0;\n }\n}\n//# sourceMappingURL=web.js.map"],"names":["registerPlugin","WebPlugin"],"mappings":"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"}
@@ -0,0 +1,88 @@
1
+ import Foundation
2
+
3
+ /// Parses configuration dictionary into typed config structs.
4
+ /// Framework-agnostic: accepts [String: Any] instead of Capacitor CAPPluginCall.
5
+ /// Bridge adapters provide: `BGLConfigParser.parse(call.options)` (Capacitor)
6
+ /// or `BGLConfigParser.parse(dict)` (React Native / Flutter).
7
+ public enum BGLConfigParser {
8
+
9
+ public struct ParsedConfig {
10
+ public let desiredAccuracy: String
11
+ public let heartbeatInterval: Int
12
+ public let distanceFilter: Double
13
+ public let distanceFilterMode: String
14
+ public let autoDistanceFilterConfig: AutoDistanceFilterConfig?
15
+ public let isDebug: Bool
16
+ public let debugSounds: Bool
17
+ public let httpUrl: String?
18
+ public let httpHeaders: [String: String]
19
+ public let bufferMaxSize: Int?
20
+ }
21
+
22
+ /// Parse all configuration values from a dictionary.
23
+ public static func parse(_ options: [String: Any]) -> ParsedConfig {
24
+ let desiredAccuracy = options["desiredAccuracy"] as? String ?? "high"
25
+ let heartbeatInterval = options["heartbeatInterval"] as? Int ?? 15
26
+
27
+ let distanceFilterRaw = options["distanceFilter"]
28
+ let isAutoMode = (distanceFilterRaw as? String) == "auto"
29
+ let distanceFilterMode = isAutoMode ? "auto" : "fixed"
30
+
31
+ var distanceFilter: Double
32
+ var autoDistanceFilterConfig: AutoDistanceFilterConfig?
33
+
34
+ if isAutoMode {
35
+ let autoObj = options["autoDistanceFilter"] as? [String: Any]
36
+ let targetInterval =
37
+ autoObj?["targetInterval"] as? Double
38
+ ?? AutoDistanceFilterConfig.defaultTargetInterval
39
+ let minDistance =
40
+ autoObj?["minDistance"] as? Double
41
+ ?? Double(AutoDistanceFilterConfig.defaultMinDistance)
42
+ let maxDistance =
43
+ autoObj?["maxDistance"] as? Double
44
+ ?? Double(AutoDistanceFilterConfig.defaultMaxDistance)
45
+ let config = AutoDistanceFilterConfig(
46
+ targetInterval: targetInterval,
47
+ minDistance: minDistance,
48
+ maxDistance: maxDistance
49
+ )
50
+ autoDistanceFilterConfig = config
51
+ distanceFilter = config.minDistance
52
+ } else {
53
+ autoDistanceFilterConfig = nil
54
+ distanceFilter = (distanceFilterRaw as? Double) ?? 15.0
55
+ }
56
+
57
+ let isDebug = options["debug"] as? Bool ?? false
58
+ let debugSounds = options["debugSounds"] as? Bool ?? false
59
+
60
+ // HTTP config
61
+ var httpUrl: String?
62
+ var httpHeaders: [String: String] = [:]
63
+ var bufferMaxSize: Int?
64
+
65
+ if let httpObj = options["http"] as? [String: Any], let url = httpObj["url"] as? String {
66
+ httpUrl = url
67
+ if let headersObj = httpObj["headers"] as? [String: String] {
68
+ httpHeaders = headersObj
69
+ }
70
+ if let bufferObj = httpObj["buffer"] as? [String: Any] {
71
+ bufferMaxSize = bufferObj["maxSize"] as? Int ?? 1000
72
+ }
73
+ }
74
+
75
+ return ParsedConfig(
76
+ desiredAccuracy: desiredAccuracy,
77
+ heartbeatInterval: heartbeatInterval,
78
+ distanceFilter: distanceFilter,
79
+ distanceFilterMode: distanceFilterMode,
80
+ autoDistanceFilterConfig: autoDistanceFilterConfig,
81
+ isDebug: isDebug,
82
+ debugSounds: debugSounds,
83
+ httpUrl: httpUrl,
84
+ httpHeaders: httpHeaders,
85
+ bufferMaxSize: bufferMaxSize
86
+ )
87
+ }
88
+ }
@@ -0,0 +1,6 @@
1
+ import Foundation
2
+
3
+ /// Version constant for the BGLocationCore native library.
4
+ public enum BGLVersion {
5
+ public static let version = "1.0.2"
6
+ }
@@ -0,0 +1,201 @@
1
+ import Foundation
2
+ import os.log
3
+
4
+ #if os(iOS)
5
+ import AVFoundation
6
+ import AudioToolbox
7
+ #endif
8
+
9
+ /// Debug logger for the BackgroundLocation plugin.
10
+ /// Emits structured log messages via os_log and optionally plays system sounds.
11
+ public class BGLDebugLogger {
12
+
13
+ /// Callback to emit debug events to the JS layer.
14
+ public var onDebug: ((String) -> Void)?
15
+
16
+ public private(set) var isEnabled = false
17
+ public private(set) var soundsEnabled = false
18
+
19
+ // Counters for debug summary
20
+ public private(set) var locationCount = 0
21
+ public private(set) var heartbeatCount = 0
22
+ public private(set) var httpSuccessCount = 0
23
+ public private(set) var httpErrorCount = 0
24
+
25
+ private let log = OSLog(subsystem: "dev.bglocation", category: "Debug")
26
+ #if os(iOS)
27
+ private var audioSessionConfigured = false
28
+ #endif
29
+
30
+ public init() {}
31
+
32
+ public func configure(debug: Bool, debugSounds: Bool) {
33
+ isEnabled = debug
34
+ soundsEnabled = debugSounds
35
+ if debug && debugSounds {
36
+ configureAudioSession()
37
+ }
38
+ if debug {
39
+ emit("Debug mode enabled (sounds: \(debugSounds))")
40
+ }
41
+ }
42
+
43
+ public func reset() {
44
+ locationCount = 0
45
+ heartbeatCount = 0
46
+ httpSuccessCount = 0
47
+ httpErrorCount = 0
48
+ }
49
+
50
+ // MARK: - Log Methods
51
+
52
+ public func logConfigure(distanceFilter: Double, heartbeat: Int, httpUrl: String?) {
53
+ guard isEnabled else { return }
54
+ let http = httpUrl ?? "disabled"
55
+ emit("CONFIGURE distance=\(distanceFilter)m heartbeat=\(heartbeat)s http=\(http)")
56
+ }
57
+
58
+ public func logStart() {
59
+ guard isEnabled else { return }
60
+ reset()
61
+ emit("START tracking")
62
+ playSound(.start)
63
+ }
64
+
65
+ public func logStop() {
66
+ guard isEnabled else { return }
67
+ emit(
68
+ "STOP tracking \u{2014} locations=\(locationCount) heartbeats=\(heartbeatCount) http_ok=\(httpSuccessCount) http_err=\(httpErrorCount)"
69
+ )
70
+ playSound(.stop)
71
+ }
72
+
73
+ public func logLocation(
74
+ lat: Double, lng: Double, accuracy: Double, speed: Double, isMoving: Bool
75
+ ) {
76
+ guard isEnabled else { return }
77
+ locationCount += 1
78
+ let status = isMoving ? "moving" : "stationary"
79
+ emit(
80
+ "LOCATION #\(locationCount) (\(String(format: "%.5f", lat)), \(String(format: "%.5f", lng))) acc=\(String(format: "%.1f", accuracy))m spd=\(String(format: "%.1f", speed))m/s \(status)"
81
+ )
82
+ playSound(.location)
83
+ }
84
+
85
+ public func logHeartbeat(hasLocation: Bool) {
86
+ guard isEnabled else { return }
87
+ heartbeatCount += 1
88
+ let locStatus = hasLocation ? "with location" : "no location (cold start)"
89
+ emit("HEARTBEAT #\(heartbeatCount) \(locStatus)")
90
+ playSound(.heartbeat)
91
+ }
92
+
93
+ public func logHttp(statusCode: Int, success: Bool, error: String?) {
94
+ guard isEnabled else { return }
95
+ if success {
96
+ httpSuccessCount += 1
97
+ emit("HTTP OK #\(httpSuccessCount) status=\(statusCode)")
98
+ } else {
99
+ httpErrorCount += 1
100
+ let errMsg = error ?? "unknown"
101
+ emit("HTTP ERROR #\(httpErrorCount) status=\(statusCode) error=\(errMsg)")
102
+ playSound(.httpError)
103
+ }
104
+ }
105
+
106
+ public func logPermission(status: String) {
107
+ guard isEnabled else { return }
108
+ emit("PERMISSION \(status)")
109
+ }
110
+
111
+ /// Log an arbitrary debug message. Used for one-off events like SLC restart.
112
+ public func logMessage(_ message: String) {
113
+ guard isEnabled else { return }
114
+ emit(message)
115
+ }
116
+
117
+ // MARK: - Geofence Logging
118
+
119
+ public func logGeofenceAdd(identifier: String) {
120
+ guard isEnabled else { return }
121
+ emit("GEOFENCE ADD \(identifier)")
122
+ playSound(.geofenceAdd)
123
+ }
124
+
125
+ public func logGeofenceAddBatch(count: Int) {
126
+ guard isEnabled else { return }
127
+ emit("GEOFENCE ADD_BATCH count=\(count)")
128
+ playSound(.geofenceAdd)
129
+ }
130
+
131
+ public func logGeofenceRemove(identifier: String) {
132
+ guard isEnabled else { return }
133
+ emit("GEOFENCE REMOVE \(identifier)")
134
+ }
135
+
136
+ public func logGeofenceRemoveAll(count: Int) {
137
+ guard isEnabled else { return }
138
+ emit("GEOFENCE REMOVE_ALL count=\(count)")
139
+ }
140
+
141
+ public func logGeofenceEvent(identifier: String, action: String) {
142
+ guard isEnabled else { return }
143
+ emit("GEOFENCE \(action.uppercased()) \(identifier)")
144
+ playSound(.geofenceEvent)
145
+ }
146
+
147
+ // MARK: - Private
148
+
149
+ private func emit(_ message: String) {
150
+ let prefixed = "[BackgroundLocation] \(message)"
151
+ os_log("%{public}@", log: log, type: .debug, prefixed)
152
+ onDebug?(message)
153
+ }
154
+
155
+ // MARK: - Sounds
156
+
157
+ enum SoundEvent {
158
+ case start, stop, location, heartbeat, httpError, geofenceAdd, geofenceEvent
159
+ }
160
+
161
+ #if os(iOS)
162
+ private func configureAudioSession() {
163
+ guard !audioSessionConfigured else { return }
164
+ do {
165
+ let session = AVAudioSession.sharedInstance()
166
+ try session.setCategory(.playback, options: .mixWithOthers)
167
+ try session.setActive(true)
168
+ audioSessionConfigured = true
169
+ } catch {
170
+ os_log(
171
+ "Audio session setup failed: %{public}@", log: log, type: .error,
172
+ error.localizedDescription)
173
+ }
174
+ }
175
+
176
+ private func playSound(_ event: SoundEvent) {
177
+ guard isEnabled, soundsEnabled else { return }
178
+ let soundId: SystemSoundID
179
+ switch event {
180
+ case .start:
181
+ soundId = 1054 // begin recording
182
+ case .stop:
183
+ soundId = 1055 // end recording
184
+ case .location:
185
+ soundId = 1052 // tink (louder — location is the primary event)
186
+ case .heartbeat:
187
+ soundId = 1057 // tock (quieter — heartbeat is a status ping)
188
+ case .httpError:
189
+ soundId = 1073 // sms alert
190
+ case .geofenceAdd:
191
+ soundId = 1116 // key press click
192
+ case .geofenceEvent:
193
+ soundId = 1117 // key press delete
194
+ }
195
+ AudioServicesPlayAlertSoundWithCompletion(soundId, nil)
196
+ }
197
+ #else
198
+ private func configureAudioSession() {}
199
+ private func playSound(_ event: SoundEvent) {}
200
+ #endif
201
+ }