expo-background-tracking 1.0.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/LICENSE +21 -0
- package/README.md +260 -0
- package/build/BackgroundGeolocation.d.ts +122 -0
- package/build/BackgroundGeolocation.js +901 -0
- package/build/Database.d.ts +30 -0
- package/build/Database.js +206 -0
- package/build/EventBus.d.ts +10 -0
- package/build/EventBus.js +37 -0
- package/build/GeofenceManager.d.ts +42 -0
- package/build/GeofenceManager.js +281 -0
- package/build/HttpService.d.ts +27 -0
- package/build/HttpService.js +265 -0
- package/build/Logger.d.ts +15 -0
- package/build/Logger.js +52 -0
- package/build/MotionDetector.d.ts +40 -0
- package/build/MotionDetector.js +141 -0
- package/build/Scheduler.d.ts +19 -0
- package/build/Scheduler.js +79 -0
- package/build/environment.d.ts +8 -0
- package/build/environment.js +26 -0
- package/build/geo.d.ts +10 -0
- package/build/geo.js +46 -0
- package/build/index.d.ts +6 -0
- package/build/index.js +28 -0
- package/build/types.d.ts +275 -0
- package/build/types.js +56 -0
- package/package.json +351 -0
- package/src/BackgroundGeolocation.ts +1032 -0
- package/src/Database.ts +236 -0
- package/src/EventBus.ts +33 -0
- package/src/GeofenceManager.ts +269 -0
- package/src/HttpService.ts +232 -0
- package/src/Logger.ts +52 -0
- package/src/MotionDetector.ts +149 -0
- package/src/Scheduler.ts +90 -0
- package/src/environment.ts +22 -0
- package/src/geo.ts +57 -0
- package/src/index.ts +7 -0
- package/src/types.ts +410 -0
|
@@ -0,0 +1,901 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.BackgroundGeolocation = exports.LOCATION_TASK = void 0;
|
|
37
|
+
const ExpoLocation = __importStar(require("expo-location"));
|
|
38
|
+
const TaskManager = __importStar(require("expo-task-manager"));
|
|
39
|
+
const Battery = __importStar(require("expo-battery"));
|
|
40
|
+
const Device = __importStar(require("expo-device"));
|
|
41
|
+
const react_native_1 = require("react-native");
|
|
42
|
+
const expo_sensors_1 = require("expo-sensors");
|
|
43
|
+
const Database_1 = require("./Database");
|
|
44
|
+
const Logger_1 = require("./Logger");
|
|
45
|
+
const EventBus_1 = require("./EventBus");
|
|
46
|
+
const HttpService_1 = require("./HttpService");
|
|
47
|
+
const MotionDetector_1 = require("./MotionDetector");
|
|
48
|
+
const GeofenceManager_1 = require("./GeofenceManager");
|
|
49
|
+
const Scheduler_1 = require("./Scheduler");
|
|
50
|
+
const geo_1 = require("./geo");
|
|
51
|
+
const environment_1 = require("./environment");
|
|
52
|
+
const types_1 = require("./types");
|
|
53
|
+
exports.LOCATION_TASK = 'expo-background-tracking.location';
|
|
54
|
+
const DEFAULT_CONFIG = {
|
|
55
|
+
desiredAccuracy: types_1.DesiredAccuracy.HIGH,
|
|
56
|
+
distanceFilter: 10,
|
|
57
|
+
stationaryRadius: 25,
|
|
58
|
+
stopTimeout: 5,
|
|
59
|
+
motionTriggerDelay: 0,
|
|
60
|
+
disableMotionActivityUpdates: false,
|
|
61
|
+
disableStopDetection: false,
|
|
62
|
+
isMoving: false,
|
|
63
|
+
elasticityMultiplier: 1,
|
|
64
|
+
disableElasticity: false,
|
|
65
|
+
geofenceProximityRadius: 1000,
|
|
66
|
+
heartbeatInterval: 60,
|
|
67
|
+
autoSync: true,
|
|
68
|
+
autoSyncThreshold: 0,
|
|
69
|
+
batchSync: false,
|
|
70
|
+
maxBatchSize: -1,
|
|
71
|
+
method: 'POST',
|
|
72
|
+
httpRootProperty: 'location',
|
|
73
|
+
httpTimeout: 60000,
|
|
74
|
+
maxDaysToPersist: 1,
|
|
75
|
+
maxRecordsToPersist: -1,
|
|
76
|
+
persistMode: types_1.PersistMode.ALL,
|
|
77
|
+
stopOnTerminate: true,
|
|
78
|
+
startOnBoot: false,
|
|
79
|
+
debug: false,
|
|
80
|
+
logLevel: types_1.LogLevel.INFO,
|
|
81
|
+
logMaxDays: 3,
|
|
82
|
+
locationAuthorizationRequest: 'Always',
|
|
83
|
+
foregroundService: true,
|
|
84
|
+
};
|
|
85
|
+
class BackgroundGeolocationImpl {
|
|
86
|
+
constructor() {
|
|
87
|
+
// re-exported enums (parity with transistorsoft statics)
|
|
88
|
+
this.DESIRED_ACCURACY_NAVIGATION = types_1.DesiredAccuracy.NAVIGATION;
|
|
89
|
+
this.DESIRED_ACCURACY_HIGH = types_1.DesiredAccuracy.HIGH;
|
|
90
|
+
this.DESIRED_ACCURACY_MEDIUM = types_1.DesiredAccuracy.MEDIUM;
|
|
91
|
+
this.DESIRED_ACCURACY_LOW = types_1.DesiredAccuracy.LOW;
|
|
92
|
+
this.DESIRED_ACCURACY_VERY_LOW = types_1.DesiredAccuracy.VERY_LOW;
|
|
93
|
+
this.LOG_LEVEL_OFF = types_1.LogLevel.OFF;
|
|
94
|
+
this.LOG_LEVEL_ERROR = types_1.LogLevel.ERROR;
|
|
95
|
+
this.LOG_LEVEL_WARNING = types_1.LogLevel.WARNING;
|
|
96
|
+
this.LOG_LEVEL_INFO = types_1.LogLevel.INFO;
|
|
97
|
+
this.LOG_LEVEL_DEBUG = types_1.LogLevel.DEBUG;
|
|
98
|
+
this.LOG_LEVEL_VERBOSE = types_1.LogLevel.VERBOSE;
|
|
99
|
+
this.PERSIST_MODE_ALL = types_1.PersistMode.ALL;
|
|
100
|
+
this.PERSIST_MODE_LOCATION = types_1.PersistMode.LOCATION;
|
|
101
|
+
this.PERSIST_MODE_GEOFENCE = types_1.PersistMode.GEOFENCE;
|
|
102
|
+
this.PERSIST_MODE_NONE = types_1.PersistMode.NONE;
|
|
103
|
+
this.AUTHORIZATION_STATUS_ALWAYS = types_1.AuthorizationStatus.ALWAYS;
|
|
104
|
+
this.AUTHORIZATION_STATUS_WHEN_IN_USE = types_1.AuthorizationStatus.WHEN_IN_USE;
|
|
105
|
+
this.AUTHORIZATION_STATUS_DENIED = types_1.AuthorizationStatus.DENIED;
|
|
106
|
+
this.logger = Logger_1.logger;
|
|
107
|
+
this.bus = new EventBus_1.EventBus();
|
|
108
|
+
this.http = new HttpService_1.HttpService(this.bus);
|
|
109
|
+
this.geofenceManager = new GeofenceManager_1.GeofenceManager(this.bus);
|
|
110
|
+
this.config = { ...DEFAULT_CONFIG };
|
|
111
|
+
this.enabled = false;
|
|
112
|
+
this.trackingMode = 1;
|
|
113
|
+
this.odometer = 0;
|
|
114
|
+
this.lastLocation = null;
|
|
115
|
+
this.lastPersistedCoords = null;
|
|
116
|
+
this.isReady = false;
|
|
117
|
+
this.foregroundSub = null;
|
|
118
|
+
this.watchSub = null;
|
|
119
|
+
this.heartbeatTimer = null;
|
|
120
|
+
this.providerTimer = null;
|
|
121
|
+
this.powerSaveSub = null;
|
|
122
|
+
this.lastProviderState = null;
|
|
123
|
+
this.headlessTask = null;
|
|
124
|
+
this.bgTaskCounter = 0;
|
|
125
|
+
this.motion = new MotionDetector_1.MotionDetector({
|
|
126
|
+
stopTimeoutMinutes: DEFAULT_CONFIG.stopTimeout,
|
|
127
|
+
motionTriggerDelayMs: DEFAULT_CONFIG.motionTriggerDelay,
|
|
128
|
+
disabled: false,
|
|
129
|
+
onMotionChange: (isMoving) => void this.handleMotionChange(isMoving),
|
|
130
|
+
onActivityChange: (activity) => this.bus.emit('activitychange', activity),
|
|
131
|
+
});
|
|
132
|
+
this.scheduler = new Scheduler_1.Scheduler((scheduleEnabled) => {
|
|
133
|
+
void (async () => {
|
|
134
|
+
if (scheduleEnabled && !this.enabled)
|
|
135
|
+
await this.start();
|
|
136
|
+
else if (!scheduleEnabled && this.enabled)
|
|
137
|
+
await this.stop();
|
|
138
|
+
const state = await this.getState();
|
|
139
|
+
this.bus.emit('schedule', { enabled: scheduleEnabled, state });
|
|
140
|
+
})();
|
|
141
|
+
});
|
|
142
|
+
try {
|
|
143
|
+
this.defineTasks();
|
|
144
|
+
}
|
|
145
|
+
catch (e) {
|
|
146
|
+
// never crash the app at import time (e.g. exotic environments)
|
|
147
|
+
console.warn('[expo-background-tracking] defineTasks failed:', e);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
// -------------------------------------------------------------------------
|
|
151
|
+
// TaskManager background tasks (effective in dev builds; inert in Expo Go)
|
|
152
|
+
// -------------------------------------------------------------------------
|
|
153
|
+
defineTasks() {
|
|
154
|
+
if (!TaskManager.isTaskDefined(exports.LOCATION_TASK)) {
|
|
155
|
+
TaskManager.defineTask(exports.LOCATION_TASK, async ({ data, error }) => {
|
|
156
|
+
if (error) {
|
|
157
|
+
Logger_1.logger.error(`Background location task error: ${error.message}`);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const { locations } = (data ?? {});
|
|
161
|
+
if (!locations?.length)
|
|
162
|
+
return;
|
|
163
|
+
for (const raw of locations) {
|
|
164
|
+
const location = await this.buildLocation(raw);
|
|
165
|
+
await this.processLocation(location);
|
|
166
|
+
await this.dispatchHeadless({ name: 'location', params: location });
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
if (!TaskManager.isTaskDefined(GeofenceManager_1.GEOFENCE_TASK)) {
|
|
171
|
+
TaskManager.defineTask(GeofenceManager_1.GEOFENCE_TASK, async ({ data, error }) => {
|
|
172
|
+
if (error) {
|
|
173
|
+
Logger_1.logger.error(`Geofence task error: ${error.message}`);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const event = data;
|
|
177
|
+
const action = event.eventType === ExpoLocation.GeofencingEventType.Enter ? 'ENTER' : 'EXIT';
|
|
178
|
+
const location = this.lastLocation ?? (await this.fetchCurrentLocation().catch(() => null));
|
|
179
|
+
if (location && event.region.identifier) {
|
|
180
|
+
await this.geofenceManager.onNativeEvent(event.region.identifier, action, location);
|
|
181
|
+
await this.dispatchHeadless({
|
|
182
|
+
name: 'geofence',
|
|
183
|
+
params: { identifier: event.region.identifier, action, location },
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async dispatchHeadless(event) {
|
|
190
|
+
if (this.headlessTask) {
|
|
191
|
+
try {
|
|
192
|
+
await this.headlessTask(event);
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
Logger_1.logger.error(`Headless task error: ${String(e)}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
// -------------------------------------------------------------------------
|
|
200
|
+
// Lifecycle
|
|
201
|
+
// -------------------------------------------------------------------------
|
|
202
|
+
async ready(config = {}) {
|
|
203
|
+
if (this.isReady && !config.reset)
|
|
204
|
+
return this.getState();
|
|
205
|
+
const persisted = config.reset
|
|
206
|
+
? null
|
|
207
|
+
: await Database_1.database.getKV('config');
|
|
208
|
+
this.config = { ...DEFAULT_CONFIG, ...(persisted ?? {}), ...config };
|
|
209
|
+
await Database_1.database.setKV('config', this.config);
|
|
210
|
+
Logger_1.logger.level = this.config.logLevel ?? types_1.LogLevel.INFO;
|
|
211
|
+
Logger_1.logger.debug = this.config.debug ?? false;
|
|
212
|
+
this.http.setConfig(this.config);
|
|
213
|
+
this.geofenceManager.setProximityRadius(this.config.geofenceProximityRadius ?? 1000);
|
|
214
|
+
this.motion.setOptions({
|
|
215
|
+
stopTimeoutMinutes: this.config.stopTimeout ?? 5,
|
|
216
|
+
motionTriggerDelayMs: this.config.motionTriggerDelay ?? 0,
|
|
217
|
+
disabled: this.config.disableMotionActivityUpdates ?? false,
|
|
218
|
+
});
|
|
219
|
+
if (this.config.schedule?.length)
|
|
220
|
+
this.scheduler.setSchedule(this.config.schedule);
|
|
221
|
+
this.odometer = (await Database_1.database.getKV('odometer')) ?? 0;
|
|
222
|
+
const wasEnabled = (await Database_1.database.getKV('enabled')) ?? false;
|
|
223
|
+
this.trackingMode = ((await Database_1.database.getKV('trackingMode')) ?? 1);
|
|
224
|
+
await Database_1.database.prune(this.config.maxDaysToPersist ?? 1, this.config.maxRecordsToPersist ?? -1);
|
|
225
|
+
await Database_1.database.pruneLogs(this.config.logMaxDays ?? 3);
|
|
226
|
+
this.http.startConnectivityMonitoring();
|
|
227
|
+
this.startProviderMonitoring();
|
|
228
|
+
void this.startPowerSaveMonitoring();
|
|
229
|
+
this.isReady = true;
|
|
230
|
+
Logger_1.logger.info(`ready() — env=${(0, environment_1.isExpoGo)() ? 'Expo Go (foreground-only)' : 'dev build (background OK)'}`);
|
|
231
|
+
if (wasEnabled && this.config.stopOnTerminate === false) {
|
|
232
|
+
// restore tracking after app relaunch
|
|
233
|
+
if (this.trackingMode === 1)
|
|
234
|
+
await this.start();
|
|
235
|
+
else
|
|
236
|
+
await this.startGeofences();
|
|
237
|
+
}
|
|
238
|
+
return this.getState();
|
|
239
|
+
}
|
|
240
|
+
async start() {
|
|
241
|
+
this.assertReady('start');
|
|
242
|
+
this.trackingMode = 1;
|
|
243
|
+
await Database_1.database.setKV('trackingMode', 1);
|
|
244
|
+
await this.requestPermission();
|
|
245
|
+
await this.startForegroundWatch();
|
|
246
|
+
await this.startBackgroundUpdates();
|
|
247
|
+
await this.geofenceManager.start();
|
|
248
|
+
await this.motion.start();
|
|
249
|
+
this.startHeartbeat();
|
|
250
|
+
this.setEnabled(true);
|
|
251
|
+
// fire an immediate motionchange location, like transistorsoft
|
|
252
|
+
const location = await this.fetchCurrentLocation().catch(() => null);
|
|
253
|
+
if (location) {
|
|
254
|
+
location.event = 'motionchange';
|
|
255
|
+
await this.processLocation(location);
|
|
256
|
+
}
|
|
257
|
+
return this.getState();
|
|
258
|
+
}
|
|
259
|
+
async stop() {
|
|
260
|
+
this.foregroundSub?.remove();
|
|
261
|
+
this.foregroundSub = null;
|
|
262
|
+
this.stopHeartbeat();
|
|
263
|
+
this.motion.stop();
|
|
264
|
+
await this.geofenceManager.stop();
|
|
265
|
+
if ((0, environment_1.isBackgroundCapable)()) {
|
|
266
|
+
try {
|
|
267
|
+
const started = await ExpoLocation.hasStartedLocationUpdatesAsync(exports.LOCATION_TASK);
|
|
268
|
+
if (started)
|
|
269
|
+
await ExpoLocation.stopLocationUpdatesAsync(exports.LOCATION_TASK);
|
|
270
|
+
}
|
|
271
|
+
catch {
|
|
272
|
+
/* ignore */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
this.setEnabled(false);
|
|
276
|
+
return this.getState();
|
|
277
|
+
}
|
|
278
|
+
/** Geofences-only mode: no continuous location tracking. */
|
|
279
|
+
async startGeofences() {
|
|
280
|
+
this.assertReady('startGeofences');
|
|
281
|
+
this.trackingMode = 0;
|
|
282
|
+
await Database_1.database.setKV('trackingMode', 0);
|
|
283
|
+
await this.requestPermission();
|
|
284
|
+
await this.geofenceManager.start();
|
|
285
|
+
// low-power significant-movement watch to evaluate software geofences
|
|
286
|
+
await this.startForegroundWatch(/* lowPower */ true);
|
|
287
|
+
this.setEnabled(true);
|
|
288
|
+
return this.getState();
|
|
289
|
+
}
|
|
290
|
+
startSchedule() {
|
|
291
|
+
this.assertReady('startSchedule');
|
|
292
|
+
if (this.config.schedule?.length)
|
|
293
|
+
this.scheduler.setSchedule(this.config.schedule);
|
|
294
|
+
this.scheduler.start();
|
|
295
|
+
return this.getState();
|
|
296
|
+
}
|
|
297
|
+
stopSchedule() {
|
|
298
|
+
this.scheduler.stop();
|
|
299
|
+
return this.getState();
|
|
300
|
+
}
|
|
301
|
+
async changePace(isMoving) {
|
|
302
|
+
this.assertReady('changePace');
|
|
303
|
+
this.motion.setMoving(isMoving);
|
|
304
|
+
}
|
|
305
|
+
setEnabled(enabled) {
|
|
306
|
+
if (this.enabled === enabled)
|
|
307
|
+
return;
|
|
308
|
+
this.enabled = enabled;
|
|
309
|
+
void Database_1.database.setKV('enabled', enabled);
|
|
310
|
+
this.bus.emit('enabledchange', enabled);
|
|
311
|
+
Logger_1.logger.info(`enabledchange: ${enabled}`);
|
|
312
|
+
}
|
|
313
|
+
assertReady(method) {
|
|
314
|
+
if (!this.isReady) {
|
|
315
|
+
throw new Error(`BackgroundGeolocation.${method}() called before ready()`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// -------------------------------------------------------------------------
|
|
319
|
+
// Location streams
|
|
320
|
+
// -------------------------------------------------------------------------
|
|
321
|
+
mapAccuracy(a) {
|
|
322
|
+
switch (a) {
|
|
323
|
+
case types_1.DesiredAccuracy.NAVIGATION:
|
|
324
|
+
return ExpoLocation.LocationAccuracy.BestForNavigation;
|
|
325
|
+
case types_1.DesiredAccuracy.HIGH:
|
|
326
|
+
return ExpoLocation.LocationAccuracy.Highest;
|
|
327
|
+
case types_1.DesiredAccuracy.MEDIUM:
|
|
328
|
+
return ExpoLocation.LocationAccuracy.Balanced;
|
|
329
|
+
case types_1.DesiredAccuracy.LOW:
|
|
330
|
+
return ExpoLocation.LocationAccuracy.Low;
|
|
331
|
+
case types_1.DesiredAccuracy.VERY_LOW:
|
|
332
|
+
case types_1.DesiredAccuracy.LOWEST:
|
|
333
|
+
return ExpoLocation.LocationAccuracy.Lowest;
|
|
334
|
+
default:
|
|
335
|
+
return ExpoLocation.LocationAccuracy.High;
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
async startForegroundWatch(lowPower = false) {
|
|
339
|
+
this.foregroundSub?.remove();
|
|
340
|
+
this.foregroundSub = await ExpoLocation.watchPositionAsync({
|
|
341
|
+
accuracy: lowPower
|
|
342
|
+
? ExpoLocation.LocationAccuracy.Balanced
|
|
343
|
+
: this.mapAccuracy(this.config.desiredAccuracy),
|
|
344
|
+
distanceInterval: this.effectiveDistanceFilter(),
|
|
345
|
+
timeInterval: this.config.deferTime,
|
|
346
|
+
}, (raw) => {
|
|
347
|
+
void (async () => {
|
|
348
|
+
const location = await this.buildLocation(raw);
|
|
349
|
+
await this.processLocation(location);
|
|
350
|
+
})();
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async startBackgroundUpdates() {
|
|
354
|
+
if (!(0, environment_1.isBackgroundCapable)()) {
|
|
355
|
+
Logger_1.logger.warn('Expo Go detected: background tracking unavailable (foreground-only). ' +
|
|
356
|
+
'Use a development build for full background support.');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
try {
|
|
360
|
+
await ExpoLocation.startLocationUpdatesAsync(exports.LOCATION_TASK, {
|
|
361
|
+
accuracy: this.mapAccuracy(this.config.desiredAccuracy),
|
|
362
|
+
distanceInterval: this.effectiveDistanceFilter(),
|
|
363
|
+
deferredUpdatesInterval: this.config.deferTime,
|
|
364
|
+
pausesUpdatesAutomatically: this.config.pausesLocationUpdatesAutomatically,
|
|
365
|
+
showsBackgroundLocationIndicator: this.config.showsBackgroundLocationIndicator,
|
|
366
|
+
activityType: this.config.activityType,
|
|
367
|
+
foregroundService: react_native_1.Platform.OS === 'android' && this.config.foregroundService !== false
|
|
368
|
+
? {
|
|
369
|
+
notificationTitle: this.config.notification?.title ?? 'Location tracking',
|
|
370
|
+
notificationBody: this.config.notification?.text ?? 'Tracking is active',
|
|
371
|
+
notificationColor: this.config.notification?.color,
|
|
372
|
+
killServiceOnDestroy: this.config.stopOnTerminate !== false,
|
|
373
|
+
}
|
|
374
|
+
: undefined,
|
|
375
|
+
});
|
|
376
|
+
Logger_1.logger.info('Background location updates started');
|
|
377
|
+
}
|
|
378
|
+
catch (e) {
|
|
379
|
+
Logger_1.logger.error(`startLocationUpdatesAsync failed: ${String(e)}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
effectiveDistanceFilter() {
|
|
383
|
+
const base = this.config.distanceFilter ?? 10;
|
|
384
|
+
if (this.config.disableElasticity)
|
|
385
|
+
return base;
|
|
386
|
+
const speed = this.lastLocation?.coords.speed ?? 0;
|
|
387
|
+
if (speed <= 0)
|
|
388
|
+
return base;
|
|
389
|
+
// elasticity: scale filter with speed, like transistorsoft
|
|
390
|
+
const multiplier = this.config.elasticityMultiplier ?? 1;
|
|
391
|
+
return Math.round(base + (Math.max(0, speed) * multiplier * base) / 10);
|
|
392
|
+
}
|
|
393
|
+
async buildLocation(raw, extras, sample = false) {
|
|
394
|
+
let batteryLevel = -1;
|
|
395
|
+
let charging = false;
|
|
396
|
+
try {
|
|
397
|
+
batteryLevel = await Battery.getBatteryLevelAsync();
|
|
398
|
+
const state = await Battery.getBatteryStateAsync();
|
|
399
|
+
charging =
|
|
400
|
+
state === Battery.BatteryState.CHARGING ||
|
|
401
|
+
state === Battery.BatteryState.FULL;
|
|
402
|
+
}
|
|
403
|
+
catch {
|
|
404
|
+
/* battery info unavailable */
|
|
405
|
+
}
|
|
406
|
+
const prev = this.lastPersistedCoords;
|
|
407
|
+
if (prev && this.motion.moving && !sample) {
|
|
408
|
+
this.odometer += (0, geo_1.haversine)(prev.latitude, prev.longitude, raw.coords.latitude, raw.coords.longitude);
|
|
409
|
+
}
|
|
410
|
+
this.motion.feedSpeed(raw.coords.speed);
|
|
411
|
+
return {
|
|
412
|
+
uuid: (0, geo_1.uuidv4)(),
|
|
413
|
+
timestamp: new Date(raw.timestamp).toISOString(),
|
|
414
|
+
odometer: Math.round(this.odometer * 100) / 100,
|
|
415
|
+
is_moving: this.motion.moving,
|
|
416
|
+
coords: {
|
|
417
|
+
latitude: raw.coords.latitude,
|
|
418
|
+
longitude: raw.coords.longitude,
|
|
419
|
+
accuracy: raw.coords.accuracy ?? -1,
|
|
420
|
+
speed: raw.coords.speed ?? -1,
|
|
421
|
+
heading: raw.coords.heading ?? -1,
|
|
422
|
+
altitude: raw.coords.altitude ?? -1,
|
|
423
|
+
altitude_accuracy: raw.coords.altitudeAccuracy ?? undefined,
|
|
424
|
+
},
|
|
425
|
+
activity: { activity: this.motion.moving ? 'on_foot' : 'still', confidence: 75 },
|
|
426
|
+
battery: { level: batteryLevel, is_charging: charging },
|
|
427
|
+
extras: { ...(this.config.extras ?? {}), ...(extras ?? {}) },
|
|
428
|
+
mock: raw.mocked,
|
|
429
|
+
sample,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
shouldPersist(location) {
|
|
433
|
+
if (location.sample)
|
|
434
|
+
return false;
|
|
435
|
+
const mode = this.config.persistMode ?? types_1.PersistMode.ALL;
|
|
436
|
+
if (mode === types_1.PersistMode.NONE)
|
|
437
|
+
return false;
|
|
438
|
+
if (mode === types_1.PersistMode.GEOFENCE && location.event !== 'geofence')
|
|
439
|
+
return false;
|
|
440
|
+
if (mode === types_1.PersistMode.LOCATION && location.event === 'geofence')
|
|
441
|
+
return false;
|
|
442
|
+
return true;
|
|
443
|
+
}
|
|
444
|
+
async processLocation(location) {
|
|
445
|
+
this.lastLocation = location;
|
|
446
|
+
this.lastPersistedCoords = {
|
|
447
|
+
latitude: location.coords.latitude,
|
|
448
|
+
longitude: location.coords.longitude,
|
|
449
|
+
};
|
|
450
|
+
await Database_1.database.setKV('odometer', this.odometer);
|
|
451
|
+
if (this.shouldPersist(location)) {
|
|
452
|
+
await Database_1.database.insertLocation(location);
|
|
453
|
+
await Database_1.database.prune(this.config.maxDaysToPersist ?? 1, this.config.maxRecordsToPersist ?? -1);
|
|
454
|
+
}
|
|
455
|
+
this.bus.emit('location', location);
|
|
456
|
+
await this.geofenceManager.evaluate(location);
|
|
457
|
+
if (this.config.url && this.config.autoSync !== false) {
|
|
458
|
+
void this.http.autoSync().catch(() => undefined);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async handleMotionChange(isMoving) {
|
|
462
|
+
Logger_1.logger.info(`motionchange: ${isMoving ? 'moving' : 'stationary'}`);
|
|
463
|
+
const location = (await this.fetchCurrentLocation().catch(() => null)) ?? this.lastLocation;
|
|
464
|
+
if (location) {
|
|
465
|
+
location.event = 'motionchange';
|
|
466
|
+
location.is_moving = isMoving;
|
|
467
|
+
this.bus.emit('motionchange', { isMoving, location });
|
|
468
|
+
if (this.enabled)
|
|
469
|
+
await this.processLocation(location);
|
|
470
|
+
}
|
|
471
|
+
// battery optimization: relax accuracy while stationary
|
|
472
|
+
if (this.enabled && this.trackingMode === 1) {
|
|
473
|
+
if (!isMoving && this.config.stopOnStationary) {
|
|
474
|
+
await this.stop();
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
await this.startForegroundWatch(!isMoving);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async fetchCurrentLocation() {
|
|
482
|
+
const raw = await ExpoLocation.getCurrentPositionAsync({
|
|
483
|
+
accuracy: this.mapAccuracy(this.config.desiredAccuracy),
|
|
484
|
+
});
|
|
485
|
+
return this.buildLocation(raw);
|
|
486
|
+
}
|
|
487
|
+
// -------------------------------------------------------------------------
|
|
488
|
+
// getCurrentPosition / watchPosition
|
|
489
|
+
// -------------------------------------------------------------------------
|
|
490
|
+
async getCurrentPosition(options = {}) {
|
|
491
|
+
const { timeout = 30, maximumAge = 0, samples = 3, persist = true } = options;
|
|
492
|
+
if (maximumAge > 0 && this.lastLocation) {
|
|
493
|
+
const age = Date.now() - Date.parse(this.lastLocation.timestamp);
|
|
494
|
+
if (age <= maximumAge)
|
|
495
|
+
return this.lastLocation;
|
|
496
|
+
}
|
|
497
|
+
const fetchOne = async () => {
|
|
498
|
+
const raw = await ExpoLocation.getCurrentPositionAsync({
|
|
499
|
+
accuracy: this.mapAccuracy(this.config.desiredAccuracy),
|
|
500
|
+
});
|
|
501
|
+
return this.buildLocation(raw, options.extras, true);
|
|
502
|
+
};
|
|
503
|
+
const withTimeout = (p) => Promise.race([
|
|
504
|
+
p,
|
|
505
|
+
new Promise((_, reject) => setTimeout(() => reject(Object.assign(new Error('timeout'), { code: 408 })), timeout * 1000)),
|
|
506
|
+
]);
|
|
507
|
+
let best = null;
|
|
508
|
+
const n = Math.max(1, Math.min(samples, 10));
|
|
509
|
+
for (let i = 0; i < n; i++) {
|
|
510
|
+
const loc = await withTimeout(fetchOne());
|
|
511
|
+
if (!best ||
|
|
512
|
+
(loc.coords.accuracy > 0 && loc.coords.accuracy < best.coords.accuracy)) {
|
|
513
|
+
best = loc;
|
|
514
|
+
}
|
|
515
|
+
if (options.desiredAccuracy &&
|
|
516
|
+
best.coords.accuracy > 0 &&
|
|
517
|
+
best.coords.accuracy <= options.desiredAccuracy) {
|
|
518
|
+
break;
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
const result = { ...best, sample: false };
|
|
522
|
+
if (persist && this.shouldPersist(result)) {
|
|
523
|
+
await Database_1.database.insertLocation(result);
|
|
524
|
+
if (this.config.url && this.config.autoSync !== false) {
|
|
525
|
+
void this.http.autoSync().catch(() => undefined);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
this.bus.emit('location', result);
|
|
529
|
+
return result;
|
|
530
|
+
}
|
|
531
|
+
async watchPosition(success, failure, options = {}) {
|
|
532
|
+
try {
|
|
533
|
+
this.watchSub?.remove();
|
|
534
|
+
this.watchSub = await ExpoLocation.watchPositionAsync({
|
|
535
|
+
accuracy: this.mapAccuracy(options.desiredAccuracy ?? this.config.desiredAccuracy),
|
|
536
|
+
timeInterval: options.interval ?? 1000,
|
|
537
|
+
distanceInterval: 0,
|
|
538
|
+
}, (raw) => {
|
|
539
|
+
void (async () => {
|
|
540
|
+
const location = await this.buildLocation(raw, options.extras, !options.persist);
|
|
541
|
+
if (options.persist)
|
|
542
|
+
await Database_1.database.insertLocation(location);
|
|
543
|
+
success(location);
|
|
544
|
+
})();
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
catch (e) {
|
|
548
|
+
failure?.(e);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
async stopWatchPosition() {
|
|
552
|
+
this.watchSub?.remove();
|
|
553
|
+
this.watchSub = null;
|
|
554
|
+
}
|
|
555
|
+
// -------------------------------------------------------------------------
|
|
556
|
+
// State & config
|
|
557
|
+
// -------------------------------------------------------------------------
|
|
558
|
+
async getState() {
|
|
559
|
+
return {
|
|
560
|
+
...this.config,
|
|
561
|
+
enabled: this.enabled,
|
|
562
|
+
isMoving: this.motion.moving,
|
|
563
|
+
schedulerEnabled: this.scheduler.enabled,
|
|
564
|
+
trackingMode: this.trackingMode,
|
|
565
|
+
odometer: Math.round(this.odometer * 100) / 100,
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
async setConfig(config) {
|
|
569
|
+
this.config = { ...this.config, ...config };
|
|
570
|
+
await Database_1.database.setKV('config', this.config);
|
|
571
|
+
Logger_1.logger.level = this.config.logLevel ?? types_1.LogLevel.INFO;
|
|
572
|
+
Logger_1.logger.debug = this.config.debug ?? false;
|
|
573
|
+
this.http.setConfig(this.config);
|
|
574
|
+
this.geofenceManager.setProximityRadius(this.config.geofenceProximityRadius ?? 1000);
|
|
575
|
+
this.motion.setOptions({
|
|
576
|
+
stopTimeoutMinutes: this.config.stopTimeout ?? 5,
|
|
577
|
+
motionTriggerDelayMs: this.config.motionTriggerDelay ?? 0,
|
|
578
|
+
disabled: this.config.disableMotionActivityUpdates ?? false,
|
|
579
|
+
});
|
|
580
|
+
if (config.schedule)
|
|
581
|
+
this.scheduler.setSchedule(config.schedule);
|
|
582
|
+
if (this.enabled && this.trackingMode === 1) {
|
|
583
|
+
await this.startForegroundWatch();
|
|
584
|
+
await this.startBackgroundUpdates();
|
|
585
|
+
}
|
|
586
|
+
return this.getState();
|
|
587
|
+
}
|
|
588
|
+
async reset(config = {}) {
|
|
589
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
590
|
+
await Database_1.database.setKV('config', this.config);
|
|
591
|
+
return this.getState();
|
|
592
|
+
}
|
|
593
|
+
// -------------------------------------------------------------------------
|
|
594
|
+
// Persistence API
|
|
595
|
+
// -------------------------------------------------------------------------
|
|
596
|
+
getLocations(query) {
|
|
597
|
+
return Database_1.database.getLocations(query);
|
|
598
|
+
}
|
|
599
|
+
getCount() {
|
|
600
|
+
return Database_1.database.getCount();
|
|
601
|
+
}
|
|
602
|
+
async insertLocation(location) {
|
|
603
|
+
const record = {
|
|
604
|
+
uuid: location.uuid ?? (0, geo_1.uuidv4)(),
|
|
605
|
+
timestamp: location.timestamp ?? new Date().toISOString(),
|
|
606
|
+
odometer: location.odometer ?? this.odometer,
|
|
607
|
+
is_moving: location.is_moving ?? this.motion.moving,
|
|
608
|
+
coords: location.coords ?? {
|
|
609
|
+
latitude: 0,
|
|
610
|
+
longitude: 0,
|
|
611
|
+
accuracy: -1,
|
|
612
|
+
speed: -1,
|
|
613
|
+
heading: -1,
|
|
614
|
+
altitude: -1,
|
|
615
|
+
},
|
|
616
|
+
activity: location.activity ?? { activity: 'unknown', confidence: 0 },
|
|
617
|
+
battery: location.battery ?? { level: -1, is_charging: false },
|
|
618
|
+
extras: location.extras,
|
|
619
|
+
};
|
|
620
|
+
await Database_1.database.insertLocation(record);
|
|
621
|
+
return record.uuid;
|
|
622
|
+
}
|
|
623
|
+
async destroyLocations() {
|
|
624
|
+
await Database_1.database.destroyLocations();
|
|
625
|
+
}
|
|
626
|
+
async destroyLocation(uuid) {
|
|
627
|
+
await Database_1.database.destroyLocation(uuid);
|
|
628
|
+
}
|
|
629
|
+
sync() {
|
|
630
|
+
return this.http.sync();
|
|
631
|
+
}
|
|
632
|
+
// -------------------------------------------------------------------------
|
|
633
|
+
// Odometer
|
|
634
|
+
// -------------------------------------------------------------------------
|
|
635
|
+
async getOdometer() {
|
|
636
|
+
return Math.round(this.odometer * 100) / 100;
|
|
637
|
+
}
|
|
638
|
+
async setOdometer(value) {
|
|
639
|
+
this.odometer = value;
|
|
640
|
+
await Database_1.database.setKV('odometer', value);
|
|
641
|
+
return this.getCurrentPosition({ persist: false, samples: 1 });
|
|
642
|
+
}
|
|
643
|
+
resetOdometer() {
|
|
644
|
+
return this.setOdometer(0);
|
|
645
|
+
}
|
|
646
|
+
// -------------------------------------------------------------------------
|
|
647
|
+
// Geofencing API
|
|
648
|
+
// -------------------------------------------------------------------------
|
|
649
|
+
addGeofence(geofence) {
|
|
650
|
+
return this.geofenceManager.add(geofence);
|
|
651
|
+
}
|
|
652
|
+
addGeofences(geofences) {
|
|
653
|
+
return this.geofenceManager.addMany(geofences);
|
|
654
|
+
}
|
|
655
|
+
removeGeofence(identifier) {
|
|
656
|
+
return this.geofenceManager.remove(identifier);
|
|
657
|
+
}
|
|
658
|
+
removeGeofences(identifiers) {
|
|
659
|
+
return this.geofenceManager.removeAll(identifiers);
|
|
660
|
+
}
|
|
661
|
+
getGeofences() {
|
|
662
|
+
return this.geofenceManager.getGeofences();
|
|
663
|
+
}
|
|
664
|
+
getGeofence(identifier) {
|
|
665
|
+
return this.geofenceManager.getGeofence(identifier);
|
|
666
|
+
}
|
|
667
|
+
geofenceExists(identifier) {
|
|
668
|
+
return this.geofenceManager.exists(identifier);
|
|
669
|
+
}
|
|
670
|
+
// -------------------------------------------------------------------------
|
|
671
|
+
// Device / sensors / power
|
|
672
|
+
// -------------------------------------------------------------------------
|
|
673
|
+
async getSensors() {
|
|
674
|
+
const [acc, gyro, mag] = await Promise.all([
|
|
675
|
+
expo_sensors_1.Accelerometer.isAvailableAsync().catch(() => false),
|
|
676
|
+
expo_sensors_1.Gyroscope.isAvailableAsync().catch(() => false),
|
|
677
|
+
expo_sensors_1.Magnetometer.isAvailableAsync().catch(() => false),
|
|
678
|
+
]);
|
|
679
|
+
return {
|
|
680
|
+
platform: react_native_1.Platform.OS,
|
|
681
|
+
accelerometer: acc,
|
|
682
|
+
gyroscope: gyro,
|
|
683
|
+
magnetometer: mag,
|
|
684
|
+
motion_hardware: acc && gyro,
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
async getDeviceInfo() {
|
|
688
|
+
return {
|
|
689
|
+
model: Device.modelName ?? 'unknown',
|
|
690
|
+
manufacturer: Device.manufacturer ?? 'unknown',
|
|
691
|
+
version: Device.osVersion ?? 'unknown',
|
|
692
|
+
platform: react_native_1.Platform.OS,
|
|
693
|
+
framework: 'react-native (expo)',
|
|
694
|
+
};
|
|
695
|
+
}
|
|
696
|
+
async isPowerSaveMode() {
|
|
697
|
+
try {
|
|
698
|
+
return await Battery.isLowPowerModeEnabledAsync();
|
|
699
|
+
}
|
|
700
|
+
catch {
|
|
701
|
+
return false;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
async startPowerSaveMonitoring() {
|
|
705
|
+
try {
|
|
706
|
+
this.powerSaveSub?.remove();
|
|
707
|
+
this.powerSaveSub = Battery.addLowPowerModeListener(({ lowPowerMode }) => {
|
|
708
|
+
this.bus.emit('powersavechange', lowPowerMode);
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
/* unsupported */
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
// -------------------------------------------------------------------------
|
|
716
|
+
// Permissions / provider
|
|
717
|
+
// -------------------------------------------------------------------------
|
|
718
|
+
async requestPermission() {
|
|
719
|
+
const fg = await ExpoLocation.requestForegroundPermissionsAsync();
|
|
720
|
+
if (fg.status !== 'granted') {
|
|
721
|
+
const e = Object.assign(new Error('Location permission denied'), {
|
|
722
|
+
code: types_1.AuthorizationStatus.DENIED,
|
|
723
|
+
});
|
|
724
|
+
throw e;
|
|
725
|
+
}
|
|
726
|
+
if (this.config.locationAuthorizationRequest !== 'WhenInUse' &&
|
|
727
|
+
(0, environment_1.isBackgroundCapable)()) {
|
|
728
|
+
const bg = await ExpoLocation.requestBackgroundPermissionsAsync().catch(() => null);
|
|
729
|
+
if (bg?.status === 'granted')
|
|
730
|
+
return types_1.AuthorizationStatus.ALWAYS;
|
|
731
|
+
}
|
|
732
|
+
return types_1.AuthorizationStatus.WHEN_IN_USE;
|
|
733
|
+
}
|
|
734
|
+
async requestTemporaryFullAccuracy(_purpose) {
|
|
735
|
+
// expo-location requests full accuracy via its permission flow;
|
|
736
|
+
// reduced accuracy is reported on the permission response (iOS 14+)
|
|
737
|
+
const fg = await ExpoLocation.getForegroundPermissionsAsync();
|
|
738
|
+
const reduced = fg.ios?.scope === 'reduced' ||
|
|
739
|
+
fg.accuracy === 'reduced';
|
|
740
|
+
return reduced ? types_1.AccuracyAuthorization.REDUCED : types_1.AccuracyAuthorization.FULL;
|
|
741
|
+
}
|
|
742
|
+
async getProviderState() {
|
|
743
|
+
const [services, fg] = await Promise.all([
|
|
744
|
+
ExpoLocation.hasServicesEnabledAsync().catch(() => false),
|
|
745
|
+
ExpoLocation.getForegroundPermissionsAsync(),
|
|
746
|
+
]);
|
|
747
|
+
const bg = (0, environment_1.isBackgroundCapable)()
|
|
748
|
+
? await ExpoLocation.getBackgroundPermissionsAsync().catch(() => null)
|
|
749
|
+
: null;
|
|
750
|
+
let status = types_1.AuthorizationStatus.NOT_DETERMINED;
|
|
751
|
+
if (fg.status === 'denied')
|
|
752
|
+
status = types_1.AuthorizationStatus.DENIED;
|
|
753
|
+
else if (bg?.status === 'granted')
|
|
754
|
+
status = types_1.AuthorizationStatus.ALWAYS;
|
|
755
|
+
else if (fg.status === 'granted')
|
|
756
|
+
status = types_1.AuthorizationStatus.WHEN_IN_USE;
|
|
757
|
+
const reduced = fg.ios?.scope === 'reduced';
|
|
758
|
+
return {
|
|
759
|
+
enabled: services && fg.status === 'granted',
|
|
760
|
+
status,
|
|
761
|
+
network: true,
|
|
762
|
+
gps: services,
|
|
763
|
+
accuracyAuthorization: reduced
|
|
764
|
+
? types_1.AccuracyAuthorization.REDUCED
|
|
765
|
+
: types_1.AccuracyAuthorization.FULL,
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
startProviderMonitoring() {
|
|
769
|
+
if (this.providerTimer)
|
|
770
|
+
return;
|
|
771
|
+
this.providerTimer = setInterval(() => {
|
|
772
|
+
void (async () => {
|
|
773
|
+
try {
|
|
774
|
+
const state = await this.getProviderState();
|
|
775
|
+
const key = JSON.stringify(state);
|
|
776
|
+
if (this.lastProviderState !== null && key !== this.lastProviderState) {
|
|
777
|
+
this.bus.emit('providerchange', state);
|
|
778
|
+
}
|
|
779
|
+
this.lastProviderState = key;
|
|
780
|
+
}
|
|
781
|
+
catch {
|
|
782
|
+
/* ignore */
|
|
783
|
+
}
|
|
784
|
+
})();
|
|
785
|
+
}, 15000);
|
|
786
|
+
}
|
|
787
|
+
// -------------------------------------------------------------------------
|
|
788
|
+
// Heartbeat
|
|
789
|
+
// -------------------------------------------------------------------------
|
|
790
|
+
startHeartbeat() {
|
|
791
|
+
this.stopHeartbeat();
|
|
792
|
+
const interval = (this.config.heartbeatInterval ?? 60) * 1000;
|
|
793
|
+
if (interval <= 0)
|
|
794
|
+
return;
|
|
795
|
+
this.heartbeatTimer = setInterval(() => {
|
|
796
|
+
void (async () => {
|
|
797
|
+
if (!this.enabled || this.motion.moving)
|
|
798
|
+
return;
|
|
799
|
+
const location = this.lastLocation;
|
|
800
|
+
if (location) {
|
|
801
|
+
this.bus.emit('heartbeat', {
|
|
802
|
+
location: { ...location, event: 'heartbeat' },
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
})();
|
|
806
|
+
}, interval);
|
|
807
|
+
}
|
|
808
|
+
stopHeartbeat() {
|
|
809
|
+
if (this.heartbeatTimer) {
|
|
810
|
+
clearInterval(this.heartbeatTimer);
|
|
811
|
+
this.heartbeatTimer = null;
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
// -------------------------------------------------------------------------
|
|
815
|
+
// Background tasks (iOS-style grace period emulation)
|
|
816
|
+
// -------------------------------------------------------------------------
|
|
817
|
+
async startBackgroundTask() {
|
|
818
|
+
return ++this.bgTaskCounter;
|
|
819
|
+
}
|
|
820
|
+
async stopBackgroundTask(_taskId) {
|
|
821
|
+
/* no-op: JS timers run while app is alive; dev-build background
|
|
822
|
+
execution is handled by expo-task-manager */
|
|
823
|
+
}
|
|
824
|
+
// -------------------------------------------------------------------------
|
|
825
|
+
// Headless
|
|
826
|
+
// -------------------------------------------------------------------------
|
|
827
|
+
registerHeadlessTask(task) {
|
|
828
|
+
this.headlessTask = task;
|
|
829
|
+
}
|
|
830
|
+
// -------------------------------------------------------------------------
|
|
831
|
+
// Logs / misc
|
|
832
|
+
// -------------------------------------------------------------------------
|
|
833
|
+
async getLog() {
|
|
834
|
+
return Logger_1.logger.getLog();
|
|
835
|
+
}
|
|
836
|
+
async destroyLog() {
|
|
837
|
+
return Logger_1.logger.destroyLog();
|
|
838
|
+
}
|
|
839
|
+
async emailLog(_email) {
|
|
840
|
+
// Without native mail-composer access we return the log text;
|
|
841
|
+
// apps can share it via expo-sharing / mailto.
|
|
842
|
+
return Logger_1.logger.getLog();
|
|
843
|
+
}
|
|
844
|
+
async playSound(_soundId) {
|
|
845
|
+
/* debug sounds require native assets — no-op */
|
|
846
|
+
}
|
|
847
|
+
// -------------------------------------------------------------------------
|
|
848
|
+
// Events
|
|
849
|
+
// -------------------------------------------------------------------------
|
|
850
|
+
onLocation(success, failure) {
|
|
851
|
+
void failure;
|
|
852
|
+
return this.bus.on('location', (e) => success(e));
|
|
853
|
+
}
|
|
854
|
+
onMotionChange(fn) {
|
|
855
|
+
return this.bus.on('motionchange', (e) => fn(e));
|
|
856
|
+
}
|
|
857
|
+
onActivityChange(fn) {
|
|
858
|
+
return this.bus.on('activitychange', (e) => fn(e));
|
|
859
|
+
}
|
|
860
|
+
onGeofence(fn) {
|
|
861
|
+
return this.bus.on('geofence', (e) => fn(e));
|
|
862
|
+
}
|
|
863
|
+
onGeofencesChange(fn) {
|
|
864
|
+
return this.bus.on('geofenceschange', (e) => fn(e));
|
|
865
|
+
}
|
|
866
|
+
onHeartbeat(fn) {
|
|
867
|
+
return this.bus.on('heartbeat', (e) => fn(e));
|
|
868
|
+
}
|
|
869
|
+
onHttp(fn) {
|
|
870
|
+
return this.bus.on('http', (e) => fn(e));
|
|
871
|
+
}
|
|
872
|
+
onProviderChange(fn) {
|
|
873
|
+
return this.bus.on('providerchange', (e) => fn(e));
|
|
874
|
+
}
|
|
875
|
+
onConnectivityChange(fn) {
|
|
876
|
+
return this.bus.on('connectivitychange', (e) => fn(e));
|
|
877
|
+
}
|
|
878
|
+
onPowerSaveChange(fn) {
|
|
879
|
+
return this.bus.on('powersavechange', (e) => fn(e));
|
|
880
|
+
}
|
|
881
|
+
onEnabledChange(fn) {
|
|
882
|
+
return this.bus.on('enabledchange', (e) => fn(e));
|
|
883
|
+
}
|
|
884
|
+
onSchedule(fn) {
|
|
885
|
+
return this.bus.on('schedule', (e) => fn(e));
|
|
886
|
+
}
|
|
887
|
+
onAuthorization(fn) {
|
|
888
|
+
return this.bus.on('authorization', (e) => fn(e));
|
|
889
|
+
}
|
|
890
|
+
onNotificationAction(fn) {
|
|
891
|
+
// Requires custom native code — never fires in Expo-managed apps.
|
|
892
|
+
return this.bus.on('notificationaction', (e) => fn(e));
|
|
893
|
+
}
|
|
894
|
+
removeListeners() {
|
|
895
|
+
this.bus.removeAll();
|
|
896
|
+
}
|
|
897
|
+
removeAllListeners() {
|
|
898
|
+
this.bus.removeAll();
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
exports.BackgroundGeolocation = new BackgroundGeolocationImpl();
|