expo-beacon 0.6.2 → 0.6.4
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/android/src/main/java/expo/modules/beacon/BeaconConstants.kt +11 -0
- package/android/src/main/java/expo/modules/beacon/BeaconEventLogger.kt +19 -6
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +121 -47
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +41 -6
- package/ios/BeaconEventLogger.swift +14 -3
- package/ios/ExpoBeaconModule.swift +91 -38
- package/package.json +1 -1
|
@@ -10,6 +10,17 @@ internal const val EDDYSTONE_PREFS_NAME = "expo.beacon.paired_eddystones"
|
|
|
10
10
|
internal const val EDDYSTONE_PREFS_KEY = "paired_eddystones"
|
|
11
11
|
internal const val NOTIFICATION_CONFIG_PREFS = "expo.beacon.notification_config"
|
|
12
12
|
internal const val MONITORING_OPTIONS_PREFS = "expo.beacon.monitoring_options"
|
|
13
|
+
internal const val EVENT_LOGGING_PREFS = "expo.beacon.event_logging"
|
|
14
|
+
internal const val EVENT_LOGGING_ENABLED_KEY = "enabled"
|
|
15
|
+
|
|
16
|
+
/** Foreground-service scan window for background monitoring responsiveness. */
|
|
17
|
+
internal const val MONITORING_SCAN_PERIOD_MS = 1100L
|
|
18
|
+
|
|
19
|
+
/** Gap between scan windows while the foreground service is active. */
|
|
20
|
+
internal const val MONITORING_BETWEEN_SCAN_PERIOD_MS = 0L
|
|
21
|
+
|
|
22
|
+
/** Ignore monitor-based exits if ranging saw the beacon within this window. */
|
|
23
|
+
internal const val RECENT_RANGING_SIGHTING_GRACE_MS = 4000L
|
|
13
24
|
|
|
14
25
|
/** Number of consecutive ranging misses before emitting a distance-based exit event. */
|
|
15
26
|
internal const val EXIT_MISS_THRESHOLD = 3
|
|
@@ -11,12 +11,30 @@ import org.json.JSONObject
|
|
|
11
11
|
* Thread-safe — all writes go through a single SQLiteOpenHelper.
|
|
12
12
|
*/
|
|
13
13
|
internal class BeaconEventLogger(context: Context) :
|
|
14
|
-
SQLiteOpenHelper(context, DB_NAME, null, DB_VERSION) {
|
|
14
|
+
SQLiteOpenHelper(context.applicationContext, DB_NAME, null, DB_VERSION) {
|
|
15
15
|
|
|
16
16
|
companion object {
|
|
17
17
|
private const val DB_NAME = "expo_beacon_events.db"
|
|
18
18
|
private const val DB_VERSION = 1
|
|
19
19
|
private const val TABLE = "events"
|
|
20
|
+
|
|
21
|
+
fun isLoggingEnabled(context: Context): Boolean {
|
|
22
|
+
return context.applicationContext
|
|
23
|
+
.getSharedPreferences(EVENT_LOGGING_PREFS, Context.MODE_PRIVATE)
|
|
24
|
+
.getBoolean(EVENT_LOGGING_ENABLED_KEY, false)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
fun setLoggingEnabled(context: Context, enabled: Boolean) {
|
|
28
|
+
context.applicationContext
|
|
29
|
+
.getSharedPreferences(EVENT_LOGGING_PREFS, Context.MODE_PRIVATE)
|
|
30
|
+
.edit()
|
|
31
|
+
.putBoolean(EVENT_LOGGING_ENABLED_KEY, enabled)
|
|
32
|
+
.apply()
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
fun deleteLogDatabase(context: Context) {
|
|
36
|
+
context.applicationContext.deleteDatabase(DB_NAME)
|
|
37
|
+
}
|
|
20
38
|
}
|
|
21
39
|
|
|
22
40
|
override fun onCreate(db: SQLiteDatabase) {
|
|
@@ -90,9 +108,4 @@ internal class BeaconEventLogger(context: Context) :
|
|
|
90
108
|
fun clearEvents() {
|
|
91
109
|
writableDatabase.delete(TABLE, null, null)
|
|
92
110
|
}
|
|
93
|
-
|
|
94
|
-
fun destroyDatabase(context: Context) {
|
|
95
|
-
close()
|
|
96
|
-
context.deleteDatabase(DB_NAME)
|
|
97
|
-
}
|
|
98
111
|
}
|
|
@@ -10,6 +10,7 @@ import android.os.Handler
|
|
|
10
10
|
import android.os.IBinder
|
|
11
11
|
import android.os.Looper
|
|
12
12
|
import android.os.RemoteException
|
|
13
|
+
import android.os.SystemClock
|
|
13
14
|
import android.util.Log
|
|
14
15
|
import java.util.concurrent.atomic.AtomicInteger
|
|
15
16
|
import androidx.core.app.NotificationCompat
|
|
@@ -34,8 +35,9 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
34
35
|
// Distance filtering
|
|
35
36
|
@Volatile private var maxDistance: Double? = null
|
|
36
37
|
@Volatile private var exitDistance: Double? = null
|
|
37
|
-
private val
|
|
38
|
+
private val monitoredRegionIds = java.util.concurrent.CopyOnWriteArraySet<String>()
|
|
38
39
|
private val enteredRegions = java.util.concurrent.CopyOnWriteArraySet<String>()
|
|
40
|
+
private val lastSeenAtMs = java.util.concurrent.ConcurrentHashMap<String, Long>()
|
|
39
41
|
|
|
40
42
|
// Hysteresis counters (synchronized on distanceLock)
|
|
41
43
|
private val distanceLock = Any()
|
|
@@ -55,6 +57,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
55
57
|
private val timeoutRunnables = java.util.concurrent.ConcurrentHashMap<String, Runnable>()
|
|
56
58
|
// Per-beacon timeout seconds lookup (identifier → seconds), loaded from paired data
|
|
57
59
|
private val beaconTimeouts = java.util.concurrent.ConcurrentHashMap<String, Int>()
|
|
60
|
+
private var eventLogger: BeaconEventLogger? = null
|
|
58
61
|
|
|
59
62
|
companion object {
|
|
60
63
|
private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
|
|
@@ -88,10 +91,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
88
91
|
beaconManager = BeaconManager.getInstanceForApplication(this).also { manager ->
|
|
89
92
|
BeaconParsers.ensureRegistered(manager)
|
|
90
93
|
try { manager.setEnableScheduledScanJobs(false) } catch (e: IllegalStateException) { Log.w(TAG, "setEnableScheduledScanJobs failed", e) }
|
|
91
|
-
manager.setBackgroundBetweenScanPeriod(
|
|
92
|
-
manager.setBackgroundScanPeriod(
|
|
93
|
-
manager.setForegroundScanPeriod(
|
|
94
|
-
manager.setForegroundBetweenScanPeriod(
|
|
94
|
+
manager.setBackgroundBetweenScanPeriod(MONITORING_BETWEEN_SCAN_PERIOD_MS)
|
|
95
|
+
manager.setBackgroundScanPeriod(MONITORING_SCAN_PERIOD_MS)
|
|
96
|
+
manager.setForegroundScanPeriod(MONITORING_SCAN_PERIOD_MS)
|
|
97
|
+
manager.setForegroundBetweenScanPeriod(MONITORING_BETWEEN_SCAN_PERIOD_MS)
|
|
95
98
|
}
|
|
96
99
|
}
|
|
97
100
|
|
|
@@ -155,6 +158,16 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
155
158
|
try { beaconManager.stopRangingBeaconsInRegion(it) } catch (_: RemoteException) {}
|
|
156
159
|
}
|
|
157
160
|
distanceLogRegions.clear()
|
|
161
|
+
monitoredRegionIds.clear()
|
|
162
|
+
enteredRegions.clear()
|
|
163
|
+
lastSeenAtMs.clear()
|
|
164
|
+
timeoutHandler.removeCallbacksAndMessages(null)
|
|
165
|
+
timeoutRunnables.clear()
|
|
166
|
+
synchronized(distanceLock) {
|
|
167
|
+
enterCounters.clear()
|
|
168
|
+
exitCounters.clear()
|
|
169
|
+
missCounters.clear()
|
|
170
|
+
}
|
|
158
171
|
monitoredRegions.forEach {
|
|
159
172
|
try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
|
|
160
173
|
}
|
|
@@ -170,6 +183,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
170
183
|
Identifier.fromInt(b.getInt("minor"))
|
|
171
184
|
)
|
|
172
185
|
monitoredRegions.add(region)
|
|
186
|
+
monitoredRegionIds.add(region.uniqueId)
|
|
173
187
|
try {
|
|
174
188
|
beaconManager.startMonitoringBeaconsInRegion(region)
|
|
175
189
|
} catch (e: RemoteException) {
|
|
@@ -199,6 +213,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
199
213
|
null
|
|
200
214
|
)
|
|
201
215
|
monitoredRegions.add(region)
|
|
216
|
+
monitoredRegionIds.add(region.uniqueId)
|
|
202
217
|
try {
|
|
203
218
|
beaconManager.startMonitoringBeaconsInRegion(region)
|
|
204
219
|
} catch (ex: RemoteException) {
|
|
@@ -223,51 +238,42 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
223
238
|
|
|
224
239
|
// Distance logging only — emits distance broadcasts. Enter/exit logic lives in rangeNotifier.
|
|
225
240
|
private val distanceLoggingRangeNotifier = RangeNotifier { beacons, region ->
|
|
241
|
+
if (!monitoredRegionIds.contains(region.uniqueId)) return@RangeNotifier
|
|
226
242
|
val closest = beacons.filter { it.distance >= 0 }.minByOrNull { it.distance }
|
|
227
243
|
if (closest != null) {
|
|
244
|
+
lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
|
|
228
245
|
sendBeaconBroadcast(region, "distance", closest.distance)
|
|
229
246
|
}
|
|
230
247
|
}
|
|
231
248
|
|
|
232
249
|
private val monitorNotifier = object : MonitorNotifier {
|
|
233
250
|
override fun didEnterRegion(region: Region) {
|
|
234
|
-
|
|
235
|
-
if (maxDist != null) {
|
|
236
|
-
// Mark region for distance confirmation — ranging is already active via distance logging
|
|
237
|
-
rangingRegions.add(region)
|
|
238
|
-
} else {
|
|
239
|
-
enteredRegions.add(region.uniqueId)
|
|
240
|
-
sendBeaconBroadcast(region, "enter", -1.0)
|
|
241
|
-
showEnterExitNotification(region, "enter")
|
|
242
|
-
scheduleTimeoutIfConfigured(region)
|
|
243
|
-
}
|
|
251
|
+
// Enter is synthesized from ranging so distance and enter/exit stay in sync.
|
|
244
252
|
}
|
|
245
253
|
|
|
246
254
|
override fun didExitRegion(region: Region) {
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
// Distance ranging normally handles exit. But if the beacon was
|
|
251
|
-
// in "entered" state when OS fires didExitRegion, we must emit
|
|
252
|
-
// the exit event — ranging will no longer receive readings.
|
|
253
|
-
val wasEntered = enteredRegions.remove(region.uniqueId)
|
|
254
|
-
synchronized(distanceLock) {
|
|
255
|
-
enterCounters.remove(region.uniqueId)
|
|
256
|
-
exitCounters.remove(region.uniqueId)
|
|
257
|
-
missCounters.remove(region.uniqueId)
|
|
258
|
-
}
|
|
259
|
-
if (wasEntered) {
|
|
260
|
-
cancelTimeout(region.uniqueId)
|
|
261
|
-
sendBeaconBroadcast(region, "exit", -1.0)
|
|
262
|
-
showEnterExitNotification(region, "exit")
|
|
263
|
-
}
|
|
255
|
+
if (!monitoredRegionIds.contains(region.uniqueId)) return
|
|
256
|
+
if (wasSeenRecently(region.uniqueId)) {
|
|
257
|
+
Log.d(TAG, "Ignoring stale didExitRegion for ${region.uniqueId}; beacon was seen by ranging recently")
|
|
264
258
|
return
|
|
265
259
|
}
|
|
266
260
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
261
|
+
lastSeenAtMs.remove(region.uniqueId)
|
|
262
|
+
|
|
263
|
+
// Ranging-based hysteresis handles exit in the normal case. If the OS
|
|
264
|
+
// fires didExitRegion after ranging has already stopped, emit exit as a
|
|
265
|
+
// safety net only if the region was previously in the entered state.
|
|
266
|
+
val wasEntered = enteredRegions.remove(region.uniqueId)
|
|
267
|
+
synchronized(distanceLock) {
|
|
268
|
+
enterCounters.remove(region.uniqueId)
|
|
269
|
+
exitCounters.remove(region.uniqueId)
|
|
270
|
+
missCounters.remove(region.uniqueId)
|
|
271
|
+
}
|
|
272
|
+
if (wasEntered) {
|
|
273
|
+
cancelTimeout(region.uniqueId)
|
|
274
|
+
sendBeaconBroadcast(region, "exit", -1.0)
|
|
275
|
+
showEnterExitNotification(region, "exit")
|
|
276
|
+
}
|
|
271
277
|
}
|
|
272
278
|
|
|
273
279
|
override fun didDetermineStateForRegion(state: Int, region: Region) {
|
|
@@ -276,11 +282,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
276
282
|
}
|
|
277
283
|
|
|
278
284
|
// Single source of truth for distance-based enter/exit with hysteresis.
|
|
279
|
-
// Processes regions
|
|
280
|
-
//
|
|
285
|
+
// Processes only actual monitoring regions and handles exit via miss counting
|
|
286
|
+
// when beacons disappear.
|
|
281
287
|
private val rangeNotifier = RangeNotifier { beacons, region ->
|
|
282
|
-
val maxDist = maxDistance
|
|
283
|
-
if (!
|
|
288
|
+
val maxDist = maxDistance
|
|
289
|
+
if (!monitoredRegionIds.contains(region.uniqueId)) return@RangeNotifier
|
|
284
290
|
|
|
285
291
|
val beacon = beacons
|
|
286
292
|
.filter { it.distance >= 0 }
|
|
@@ -289,13 +295,13 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
289
295
|
synchronized(distanceLock) {
|
|
290
296
|
if (beacon != null) {
|
|
291
297
|
// Got a valid reading — reset miss counter
|
|
298
|
+
lastSeenAtMs[region.uniqueId] = SystemClock.elapsedRealtime()
|
|
292
299
|
missCounters[region.uniqueId] = 0
|
|
293
300
|
|
|
294
301
|
val action = evaluateDistanceHysteresis(region.uniqueId, beacon.distance, maxDist)
|
|
295
302
|
when (action) {
|
|
296
303
|
HysteresisAction.ENTER -> {
|
|
297
304
|
enteredRegions.add(region.uniqueId)
|
|
298
|
-
rangingRegions.remove(region)
|
|
299
305
|
sendBeaconBroadcast(region, "enter", beacon.distance)
|
|
300
306
|
showEnterExitNotification(region, "enter")
|
|
301
307
|
scheduleTimeoutIfConfigured(region)
|
|
@@ -303,7 +309,6 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
303
309
|
HysteresisAction.EXIT -> {
|
|
304
310
|
cancelTimeout(region.uniqueId)
|
|
305
311
|
enteredRegions.remove(region.uniqueId)
|
|
306
|
-
rangingRegions.add(region)
|
|
307
312
|
sendBeaconBroadcast(region, "exit", beacon.distance)
|
|
308
313
|
showEnterExitNotification(region, "exit")
|
|
309
314
|
}
|
|
@@ -348,8 +353,23 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
348
353
|
private fun evaluateDistanceHysteresis(
|
|
349
354
|
regionId: String,
|
|
350
355
|
distance: Double,
|
|
351
|
-
maxDist: Double
|
|
356
|
+
maxDist: Double?
|
|
352
357
|
): HysteresisAction {
|
|
358
|
+
if (maxDist == null) {
|
|
359
|
+
exitCounters[regionId] = 0
|
|
360
|
+
if (enteredRegions.contains(regionId)) {
|
|
361
|
+
enterCounters[regionId] = 0
|
|
362
|
+
return HysteresisAction.NONE
|
|
363
|
+
}
|
|
364
|
+
val count = (enterCounters[regionId] ?: 0) + 1
|
|
365
|
+
enterCounters[regionId] = count
|
|
366
|
+
if (count >= HYSTERESIS_COUNT) {
|
|
367
|
+
enterCounters[regionId] = 0
|
|
368
|
+
return HysteresisAction.ENTER
|
|
369
|
+
}
|
|
370
|
+
return HysteresisAction.NONE
|
|
371
|
+
}
|
|
372
|
+
|
|
353
373
|
val exitDist = effectiveExitDistance(maxDist)
|
|
354
374
|
if (distance <= maxDist) {
|
|
355
375
|
// Inside enter threshold
|
|
@@ -377,6 +397,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
377
397
|
return HysteresisAction.NONE
|
|
378
398
|
}
|
|
379
399
|
|
|
400
|
+
private fun wasSeenRecently(regionId: String): Boolean {
|
|
401
|
+
val lastSeen = lastSeenAtMs[regionId] ?: return false
|
|
402
|
+
return SystemClock.elapsedRealtime() - lastSeen <= RECENT_RANGING_SIGHTING_GRACE_MS
|
|
403
|
+
}
|
|
404
|
+
|
|
380
405
|
// MARK: - Timeout timer helpers
|
|
381
406
|
|
|
382
407
|
private fun scheduleTimeoutIfConfigured(region: Region) {
|
|
@@ -404,6 +429,26 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
404
429
|
val id1Str = region.id1?.toString() ?: ""
|
|
405
430
|
val isEddystone = id1Str.startsWith("0x")
|
|
406
431
|
|
|
432
|
+
val params = if (isEddystone) {
|
|
433
|
+
buildMap<String, Any?> {
|
|
434
|
+
put("identifier", region.uniqueId)
|
|
435
|
+
put("namespace", id1Str.removePrefix("0x"))
|
|
436
|
+
put("instance", region.id2?.toString()?.removePrefix("0x") ?: "")
|
|
437
|
+
put("event", eventType)
|
|
438
|
+
put("distance", distance)
|
|
439
|
+
}
|
|
440
|
+
} else {
|
|
441
|
+
buildMap<String, Any?> {
|
|
442
|
+
put("identifier", region.uniqueId)
|
|
443
|
+
put("uuid", id1Str)
|
|
444
|
+
put("major", region.id2?.toInt() ?: 0)
|
|
445
|
+
put("minor", region.id3?.toInt() ?: 0)
|
|
446
|
+
put("event", eventType)
|
|
447
|
+
put("distance", distance)
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
monitoringEventName(isEddystone, eventType)?.let { logBeaconEvent(it, params) }
|
|
451
|
+
|
|
407
452
|
val intent = Intent(ACTION_BEACON_EVENT).apply {
|
|
408
453
|
putExtra("identifier", region.uniqueId)
|
|
409
454
|
putExtra("event", eventType)
|
|
@@ -424,6 +469,36 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
424
469
|
sendBroadcast(intent)
|
|
425
470
|
}
|
|
426
471
|
|
|
472
|
+
private fun monitoringEventName(isEddystone: Boolean, eventType: String): String? {
|
|
473
|
+
return when (eventType) {
|
|
474
|
+
"enter" -> if (isEddystone) "onEddystoneEnter" else "onBeaconEnter"
|
|
475
|
+
"exit" -> if (isEddystone) "onEddystoneExit" else "onBeaconExit"
|
|
476
|
+
"distance" -> if (isEddystone) "onEddystoneDistance" else "onBeaconDistance"
|
|
477
|
+
"timeout" -> if (isEddystone) "onEddystoneTimeout" else "onBeaconTimeout"
|
|
478
|
+
else -> null
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
@Synchronized
|
|
483
|
+
private fun getOrCreateEventLogger(): BeaconEventLogger {
|
|
484
|
+
return eventLogger ?: BeaconEventLogger(applicationContext).also { eventLogger = it }
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
@Synchronized
|
|
488
|
+
private fun releaseEventLogger() {
|
|
489
|
+
eventLogger?.close()
|
|
490
|
+
eventLogger = null
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
private fun logBeaconEvent(eventType: String, params: Map<String, Any?>) {
|
|
494
|
+
if (!BeaconEventLogger.isLoggingEnabled(this)) {
|
|
495
|
+
releaseEventLogger()
|
|
496
|
+
return
|
|
497
|
+
}
|
|
498
|
+
val identifier = params["identifier"] as? String
|
|
499
|
+
getOrCreateEventLogger().logEvent(eventType, identifier, params)
|
|
500
|
+
}
|
|
501
|
+
|
|
427
502
|
private fun showEnterExitNotification(region: Region, eventType: String) {
|
|
428
503
|
val config = readNotificationConfig()
|
|
429
504
|
val eventsConfig = config.optJSONObject("beaconEvents")
|
|
@@ -527,13 +602,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
527
602
|
timeoutHandler.removeCallbacksAndMessages(null)
|
|
528
603
|
timeoutRunnables.clear()
|
|
529
604
|
beaconTimeouts.clear()
|
|
605
|
+
lastSeenAtMs.clear()
|
|
606
|
+
monitoredRegionIds.clear()
|
|
607
|
+
releaseEventLogger()
|
|
530
608
|
beaconManager.removeMonitorNotifier(monitorNotifier)
|
|
531
609
|
beaconManager.removeRangeNotifier(rangeNotifier)
|
|
532
610
|
beaconManager.removeRangeNotifier(distanceLoggingRangeNotifier)
|
|
533
|
-
rangingRegions.forEach {
|
|
534
|
-
try { beaconManager.stopRangingBeaconsInRegion(it) } catch (_: RemoteException) {}
|
|
535
|
-
}
|
|
536
|
-
rangingRegions.clear()
|
|
537
611
|
distanceLogRegions.forEach {
|
|
538
612
|
try { beaconManager.stopRangingBeaconsInRegion(it) } catch (_: RemoteException) {}
|
|
539
613
|
}
|
|
@@ -294,6 +294,22 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
294
294
|
}
|
|
295
295
|
}
|
|
296
296
|
}
|
|
297
|
+
if (maxDistance != null && (!maxDistance.isFinite() || maxDistance <= 0.0)) {
|
|
298
|
+
promise.reject("INVALID_MAX_DISTANCE", "maxDistance must be a finite number greater than 0", null)
|
|
299
|
+
return@AsyncFunction
|
|
300
|
+
}
|
|
301
|
+
if (exitDistance != null && (!exitDistance.isFinite() || exitDistance <= 0.0)) {
|
|
302
|
+
promise.reject("INVALID_EXIT_DISTANCE", "exitDistance must be a finite number greater than 0", null)
|
|
303
|
+
return@AsyncFunction
|
|
304
|
+
}
|
|
305
|
+
if (exitDistance != null && maxDistance == null) {
|
|
306
|
+
promise.reject("INVALID_EXIT_DISTANCE", "exitDistance requires maxDistance to be set", null)
|
|
307
|
+
return@AsyncFunction
|
|
308
|
+
}
|
|
309
|
+
if (maxDistance != null && exitDistance != null && exitDistance < maxDistance) {
|
|
310
|
+
promise.reject("INVALID_EXIT_DISTANCE", "exitDistance must be greater than or equal to maxDistance", null)
|
|
311
|
+
return@AsyncFunction
|
|
312
|
+
}
|
|
297
313
|
ctx.getSharedPreferences(MONITORING_OPTIONS_PREFS, Context.MODE_PRIVATE)
|
|
298
314
|
.edit().apply {
|
|
299
315
|
if (maxDistance != null) putString("max_distance", maxDistance.toString())
|
|
@@ -385,15 +401,17 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
385
401
|
if (eventLogger == null) {
|
|
386
402
|
eventLogger = BeaconEventLogger(ctx)
|
|
387
403
|
}
|
|
404
|
+
BeaconEventLogger.setLoggingEnabled(ctx, true)
|
|
388
405
|
loggingEnabled = true
|
|
389
406
|
}
|
|
390
407
|
|
|
391
408
|
Function("disableEventLogging") {
|
|
409
|
+
appContext.reactContext?.let { BeaconEventLogger.setLoggingEnabled(it, false) }
|
|
392
410
|
loggingEnabled = false
|
|
393
411
|
}
|
|
394
412
|
|
|
395
413
|
Function("getEventLogs") { options: Map<String, Any?>? ->
|
|
396
|
-
val logger =
|
|
414
|
+
val logger = getOrCreateEventLogger() ?: return@Function emptyList<Map<String, Any?>>()
|
|
397
415
|
val limit = (options?.get("limit") as? Number)?.toInt() ?: 1000
|
|
398
416
|
val eventType = options?.get("eventType") as? String
|
|
399
417
|
val sinceTimestamp = (options?.get("sinceTimestamp") as? Number)?.toLong()
|
|
@@ -401,14 +419,16 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
401
419
|
}
|
|
402
420
|
|
|
403
421
|
Function("clearEventLogs") {
|
|
404
|
-
|
|
422
|
+
getOrCreateEventLogger()?.clearEvents()
|
|
405
423
|
}
|
|
406
424
|
|
|
407
425
|
Function("destroyEventLogs") {
|
|
408
426
|
loggingEnabled = false
|
|
409
427
|
val ctx = appContext.reactContext ?: return@Function null
|
|
410
|
-
|
|
428
|
+
BeaconEventLogger.setLoggingEnabled(ctx, false)
|
|
429
|
+
eventLogger?.close()
|
|
411
430
|
eventLogger = null
|
|
431
|
+
BeaconEventLogger.deleteLogDatabase(ctx)
|
|
412
432
|
}
|
|
413
433
|
|
|
414
434
|
OnDestroy {
|
|
@@ -699,7 +719,6 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
699
719
|
private fun registerEventReceiver() {
|
|
700
720
|
if (eventReceiver != null) return
|
|
701
721
|
val receiver = BeaconEventReceiver { eventName, params ->
|
|
702
|
-
logBeaconEvent(eventName, params)
|
|
703
722
|
sendEvent(eventName, params)
|
|
704
723
|
}
|
|
705
724
|
eventReceiver = receiver
|
|
@@ -740,10 +759,26 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
740
759
|
}
|
|
741
760
|
}
|
|
742
761
|
|
|
762
|
+
private fun getOrCreateEventLogger(): BeaconEventLogger? {
|
|
763
|
+
val context = appContext.reactContext ?: return null
|
|
764
|
+
return eventLogger ?: BeaconEventLogger(context).also { eventLogger = it }
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private fun isEventLoggingEnabled(): Boolean {
|
|
768
|
+
if (loggingEnabled) return true
|
|
769
|
+
val context = appContext.reactContext ?: return false
|
|
770
|
+
if (!BeaconEventLogger.isLoggingEnabled(context)) return false
|
|
771
|
+
loggingEnabled = true
|
|
772
|
+
if (eventLogger == null) {
|
|
773
|
+
eventLogger = BeaconEventLogger(context)
|
|
774
|
+
}
|
|
775
|
+
return true
|
|
776
|
+
}
|
|
777
|
+
|
|
743
778
|
/** Log an event to SQLite if logging is enabled. */
|
|
744
779
|
private fun logBeaconEvent(eventType: String, params: Map<String, Any?>) {
|
|
745
|
-
if (!
|
|
780
|
+
if (!isEventLoggingEnabled()) return
|
|
746
781
|
val identifier = params["identifier"] as? String
|
|
747
|
-
|
|
782
|
+
getOrCreateEventLogger()?.logEvent(eventType, identifier, params)
|
|
748
783
|
}
|
|
749
784
|
}
|
|
@@ -4,17 +4,28 @@ import SQLite3
|
|
|
4
4
|
/// SQLite-backed event logger for beacon events (iOS).
|
|
5
5
|
/// All access is expected from the main thread (same as ExpoBeaconModule).
|
|
6
6
|
final class BeaconEventLogger {
|
|
7
|
+
private static let databaseFileName = "expo_beacon_events.db"
|
|
8
|
+
|
|
9
|
+
private static var databaseURL: URL {
|
|
10
|
+
let dir = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!
|
|
11
|
+
return dir.appendingPathComponent(databaseFileName)
|
|
12
|
+
}
|
|
13
|
+
|
|
7
14
|
private var db: OpaquePointer?
|
|
8
15
|
private let dbPath: String
|
|
9
16
|
|
|
10
17
|
init() {
|
|
11
|
-
let dir =
|
|
18
|
+
let dir = Self.databaseURL.deletingLastPathComponent()
|
|
12
19
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
13
|
-
dbPath =
|
|
20
|
+
dbPath = Self.databaseURL.path
|
|
14
21
|
openDatabase()
|
|
15
22
|
createTableIfNeeded()
|
|
16
23
|
}
|
|
17
24
|
|
|
25
|
+
static func destroyPersistentStore() {
|
|
26
|
+
try? FileManager.default.removeItem(at: databaseURL)
|
|
27
|
+
}
|
|
28
|
+
|
|
18
29
|
private func openDatabase() {
|
|
19
30
|
guard sqlite3_open(dbPath, &db) == SQLITE_OK else {
|
|
20
31
|
print("[ExpoBeacon] Failed to open event log database")
|
|
@@ -126,7 +137,7 @@ final class BeaconEventLogger {
|
|
|
126
137
|
sqlite3_close(db)
|
|
127
138
|
db = nil
|
|
128
139
|
}
|
|
129
|
-
|
|
140
|
+
Self.destroyPersistentStore()
|
|
130
141
|
}
|
|
131
142
|
|
|
132
143
|
deinit {
|
|
@@ -10,6 +10,7 @@ private let IS_MONITORING_KEY = "expo.beacon.is_monitoring"
|
|
|
10
10
|
private let MAX_DISTANCE_KEY = "expo.beacon.max_distance"
|
|
11
11
|
private let EXIT_DISTANCE_KEY = "expo.beacon.exit_distance"
|
|
12
12
|
private let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
|
|
13
|
+
private let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
|
|
13
14
|
|
|
14
15
|
/// Number of consecutive ranging misses before emitting a distance-based exit event.
|
|
15
16
|
/// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
|
|
@@ -312,6 +313,22 @@ public class ExpoBeaconModule: Module {
|
|
|
312
313
|
self.defaults.set(json, forKey: NOTIFICATION_CONFIG_KEY)
|
|
313
314
|
}
|
|
314
315
|
}
|
|
316
|
+
if let dist = maxDistance, (!dist.isFinite || dist <= 0) {
|
|
317
|
+
promise.reject("INVALID_MAX_DISTANCE", "maxDistance must be a finite number greater than 0")
|
|
318
|
+
return
|
|
319
|
+
}
|
|
320
|
+
if let exitDist = exitDistance, (!exitDist.isFinite || exitDist <= 0) {
|
|
321
|
+
promise.reject("INVALID_EXIT_DISTANCE", "exitDistance must be a finite number greater than 0")
|
|
322
|
+
return
|
|
323
|
+
}
|
|
324
|
+
if exitDistance != nil && maxDistance == nil {
|
|
325
|
+
promise.reject("INVALID_EXIT_DISTANCE", "exitDistance requires maxDistance to be set")
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
if let dist = maxDistance, let exitDist = exitDistance, exitDist < dist {
|
|
329
|
+
promise.reject("INVALID_EXIT_DISTANCE", "exitDistance must be greater than or equal to maxDistance")
|
|
330
|
+
return
|
|
331
|
+
}
|
|
315
332
|
if let dist = maxDistance {
|
|
316
333
|
self.defaults.set(dist, forKey: MAX_DISTANCE_KEY)
|
|
317
334
|
} else {
|
|
@@ -402,15 +419,17 @@ public class ExpoBeaconModule: Module {
|
|
|
402
419
|
if self.eventLogger == nil {
|
|
403
420
|
self.eventLogger = BeaconEventLogger()
|
|
404
421
|
}
|
|
422
|
+
self.defaults.set(true, forKey: EVENT_LOGGING_ENABLED_KEY)
|
|
405
423
|
self.loggingEnabled = true
|
|
406
424
|
}
|
|
407
425
|
|
|
408
426
|
Function("disableEventLogging") { () -> Void in
|
|
427
|
+
self.defaults.set(false, forKey: EVENT_LOGGING_ENABLED_KEY)
|
|
409
428
|
self.loggingEnabled = false
|
|
410
429
|
}
|
|
411
430
|
|
|
412
431
|
Function("getEventLogs") { (options: [String: Any]?) -> [[String: Any]] in
|
|
413
|
-
|
|
432
|
+
let logger = self.getOrCreateEventLogger()
|
|
414
433
|
let limit = (options?["limit"] as? Int) ?? 1000
|
|
415
434
|
let eventType = options?["eventType"] as? String
|
|
416
435
|
let sinceTimestamp: Int64? = (options?["sinceTimestamp"] as? NSNumber)?.int64Value
|
|
@@ -418,12 +437,17 @@ public class ExpoBeaconModule: Module {
|
|
|
418
437
|
}
|
|
419
438
|
|
|
420
439
|
Function("clearEventLogs") { () -> Void in
|
|
421
|
-
self.
|
|
440
|
+
self.getOrCreateEventLogger().clearEvents()
|
|
422
441
|
}
|
|
423
442
|
|
|
424
443
|
Function("destroyEventLogs") { () -> Void in
|
|
444
|
+
self.defaults.set(false, forKey: EVENT_LOGGING_ENABLED_KEY)
|
|
425
445
|
self.loggingEnabled = false
|
|
426
|
-
self.eventLogger
|
|
446
|
+
if let logger = self.eventLogger {
|
|
447
|
+
logger.destroy()
|
|
448
|
+
} else {
|
|
449
|
+
BeaconEventLogger.destroyPersistentStore()
|
|
450
|
+
}
|
|
427
451
|
self.eventLogger = nil
|
|
428
452
|
}
|
|
429
453
|
|
|
@@ -736,40 +760,45 @@ public class ExpoBeaconModule: Module {
|
|
|
736
760
|
// reliably regardless of advertisement rate.
|
|
737
761
|
let maxDist = self.defaults.object(forKey: MAX_DISTANCE_KEY) as? Double
|
|
738
762
|
let exitDist = self.defaults.object(forKey: EXIT_DISTANCE_KEY) as? Double
|
|
739
|
-
let
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
"
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
"
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
763
|
+
let hasValidDistance = distance.isFinite && distance >= 0
|
|
764
|
+
if hasValidDistance || maxDist == nil {
|
|
765
|
+
let action = evaluateDistanceHysteresis(
|
|
766
|
+
identifier: identifier,
|
|
767
|
+
distance: distance,
|
|
768
|
+
maxDistance: maxDist,
|
|
769
|
+
exitDistance: exitDist,
|
|
770
|
+
entered: &eddystoneEnteredRegions,
|
|
771
|
+
enterCtrs: &eddystoneEnterCounters,
|
|
772
|
+
exitCtrs: &eddystoneExitCounters
|
|
773
|
+
)
|
|
774
|
+
switch action {
|
|
775
|
+
case .enter:
|
|
776
|
+
sendLoggedEvent("onEddystoneEnter", [
|
|
777
|
+
"identifier": identifier,
|
|
778
|
+
"namespace": ns,
|
|
779
|
+
"instance": inst,
|
|
780
|
+
"event": "enter",
|
|
781
|
+
"distance": distance
|
|
782
|
+
])
|
|
783
|
+
postBeaconNotification(identifier: identifier, eventType: "enter")
|
|
784
|
+
scheduleEddystoneTimeout(identifier: identifier, namespace: ns, instance: inst)
|
|
785
|
+
case .exit:
|
|
786
|
+
cancelEddystoneTimeout(identifier: identifier)
|
|
787
|
+
sendLoggedEvent("onEddystoneExit", [
|
|
788
|
+
"identifier": identifier,
|
|
789
|
+
"namespace": ns,
|
|
790
|
+
"instance": inst,
|
|
791
|
+
"event": "exit",
|
|
792
|
+
"distance": distance
|
|
793
|
+
])
|
|
794
|
+
postBeaconNotification(identifier: identifier, eventType: "exit")
|
|
795
|
+
case .none:
|
|
796
|
+
break
|
|
797
|
+
}
|
|
771
798
|
}
|
|
772
799
|
|
|
800
|
+
guard hasValidDistance else { break }
|
|
801
|
+
|
|
773
802
|
// Throttle distance events — enter/exit above is evaluated on every
|
|
774
803
|
// callback, but distance events are rate-limited to avoid flooding JS.
|
|
775
804
|
let now = Date()
|
|
@@ -855,13 +884,36 @@ public class ExpoBeaconModule: Module {
|
|
|
855
884
|
|
|
856
885
|
/// Sends an event to JS and logs it to SQLite if logging is enabled.
|
|
857
886
|
private func sendLoggedEvent(_ eventName: String, _ params: [String: Any]) {
|
|
858
|
-
if
|
|
887
|
+
if isEventLoggingEnabled() {
|
|
859
888
|
let identifier = params["identifier"] as? String
|
|
860
|
-
|
|
889
|
+
getOrCreateEventLogger().logEvent(eventType: eventName, identifier: identifier, data: params)
|
|
861
890
|
}
|
|
862
891
|
sendEvent(eventName, params)
|
|
863
892
|
}
|
|
864
893
|
|
|
894
|
+
private func getOrCreateEventLogger() -> BeaconEventLogger {
|
|
895
|
+
if let logger = eventLogger {
|
|
896
|
+
return logger
|
|
897
|
+
}
|
|
898
|
+
let logger = BeaconEventLogger()
|
|
899
|
+
eventLogger = logger
|
|
900
|
+
return logger
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
private func isEventLoggingEnabled() -> Bool {
|
|
904
|
+
if loggingEnabled {
|
|
905
|
+
return true
|
|
906
|
+
}
|
|
907
|
+
guard defaults.bool(forKey: EVENT_LOGGING_ENABLED_KEY) else {
|
|
908
|
+
return false
|
|
909
|
+
}
|
|
910
|
+
loggingEnabled = true
|
|
911
|
+
if eventLogger == nil {
|
|
912
|
+
eventLogger = BeaconEventLogger()
|
|
913
|
+
}
|
|
914
|
+
return true
|
|
915
|
+
}
|
|
916
|
+
|
|
865
917
|
private func loadPairedBeaconsRaw() -> [[String: Any]] {
|
|
866
918
|
if let cached = cachedPairedBeacons { return cached }
|
|
867
919
|
let value = self.defaults.array(forKey: PAIRED_BEACONS_KEY) as? [[String: Any]] ?? []
|
|
@@ -881,7 +933,8 @@ public class ExpoBeaconModule: Module {
|
|
|
881
933
|
guard !defaults.bool(forKey: migrationKey) else { return }
|
|
882
934
|
let keysToMigrate = [
|
|
883
935
|
PAIRED_BEACONS_KEY, PAIRED_EDDYSTONES_KEY,
|
|
884
|
-
IS_MONITORING_KEY, MAX_DISTANCE_KEY, NOTIFICATION_CONFIG_KEY
|
|
936
|
+
IS_MONITORING_KEY, MAX_DISTANCE_KEY, NOTIFICATION_CONFIG_KEY,
|
|
937
|
+
EVENT_LOGGING_ENABLED_KEY
|
|
885
938
|
]
|
|
886
939
|
for key in keysToMigrate {
|
|
887
940
|
if let value = UserDefaults.standard.object(forKey: key) {
|