expo-beacon 0.7.0 → 0.7.2

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.
@@ -48,13 +48,17 @@
48
48
  android:foregroundServiceType="connectedDevice"
49
49
  android:exported="false" />
50
50
 
51
- <!-- Restart monitoring after boot -->
51
+ <!-- Restart monitoring after boot.
52
+ android:exported="true" is required for the system to deliver BOOT_COMPLETED.
53
+ No android:permission — the incorrect RECEIVE_BOOT_COMPLETED attribute was
54
+ restricting incoming broadcasts instead of restricting who can receive them. -->
52
55
  <receiver
53
56
  android:name="expo.modules.beacon.BootReceiver"
54
- android:exported="true"
55
- android:permission="android.permission.RECEIVE_BOOT_COMPLETED">
57
+ android:exported="true">
56
58
  <intent-filter>
57
59
  <action android:name="android.intent.action.BOOT_COMPLETED" />
60
+ <!-- Self-scheduled retry when BT isn't ready at boot -->
61
+ <action android:name="expo.modules.beacon.ACTION_RETRY_MONITORING" />
58
62
  </intent-filter>
59
63
  </receiver>
60
64
  </application>
@@ -26,6 +26,13 @@ internal const val MONITORING_SCAN_PERIOD_MS = 1100L
26
26
  */
27
27
  internal const val MONITORING_BETWEEN_SCAN_PERIOD_MS = 1000L
28
28
 
29
+ /**
30
+ * AltBeacon region exit period — how long after the last sighting before
31
+ * MonitorNotifier.didExitRegion fires. Set generously to avoid premature
32
+ * monitor-level exits that bypass ranging hysteresis.
33
+ */
34
+ internal const val REGION_EXIT_PERIOD_MS = 60000L
35
+
29
36
  /**
30
37
  * Grace window after the last ranging sighting during which MonitorNotifier.didExitRegion
31
38
  * is suppressed. Matched to REGION_EXIT_PERIOD_MS so that a monitor-level exit is only
@@ -52,13 +59,6 @@ internal const val DISTANCE_INACTIVITY_MS = 60_000L
52
59
  /** Number of consecutive readings required to confirm a distance-based enter or exit transition. */
53
60
  internal const val HYSTERESIS_COUNT = 3
54
61
 
55
- /**
56
- * AltBeacon region exit period — how long after the last sighting before
57
- * MonitorNotifier.didExitRegion fires. Set generously to avoid premature
58
- * monitor-level exits that bypass ranging hysteresis.
59
- */
60
- internal const val REGION_EXIT_PERIOD_MS = 60000L
61
-
62
62
  /** Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable. */
63
63
  internal const val DEFAULT_MIN_RSSI = -85
64
64
 
@@ -112,10 +112,17 @@ class BeaconForegroundService : Service(), BeaconConsumer {
112
112
  startForeground(FOREGROUND_NOTIF_ID, buildForegroundNotification())
113
113
  }
114
114
  } catch (e: Exception) {
115
- // SecurityException on Android 14+ if BT permissions missing,
116
- // or other platform-specific issues. Stop gracefully instead of crashing.
117
- Log.e(TAG, "startForeground failed stopping service", e)
118
- sendErrorBroadcast(null, "SERVICE_START_FAILED", "startForeground failed stopping service: ${e.message}")
115
+ // SecurityException on Android 14+ if BT runtime permissions weren't yet granted,
116
+ // or ForegroundServiceStartNotAllowedException on Android 12+ / Android 17 beta
117
+ // if the service start window was missed (e.g. BT not yet initialized at boot).
118
+ val retryCount = intent?.getIntExtra(EXTRA_RETRY_COUNT, 0) ?: 0
119
+ Log.e(TAG, "startForeground failed (retry=$retryCount) — stopping service", e)
120
+ sendErrorBroadcast(null, "SERVICE_START_FAILED", "startForeground failed (retry=$retryCount): ${e.message}")
121
+ // Schedule a retry so monitoring can recover without user interaction, capped to
122
+ // avoid infinite crash loops. Only retry if monitoring should still be active.
123
+ if (retryCount < MAX_STARTFOREGROUND_RETRIES && isMonitoringActive(this)) {
124
+ scheduleServiceRetry(retryCount + 1)
125
+ }
119
126
  stopSelf()
120
127
  return START_NOT_STICKY
121
128
  }
