expo-beacon 0.8.8 → 0.8.9
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/build.gradle +5 -0
- package/android/src/main/AndroidManifest.xml +10 -0
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +15 -0
- package/android/src/main/java/expo/modules/beacon/BootReceiver.kt +102 -3
- package/android/src/main/java/expo/modules/beacon/CarPlayWatchdogWorker.kt +89 -0
- package/package.json +1 -1
package/android/build.gradle
CHANGED
|
@@ -23,4 +23,9 @@ dependencies {
|
|
|
23
23
|
// androidx.car.app provides CarConnection LiveData for detecting Android Auto sessions.
|
|
24
24
|
// No Android Auto certification or extra permissions required for read-only state.
|
|
25
25
|
implementation 'androidx.car.app:app:1.4.0'
|
|
26
|
+
// WorkManager powers the CarPlay watchdog (periodic restart of the foreground
|
|
27
|
+
// service if it was killed by the OS / OEM cleaners while CarPlay observation
|
|
28
|
+
// is enabled). Minimum periodic interval is 15 min; an AlarmManager loop in
|
|
29
|
+
// BootReceiver covers the sub-15-min gap.
|
|
30
|
+
implementation 'androidx.work:work-runtime-ktx:2.9.1'
|
|
26
31
|
}
|
|
@@ -79,8 +79,18 @@
|
|
|
79
79
|
android:exported="true">
|
|
80
80
|
<intent-filter>
|
|
81
81
|
<action android:name="android.intent.action.BOOT_COMPLETED" />
|
|
82
|
+
<!-- Direct-boot phase boot (Android 7+). Harmless without
|
|
83
|
+
directBootAware="true"; declaring early so the only
|
|
84
|
+
follow-up needed for true direct-boot support is that
|
|
85
|
+
single attribute. -->
|
|
86
|
+
<action android:name="android.intent.action.LOCKED_BOOT_COMPLETED" />
|
|
87
|
+
<!-- Restart observers after the host app is updated. -->
|
|
88
|
+
<action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
|
|
82
89
|
<!-- Self-scheduled retry when BT isn't ready at boot -->
|
|
83
90
|
<action android:name="expo.modules.beacon.ACTION_RETRY_MONITORING" />
|
|
91
|
+
<!-- Self-scheduled CarPlay watchdog (11-min cadence; above
|
|
92
|
+
setExactAndAllowWhileIdle per-app quota). -->
|
|
93
|
+
<action android:name="expo.modules.beacon.ACTION_CARPLAY_WATCHDOG" />
|
|
84
94
|
</intent-filter>
|
|
85
95
|
</receiver>
|
|
86
96
|
</application>
|
|
@@ -131,6 +131,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
131
131
|
// observer with no JS interaction required.
|
|
132
132
|
if (isCarPlayEnabled(this)) {
|
|
133
133
|
startCarPlayObserverInternal()
|
|
134
|
+
// Re-arm the WorkManager watchdog on every cold start. KEEP policy
|
|
135
|
+
// makes this a cheap no-op if a schedule already exists, but it
|
|
136
|
+
// recovers the schedule if WorkManager's DB was wiped by an OEM
|
|
137
|
+
// cleaner or by a `pm clear` from adb.
|
|
138
|
+
CarPlayWatchdogWorker.schedule(this)
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -1133,6 +1138,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
1133
1138
|
setCarPlayEnabled(context, true)
|
|
1134
1139
|
ensureNotificationChannel(context)
|
|
1135
1140
|
ensureCarPlayNotificationChannel(context)
|
|
1141
|
+
// Arm the WorkManager watchdog (15-min periodic safety net) and the
|
|
1142
|
+
// AlarmManager loop (~11-min cadence, above the OS quota). Both
|
|
1143
|
+
// call back into this same idempotent entry point if the service
|
|
1144
|
+
// is killed while CarPlay observation is enabled.
|
|
1145
|
+
CarPlayWatchdogWorker.schedule(context)
|
|
1146
|
+
BootReceiver.scheduleCarPlayWatchdogAlarm(context)
|
|
1136
1147
|
val intent = Intent(context, BeaconForegroundService::class.java)
|
|
1137
1148
|
.setAction(ACTION_ENABLE_CARPLAY)
|
|
1138
1149
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
@@ -1148,6 +1159,10 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
1148
1159
|
*/
|
|
1149
1160
|
fun disableCarPlay(context: Context) {
|
|
1150
1161
|
setCarPlayEnabled(context, false)
|
|
1162
|
+
// Stop the safety-net watchdogs — without this they would keep
|
|
1163
|
+
// restarting the service on every tick.
|
|
1164
|
+
CarPlayWatchdogWorker.cancel(context)
|
|
1165
|
+
BootReceiver.cancelCarPlayWatchdogAlarm(context)
|
|
1151
1166
|
// Wipe the monitor's persisted last-known connection so a future
|
|
1152
1167
|
// re-enable starts from a clean slate (no stale "was connected"
|
|
1153
1168
|
// assumption that would arm the bootstrap-grace re-check).
|
|
@@ -11,8 +11,24 @@ import android.os.SystemClock
|
|
|
11
11
|
import android.util.Log
|
|
12
12
|
|
|
13
13
|
private const val ACTION_RETRY_MONITORING = "expo.modules.beacon.ACTION_RETRY_MONITORING"
|
|
14
|
+
/**
|
|
15
|
+
* Periodic self-rescheduling watchdog action handled by [BootReceiver].
|
|
16
|
+
* Public on the manifest so the AlarmManager can route back to us after the
|
|
17
|
+
* process has been killed.
|
|
18
|
+
*/
|
|
19
|
+
internal const val ACTION_CARPLAY_WATCHDOG = "expo.modules.beacon.ACTION_CARPLAY_WATCHDOG"
|
|
14
20
|
private const val RETRY_DELAY_MS = 10_000L
|
|
15
21
|
private const val RETRY_REQUEST_CODE = 0x424F4F54 // "BOOT"
|
|
22
|
+
private const val CARPLAY_WATCHDOG_REQUEST_CODE = 0x43504C57 // "CPLW"
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cadence for the AlarmManager-based CarPlay watchdog. Set to **11 minutes**
|
|
26
|
+
* so we stay safely above Android's per-app rate limit on
|
|
27
|
+
* `setExactAndAllowWhileIdle()` (~10 minutes on API 23+, tightened again on
|
|
28
|
+
* API 31+). Going lower risks silent coalescing or dropping by the framework.
|
|
29
|
+
* This still beats WorkManager's 15-minute periodic floor.
|
|
30
|
+
*/
|
|
31
|
+
private const val CARPLAY_WATCHDOG_INTERVAL_MS = 11L * 60L * 1000L
|
|
16
32
|
|
|
17
33
|
/**
|
|
18
34
|
* Restarts beacon monitoring after device reboot if it was active before shutdown.
|
|
@@ -25,13 +41,19 @@ private const val RETRY_REQUEST_CODE = 0x424F4F54 // "BOOT"
|
|
|
25
41
|
class BootReceiver : BroadcastReceiver() {
|
|
26
42
|
override fun onReceive(context: Context, intent: Intent) {
|
|
27
43
|
when (intent.action) {
|
|
28
|
-
Intent.ACTION_BOOT_COMPLETED
|
|
44
|
+
Intent.ACTION_BOOT_COMPLETED,
|
|
45
|
+
Intent.ACTION_LOCKED_BOOT_COMPLETED,
|
|
46
|
+
Intent.ACTION_MY_PACKAGE_REPLACED -> {
|
|
29
47
|
logMemoryKillDiagnostics(context)
|
|
30
48
|
if (BeaconForegroundService.isMonitoringActive(context)) {
|
|
31
49
|
tryStartService(context)
|
|
32
|
-
}
|
|
33
|
-
|
|
50
|
+
}
|
|
51
|
+
if (BeaconForegroundService.isCarPlayEnabled(context)) {
|
|
52
|
+
// CarPlay observation: re-attach the observer so it survives
|
|
53
|
+
// reboot / app update / direct-boot, and arm the
|
|
54
|
+
// self-rescheduling alarm loop.
|
|
34
55
|
tryEnableCarPlay(context)
|
|
56
|
+
scheduleCarPlayWatchdogAlarm(context)
|
|
35
57
|
}
|
|
36
58
|
}
|
|
37
59
|
ACTION_RETRY_MONITORING -> {
|
|
@@ -41,6 +63,18 @@ class BootReceiver : BroadcastReceiver() {
|
|
|
41
63
|
tryEnableCarPlay(context)
|
|
42
64
|
}
|
|
43
65
|
}
|
|
66
|
+
ACTION_CARPLAY_WATCHDOG -> {
|
|
67
|
+
if (!BeaconForegroundService.isCarPlayEnabled(context)) {
|
|
68
|
+
// CarPlay was disabled since the alarm was set — let the
|
|
69
|
+
// chain die. cancelCarPlayWatchdogAlarm() is also called
|
|
70
|
+
// from disableCarPlay(), this is just defence-in-depth.
|
|
71
|
+
Log.d(TAG, "BootReceiver: watchdog tick skipped (CarPlay disabled)")
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
tryEnableCarPlay(context)
|
|
75
|
+
// Reschedule the next tick. setExactAndAllowWhileIdle is one-shot.
|
|
76
|
+
scheduleCarPlayWatchdogAlarm(context)
|
|
77
|
+
}
|
|
44
78
|
}
|
|
45
79
|
}
|
|
46
80
|
|
|
@@ -102,4 +136,69 @@ class BootReceiver : BroadcastReceiver() {
|
|
|
102
136
|
}
|
|
103
137
|
}
|
|
104
138
|
}
|
|
139
|
+
|
|
140
|
+
companion object {
|
|
141
|
+
/**
|
|
142
|
+
* Arm (or re-arm) the AlarmManager-based CarPlay watchdog. One-shot
|
|
143
|
+
* alarm using `setExactAndAllowWhileIdle` — the receiver re-schedules
|
|
144
|
+
* itself on each fire as long as CarPlay observation remains enabled.
|
|
145
|
+
*
|
|
146
|
+
* Cadence is governed by [CARPLAY_WATCHDOG_INTERVAL_MS], deliberately
|
|
147
|
+
* above the per-app exact-alarm quota.
|
|
148
|
+
*
|
|
149
|
+
* Safe to call from any context; idempotent thanks to
|
|
150
|
+
* `FLAG_UPDATE_CURRENT` on the PendingIntent.
|
|
151
|
+
*/
|
|
152
|
+
@JvmStatic
|
|
153
|
+
fun scheduleCarPlayWatchdogAlarm(context: Context) {
|
|
154
|
+
val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return
|
|
155
|
+
val intent = Intent(context, BootReceiver::class.java).apply {
|
|
156
|
+
action = ACTION_CARPLAY_WATCHDOG
|
|
157
|
+
// Explicit package so the implicit-broadcast restriction on
|
|
158
|
+
// Android 8+ doesn't drop the delivery.
|
|
159
|
+
`package` = context.packageName
|
|
160
|
+
}
|
|
161
|
+
val pendingIntent = PendingIntent.getBroadcast(
|
|
162
|
+
context,
|
|
163
|
+
CARPLAY_WATCHDOG_REQUEST_CODE,
|
|
164
|
+
intent,
|
|
165
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
166
|
+
)
|
|
167
|
+
try {
|
|
168
|
+
alarmManager.setExactAndAllowWhileIdle(
|
|
169
|
+
AlarmManager.ELAPSED_REALTIME_WAKEUP,
|
|
170
|
+
SystemClock.elapsedRealtime() + CARPLAY_WATCHDOG_INTERVAL_MS,
|
|
171
|
+
pendingIntent,
|
|
172
|
+
)
|
|
173
|
+
Log.d(TAG, "BootReceiver: CarPlay watchdog alarm armed (${CARPLAY_WATCHDOG_INTERVAL_MS}ms)")
|
|
174
|
+
} catch (t: Throwable) {
|
|
175
|
+
// Defensive: setExactAndAllowWhileIdle can throw SecurityException on
|
|
176
|
+
// some configurations even though it doesn't strictly require
|
|
177
|
+
// SCHEDULE_EXACT_ALARM. Fall back is the WorkManager 15-min job.
|
|
178
|
+
Log.w(TAG, "BootReceiver: failed to arm CarPlay watchdog alarm", t)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Cancel the periodic CarPlay watchdog alarm. */
|
|
183
|
+
@JvmStatic
|
|
184
|
+
fun cancelCarPlayWatchdogAlarm(context: Context) {
|
|
185
|
+
val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return
|
|
186
|
+
val intent = Intent(context, BootReceiver::class.java).apply {
|
|
187
|
+
action = ACTION_CARPLAY_WATCHDOG
|
|
188
|
+
`package` = context.packageName
|
|
189
|
+
}
|
|
190
|
+
val pendingIntent = PendingIntent.getBroadcast(
|
|
191
|
+
context,
|
|
192
|
+
CARPLAY_WATCHDOG_REQUEST_CODE,
|
|
193
|
+
intent,
|
|
194
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
195
|
+
)
|
|
196
|
+
try {
|
|
197
|
+
alarmManager.cancel(pendingIntent)
|
|
198
|
+
Log.d(TAG, "BootReceiver: CarPlay watchdog alarm cancelled")
|
|
199
|
+
} catch (t: Throwable) {
|
|
200
|
+
Log.w(TAG, "BootReceiver: failed to cancel CarPlay watchdog alarm", t)
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
105
204
|
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
package expo.modules.beacon
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.util.Log
|
|
5
|
+
import androidx.work.ExistingPeriodicWorkPolicy
|
|
6
|
+
import androidx.work.PeriodicWorkRequestBuilder
|
|
7
|
+
import androidx.work.WorkManager
|
|
8
|
+
import androidx.work.Worker
|
|
9
|
+
import androidx.work.WorkerParameters
|
|
10
|
+
import java.util.concurrent.TimeUnit
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Periodic safety-net that re-arms the [BeaconForegroundService] whenever
|
|
14
|
+
* CarPlay observation is enabled but the service has been killed (low memory,
|
|
15
|
+
* OEM cleaners, etc.).
|
|
16
|
+
*
|
|
17
|
+
* [BeaconForegroundService.enableCarPlay] is idempotent: if the service is
|
|
18
|
+
* alive this is a cheap no-op; if it is dead it cold-starts a fresh
|
|
19
|
+
* foreground instance and re-attaches [CarPlayMonitor].
|
|
20
|
+
*
|
|
21
|
+
* WorkManager guarantees a minimum 15-minute period. A second
|
|
22
|
+
* `AlarmManager` loop in [BootReceiver] covers the sub-15-min gap.
|
|
23
|
+
*/
|
|
24
|
+
internal class CarPlayWatchdogWorker(
|
|
25
|
+
appContext: Context,
|
|
26
|
+
params: WorkerParameters,
|
|
27
|
+
) : Worker(appContext, params) {
|
|
28
|
+
|
|
29
|
+
override fun doWork(): Result {
|
|
30
|
+
val ctx = applicationContext
|
|
31
|
+
if (!BeaconForegroundService.isCarPlayEnabled(ctx)) {
|
|
32
|
+
// User turned CarPlay off — let the periodic work fade out via cancel().
|
|
33
|
+
Log.d(TAG, "Watchdog tick: CarPlay disabled, no action")
|
|
34
|
+
return Result.success()
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
BeaconForegroundService.enableCarPlay(ctx)
|
|
38
|
+
Log.d(TAG, "Watchdog tick: ensured BeaconForegroundService is running for CarPlay")
|
|
39
|
+
} catch (t: Throwable) {
|
|
40
|
+
Log.w(TAG, "Watchdog tick: enableCarPlay failed", t)
|
|
41
|
+
// Returning retry would just defer to the next period; success keeps
|
|
42
|
+
// the periodic schedule running and avoids burning the retry budget.
|
|
43
|
+
}
|
|
44
|
+
return Result.success()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
companion object {
|
|
48
|
+
private const val TAG = "CarPlayWatchdog"
|
|
49
|
+
private const val WORK_NAME = "expo-beacon-carplay-watchdog"
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Schedule the periodic watchdog. Idempotent — [ExistingPeriodicWorkPolicy.KEEP]
|
|
53
|
+
* preserves the existing schedule across calls so we don't reset the
|
|
54
|
+
* next-run timer every time the user re-enables CarPlay.
|
|
55
|
+
*/
|
|
56
|
+
@JvmStatic
|
|
57
|
+
fun schedule(context: Context) {
|
|
58
|
+
try {
|
|
59
|
+
val request = PeriodicWorkRequestBuilder<CarPlayWatchdogWorker>(
|
|
60
|
+
15, TimeUnit.MINUTES,
|
|
61
|
+
).addTag(WORK_NAME).build()
|
|
62
|
+
WorkManager.getInstance(context.applicationContext)
|
|
63
|
+
.enqueueUniquePeriodicWork(
|
|
64
|
+
WORK_NAME,
|
|
65
|
+
ExistingPeriodicWorkPolicy.KEEP,
|
|
66
|
+
request,
|
|
67
|
+
)
|
|
68
|
+
Log.d(TAG, "Watchdog scheduled (15 min period)")
|
|
69
|
+
} catch (t: Throwable) {
|
|
70
|
+
// WorkManager.getInstance() can throw IllegalStateException if the
|
|
71
|
+
// host app has not initialised it. Fall back silently — the
|
|
72
|
+
// AlarmManager loop in BootReceiver still provides coverage.
|
|
73
|
+
Log.w(TAG, "Failed to schedule CarPlay watchdog", t)
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Cancel the periodic watchdog. Safe to call when not scheduled. */
|
|
78
|
+
@JvmStatic
|
|
79
|
+
fun cancel(context: Context) {
|
|
80
|
+
try {
|
|
81
|
+
WorkManager.getInstance(context.applicationContext)
|
|
82
|
+
.cancelUniqueWork(WORK_NAME)
|
|
83
|
+
Log.d(TAG, "Watchdog cancelled")
|
|
84
|
+
} catch (t: Throwable) {
|
|
85
|
+
Log.w(TAG, "Failed to cancel CarPlay watchdog", t)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|