@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
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,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
|
+
}
|