expo-beacon 0.6.6 → 0.6.8

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.
@@ -76,29 +76,31 @@ class BeaconForegroundService : Service(), BeaconConsumer {
76
76
  // Increase AltBeacon's region exit period so didExitRegion doesn't fire
77
77
  // prematurely during brief BLE scan gaps.
78
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
- }
79
+ // NOTE: We intentionally do NOT call enableForegroundServiceScanning().
80
+ // Our BeaconForegroundService is already a foreground service (startForeground
81
+ // in onStartCommand). AltBeacon's internal BeaconService runs in the same
82
+ // process and inherits the elevated priority. Calling enable/disable on the
83
+ // shared singleton causes crashes when the ExpoBeaconModule has an active
84
+ // scan bound to the same BeaconManager.
91
85
  }
92
86
 
93
87
  override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
94
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
95
- startForeground(
96
- FOREGROUND_NOTIF_ID,
97
- buildForegroundNotification(),
98
- ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
99
- )
100
- } else {
101
- startForeground(FOREGROUND_NOTIF_ID, buildForegroundNotification())
88
+ try {
89
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
90
+ startForeground(
91
+ FOREGROUND_NOTIF_ID,
92
+ buildForegroundNotification(),
93
+ ServiceInfo.FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE
94
+ )
95
+ } else {
96
+ startForeground(FOREGROUND_NOTIF_ID, buildForegroundNotification())
97
+ }
98
+ } catch (e: Exception) {
99
+ // SecurityException on Android 14+ if BT permissions missing,
100
+ // or other platform-specific issues. Stop gracefully instead of crashing.
101
+ Log.e(TAG, "startForeground failed — stopping service", e)
102
+ stopSelf()
103
+ return START_NOT_STICKY
102
104
  }
103
105
  if (serviceConnected) {
104
106
  // Already bound from a prior onStartCommand — reload regions directly
@@ -157,8 +159,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
157
159
  try { beaconManager.stopRangingBeaconsInRegion(it) } catch (_: RemoteException) {}
158
160
  }
159
161
  distanceLogRegions.clear()
162
+ monitoredRegions.forEach {
163
+ try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
164
+ }
165
+ monitoredRegions.clear()
160
166
  monitoredRegionIds.clear()
161
- enteredRegions.clear()
162
167
  lastSeenAtMs.clear()
163
168
  timeoutHandler.removeCallbacksAndMessages(null)
164
169
  timeoutRunnables.clear()
@@ -167,10 +172,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
167
172
  exitCounters.clear()
168
173
  missCounters.clear()
169
174
  }
170
- monitoredRegions.forEach {
171
- try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
172
- }
173
- monitoredRegions.clear()
175
+ // NOTE: enteredRegions is intentionally NOT cleared here.
176
+ // Clearing it on every reload (e.g. START_STICKY restart or repeated
177
+ // startMonitoring calls) would reset the "already entered" state and
178
+ // cause the hysteresis to fire another ENTER event for beacons that
179
+ // are still nearby. Stale entries are pruned below after new regions
180
+ // are determined.
174
181
 
175
182
  // iBeacon regions
176
183
  for (i in 0 until beacons.length()) {
@@ -230,8 +237,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
230
237
 
231
238
  // If no regions to monitor, stop the service to avoid idling
232
239
  if (monitoredRegions.isEmpty()) {
240
+ enteredRegions.clear()
233
241
  Log.d(TAG, "No paired beacons — stopping idle foreground service")
234
242
  stopSelf()
243
+ } else {
244
+ // Prune enteredRegions for regions that are no longer monitored
245
+ enteredRegions.retainAll(monitoredRegionIds)
235
246
  }
236
247
  }
237
248
 
@@ -581,6 +592,7 @@ class BeaconForegroundService : Service(), BeaconConsumer {
581
592
  fun start(context: Context) {
582
593
  context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
583
594
  .edit().putBoolean("active", true).apply()
595
+ ensureNotificationChannel(context)
584
596
  val intent = Intent(context, BeaconForegroundService::class.java)
585
597
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
586
598
  context.startForegroundService(intent)
@@ -691,7 +703,6 @@ class BeaconForegroundService : Service(), BeaconConsumer {
691
703
  monitoredRegions.forEach {
692
704
  try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
693
705
  }
694
- try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
695
706
  beaconManager.unbind(this)
696
707
  super.onDestroy()
697
708
  }
@@ -327,24 +327,25 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
327
327
  promise.reject("PERMISSION_DENIED", "Location permissions required for background monitoring. Call requestPermissionsAsync() first.", null)
328
328
  return@AsyncFunction
329
329
  }
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.
330
+ // Android 12+ requires BLUETOOTH_SCAN for BLE operations;
331
+ // Android 14+ additionally requires it for connectedDevice foreground services.
332
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
333
+ val hasBtScan = ContextCompat.checkSelfPermission(ctx, Manifest.permission.BLUETOOTH_SCAN) == PackageManager.PERMISSION_GRANTED
334
+ val hasBtConnect = ContextCompat.checkSelfPermission(ctx, Manifest.permission.BLUETOOTH_CONNECT) == PackageManager.PERMISSION_GRANTED
335
+ if (!hasBtScan || !hasBtConnect) {
336
+ promise.reject("PERMISSION_DENIED", "Bluetooth permissions required for beacon monitoring. Call requestPermissionsAsync() first.", null)
337
+ return@AsyncFunction
343
338
  }
344
339
  }
345
340
 
346
341
  registerEventReceiver()
347
- BeaconForegroundService.start(ctx)
342
+ try {
343
+ BeaconForegroundService.start(ctx)
344
+ } catch (e: Exception) {
345
+ unregisterEventReceiver()
346
+ promise.reject("SERVICE_START_FAILED", "Failed to start monitoring service: ${e.message}", e)
347
+ return@AsyncFunction
348
+ }
348
349
  promise.resolve(null)
349
350
  }
350
351
 
@@ -360,7 +361,6 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
360
361
  return@AsyncFunction
361
362
  }
362
363
  BeaconForegroundService.stop(ctx)
363
- try { beaconManager.disableForegroundServiceScanning() } catch (_: Exception) {}
364
364
  unregisterEventReceiver()
365
365
  promise.resolve(null)
366
366
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.6.6",
3
+ "version": "0.6.8",
4
4
  "description": "Expo module for scanning, pairing, and monitoring iBeacons on Android and iOS",
5
5
  "main": "build/index.js",
6
6
  "types": "build/index.d.ts",