expo-beacon 0.7.1 → 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>
|
|
@@ -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
|
|
116
|
-
// or
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
}
|