expo-beacon 0.6.4 → 0.6.6
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/AndroidManifest.xml +5 -1
- package/android/src/main/java/expo/modules/beacon/BeaconConstants.kt +26 -7
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +119 -47
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +55 -0
- package/build/ExpoBeaconModule.d.ts +12 -0
- package/build/ExpoBeaconModule.d.ts.map +1 -1
- package/build/ExpoBeaconModule.js.map +1 -1
- package/build/ExpoBeaconModule.web.d.ts +2 -0
- package/build/ExpoBeaconModule.web.d.ts.map +1 -1
- package/build/ExpoBeaconModule.web.js +2 -0
- package/build/ExpoBeaconModule.web.js.map +1 -1
- package/ios/ExpoBeaconModule.swift +22 -4
- package/package.json +1 -1
- package/src/ExpoBeaconModule.ts +14 -0
- package/src/ExpoBeaconModule.web.ts +2 -0
|
@@ -22,6 +22,9 @@
|
|
|
22
22
|
<!-- Receive boot broadcast to restart monitoring after device reboot -->
|
|
23
23
|
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
|
24
24
|
|
|
25
|
+
<!-- Battery optimization exemption for reliable background BLE scanning -->
|
|
26
|
+
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
|
|
27
|
+
|
|
25
28
|
<uses-feature android:name="android.hardware.bluetooth_le" android:required="true" />
|
|
26
29
|
|
|
27
30
|
<application>
|
|
@@ -30,9 +33,10 @@
|
|
|
30
33
|
android:name="org.altbeacon.beacon.service.BeaconService"
|
|
31
34
|
android:enabled="true"
|
|
32
35
|
android:exported="false"
|
|
36
|
+
android:foregroundServiceType="connectedDevice"
|
|
33
37
|
android:isolatedProcess="false"
|
|
34
38
|
android:label="iBeacon Service"
|
|
35
|
-
tools:replace="android:label" />
|
|
39
|
+
tools:replace="android:label,android:foregroundServiceType" />
|
|
36
40
|
|
|
37
41
|
<service
|
|
38
42
|
android:name="org.altbeacon.beacon.BeaconIntentProcessor"
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
package expo.modules.beacon
|
|
2
2
|
|
|
3
3
|
// Shared constants used across ExpoBeaconModule and BeaconForegroundService.
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// NOTE: EXIT_MISS_THRESHOLD and scan timing deviate from iOS intentionally —
|
|
5
|
+
// Android requires a non-zero between-scan gap to prevent OS-level BLE
|
|
6
|
+
// throttling, and a higher miss threshold to tolerate brief scan pauses.
|
|
7
|
+
// HYSTERESIS_COUNT should stay in sync with ExpoBeaconModule.swift (iOS).
|
|
6
8
|
|
|
7
9
|
internal const val PREFS_NAME = "expo.beacon.paired"
|
|
8
10
|
internal const val PREFS_KEY = "paired_beacons"
|
|
@@ -16,18 +18,35 @@ internal const val EVENT_LOGGING_ENABLED_KEY = "enabled"
|
|
|
16
18
|
/** Foreground-service scan window for background monitoring responsiveness. */
|
|
17
19
|
internal const val MONITORING_SCAN_PERIOD_MS = 1100L
|
|
18
20
|
|
|
19
|
-
/**
|
|
20
|
-
|
|
21
|
+
/**
|
|
22
|
+
* Gap between scan windows while the foreground service is active.
|
|
23
|
+
* A non-zero gap prevents Android from throttling continuous BLE scans
|
|
24
|
+
* after a few minutes — the BLE stack needs brief idle periods to avoid
|
|
25
|
+
* OS-level scan suppression on many devices (Samsung, Pixel, etc.).
|
|
26
|
+
*/
|
|
27
|
+
internal const val MONITORING_BETWEEN_SCAN_PERIOD_MS = 1000L
|
|
21
28
|
|
|
22
29
|
/** Ignore monitor-based exits if ranging saw the beacon within this window. */
|
|
23
|
-
internal const val RECENT_RANGING_SIGHTING_GRACE_MS =
|
|
30
|
+
internal const val RECENT_RANGING_SIGHTING_GRACE_MS = 25000L
|
|
24
31
|
|
|
25
|
-
/**
|
|
26
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Number of consecutive ranging misses before emitting a distance-based exit event.
|
|
34
|
+
* With a ~2.1 s scan cycle (1100 ms scan + 1000 ms gap), 10 misses ≈ 21 s of
|
|
35
|
+
* silence before declaring exit — tolerant of brief BLE gaps while still
|
|
36
|
+
* responsive to actual departures.
|
|
37
|
+
*/
|
|
38
|
+
internal const val EXIT_MISS_THRESHOLD = 10
|
|
27
39
|
|
|
28
40
|
/** Number of consecutive readings required to confirm a distance-based enter or exit transition. */
|
|
29
41
|
internal const val HYSTERESIS_COUNT = 3
|
|
30
42
|
|
|
43
|
+
/**
|
|
44
|
+
* AltBeacon region exit period — how long after the last sighting before
|
|
45
|
+
* MonitorNotifier.didExitRegion fires. Set generously to avoid premature
|
|
46
|
+
* monitor-level exits that bypass ranging hysteresis.
|
|
47
|
+
*/
|
|
48
|
+
internal const val REGION_EXIT_PERIOD_MS = 60000L
|
|
49
|
+
|
|
31
50
|
/** Shared log tag for the expo-beacon module. */
|
|
32
51
|
internal const val TAG = "ExpoBeacon"
|
|
33
52
|
|
|
@@ -19,7 +19,7 @@ import org.altbeacon.beacon.*
|
|
|
19
19
|
import org.json.JSONArray
|
|
20
20
|
|
|
21
21
|
private const val CHANNEL_ID = "expo_beacon_channel"
|
|
22
|
-
|
|
22
|
+
internal const val FOREGROUND_NOTIF_ID = 1001
|
|
23
23
|
/**
|
|
24
24
|
* Base ID for per-beacon enter/exit notifications; incremented per unique region.
|
|
25
25
|
* With FOREGROUND_NOTIF_ID at 1001, this allows up to 999 unique regions
|
|
@@ -32,6 +32,9 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
32
32
|
private lateinit var beaconManager: BeaconManager
|
|
33
33
|
private val monitoredRegions = mutableListOf<Region>()
|
|
34
34
|
|
|
35
|
+
/** Tracks whether onBeaconServiceConnect has fired for the current bind. */
|
|
36
|
+
@Volatile private var serviceConnected = false
|
|
37
|
+
|
|
35
38
|
// Distance filtering
|
|
36
39
|
@Volatile private var maxDistance: Double? = null
|
|
37
40
|
@Volatile private var exitDistance: Double? = null
|
|
@@ -59,32 +62,6 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
59
62
|
private val beaconTimeouts = java.util.concurrent.ConcurrentHashMap<String, Int>()
|
|
60
63
|
private var eventLogger: BeaconEventLogger? = null
|
|
61
64
|
|
|
62
|
-
companion object {
|
|
63
|
-
private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
|
|
64
|
-
|
|
65
|
-
fun start(context: Context) {
|
|
66
|
-
context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
|
|
67
|
-
.edit().putBoolean("active", true).apply()
|
|
68
|
-
val intent = Intent(context, BeaconForegroundService::class.java)
|
|
69
|
-
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
70
|
-
context.startForegroundService(intent)
|
|
71
|
-
} else {
|
|
72
|
-
context.startService(intent)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
fun stop(context: Context) {
|
|
77
|
-
context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
|
|
78
|
-
.edit().putBoolean("active", false).apply()
|
|
79
|
-
context.stopService(Intent(context, BeaconForegroundService::class.java))
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
fun isMonitoringActive(context: Context): Boolean {
|
|
83
|
-
return context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
|
|
84
|
-
.getBoolean("active", false)
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
65
|
override fun onCreate() {
|
|
89
66
|
super.onCreate()
|
|
90
67
|
createNotificationChannel()
|
|
@@ -96,6 +73,21 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
96
73
|
manager.setForegroundScanPeriod(MONITORING_SCAN_PERIOD_MS)
|
|
97
74
|
manager.setForegroundBetweenScanPeriod(MONITORING_BETWEEN_SCAN_PERIOD_MS)
|
|
98
75
|
}
|
|
76
|
+
// Increase AltBeacon's region exit period so didExitRegion doesn't fire
|
|
77
|
+
// prematurely during brief BLE scan gaps.
|
|
78
|
+
BeaconManager.setRegionExitPeriod(REGION_EXIT_PERIOD_MS)
|
|
79
|
+
// Let AltBeacon's internal BeaconService run as a foreground service so
|
|
80
|
+
// Samsung/OEM battery enforcement doesn't kill BLE scanning after ~4 min.
|
|
81
|
+
// Reset first to clear any stale state from a previous session.
|
|
82
|
+
try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
|
|
83
|
+
try {
|
|
84
|
+
beaconManager.enableForegroundServiceScanning(
|
|
85
|
+
buildForegroundNotification(), FOREGROUND_NOTIF_ID
|
|
86
|
+
)
|
|
87
|
+
} catch (e: IllegalStateException) {
|
|
88
|
+
// Already bound by another consumer (e.g. a scan in progress) — non-fatal.
|
|
89
|
+
Log.w(TAG, "enableForegroundServiceScanning skipped (already bound)", e)
|
|
90
|
+
}
|
|
99
91
|
}
|
|
100
92
|
|
|
101
93
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
@@ -108,11 +100,18 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
108
100
|
} else {
|
|
109
101
|
startForeground(FOREGROUND_NOTIF_ID, buildForegroundNotification())
|
|
110
102
|
}
|
|
111
|
-
|
|
103
|
+
if (serviceConnected) {
|
|
104
|
+
// Already bound from a prior onStartCommand — reload regions directly
|
|
105
|
+
// so that re-starting monitoring from JS always takes effect.
|
|
106
|
+
loadAndMonitorRegions()
|
|
107
|
+
} else {
|
|
108
|
+
beaconManager.bind(this)
|
|
109
|
+
}
|
|
112
110
|
return START_STICKY
|
|
113
111
|
}
|
|
114
112
|
|
|
115
113
|
override fun onBeaconServiceConnect() {
|
|
114
|
+
serviceConnected = true
|
|
116
115
|
// Read max distance and exit distance from options prefs
|
|
117
116
|
val optPrefs = getSharedPreferences(MONITORING_OPTIONS_PREFS, Context.MODE_PRIVATE)
|
|
118
117
|
maxDistance = optPrefs.getString("max_distance", null)?.toDoubleOrNull()
|
|
@@ -315,7 +314,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
315
314
|
HysteresisAction.NONE -> {}
|
|
316
315
|
}
|
|
317
316
|
} else {
|
|
318
|
-
// No valid beacon reading —
|
|
317
|
+
// No valid beacon reading — break distance hysteresis streaks and
|
|
318
|
+
// track consecutive misses for disappearance-based exit detection.
|
|
319
|
+
enterCounters[region.uniqueId] = 0
|
|
320
|
+
exitCounters[region.uniqueId] = 0
|
|
319
321
|
val count = (missCounters[region.uniqueId] ?: 0) + 1
|
|
320
322
|
missCounters[region.uniqueId] = count
|
|
321
323
|
|
|
@@ -570,26 +572,94 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
570
572
|
}
|
|
571
573
|
|
|
572
574
|
private fun buildForegroundNotification(): Notification {
|
|
573
|
-
|
|
574
|
-
|
|
575
|
+
return Companion.buildForegroundNotification(this)
|
|
576
|
+
}
|
|
575
577
|
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() }
|
|
579
|
-
?: "Monitoring for iBeacons in the background"
|
|
580
|
-
val iconName = fgConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
|
|
581
|
-
val iconResId = iconName?.let { name ->
|
|
582
|
-
try { resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 } }
|
|
583
|
-
catch (_: Exception) { null }
|
|
584
|
-
} ?: android.R.drawable.ic_dialog_info
|
|
578
|
+
companion object {
|
|
579
|
+
private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
|
|
585
580
|
|
|
586
|
-
|
|
587
|
-
.
|
|
588
|
-
|
|
589
|
-
.
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
581
|
+
fun start(context: Context) {
|
|
582
|
+
context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
|
|
583
|
+
.edit().putBoolean("active", true).apply()
|
|
584
|
+
val intent = Intent(context, BeaconForegroundService::class.java)
|
|
585
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
586
|
+
context.startForegroundService(intent)
|
|
587
|
+
} else {
|
|
588
|
+
context.startService(intent)
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
fun stop(context: Context) {
|
|
593
|
+
context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
|
|
594
|
+
.edit().putBoolean("active", false).apply()
|
|
595
|
+
context.stopService(Intent(context, BeaconForegroundService::class.java))
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
fun isMonitoringActive(context: Context): Boolean {
|
|
599
|
+
return context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
|
|
600
|
+
.getBoolean("active", false)
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Ensure the notification channel exists. Must be called before building
|
|
605
|
+
* a notification from a non-service context (e.g. ExpoBeaconModule).
|
|
606
|
+
*/
|
|
607
|
+
fun ensureNotificationChannel(context: Context) {
|
|
608
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
609
|
+
val json = context.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
|
|
610
|
+
.getString("config", null)
|
|
611
|
+
val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
|
|
612
|
+
val channelConfig = config.optJSONObject("channel")
|
|
613
|
+
|
|
614
|
+
val channelName = channelConfig?.optString("name")?.takeIf { it.isNotEmpty() }
|
|
615
|
+
?: "Beacon Monitoring"
|
|
616
|
+
val channelDesc = channelConfig?.optString("description")?.takeIf { it.isNotEmpty() }
|
|
617
|
+
?: "Used for background iBeacon region monitoring"
|
|
618
|
+
val importance = when (channelConfig?.optString("importance")) {
|
|
619
|
+
"high" -> NotificationManager.IMPORTANCE_HIGH
|
|
620
|
+
"default" -> NotificationManager.IMPORTANCE_DEFAULT
|
|
621
|
+
else -> NotificationManager.IMPORTANCE_LOW
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
val notifMgr = context.getSystemService(NotificationManager::class.java)
|
|
625
|
+
if (notifMgr?.getNotificationChannel(CHANNEL_ID) == null) {
|
|
626
|
+
val channel = NotificationChannel(CHANNEL_ID, channelName, importance).apply {
|
|
627
|
+
description = channelDesc
|
|
628
|
+
}
|
|
629
|
+
notifMgr?.createNotificationChannel(channel)
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Build the foreground notification from any Context (service or module).
|
|
636
|
+
* Shared so that ExpoBeaconModule can pass the same notification to
|
|
637
|
+
* enableForegroundServiceScanning() before the service starts.
|
|
638
|
+
*/
|
|
639
|
+
fun buildForegroundNotification(context: Context): Notification {
|
|
640
|
+
val json = context.getSharedPreferences(NOTIFICATION_CONFIG_PREFS, Context.MODE_PRIVATE)
|
|
641
|
+
.getString("config", null)
|
|
642
|
+
val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
|
|
643
|
+
val fgConfig = config.optJSONObject("foregroundService")
|
|
644
|
+
|
|
645
|
+
val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() }
|
|
646
|
+
?: "Beacon Monitoring Active"
|
|
647
|
+
val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() }
|
|
648
|
+
?: "Monitoring for iBeacons in the background"
|
|
649
|
+
val iconName = fgConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
|
|
650
|
+
val iconResId = iconName?.let { name ->
|
|
651
|
+
try { context.resources.getIdentifier(name, "drawable", context.packageName).takeIf { it != 0 } }
|
|
652
|
+
catch (_: Exception) { null }
|
|
653
|
+
} ?: android.R.drawable.ic_dialog_info
|
|
654
|
+
|
|
655
|
+
return NotificationCompat.Builder(context, CHANNEL_ID)
|
|
656
|
+
.setSmallIcon(iconResId)
|
|
657
|
+
.setContentTitle(title)
|
|
658
|
+
.setContentText(text)
|
|
659
|
+
.setPriority(NotificationCompat.PRIORITY_LOW)
|
|
660
|
+
.setOngoing(true)
|
|
661
|
+
.build()
|
|
662
|
+
}
|
|
593
663
|
}
|
|
594
664
|
|
|
595
665
|
private fun readNotificationConfig(): org.json.JSONObject {
|
|
@@ -599,6 +669,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
599
669
|
}
|
|
600
670
|
|
|
601
671
|
override fun onDestroy() {
|
|
672
|
+
serviceConnected = false
|
|
602
673
|
timeoutHandler.removeCallbacksAndMessages(null)
|
|
603
674
|
timeoutRunnables.clear()
|
|
604
675
|
beaconTimeouts.clear()
|
|
@@ -620,6 +691,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
620
691
|
monitoredRegions.forEach {
|
|
621
692
|
try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
|
|
622
693
|
}
|
|
694
|
+
try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
|
|
623
695
|
beaconManager.unbind(this)
|
|
624
696
|
super.onDestroy()
|
|
625
697
|
}
|
|
@@ -7,7 +7,9 @@ import android.content.IntentFilter
|
|
|
7
7
|
import android.content.ServiceConnection
|
|
8
8
|
import android.content.SharedPreferences
|
|
9
9
|
import android.content.pm.PackageManager
|
|
10
|
+
import android.net.Uri
|
|
10
11
|
import android.os.Build
|
|
12
|
+
import android.os.PowerManager
|
|
11
13
|
import android.os.RemoteException
|
|
12
14
|
import androidx.core.content.ContextCompat
|
|
13
15
|
import expo.modules.interfaces.permissions.PermissionsStatus
|
|
@@ -326,6 +328,21 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
326
328
|
return@AsyncFunction
|
|
327
329
|
}
|
|
328
330
|
|
|
331
|
+
// Enable AltBeacon foreground service scanning so the internal BeaconService
|
|
332
|
+
// runs as a foreground service, preventing Samsung/OEM BLE throttling.
|
|
333
|
+
// Reset first to clear stale state from a previous session / hot-reload.
|
|
334
|
+
if (!isBoundForScan) {
|
|
335
|
+
try {
|
|
336
|
+
BeaconForegroundService.ensureNotificationChannel(ctx)
|
|
337
|
+
try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
|
|
338
|
+
beaconManager.enableForegroundServiceScanning(
|
|
339
|
+
BeaconForegroundService.buildForegroundNotification(ctx), FOREGROUND_NOTIF_ID
|
|
340
|
+
)
|
|
341
|
+
} catch (_: IllegalStateException) {
|
|
342
|
+
// Already bound — service onCreate() will enable it instead.
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
329
346
|
registerEventReceiver()
|
|
330
347
|
BeaconForegroundService.start(ctx)
|
|
331
348
|
promise.resolve(null)
|
|
@@ -343,6 +360,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
343
360
|
return@AsyncFunction
|
|
344
361
|
}
|
|
345
362
|
BeaconForegroundService.stop(ctx)
|
|
363
|
+
try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
|
|
346
364
|
unregisterEventReceiver()
|
|
347
365
|
promise.resolve(null)
|
|
348
366
|
}
|
|
@@ -431,6 +449,43 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
431
449
|
BeaconEventLogger.deleteLogDatabase(ctx)
|
|
432
450
|
}
|
|
433
451
|
|
|
452
|
+
// MARK: - Battery Optimization
|
|
453
|
+
|
|
454
|
+
Function("isBatteryOptimizationExempt") {
|
|
455
|
+
val ctx = appContext.reactContext ?: return@Function false
|
|
456
|
+
val pm = ctx.getSystemService(Context.POWER_SERVICE) as? PowerManager
|
|
457
|
+
?: return@Function false
|
|
458
|
+
pm.isIgnoringBatteryOptimizations(ctx.packageName)
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
AsyncFunction("requestBatteryOptimizationExemption") { promise: Promise ->
|
|
462
|
+
val ctx = appContext.reactContext ?: run {
|
|
463
|
+
promise.reject("NO_CONTEXT", "React context is not available", null)
|
|
464
|
+
return@AsyncFunction
|
|
465
|
+
}
|
|
466
|
+
val pm = ctx.getSystemService(Context.POWER_SERVICE) as? PowerManager
|
|
467
|
+
if (pm == null) {
|
|
468
|
+
promise.resolve(false)
|
|
469
|
+
return@AsyncFunction
|
|
470
|
+
}
|
|
471
|
+
if (pm.isIgnoringBatteryOptimizations(ctx.packageName)) {
|
|
472
|
+
promise.resolve(true)
|
|
473
|
+
return@AsyncFunction
|
|
474
|
+
}
|
|
475
|
+
try {
|
|
476
|
+
val intent = Intent(android.provider.Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS).apply {
|
|
477
|
+
data = Uri.parse("package:${ctx.packageName}")
|
|
478
|
+
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
479
|
+
}
|
|
480
|
+
ctx.startActivity(intent)
|
|
481
|
+
// The system dialog is fire-and-forget; we cannot observe the result
|
|
482
|
+
// from a non-Activity context. Resolve true to indicate the dialog was shown.
|
|
483
|
+
promise.resolve(true)
|
|
484
|
+
} catch (e: Exception) {
|
|
485
|
+
promise.reject("BATTERY_OPT_ERROR", "Failed to open battery optimization settings: ${e.message}", e)
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
434
489
|
OnDestroy {
|
|
435
490
|
with(this@ExpoBeaconModule) {
|
|
436
491
|
unregisterEventReceiver()
|
|
@@ -77,6 +77,18 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
77
77
|
cancelScan(): void;
|
|
78
78
|
/** Request Bluetooth + Location permissions. Returns true if granted. */
|
|
79
79
|
requestPermissionsAsync(): Promise<boolean>;
|
|
80
|
+
/**
|
|
81
|
+
* Check whether the app is exempt from Android battery optimizations.
|
|
82
|
+
* Always returns true on iOS and web (no equivalent concept).
|
|
83
|
+
*/
|
|
84
|
+
isBatteryOptimizationExempt(): boolean;
|
|
85
|
+
/**
|
|
86
|
+
* Request exemption from Android battery optimizations.
|
|
87
|
+
* Opens the system dialog asking the user to whitelist this app.
|
|
88
|
+
* Returns true if the dialog was shown (or already exempt), false on failure.
|
|
89
|
+
* Always resolves true on iOS and web.
|
|
90
|
+
*/
|
|
91
|
+
requestBatteryOptimizationExemption(): Promise<boolean>;
|
|
80
92
|
/** Enable SQLite event logging. All beacon events will be persisted to a local database. */
|
|
81
93
|
enableEventLogging(): void;
|
|
82
94
|
/** Disable event logging. Previously logged events are retained. */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE;;;;;;;;;;;OAWG;IACH,mBAAmB,CACjB,KAAK,CAAC,EAAE,MAAM,EAAE,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAE9B;;;;;OAKG;IACH,sBAAsB,CACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAEjC;;OAEG;IACH,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEtC;;OAEG;IACH,gBAAgB,IAAI,YAAY,EAAE;IAElC;;OAEG;IACH,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEzC;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAExC;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAEvD;;;;;;;OAOG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpE;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAE3B,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IAE1B;;;OAGG;IACH,UAAU,IAAI,IAAI;IAElB,yEAAyE;IACzE,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAE3C,4FAA4F;IAC5F,kBAAkB,IAAI,IAAI;IAE1B,oEAAoE;IACpE,mBAAmB,IAAI,IAAI;IAE3B;;;OAGG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,EAAE;IAE7D,kDAAkD;IAClD,cAAc,IAAI,IAAI;IAEtB,mEAAmE;IACnE,gBAAgB,IAAI,IAAI;CACzB;;AAED,wBAAmE"}
|
|
1
|
+
{"version":3,"file":"ExpoBeaconModule.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAuB,MAAM,MAAM,CAAC;AAEzD,OAAO,EACL,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,kBAAkB,EAClB,iBAAiB,EACjB,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAE5B,OAAO,OAAO,gBAAiB,SAAQ,YAAY,CAAC,sBAAsB,CAAC;IACzE;;;;;;;;;;;OAWG;IACH,mBAAmB,CACjB,KAAK,CAAC,EAAE,MAAM,EAAE,EAChB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAE9B;;;;;OAKG;IACH,sBAAsB,CACpB,YAAY,CAAC,EAAE,MAAM,GACpB,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAEjC;;OAEG;IACH,UAAU,CACR,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,YAAY,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEtC;;OAEG;IACH,gBAAgB,IAAI,YAAY,EAAE;IAElC;;OAEG;IACH,aAAa,CACX,UAAU,EAAE,MAAM,EAClB,SAAS,EAAE,MAAM,EACjB,QAAQ,EAAE,MAAM,EAChB,IAAI,CAAC,EAAE,MAAM,EACb,cAAc,CAAC,EAAE,MAAM,GACtB,IAAI;IAEP;;OAEG;IACH,eAAe,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAEzC;;OAEG;IACH,mBAAmB,IAAI,eAAe,EAAE;IAExC;;;OAGG;IACH,qBAAqB,CAAC,MAAM,EAAE,kBAAkB,GAAG,IAAI;IAEvD;;;;;;;OAOG;IACH,eAAe,CAAC,OAAO,CAAC,EAAE,iBAAiB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAEpE;;OAEG;IACH,cAAc,IAAI,OAAO,CAAC,IAAI,CAAC;IAE/B;;;OAGG;IACH,mBAAmB,IAAI,IAAI;IAE3B,iEAAiE;IACjE,kBAAkB,IAAI,IAAI;IAE1B;;;OAGG;IACH,UAAU,IAAI,IAAI;IAElB,yEAAyE;IACzE,uBAAuB,IAAI,OAAO,CAAC,OAAO,CAAC;IAE3C;;;OAGG;IACH,2BAA2B,IAAI,OAAO;IAEtC;;;;;OAKG;IACH,mCAAmC,IAAI,OAAO,CAAC,OAAO,CAAC;IAEvD,4FAA4F;IAC5F,kBAAkB,IAAI,IAAI;IAE1B,oEAAoE;IACpE,mBAAmB,IAAI,IAAI;IAE3B;;;OAGG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,EAAE;IAE7D,kDAAkD;IAClD,cAAc,IAAI,IAAI;IAEtB,mEAAmE;IACnE,gBAAgB,IAAI,IAAI;CACzB;;AAED,wBAAmE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"ExpoBeaconModule.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAgB,mBAAmB,EAAE,MAAM,MAAM,CAAC;AA6JzD,eAAe,mBAAmB,CAAmB,YAAY,CAAC,CAAC","sourcesContent":["import { NativeModule, requireNativeModule } from \"expo\";\r\n\r\nimport {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n NotificationConfig,\r\n MonitoringOptions,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\ndeclare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {\r\n /**\r\n * Start a one-shot iBeacon scan. Resolves with discovered beacons after scanDuration ms.\r\n *\r\n * Pass one or more UUIDs to scan for specific beacons (uses CoreLocation on iOS).\r\n * On iOS, at least one UUID is required — Apple strips iBeacon data from BLE\r\n * advertisements, making wildcard discovery impossible. When you pass an empty\r\n * array, the module automatically uses UUIDs from paired beacons.\r\n * On Android, pass an empty array to discover all nearby iBeacons.\r\n *\r\n * @param uuids Proximity UUIDs to filter by. Empty/omitted = use paired UUIDs (iOS) or wildcard (Android).\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForBeaconsAsync(\r\n uuids?: string[],\r\n scanDuration?: number,\r\n ): Promise<BeaconScanResult[]>;\r\n\r\n /**\r\n * Start a one-shot Eddystone beacon scan using BLE.\r\n * Discovers Eddystone-UID and Eddystone-URL frames.\r\n *\r\n * @param scanDuration Duration in ms (default 5000)\r\n */\r\n scanForEddystonesAsync(\r\n scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]>;\r\n\r\n /**\r\n * Register a beacon for persistent region monitoring.\r\n */\r\n pairBeacon(\r\n identifier: string,\r\n uuid: string,\r\n major: number,\r\n minor: number,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired beacon.\r\n */\r\n unpairBeacon(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired beacons.\r\n */\r\n getPairedBeacons(): PairedBeacon[];\r\n\r\n /**\r\n * Register an Eddystone-UID beacon for persistent monitoring.\r\n */\r\n pairEddystone(\r\n identifier: string,\r\n namespace: string,\r\n instance: string,\r\n name?: string,\r\n timeoutSeconds?: number,\r\n ): void;\r\n\r\n /**\r\n * Remove a previously paired Eddystone beacon.\r\n */\r\n unpairEddystone(identifier: string): void;\r\n\r\n /**\r\n * Return all currently paired Eddystone beacons.\r\n */\r\n getPairedEddystones(): PairedEddystone[];\r\n\r\n /**\r\n * Set persistent notification configuration. Settings are saved and applied to all\r\n * subsequent monitoring sessions until explicitly changed.\r\n */\r\n setNotificationConfig(config: NotificationConfig): void;\r\n\r\n /**\r\n * Start background region monitoring for all paired beacons.\r\n * On Android starts a foreground service.\r\n * On iOS starts CLLocationManager region monitoring.\r\n *\r\n * Accepts a plain number (backward-compatible maxDistance shorthand) or a\r\n * MonitoringOptions object with maxDistance and/or notification overrides.\r\n */\r\n startMonitoring(options?: MonitoringOptions | number): Promise<void>;\r\n\r\n /**\r\n * Stop background region monitoring.\r\n */\r\n stopMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Start a continuous BLE scan. Fires `onBeaconFound` events as beacons are detected.\r\n * Call stopContinuousScan() to end the scan.\r\n */\r\n startContinuousScan(): void;\r\n\r\n /** Stop the continuous scan started by startContinuousScan(). */\r\n stopContinuousScan(): void;\r\n\r\n /**\r\n * Cancel any in-progress one-shot scan (iBeacon or Eddystone).\r\n * The pending promise will be rejected with code \"SCAN_CANCELLED\".\r\n */\r\n cancelScan(): void;\r\n\r\n /** Request Bluetooth + Location permissions. Returns true if granted. */\r\n requestPermissionsAsync(): Promise<boolean>;\r\n\r\n /**\r\n * Check whether the app is exempt from Android battery optimizations.\r\n * Always returns true on iOS and web (no equivalent concept).\r\n */\r\n isBatteryOptimizationExempt(): boolean;\r\n\r\n /**\r\n * Request exemption from Android battery optimizations.\r\n * Opens the system dialog asking the user to whitelist this app.\r\n * Returns true if the dialog was shown (or already exempt), false on failure.\r\n * Always resolves true on iOS and web.\r\n */\r\n requestBatteryOptimizationExemption(): Promise<boolean>;\r\n\r\n /** Enable SQLite event logging. All beacon events will be persisted to a local database. */\r\n enableEventLogging(): void;\r\n\r\n /** Disable event logging. Previously logged events are retained. */\r\n disableEventLogging(): void;\r\n\r\n /**\r\n * Retrieve logged beacon events from the SQLite database.\r\n * @param options Optional filters (limit, eventType, sinceTimestamp).\r\n */\r\n getEventLogs(options?: EventLogQueryOptions): EventLogEntry[];\r\n\r\n /** Delete all logged events from the database. */\r\n clearEventLogs(): void;\r\n\r\n /** Delete the entire event log database. Also disables logging. */\r\n destroyEventLogs(): void;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
|
|
@@ -20,6 +20,8 @@ declare const stub: {
|
|
|
20
20
|
getEventLogs: (_options?: EventLogQueryOptions) => EventLogEntry[];
|
|
21
21
|
clearEventLogs: () => void;
|
|
22
22
|
destroyEventLogs: () => void;
|
|
23
|
+
isBatteryOptimizationExempt: () => boolean;
|
|
24
|
+
requestBatteryOptimizationExemption: () => Promise<boolean>;
|
|
23
25
|
addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => {
|
|
24
26
|
remove: () => void;
|
|
25
27
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeaconModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAM5B,QAAA,MAAM,IAAI;kCAEE,MAAM,EAAE,kBACA,MAAM,KACrB,OAAO,CAAC,gBAAgB,EAAE,CAAC;6CAEZ,MAAM,KACrB,OAAO,CAAC,mBAAmB,EAAE,CAAC;8BAElB,MAAM,SACZ,MAAM,UACL,MAAM,UACN,MAAM,KACb,IAAI;gCACqB,MAAM,KAAG,IAAI;4BACnB,YAAY,EAAE;iCAErB,MAAM,cACP,MAAM,aACP,MAAM,KAChB,IAAI;mCACwB,MAAM,KAAG,IAAI;+BACnB,eAAe,EAAE;2BACrB,OAAO,CAAC,IAAI,CAAC;0BACd,OAAO,CAAC,IAAI,CAAC;+BACR,IAAI;8BACL,IAAI;sBACZ,IAAI;qCACa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAAI;mCAClC,OAAO,CAAC,OAAO,CAAC;8BACrB,IAAI;+BACH,IAAI;8BACH,oBAAoB,KAAG,aAAa,EAAE;0BAC5C,IAAI;4BACF,IAAI;
|
|
1
|
+
{"version":3,"file":"ExpoBeaconModule.web.d.ts","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,sBAAsB,EACtB,gBAAgB,EAChB,mBAAmB,EACnB,YAAY,EACZ,eAAe,EACf,oBAAoB,EACpB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAM5B,QAAA,MAAM,IAAI;kCAEE,MAAM,EAAE,kBACA,MAAM,KACrB,OAAO,CAAC,gBAAgB,EAAE,CAAC;6CAEZ,MAAM,KACrB,OAAO,CAAC,mBAAmB,EAAE,CAAC;8BAElB,MAAM,SACZ,MAAM,UACL,MAAM,UACN,MAAM,KACb,IAAI;gCACqB,MAAM,KAAG,IAAI;4BACnB,YAAY,EAAE;iCAErB,MAAM,cACP,MAAM,aACP,MAAM,KAChB,IAAI;mCACwB,MAAM,KAAG,IAAI;+BACnB,eAAe,EAAE;2BACrB,OAAO,CAAC,IAAI,CAAC;0BACd,OAAO,CAAC,IAAI,CAAC;+BACR,IAAI;8BACL,IAAI;sBACZ,IAAI;qCACa,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAG,IAAI;mCAClC,OAAO,CAAC,OAAO,CAAC;8BACrB,IAAI;+BACH,IAAI;8BACH,oBAAoB,KAAG,aAAa,EAAE;0BAC5C,IAAI;4BACF,IAAI;uCACO,OAAO;+CACC,OAAO,CAAC,OAAO,CAAC;8BAC/B,MAAM,sBAAsB,aAAa,GAAG;;;qCAGrC,MAAM,sBAAsB;CAC9D,CAAC;AAEF,eAAe,IAAI,CAAC"}
|
|
@@ -22,6 +22,8 @@ const stub = {
|
|
|
22
22
|
getEventLogs: (_options) => notSupported(),
|
|
23
23
|
clearEventLogs: () => notSupported(),
|
|
24
24
|
destroyEventLogs: () => notSupported(),
|
|
25
|
+
isBatteryOptimizationExempt: () => true,
|
|
26
|
+
requestBatteryOptimizationExemption: () => Promise.resolve(true),
|
|
25
27
|
addListener: (_eventName, _listener) => ({
|
|
26
28
|
remove: () => { },
|
|
27
29
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeaconModule.web.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAUA,MAAM,YAAY,GAAG,GAAU,EAAE;IAC/B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,IAAI,GAAG;IACX,mBAAmB,EAAE,CACnB,MAAgB,EAChB,aAAsB,EACO,EAAE,CAAC,YAAY,EAAE;IAChD,sBAAsB,EAAE,CACtB,aAAsB,EACU,EAAE,CAAC,YAAY,EAAE;IACnD,UAAU,EAAE,CACV,WAAmB,EACnB,KAAa,EACb,MAAc,EACd,MAAc,EACR,EAAE,CAAC,YAAY,EAAE;IACzB,YAAY,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC3D,gBAAgB,EAAE,GAAmB,EAAE,CAAC,YAAY,EAAE;IACtD,aAAa,EAAE,CACb,WAAmB,EACnB,UAAkB,EAClB,SAAiB,EACX,EAAE,CAAC,YAAY,EAAE;IACzB,eAAe,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC9D,mBAAmB,EAAE,GAAsB,EAAE,CAAC,YAAY,EAAE;IAC5D,eAAe,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACpD,cAAc,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACnD,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,UAAU,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IACtC,qBAAqB,EAAE,CAAC,OAAgC,EAAQ,EAAE,CAAC,YAAY,EAAE;IACjF,uBAAuB,EAAE,GAAqB,EAAE,CAAC,YAAY,EAAE;IAC/D,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,YAAY,EAAE,CAAC,QAA+B,EAAmB,EAAE,CAAC,YAAY,EAAE;IAClF,cAAc,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC1C,gBAAgB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC5C,WAAW,EAAE,CAAC,UAAwC,EAAE,SAAc,EAAE,EAAE,CAAC,CAAC;QAC1E,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IACF,kBAAkB,EAAE,CAAC,UAAwC,EAAE,EAAE,GAAE,CAAC;CACrE,CAAC;AAEF,eAAe,IAAI,CAAC","sourcesContent":["import type {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\nconst notSupported = (): never => {\r\n throw new Error(\"expo-beacon is not supported on web.\");\r\n};\r\n\r\nconst stub = {\r\n scanForBeaconsAsync: (\r\n _uuids: string[],\r\n _scanDuration?: number,\r\n ): Promise<BeaconScanResult[]> => notSupported(),\r\n scanForEddystonesAsync: (\r\n _scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]> => notSupported(),\r\n pairBeacon: (\r\n _identifier: string,\r\n _uuid: string,\r\n _major: number,\r\n _minor: number,\r\n ): void => notSupported(),\r\n unpairBeacon: (_identifier: string): void => notSupported(),\r\n getPairedBeacons: (): PairedBeacon[] => notSupported(),\r\n pairEddystone: (\r\n _identifier: string,\r\n _namespace: string,\r\n _instance: string,\r\n ): void => notSupported(),\r\n unpairEddystone: (_identifier: string): void => notSupported(),\r\n getPairedEddystones: (): PairedEddystone[] => notSupported(),\r\n startMonitoring: (): Promise<void> => notSupported(),\r\n stopMonitoring: (): Promise<void> => notSupported(),\r\n startContinuousScan: (): void => notSupported(),\r\n stopContinuousScan: (): void => notSupported(),\r\n cancelScan: (): void => notSupported(),\r\n setNotificationConfig: (_config: Record<string, unknown>): void => notSupported(),\r\n requestPermissionsAsync: (): Promise<boolean> => notSupported(),\r\n enableEventLogging: (): void => notSupported(),\r\n disableEventLogging: (): void => notSupported(),\r\n getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),\r\n clearEventLogs: (): void => notSupported(),\r\n destroyEventLogs: (): void => notSupported(),\r\n addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({\r\n remove: () => {},\r\n }),\r\n removeAllListeners: (_eventName: keyof ExpoBeaconModuleEvents) => {},\r\n};\r\n\r\nexport default stub;\r\n"]}
|
|
1
|
+
{"version":3,"file":"ExpoBeaconModule.web.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAUA,MAAM,YAAY,GAAG,GAAU,EAAE;IAC/B,MAAM,IAAI,KAAK,CAAC,sCAAsC,CAAC,CAAC;AAC1D,CAAC,CAAC;AAEF,MAAM,IAAI,GAAG;IACX,mBAAmB,EAAE,CACnB,MAAgB,EAChB,aAAsB,EACO,EAAE,CAAC,YAAY,EAAE;IAChD,sBAAsB,EAAE,CACtB,aAAsB,EACU,EAAE,CAAC,YAAY,EAAE;IACnD,UAAU,EAAE,CACV,WAAmB,EACnB,KAAa,EACb,MAAc,EACd,MAAc,EACR,EAAE,CAAC,YAAY,EAAE;IACzB,YAAY,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC3D,gBAAgB,EAAE,GAAmB,EAAE,CAAC,YAAY,EAAE;IACtD,aAAa,EAAE,CACb,WAAmB,EACnB,UAAkB,EAClB,SAAiB,EACX,EAAE,CAAC,YAAY,EAAE;IACzB,eAAe,EAAE,CAAC,WAAmB,EAAQ,EAAE,CAAC,YAAY,EAAE;IAC9D,mBAAmB,EAAE,GAAsB,EAAE,CAAC,YAAY,EAAE;IAC5D,eAAe,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACpD,cAAc,EAAE,GAAkB,EAAE,CAAC,YAAY,EAAE;IACnD,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,UAAU,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IACtC,qBAAqB,EAAE,CAAC,OAAgC,EAAQ,EAAE,CAAC,YAAY,EAAE;IACjF,uBAAuB,EAAE,GAAqB,EAAE,CAAC,YAAY,EAAE;IAC/D,kBAAkB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC9C,mBAAmB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC/C,YAAY,EAAE,CAAC,QAA+B,EAAmB,EAAE,CAAC,YAAY,EAAE;IAClF,cAAc,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC1C,gBAAgB,EAAE,GAAS,EAAE,CAAC,YAAY,EAAE;IAC5C,2BAA2B,EAAE,GAAY,EAAE,CAAC,IAAI;IAChD,mCAAmC,EAAE,GAAqB,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;IAClF,WAAW,EAAE,CAAC,UAAwC,EAAE,SAAc,EAAE,EAAE,CAAC,CAAC;QAC1E,MAAM,EAAE,GAAG,EAAE,GAAE,CAAC;KACjB,CAAC;IACF,kBAAkB,EAAE,CAAC,UAAwC,EAAE,EAAE,GAAE,CAAC;CACrE,CAAC;AAEF,eAAe,IAAI,CAAC","sourcesContent":["import type {\r\n ExpoBeaconModuleEvents,\r\n BeaconScanResult,\r\n EddystoneScanResult,\r\n PairedBeacon,\r\n PairedEddystone,\r\n EventLogQueryOptions,\r\n EventLogEntry,\r\n} from \"./ExpoBeacon.types\";\r\n\r\nconst notSupported = (): never => {\r\n throw new Error(\"expo-beacon is not supported on web.\");\r\n};\r\n\r\nconst stub = {\r\n scanForBeaconsAsync: (\r\n _uuids: string[],\r\n _scanDuration?: number,\r\n ): Promise<BeaconScanResult[]> => notSupported(),\r\n scanForEddystonesAsync: (\r\n _scanDuration?: number,\r\n ): Promise<EddystoneScanResult[]> => notSupported(),\r\n pairBeacon: (\r\n _identifier: string,\r\n _uuid: string,\r\n _major: number,\r\n _minor: number,\r\n ): void => notSupported(),\r\n unpairBeacon: (_identifier: string): void => notSupported(),\r\n getPairedBeacons: (): PairedBeacon[] => notSupported(),\r\n pairEddystone: (\r\n _identifier: string,\r\n _namespace: string,\r\n _instance: string,\r\n ): void => notSupported(),\r\n unpairEddystone: (_identifier: string): void => notSupported(),\r\n getPairedEddystones: (): PairedEddystone[] => notSupported(),\r\n startMonitoring: (): Promise<void> => notSupported(),\r\n stopMonitoring: (): Promise<void> => notSupported(),\r\n startContinuousScan: (): void => notSupported(),\r\n stopContinuousScan: (): void => notSupported(),\r\n cancelScan: (): void => notSupported(),\r\n setNotificationConfig: (_config: Record<string, unknown>): void => notSupported(),\r\n requestPermissionsAsync: (): Promise<boolean> => notSupported(),\r\n enableEventLogging: (): void => notSupported(),\r\n disableEventLogging: (): void => notSupported(),\r\n getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),\r\n clearEventLogs: (): void => notSupported(),\r\n destroyEventLogs: (): void => notSupported(),\r\n isBatteryOptimizationExempt: (): boolean => true,\r\n requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),\r\n addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({\r\n remove: () => {},\r\n }),\r\n removeAllListeners: (_eventName: keyof ExpoBeaconModuleEvents) => {},\r\n};\r\n\r\nexport default stub;\r\n"]}
|
|
@@ -13,8 +13,10 @@ private let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
|
|
|
13
13
|
private let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
|
|
14
14
|
|
|
15
15
|
/// Number of consecutive ranging misses before emitting a distance-based exit event.
|
|
16
|
-
///
|
|
17
|
-
|
|
16
|
+
/// With ~1 s CoreLocation ranging cycles, 10 misses ≈ 10 s of silence before
|
|
17
|
+
/// declaring exit — tolerant of brief ranging gaps while still responsive.
|
|
18
|
+
/// NOTE: Android uses the same value but with a different scan cycle (~2.1 s).
|
|
19
|
+
private let EXIT_MISS_THRESHOLD = 10
|
|
18
20
|
/// Number of consecutive readings required to confirm a distance-based enter or exit transition.
|
|
19
21
|
/// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
|
|
20
22
|
private let HYSTERESIS_COUNT = 3
|
|
@@ -22,7 +24,8 @@ private let HYSTERESIS_COUNT = 3
|
|
|
22
24
|
/// Eddystone monitoring timer interval in seconds.
|
|
23
25
|
private let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
|
|
24
26
|
/// Maximum age (in seconds) before a beacon is considered "not recently seen".
|
|
25
|
-
|
|
27
|
+
/// Set higher than a few tick intervals to tolerate brief BLE advertisement gaps.
|
|
28
|
+
private let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 8.0
|
|
26
29
|
/// Minimum interval between consecutive distance event emissions per identifier.
|
|
27
30
|
private let DISTANCE_EVENT_THROTTLE_INTERVAL: TimeInterval = 1.0
|
|
28
31
|
|
|
@@ -451,6 +454,16 @@ public class ExpoBeaconModule: Module {
|
|
|
451
454
|
self.eventLogger = nil
|
|
452
455
|
}
|
|
453
456
|
|
|
457
|
+
// MARK: - Battery Optimization (Android-only; no-op on iOS)
|
|
458
|
+
|
|
459
|
+
Function("isBatteryOptimizationExempt") { () -> Bool in
|
|
460
|
+
return true
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
AsyncFunction("requestBatteryOptimizationExemption") { (promise: Promise) in
|
|
464
|
+
promise.resolve(true)
|
|
465
|
+
}
|
|
466
|
+
|
|
454
467
|
// MARK: - Lifecycle
|
|
455
468
|
|
|
456
469
|
OnDestroy {
|
|
@@ -988,7 +1001,10 @@ public class ExpoBeaconModule: Module {
|
|
|
988
1001
|
continue
|
|
989
1002
|
}
|
|
990
1003
|
|
|
991
|
-
// Not seen recently —
|
|
1004
|
+
// Not seen recently — break distance hysteresis streaks and, if the
|
|
1005
|
+
// beacon was already entered, increment miss counter toward exit.
|
|
1006
|
+
eddystoneEnterCounters[identifier] = 0
|
|
1007
|
+
eddystoneExitCounters[identifier] = 0
|
|
992
1008
|
guard eddystoneEnteredRegions.contains(identifier) else { continue }
|
|
993
1009
|
|
|
994
1010
|
let count = (eddystoneMissCounters[identifier] ?? 0) + 1
|
|
@@ -1263,6 +1279,8 @@ public class ExpoBeaconModule: Module {
|
|
|
1263
1279
|
// duplicate events when both monitoring and continuous scan are active.
|
|
1264
1280
|
} else {
|
|
1265
1281
|
// No valid beacon reading — beacon may have disappeared
|
|
1282
|
+
enterCounters[identifier] = 0
|
|
1283
|
+
exitCounters[identifier] = 0
|
|
1266
1284
|
let count = (missCounters[identifier] ?? 0) + 1
|
|
1267
1285
|
missCounters[identifier] = count
|
|
1268
1286
|
|
package/package.json
CHANGED
package/src/ExpoBeaconModule.ts
CHANGED
|
@@ -122,6 +122,20 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
122
122
|
/** Request Bluetooth + Location permissions. Returns true if granted. */
|
|
123
123
|
requestPermissionsAsync(): Promise<boolean>;
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* Check whether the app is exempt from Android battery optimizations.
|
|
127
|
+
* Always returns true on iOS and web (no equivalent concept).
|
|
128
|
+
*/
|
|
129
|
+
isBatteryOptimizationExempt(): boolean;
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Request exemption from Android battery optimizations.
|
|
133
|
+
* Opens the system dialog asking the user to whitelist this app.
|
|
134
|
+
* Returns true if the dialog was shown (or already exempt), false on failure.
|
|
135
|
+
* Always resolves true on iOS and web.
|
|
136
|
+
*/
|
|
137
|
+
requestBatteryOptimizationExemption(): Promise<boolean>;
|
|
138
|
+
|
|
125
139
|
/** Enable SQLite event logging. All beacon events will be persisted to a local database. */
|
|
126
140
|
enableEventLogging(): void;
|
|
127
141
|
|
|
@@ -47,6 +47,8 @@ const stub = {
|
|
|
47
47
|
getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),
|
|
48
48
|
clearEventLogs: (): void => notSupported(),
|
|
49
49
|
destroyEventLogs: (): void => notSupported(),
|
|
50
|
+
isBatteryOptimizationExempt: (): boolean => true,
|
|
51
|
+
requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),
|
|
50
52
|
addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({
|
|
51
53
|
remove: () => {},
|
|
52
54
|
}),
|