@@ -129,6 +136,43 @@ class BeaconForegroundService : Service(), BeaconConsumer {
129
136
  return START_STICKY
130
137
  }
131
138
 
139
+ /**
140
+ * Schedules a one-shot alarm to restart this service via startForegroundService().
141
+ * Used when startForeground() fails transiently (e.g. BT not yet ready at boot).
142
+ */
143
+ private fun scheduleServiceRetry(retryCount: Int) {
144
+ val alarmManager = getSystemService(AlarmManager::class.java) ?: return
145
+ val retryIntent = Intent(this, BeaconForegroundService::class.java)
146
+ .putExtra(EXTRA_RETRY_COUNT, retryCount)
147
+ val pendingIntent = PendingIntent.getService(
148
+ this,
149
+ RETRY_SERVICE_REQUEST_CODE,
150
+ retryIntent,
151
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
152
+ )
153
+ alarmManager.setExactAndAllowWhileIdle(
154
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
155
+ SystemClock.elapsedRealtime() + RETRY_DELAY_MS,
156
+ pendingIntent
157
+ )
158
+ Log.w(TAG, "startForeground retry $retryCount scheduled in ${RETRY_DELAY_MS}ms")
159
+ }
160
+
161
+ /**
162
+ * Called by the system on Android 14+ (API 34) if a foreground service with a time-limited
163
+ * type exceeds its allowed duration. connectedDevice has no documented time limit as of
164
+ * Android 17, but this override ensures we handle a future limit gracefully rather than
165
+ * receiving an ANR.
166
+ */
167
+ @Suppress("NewApi")
168
+ override fun onTimeout(startId: Int, fgsType: Int) {
169
+ Log.w(TAG, "BeaconForegroundService.onTimeout(startId=$startId, fgsType=$fgsType) — scheduling restart")
170
+ if (isMonitoringActive(this)) {
171
+ scheduleServiceRetry(0)
172
+ }
173
+ stopSelf()
174
+ }
175
+
132
176
  override fun onBeaconServiceConnect() {
133
177
  serviceConnected = true
134
178
  // Read max distance, exit distance, and min RSSI from options prefs
@@ -736,6 +780,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
736
780
  const val DISTANCE_JUMP_FACTOR = 5.0
737
781
 
738
782
  private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
783
+ private const val EXTRA_RETRY_COUNT = "retryCount"
784
+ private const val MAX_STARTFOREGROUND_RETRIES = 3
785
+ private const val RETRY_DELAY_MS = 10_000L
786
+ private const val RETRY_SERVICE_REQUEST_CODE = 0x42454143 // "BEAC"
739
787
  @Volatile private var activeService: BeaconForegroundService? = null
740
788
 
741
789
  fun start(context: Context) {
@@ -1,17 +1,89 @@
1
1
  package expo.modules.beacon
2
2
 
3
+ import android.app.ActivityManager
4
+ import android.app.AlarmManager
5
+ import android.app.PendingIntent
3
6
  import android.content.BroadcastReceiver
4
7
  import android.content.Context
5
8
  import android.content.Intent
9
+ import android.os.Build
10
+ import android.os.SystemClock
11
+ import android.util.Log
12
+
13
+ private const val ACTION_RETRY_MONITORING = "expo.modules.beacon.ACTION_RETRY_MONITORING"
14
+ private const val RETRY_DELAY_MS = 10_000L
15
+ private const val RETRY_REQUEST_CODE = 0x424F4F54 // "BOOT"
6
16
 
7
17
  /**
8
18
  * Restarts beacon monitoring after device reboot if it was active before shutdown.
19
+ *
20
+ * Also handles ACTION_RETRY_MONITORING — a self-scheduled alarm used to retry the
21
+ * service start if Bluetooth is not yet fully initialized when BOOT_COMPLETED fires
22
+ * (observed on Android 17 beta where startForegroundService() throws SecurityException
23
+ * or ForegroundServiceStartNotAllowedException shortly after boot).
9
24
  */
10
25
  class BootReceiver : BroadcastReceiver() {
11
26
  override fun onReceive(context: Context, intent: Intent) {
12
- if (intent.action == Intent.ACTION_BOOT_COMPLETED) {
13
- if (BeaconForegroundService.isMonitoringActive(context)) {
14
- BeaconForegroundService.start(context)
27
+ when (intent.action) {
28
+ Intent.ACTION_BOOT_COMPLETED -> {
29
+ logMemoryKillDiagnostics(context)
30
+ if (BeaconForegroundService.isMonitoringActive(context)) {
31
+ tryStartService(context)
32
+ }
33
+ }
34
+ ACTION_RETRY_MONITORING -> {
35
+ if (BeaconForegroundService.isMonitoringActive(context)) {
36
+ tryStartService(context)
37
+ }
38
+ }
39
+ }
40
+ }
41
+
42
+ private fun tryStartService(context: Context) {
43
+ try {
44
+ BeaconForegroundService.start(context)
45
+ Log.d(TAG, "BootReceiver: BeaconForegroundService started successfully")
46
+ } catch (e: SecurityException) {
47
+ // ForegroundServiceStartNotAllowedException extends SecurityException.
48
+ // Can occur on Android 17 beta if Bluetooth is not yet fully initialized at boot.
49
+ Log.e(TAG, "BootReceiver: Failed to start service (SecurityException) — retrying in ${RETRY_DELAY_MS}ms", e)
50
+ scheduleRetry(context)
51
+ } catch (e: Exception) {
52
+ Log.e(TAG, "BootReceiver: Failed to start service — retrying in ${RETRY_DELAY_MS}ms", e)
53
+ scheduleRetry(context)
54
+ }
55
+ }
56
+
57
+ private fun scheduleRetry(context: Context) {
58
+ val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return
59
+ val retryIntent = Intent(context, BootReceiver::class.java).apply {
60
+ action = ACTION_RETRY_MONITORING
61
+ }
62
+ val pendingIntent = PendingIntent.getBroadcast(
63
+ context,
64
+ RETRY_REQUEST_CODE,
65
+ retryIntent,
66
+ PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
67
+ )
68
+ alarmManager.setExactAndAllowWhileIdle(
69
+ AlarmManager.ELAPSED_REALTIME_WAKEUP,
70
+ SystemClock.elapsedRealtime() + RETRY_DELAY_MS,
71
+ pendingIntent
72
+ )
73
+ }
74
+
75
+ /**
76
+ * Logs if the previous process was killed by Android 17's app memory limits
77
+ * (ApplicationExitInfo.description contains "MemoryLimiter") so the cause is
78
+ * visible in Logcat when the service restarts after being killed.
79
+ */
80
+ private fun logMemoryKillDiagnostics(context: Context) {
81
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
82
+ val am = context.getSystemService(ActivityManager::class.java)
83
+ am?.getHistoricalProcessExitReasons(null, 0, 5)?.forEach { info ->
84
+ if (info.description?.contains("MemoryLimiter") == true) {
85
+ Log.w(TAG, "BootReceiver: previous process killed by Android 17 memory limits: ${info.description}")
86
+ }
15
87
  }
16
88
  }
17
89
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "expo-beacon",
3
- "version": "0.7.0",
3
+ "version": "0.7.2",
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",