expo-beacon 0.8.0 → 0.8.1
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/README.md +9 -0
- package/android/src/main/java/expo/modules/beacon/BeaconForegroundService.kt +193 -6
- package/android/src/main/java/expo/modules/beacon/BootReceiver.kt +15 -0
- package/android/src/main/java/expo/modules/beacon/ExpoBeaconModule.kt +40 -27
- package/build/ExpoBeaconModule.d.ts +35 -2
- 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/CarPlayMonitor.swift +21 -0
- package/ios/ExpoBeaconModule.swift +52 -4
- package/package.json +1 -1
- package/src/ExpoBeaconModule.ts +37 -2
- package/src/ExpoBeaconModule.web.ts +2 -0
package/README.md
CHANGED
|
@@ -729,11 +729,20 @@ disconnectSub.remove();
|
|
|
729
729
|
- Connect/disconnect events flow through the same SQLite event log and remote API forwarder as beacon events.
|
|
730
730
|
- When the config plugin is installed, the auto-generated `BeaconGeoPlugin` also calls `BackgroundGeolocation.start()` on connect and `.stop()` on disconnect — no extra wiring required.
|
|
731
731
|
|
|
732
|
+
**Background detection**
|
|
733
|
+
|
|
734
|
+
CarPlay observation is **persistent** — the enabled flag is stored in native preferences and the observer is automatically re-attached after app kill or device reboot. `startMonitoring()` also enables CarPlay observation by default; calling `startCarPlayMonitoring()` explicitly is only required if you want CarPlay events without beacon monitoring.
|
|
735
|
+
|
|
736
|
+
- **Android:** the foreground service hosts the `CarConnection` observer. As long as the service runs (which it does whenever beacon monitoring or CarPlay monitoring is enabled, and is restarted on boot by `BootReceiver`), CarPlay events are captured even after the app process is killed. **Guaranteed background detection.**
|
|
737
|
+
- **iOS:** the observer auto-restarts in the module's `OnCreate`, including background-launches triggered by beacon region monitoring. **iOS cannot wake a terminated app on CarPlay alone** — for guaranteed wake-from-suspension, also call `startMonitoring()` with at least one paired beacon (e.g. a beacon left in the vehicle). Region-wake events trigger a CarPlay state resync to reconcile any route changes that happened while the app was suspended.
|
|
738
|
+
|
|
732
739
|
**Notes**
|
|
733
740
|
|
|
734
741
|
- `startCarPlayMonitoring()` is idempotent. Calling it twice does not register a duplicate observer.
|
|
742
|
+
- `stopCarPlayMonitoring()` clears the persisted flag, so the observer will not auto-restart on next launch.
|
|
735
743
|
- The iOS detector does not require the CarPlay entitlement because it only reads the active audio route; you do not need to ship a CarPlay app.
|
|
736
744
|
- On iOS, if the JS bundle is suspended in the background, the JS event delivery is deferred until the app resumes, but the native lifecycle delegate (used by the geolocation plugin) fires immediately on connect.
|
|
745
|
+
- On Android, when CarPlay monitoring is enabled without beacon monitoring, the foreground service shows a generic "Connected device monitoring active" notification.
|
|
737
746
|
|
|
738
747
|
---
|
|
739
748
|
|
|
@@ -84,6 +84,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
84
84
|
// Seconds of silence after last valid sighting before a disappearance-based exit fires.
|
|
85
85
|
@Volatile private var exitTimeoutMs: Long = (DEFAULT_EXIT_TIMEOUT_SECONDS * 1000.0).toLong()
|
|
86
86
|
|
|
87
|
+
// CarPlay / Android Auto observer hosted by the foreground service so that
|
|
88
|
+
// CarConnection events are captured for as long as the service runs —
|
|
89
|
+
// independent of the JS bridge / module lifecycle. Survives app suspension
|
|
90
|
+
// and (via BootReceiver) device reboot.
|
|
91
|
+
@Volatile private var carPlayMonitor: CarPlayMonitor? = null
|
|
92
|
+
|
|
87
93
|
override fun onCreate() {
|
|
88
94
|
super.onCreate()
|
|
89
95
|
activeService = this
|
|
@@ -106,6 +112,14 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
106
112
|
// process and inherits the elevated priority. Calling enable/disable on the
|
|
107
113
|
// shared singleton causes crashes when the ExpoBeaconModule has an active
|
|
108
114
|
// scan bound to the same BeaconManager.
|
|
115
|
+
|
|
116
|
+
// Restore CarPlay observer if the user previously enabled CarPlay
|
|
117
|
+
// monitoring. Reading the flag here means cold-started services
|
|
118
|
+
// (e.g. via BootReceiver after reboot) automatically re-attach the
|
|
119
|
+
// observer with no JS interaction required.
|
|
120
|
+
if (isCarPlayEnabled(this)) {
|
|
121
|
+
startCarPlayObserverInternal()
|
|
122
|
+
}
|
|
109
123
|
}
|
|
110
124
|
|
|
111
125
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
|
@@ -134,11 +148,32 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
134
148
|
stopSelf()
|
|
135
149
|
return START_NOT_STICKY
|
|
136
150
|
}
|
|
151
|
+
// Handle CarPlay enable/disable actions. These can arrive after the
|
|
152
|
+
// service is already running for beacon monitoring, OR be the reason
|
|
153
|
+
// the service was started in the first place (CarPlay-only mode).
|
|
154
|
+
when (intent?.action) {
|
|
155
|
+
ACTION_ENABLE_CARPLAY -> {
|
|
156
|
+
setCarPlayEnabled(this, true)
|
|
157
|
+
startCarPlayObserverInternal()
|
|
158
|
+
}
|
|
159
|
+
ACTION_DISABLE_CARPLAY -> {
|
|
160
|
+
setCarPlayEnabled(this, false)
|
|
161
|
+
stopCarPlayObserverInternal()
|
|
162
|
+
// If the service is only alive for CarPlay (no beacon monitoring),
|
|
163
|
+
// shut it down so we don't keep an unnecessary foreground notification.
|
|
164
|
+
if (!isMonitoringActive(this)) {
|
|
165
|
+
stopSelf()
|
|
166
|
+
return START_NOT_STICKY
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
137
170
|
if (serviceConnected) {
|
|
138
171
|
// Already bound from a prior onStartCommand — reload regions directly
|
|
139
172
|
// so that re-starting monitoring from JS always takes effect.
|
|
140
173
|
loadAndMonitorRegions()
|
|
141
|
-
} else {
|
|
174
|
+
} else if (isMonitoringActive(this)) {
|
|
175
|
+
// Only bind to AltBeacon when beacon monitoring is active.
|
|
176
|
+
// CarPlay-only mode keeps the service alive without scanning.
|
|
142
177
|
beaconManager.bind(this)
|
|
143
178
|
}
|
|
144
179
|
return START_STICKY
|
|
@@ -765,6 +800,64 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
765
800
|
}
|
|
766
801
|
}
|
|
767
802
|
|
|
803
|
+
// MARK: - CarPlay observer (service-hosted)
|
|
804
|
+
|
|
805
|
+
/**
|
|
806
|
+
* Lazily instantiate and start the CarPlay observer. Idempotent —
|
|
807
|
+
* `CarPlayMonitor.start` itself is safe to call multiple times.
|
|
808
|
+
* Must be called from any thread; the monitor hops to main internally.
|
|
809
|
+
*/
|
|
810
|
+
private fun startCarPlayObserverInternal() {
|
|
811
|
+
val monitor = carPlayMonitor ?: try {
|
|
812
|
+
CarPlayMonitor(applicationContext).also { carPlayMonitor = it }
|
|
813
|
+
} catch (e: Throwable) {
|
|
814
|
+
Log.w(TAG, "Failed to create CarPlayMonitor", e)
|
|
815
|
+
return
|
|
816
|
+
}
|
|
817
|
+
try {
|
|
818
|
+
monitor.start { eventName, payload ->
|
|
819
|
+
emitCarPlayEvent(eventName, payload)
|
|
820
|
+
}
|
|
821
|
+
} catch (e: Throwable) {
|
|
822
|
+
Log.w(TAG, "Failed to start CarPlayMonitor", e)
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
private fun stopCarPlayObserverInternal() {
|
|
827
|
+
carPlayMonitor?.stop()
|
|
828
|
+
// Keep the instance — it's idempotent, and recreating costs nothing
|
|
829
|
+
// beyond the `CarConnection` LiveData wrapper. Setting null here is
|
|
830
|
+
// also safe but unnecessary.
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
/**
|
|
834
|
+
* Fan out a CarPlay event to all sinks: SQLite log, remote API forwarder,
|
|
835
|
+
* native plugin registry, and (best-effort) the live JS bridge. Runs from
|
|
836
|
+
* the main thread (CarPlayMonitor's emit hop).
|
|
837
|
+
*/
|
|
838
|
+
private fun emitCarPlayEvent(eventName: String, payload: Map<String, Any?>) {
|
|
839
|
+
// SQLite log (only if event logging is enabled).
|
|
840
|
+
try {
|
|
841
|
+
if (BeaconEventLogger.isLoggingEnabled(this)) {
|
|
842
|
+
val identifier = payload["identifier"] as? String
|
|
843
|
+
getOrCreateEventLogger().logEvent(eventName, identifier, payload)
|
|
844
|
+
}
|
|
845
|
+
} catch (e: Throwable) {
|
|
846
|
+
Log.w(TAG, "CarPlay log write failed", e)
|
|
847
|
+
}
|
|
848
|
+
// Remote API forwarder (no-op if unconfigured).
|
|
849
|
+
try { apiForwarder?.forwardEvent(payload) } catch (_: Throwable) {}
|
|
850
|
+
// Native plugin registry (BeaconGeoPlugin etc.).
|
|
851
|
+
when (eventName) {
|
|
852
|
+
"onCarPlayConnected" -> BeaconPluginRegistry.dispatchCarPlayConnected(
|
|
853
|
+
payload["transport"] as? String ?: "unknown"
|
|
854
|
+
)
|
|
855
|
+
"onCarPlayDisconnected" -> BeaconPluginRegistry.dispatchCarPlayDisconnected()
|
|
856
|
+
}
|
|
857
|
+
// Best-effort delivery to the live JS bridge if a module instance is bound.
|
|
858
|
+
try { boundModule?.get()?.forwardCarPlayEventFromService(eventName, payload) } catch (_: Throwable) {}
|
|
859
|
+
}
|
|
860
|
+
|
|
768
861
|
private fun createNotificationChannel() {
|
|
769
862
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
770
863
|
val config = readNotificationConfig()
|
|
@@ -814,13 +907,20 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
814
907
|
const val DISTANCE_JUMP_FACTOR = 5.0
|
|
815
908
|
|
|
816
909
|
private const val PREF_IS_MONITORING = "expo.beacon.is_monitoring"
|
|
910
|
+
private const val PREF_CARPLAY_ENABLED = "expo.beacon.carplay_enabled"
|
|
817
911
|
private const val EXTRA_RETRY_COUNT = "retryCount"
|
|
912
|
+
/** Intent action: enable CarPlay/Android Auto observation in the foreground service. */
|
|
913
|
+
const val ACTION_ENABLE_CARPLAY = "expo.modules.beacon.ENABLE_CARPLAY"
|
|
914
|
+
/** Intent action: disable CarPlay/Android Auto observation. Stops the service if no other reason to run. */
|
|
915
|
+
const val ACTION_DISABLE_CARPLAY = "expo.modules.beacon.DISABLE_CARPLAY"
|
|
818
916
|
private const val MAX_STARTFOREGROUND_RETRIES = 3
|
|
819
917
|
private const val RETRY_DELAY_MS = 10_000L
|
|
820
918
|
private const val RETRY_SERVICE_REQUEST_CODE = 0x42454143 // "BEAC"
|
|
821
919
|
/** Minimum milliseconds between consecutive loadAndMonitorRegions() calls. */
|
|
822
920
|
private const val LOAD_REGIONS_DEBOUNCE_MS = 500L
|
|
823
921
|
@Volatile private var activeService: BeaconForegroundService? = null
|
|
922
|
+
/** Weak reference to the live ExpoBeaconModule for best-effort JS bridge fan-out. */
|
|
923
|
+
@Volatile private var boundModule: java.lang.ref.WeakReference<ExpoBeaconModule>? = null
|
|
824
924
|
|
|
825
925
|
fun start(context: Context) {
|
|
826
926
|
context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
|
|
@@ -837,6 +937,12 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
837
937
|
fun stop(context: Context) {
|
|
838
938
|
context.getSharedPreferences(PREF_IS_MONITORING, Context.MODE_PRIVATE)
|
|
839
939
|
.edit().putBoolean("active", false).apply()
|
|
940
|
+
// Keep the service alive if it's still needed for CarPlay observation;
|
|
941
|
+
// otherwise the user would silently lose CarPlay events when calling
|
|
942
|
+
// stopMonitoring() while CarPlay monitoring was independently enabled.
|
|
943
|
+
if (isCarPlayEnabled(context)) {
|
|
944
|
+
return
|
|
945
|
+
}
|
|
840
946
|
context.stopService(Intent(context, BeaconForegroundService::class.java))
|
|
841
947
|
}
|
|
842
948
|
|
|
@@ -845,6 +951,70 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
845
951
|
.getBoolean("active", false)
|
|
846
952
|
}
|
|
847
953
|
|
|
954
|
+
// MARK: - CarPlay public API
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* Persist whether the user wants CarPlay observation. Cold service starts
|
|
958
|
+
* (e.g. via [BootReceiver]) read this flag in [onCreate] and re-attach
|
|
959
|
+
* the observer automatically.
|
|
960
|
+
*/
|
|
961
|
+
internal fun setCarPlayEnabled(context: Context, enabled: Boolean) {
|
|
962
|
+
context.getSharedPreferences(PREF_CARPLAY_ENABLED, Context.MODE_PRIVATE)
|
|
963
|
+
.edit().putBoolean("enabled", enabled).apply()
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
fun isCarPlayEnabled(context: Context): Boolean {
|
|
967
|
+
return context.getSharedPreferences(PREF_CARPLAY_ENABLED, Context.MODE_PRIVATE)
|
|
968
|
+
.getBoolean("enabled", false)
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Enable CarPlay observation. Starts the foreground service if it's not
|
|
973
|
+
* already running so the observer survives app suspension and process death.
|
|
974
|
+
* Idempotent and safe to call from any context.
|
|
975
|
+
*/
|
|
976
|
+
fun enableCarPlay(context: Context) {
|
|
977
|
+
setCarPlayEnabled(context, true)
|
|
978
|
+
ensureNotificationChannel(context)
|
|
979
|
+
val intent = Intent(context, BeaconForegroundService::class.java)
|
|
980
|
+
.setAction(ACTION_ENABLE_CARPLAY)
|
|
981
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
982
|
+
context.startForegroundService(intent)
|
|
983
|
+
} else {
|
|
984
|
+
context.startService(intent)
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* Disable CarPlay observation. The foreground service stops itself
|
|
990
|
+
* if no other monitoring reason remains.
|
|
991
|
+
*/
|
|
992
|
+
fun disableCarPlay(context: Context) {
|
|
993
|
+
setCarPlayEnabled(context, false)
|
|
994
|
+
val intent = Intent(context, BeaconForegroundService::class.java)
|
|
995
|
+
.setAction(ACTION_DISABLE_CARPLAY)
|
|
996
|
+
// Best-effort: if the service isn't running, sending the intent will
|
|
997
|
+
// start it just to stop it. Skip startForegroundService unless beacon
|
|
998
|
+
// monitoring is also active (otherwise the start might fail with
|
|
999
|
+
// ForegroundServiceStartNotAllowedException on some Android versions).
|
|
1000
|
+
if (activeService != null || isMonitoringActive(context)) {
|
|
1001
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
1002
|
+
context.startForegroundService(intent)
|
|
1003
|
+
} else {
|
|
1004
|
+
context.startService(intent)
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
/**
|
|
1010
|
+
* Bind a live module instance for best-effort JS-bridge delivery of
|
|
1011
|
+
* service-emitted events. The reference is weak; pass `null` from the
|
|
1012
|
+
* module's OnDestroy to clear it.
|
|
1013
|
+
*/
|
|
1014
|
+
fun bindModule(module: ExpoBeaconModule?) {
|
|
1015
|
+
boundModule = module?.let { java.lang.ref.WeakReference(it) }
|
|
1016
|
+
}
|
|
1017
|
+
|
|
848
1018
|
fun getMonitoringRuntimeSnapshot(): Map<String, MonitoringRuntimeState> {
|
|
849
1019
|
return activeService?.snapshotMonitoringRuntimeState() ?: emptyMap()
|
|
850
1020
|
}
|
|
@@ -891,10 +1061,16 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
891
1061
|
val config = try { org.json.JSONObject(json ?: "") } catch (_: Exception) { org.json.JSONObject() }
|
|
892
1062
|
val fgConfig = config.optJSONObject("foregroundService")
|
|
893
1063
|
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
1064
|
+
// If beacon monitoring is not active and the service is alive only
|
|
1065
|
+
// for CarPlay observation, fall back to a generic notification so
|
|
1066
|
+
// users don't see "Monitoring for iBeacons" when no beacons are being
|
|
1067
|
+
// monitored.
|
|
1068
|
+
val carPlayOnly = !isMonitoringActive(context) && isCarPlayEnabled(context)
|
|
1069
|
+
val defaultTitle = if (carPlayOnly) "Connected device monitoring active" else "Beacon Monitoring Active"
|
|
1070
|
+
val defaultText = if (carPlayOnly) "Monitoring connected vehicle (CarPlay/Android Auto)" else "Monitoring for iBeacons in the background"
|
|
1071
|
+
|
|
1072
|
+
val title = fgConfig?.optString("title")?.takeIf { it.isNotEmpty() } ?: defaultTitle
|
|
1073
|
+
val text = fgConfig?.optString("text")?.takeIf { it.isNotEmpty() } ?: defaultText
|
|
898
1074
|
val iconName = fgConfig?.optString("icon")?.takeIf { it.isNotEmpty() }
|
|
899
1075
|
val iconResId = iconName?.let { name ->
|
|
900
1076
|
try { context.resources.getIdentifier(name, "drawable", context.packageName).takeIf { it != 0 } }
|
|
@@ -921,6 +1097,13 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
921
1097
|
if (activeService === this) {
|
|
922
1098
|
activeService = null
|
|
923
1099
|
}
|
|
1100
|
+
// Stop CarPlay observer first so the LiveData observer doesn't leak
|
|
1101
|
+
// past the service lifecycle. Safe even if it was never started.
|
|
1102
|
+
try {
|
|
1103
|
+
carPlayMonitor?.stop()
|
|
1104
|
+
} catch (_: Throwable) {}
|
|
1105
|
+
carPlayMonitor = null
|
|
1106
|
+
val wasBound = serviceConnected
|
|
924
1107
|
serviceConnected = false
|
|
925
1108
|
timeoutHandler.removeCallbacksAndMessages(null)
|
|
926
1109
|
timeoutRunnables.clear()
|
|
@@ -944,7 +1127,11 @@ class BeaconForegroundService : Service(), BeaconConsumer {
|
|
|
944
1127
|
monitoredRegions.forEach {
|
|
945
1128
|
try { beaconManager.stopMonitoringBeaconsInRegion(it) } catch (_: RemoteException) {}
|
|
946
1129
|
}
|
|
947
|
-
|
|
1130
|
+
// Only unbind if we successfully bound — CarPlay-only service
|
|
1131
|
+
// instances skip beaconManager.bind() in onStartCommand.
|
|
1132
|
+
if (wasBound) {
|
|
1133
|
+
try { beaconManager.unbind(this) } catch (_: Throwable) {}
|
|
1134
|
+
}
|
|
948
1135
|
super.onDestroy()
|
|
949
1136
|
}
|
|
950
1137
|
|
|
@@ -29,11 +29,16 @@ class BootReceiver : BroadcastReceiver() {
|
|
|
29
29
|
logMemoryKillDiagnostics(context)
|
|
30
30
|
if (BeaconForegroundService.isMonitoringActive(context)) {
|
|
31
31
|
tryStartService(context)
|
|
32
|
+
} else if (BeaconForegroundService.isCarPlayEnabled(context)) {
|
|
33
|
+
// CarPlay-only mode: re-attach the observer so it survives reboot.
|
|
34
|
+
tryEnableCarPlay(context)
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
ACTION_RETRY_MONITORING -> {
|
|
35
38
|
if (BeaconForegroundService.isMonitoringActive(context)) {
|
|
36
39
|
tryStartService(context)
|
|
40
|
+
} else if (BeaconForegroundService.isCarPlayEnabled(context)) {
|
|
41
|
+
tryEnableCarPlay(context)
|
|
37
42
|
}
|
|
38
43
|
}
|
|
39
44
|
}
|
|
@@ -54,6 +59,16 @@ class BootReceiver : BroadcastReceiver() {
|
|
|
54
59
|
}
|
|
55
60
|
}
|
|
56
61
|
|
|
62
|
+
private fun tryEnableCarPlay(context: Context) {
|
|
63
|
+
try {
|
|
64
|
+
BeaconForegroundService.enableCarPlay(context)
|
|
65
|
+
Log.d(TAG, "BootReceiver: CarPlay-only service started successfully")
|
|
66
|
+
} catch (e: Exception) {
|
|
67
|
+
Log.e(TAG, "BootReceiver: Failed to start CarPlay-only service — retrying in ${RETRY_DELAY_MS}ms", e)
|
|
68
|
+
scheduleRetry(context)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
57
72
|
private fun scheduleRetry(context: Context) {
|
|
58
73
|
val alarmManager = context.getSystemService(AlarmManager::class.java) ?: return
|
|
59
74
|
val retryIntent = Intent(context, BootReceiver::class.java).apply {
|
|
@@ -82,9 +82,6 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
82
82
|
private var eventLogger: BeaconEventLogger? = null
|
|
83
83
|
@Volatile private var loggingEnabled = false
|
|
84
84
|
|
|
85
|
-
// CarPlay / Android Auto monitor (lazy because it requires reactContext)
|
|
86
|
-
private var carPlayMonitor: CarPlayMonitor? = null
|
|
87
|
-
|
|
88
85
|
override fun definition() = ModuleDefinition {
|
|
89
86
|
Name("ExpoBeacon")
|
|
90
87
|
|
|
@@ -379,6 +376,10 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
379
376
|
sendEvent("onBeaconError", mapOf("identifier" to "", "code" to "SERVICE_START_FAILED", "message" to "Failed to start monitoring service: ${e.message}"))
|
|
380
377
|
return@AsyncFunction
|
|
381
378
|
}
|
|
379
|
+
// Auto-enable CarPlay observation alongside beacon monitoring so
|
|
380
|
+
// the service captures CarPlay/Android Auto events for the same
|
|
381
|
+
// lifetime. Users can opt out at any time via stopCarPlayMonitoring().
|
|
382
|
+
try { BeaconForegroundService.enableCarPlay(ctx) } catch (_: Throwable) {}
|
|
382
383
|
promise.resolve(null)
|
|
383
384
|
}
|
|
384
385
|
|
|
@@ -462,6 +463,11 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
462
463
|
loggingEnabled = false
|
|
463
464
|
}
|
|
464
465
|
|
|
466
|
+
Function("isEventLoggingEnabled") {
|
|
467
|
+
val ctx = appContext.reactContext ?: return@Function false
|
|
468
|
+
BeaconEventLogger.isLoggingEnabled(ctx)
|
|
469
|
+
}
|
|
470
|
+
|
|
465
471
|
Function("getEventLogs") { options: Map<String, Any?>? ->
|
|
466
472
|
val logger = getOrCreateEventLogger() ?: return@Function emptyList<Map<String, Any?>>()
|
|
467
473
|
val limit = (options?.get("limit") as? Number)?.toInt() ?: 1000
|
|
@@ -574,10 +580,7 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
574
580
|
return@AsyncFunction
|
|
575
581
|
}
|
|
576
582
|
try {
|
|
577
|
-
|
|
578
|
-
monitor.start { eventName, payload ->
|
|
579
|
-
emitCarPlayEvent(eventName, payload)
|
|
580
|
-
}
|
|
583
|
+
BeaconForegroundService.enableCarPlay(ctx)
|
|
581
584
|
promise.resolve(null)
|
|
582
585
|
} catch (e: Throwable) {
|
|
583
586
|
promise.reject("CARPLAY_START_FAILED", "Failed to start CarPlay monitoring: ${e.message}", e)
|
|
@@ -585,15 +588,35 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
585
588
|
}
|
|
586
589
|
|
|
587
590
|
AsyncFunction("stopCarPlayMonitoring") { promise: Promise ->
|
|
588
|
-
|
|
589
|
-
|
|
591
|
+
val ctx = appContext.reactContext
|
|
592
|
+
if (ctx == null) {
|
|
593
|
+
promise.reject("NO_CONTEXT", "React context is not available", null)
|
|
594
|
+
return@AsyncFunction
|
|
595
|
+
}
|
|
596
|
+
try {
|
|
597
|
+
BeaconForegroundService.disableCarPlay(ctx)
|
|
598
|
+
promise.resolve(null)
|
|
599
|
+
} catch (e: Throwable) {
|
|
600
|
+
promise.reject("CARPLAY_STOP_FAILED", "Failed to stop CarPlay monitoring: ${e.message}", e)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
Function("isCarPlayMonitoringEnabled") {
|
|
605
|
+
val ctx = appContext.reactContext ?: return@Function false
|
|
606
|
+
BeaconForegroundService.isCarPlayEnabled(ctx)
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
OnCreate {
|
|
610
|
+
// Register this module instance for best-effort JS-bridge fan-out
|
|
611
|
+
// of CarPlay events emitted by the foreground service. The service
|
|
612
|
+
// holds a weak reference; cleared in OnDestroy.
|
|
613
|
+
BeaconForegroundService.bindModule(this@ExpoBeaconModule)
|
|
590
614
|
}
|
|
591
615
|
|
|
592
616
|
OnDestroy {
|
|
593
617
|
with(this@ExpoBeaconModule) {
|
|
618
|
+
BeaconForegroundService.bindModule(null)
|
|
594
619
|
unregisterEventReceiver()
|
|
595
|
-
carPlayMonitor?.stop()
|
|
596
|
-
carPlayMonitor = null
|
|
597
620
|
loggingEnabled = false
|
|
598
621
|
eventLogger?.close()
|
|
599
622
|
eventLogger = null
|
|
@@ -1038,22 +1061,12 @@ class ExpoBeaconModule : Module(), BeaconConsumer {
|
|
|
1038
1061
|
}
|
|
1039
1062
|
|
|
1040
1063
|
/**
|
|
1041
|
-
*
|
|
1042
|
-
*
|
|
1064
|
+
* Called by [BeaconForegroundService] (via weak reference) when it emits a
|
|
1065
|
+
* CarPlay event from its own observer. Best-effort delivery to the JS bridge
|
|
1066
|
+
* — the service has already handled SQLite logging, API forwarding, and
|
|
1067
|
+
* native plugin dispatch, so this method only needs to relay to JS.
|
|
1043
1068
|
*/
|
|
1044
|
-
|
|
1045
|
-
sendEvent(eventName, payload)
|
|
1046
|
-
logBeaconEvent(eventName, payload)
|
|
1047
|
-
appContext.reactContext?.let { ctx ->
|
|
1048
|
-
try {
|
|
1049
|
-
BeaconApiForwarder(ctx).forwardEvent(payload)
|
|
1050
|
-
} catch (_: Throwable) { /* best-effort */ }
|
|
1051
|
-
}
|
|
1052
|
-
when (eventName) {
|
|
1053
|
-
"onCarPlayConnected" -> BeaconPluginRegistry.dispatchCarPlayConnected(
|
|
1054
|
-
payload["transport"] as? String ?: "unknown"
|
|
1055
|
-
)
|
|
1056
|
-
"onCarPlayDisconnected" -> BeaconPluginRegistry.dispatchCarPlayDisconnected()
|
|
1057
|
-
}
|
|
1069
|
+
fun forwardCarPlayEventFromService(eventName: String, payload: Map<String, Any?>) {
|
|
1070
|
+
try { sendEvent(eventName, payload) } catch (_: Throwable) { /* JS bridge may be torn down */ }
|
|
1058
1071
|
}
|
|
1059
1072
|
}
|
|
@@ -93,6 +93,11 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
93
93
|
enableEventLogging(): void;
|
|
94
94
|
/** Disable event logging. Previously logged events are retained. */
|
|
95
95
|
disableEventLogging(): void;
|
|
96
|
+
/**
|
|
97
|
+
* Returns whether SQLite event logging is currently enabled.
|
|
98
|
+
* Reads the persisted flag, so this stays accurate across app cold-starts.
|
|
99
|
+
*/
|
|
100
|
+
isEventLoggingEnabled(): boolean;
|
|
96
101
|
/**
|
|
97
102
|
* Retrieve logged beacon events from the SQLite database.
|
|
98
103
|
* @param options Optional filters (limit, eventType, sinceTimestamp).
|
|
@@ -143,7 +148,22 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
143
148
|
* endpoint, and dispatched to native lifecycle plugins (used by the auto-
|
|
144
149
|
* generated geolocation plugin to start/stop background-geolocation).
|
|
145
150
|
*
|
|
146
|
-
*
|
|
151
|
+
* **Persistent.** The enabled state is stored in native preferences and
|
|
152
|
+
* survives app kill / device reboot. Call `stopCarPlayMonitoring()` to
|
|
153
|
+
* disable. `startMonitoring()` also enables CarPlay observation
|
|
154
|
+
* automatically — calling this method explicitly is only required if you
|
|
155
|
+
* want CarPlay events without beacon monitoring.
|
|
156
|
+
*
|
|
157
|
+
* **Background behavior:**
|
|
158
|
+
* - **Android**: the foreground service hosts the observer and continues to
|
|
159
|
+
* receive `CarConnection` events even after the app process is killed and
|
|
160
|
+
* restarted via `BootReceiver`. Guaranteed background detection.
|
|
161
|
+
* - **iOS**: the observer auto-restarts in `OnCreate` whenever the module
|
|
162
|
+
* is recreated, including background-launches triggered by beacon region
|
|
163
|
+
* monitoring. **iOS cannot wake a terminated app on CarPlay alone** — for
|
|
164
|
+
* guaranteed wake-from-suspension, also call `startMonitoring()` with at
|
|
165
|
+
* least one paired beacon. Region wake events trigger a CarPlay state
|
|
166
|
+
* resync to reconcile any route changes that occurred during suspension.
|
|
147
167
|
*
|
|
148
168
|
* - iOS: observes `AVAudioSession.routeChangeNotification` for `.carAudio` ports.
|
|
149
169
|
* No CarPlay entitlement required.
|
|
@@ -152,8 +172,21 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
152
172
|
* - Web: no-op (resolves immediately).
|
|
153
173
|
*/
|
|
154
174
|
startCarPlayMonitoring(): Promise<void>;
|
|
155
|
-
/**
|
|
175
|
+
/**
|
|
176
|
+
* Stop CarPlay / Android Auto connection monitoring and clear the persisted
|
|
177
|
+
* "enabled" flag so the observer is not auto-restarted on next launch / boot.
|
|
178
|
+
*
|
|
179
|
+
* On Android, if no beacon monitoring is active, the foreground service
|
|
180
|
+
* stops itself.
|
|
181
|
+
*/
|
|
156
182
|
stopCarPlayMonitoring(): Promise<void>;
|
|
183
|
+
/**
|
|
184
|
+
* Returns whether CarPlay / Android Auto observation is currently enabled.
|
|
185
|
+
* Reads the persisted flag, so the value survives app cold-starts and
|
|
186
|
+
* reflects the native source of truth (foreground service on Android,
|
|
187
|
+
* UserDefaults suite on iOS).
|
|
188
|
+
*/
|
|
189
|
+
isCarPlayMonitoringEnabled(): boolean;
|
|
157
190
|
}
|
|
158
191
|
declare const _default: ExpoBeaconModule;
|
|
159
192
|
export default _default;
|
|
@@ -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,gBAAgB,EAChB,oBAAoB,EACpB,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;IAExB;;;;;;;;OAQG;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAE/D;;;OAGG;IACH,mBAAmB,IAAI,gBAAgB;IAEvC;;;OAGG;IACH,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI;IAExE;;OAEG;IACH,wBAAwB,IAAI,oBAAoB,EAAE;IAElD;;;OAGG;IACH,cAAc,IAAI;QAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAElF
|
|
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,gBAAgB,EAChB,oBAAoB,EACpB,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,qBAAqB,IAAI,OAAO;IAEhC;;;OAGG;IACH,YAAY,CAAC,OAAO,CAAC,EAAE,oBAAoB,GAAG,aAAa,EAAE;IAE7D,kDAAkD;IAClD,cAAc,IAAI,IAAI;IAEtB,mEAAmE;IACnE,gBAAgB,IAAI,IAAI;IAExB;;;;;;;;OAQG;IACH,cAAc,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,IAAI;IAE/D;;;OAGG;IACH,mBAAmB,IAAI,gBAAgB;IAEvC;;;OAGG;IACH,uBAAuB,CAAC,UAAU,EAAE,MAAM,GAAG,oBAAoB,GAAG,IAAI;IAExE;;OAEG;IACH,wBAAwB,IAAI,oBAAoB,EAAE;IAElD;;;OAGG;IACH,cAAc,IAAI;QAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;IAElF;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8BG;IACH,sBAAsB,IAAI,OAAO,CAAC,IAAI,CAAC;IAEvC;;;;;;OAMG;IACH,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC;IAEtC;;;;;OAKG;IACH,0BAA0B,IAAI,OAAO;CACtC;;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;AAyPzD,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 MonitoringConfig,\r\n MonitoredDeviceState,\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 * Returns whether SQLite event logging is currently enabled.\r\n * Reads the persisted flag, so this stays accurate across app cold-starts.\r\n */\r\n isEventLoggingEnabled(): boolean;\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\n * Configure a remote API endpoint for native event forwarding.\r\n * Once set, beacon events are POSTed directly from native code,\r\n * ensuring delivery even when the JS bridge is not active (app backgrounded).\r\n *\r\n * @param url The API endpoint URL to POST events to.\r\n * @param apiKey Optional API key sent as X-CSFR-Token header.\r\n * @param id Optional identifier appended to every forwarded event payload.\r\n */\r\n setApiEndpoint(url: string, apiKey?: string, id?: string): void;\r\n\r\n /**\r\n * Return the current monitoring configuration and active state.\r\n * Option fields are undefined if not explicitly set.\r\n */\r\n getMonitoringConfig(): MonitoringConfig;\r\n\r\n /**\r\n * Return the current state snapshot for a paired monitored device.\r\n * Returns null when no paired device matches the identifier.\r\n */\r\n getMonitoredDeviceState(identifier: string): MonitoredDeviceState | null;\r\n\r\n /**\r\n * Return the current state snapshot for all paired monitored devices.\r\n */\r\n getMonitoredDeviceStates(): MonitoredDeviceState[];\r\n\r\n /**\r\n * Return the current API forwarding configuration.\r\n * Each field is `null` if not set.\r\n */\r\n getApiEndpoint(): { url: string | null; apiKey: string | null; id: string | null };\r\n\r\n /**\r\n * Start observing CarPlay (iOS) / Android Auto (Android) connection state.\r\n *\r\n * Emits `onCarPlayConnected` and `onCarPlayDisconnected` JS events. Events are\r\n * also written to the SQLite event log and forwarded to the configured API\r\n * endpoint, and dispatched to native lifecycle plugins (used by the auto-\r\n * generated geolocation plugin to start/stop background-geolocation).\r\n *\r\n * **Persistent.** The enabled state is stored in native preferences and\r\n * survives app kill / device reboot. Call `stopCarPlayMonitoring()` to\r\n * disable. `startMonitoring()` also enables CarPlay observation\r\n * automatically — calling this method explicitly is only required if you\r\n * want CarPlay events without beacon monitoring.\r\n *\r\n * **Background behavior:**\r\n * - **Android**: the foreground service hosts the observer and continues to\r\n * receive `CarConnection` events even after the app process is killed and\r\n * restarted via `BootReceiver`. Guaranteed background detection.\r\n * - **iOS**: the observer auto-restarts in `OnCreate` whenever the module\r\n * is recreated, including background-launches triggered by beacon region\r\n * monitoring. **iOS cannot wake a terminated app on CarPlay alone** — for\r\n * guaranteed wake-from-suspension, also call `startMonitoring()` with at\r\n * least one paired beacon. Region wake events trigger a CarPlay state\r\n * resync to reconcile any route changes that occurred during suspension.\r\n *\r\n * - iOS: observes `AVAudioSession.routeChangeNotification` for `.carAudio` ports.\r\n * No CarPlay entitlement required.\r\n * - Android: observes `androidx.car.app.connection.CarConnection` LiveData.\r\n * No Android Auto certification required.\r\n * - Web: no-op (resolves immediately).\r\n */\r\n startCarPlayMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Stop CarPlay / Android Auto connection monitoring and clear the persisted\r\n * \"enabled\" flag so the observer is not auto-restarted on next launch / boot.\r\n *\r\n * On Android, if no beacon monitoring is active, the foreground service\r\n * stops itself.\r\n */\r\n stopCarPlayMonitoring(): Promise<void>;\r\n\r\n /**\r\n * Returns whether CarPlay / Android Auto observation is currently enabled.\r\n * Reads the persisted flag, so the value survives app cold-starts and\r\n * reflects the native source of truth (foreground service on Android,\r\n * UserDefaults suite on iOS).\r\n */\r\n isCarPlayMonitoringEnabled(): boolean;\r\n}\r\n\r\nexport default requireNativeModule<ExpoBeaconModule>(\"ExpoBeacon\");\r\n"]}
|
|
@@ -17,6 +17,7 @@ declare const stub: {
|
|
|
17
17
|
requestPermissionsAsync: () => Promise<boolean>;
|
|
18
18
|
enableEventLogging: () => void;
|
|
19
19
|
disableEventLogging: () => void;
|
|
20
|
+
isEventLoggingEnabled: () => boolean;
|
|
20
21
|
getEventLogs: (_options?: EventLogQueryOptions) => EventLogEntry[];
|
|
21
22
|
clearEventLogs: () => void;
|
|
22
23
|
destroyEventLogs: () => void;
|
|
@@ -32,6 +33,7 @@ declare const stub: {
|
|
|
32
33
|
requestBatteryOptimizationExemption: () => Promise<boolean>;
|
|
33
34
|
startCarPlayMonitoring: () => Promise<void>;
|
|
34
35
|
stopCarPlayMonitoring: () => Promise<void>;
|
|
36
|
+
isCarPlayMonitoringEnabled: () => boolean;
|
|
35
37
|
addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => {
|
|
36
38
|
remove: () => void;
|
|
37
39
|
};
|
|
@@ -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,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;
|
|
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,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;iCACF,OAAO;8BACR,oBAAoB,KAAG,aAAa,EAAE;0BAC5C,IAAI;4BACF,IAAI;;2CAEa,MAAM,KAAG,oBAAoB,GAAG,IAAI;oCAC7C,oBAAoB,EAAE;0BAChC;QAAE,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;QAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;uCACnD,OAAO;+CACC,OAAO,CAAC,OAAO,CAAC;kCAC7B,OAAO,CAAC,IAAI,CAAC;iCACd,OAAO,CAAC,IAAI,CAAC;sCACR,OAAO;8BACb,MAAM,sBAAsB,aAAa,GAAG;;;qCAGrC,MAAM,sBAAsB;CAC9D,CAAC;AAEF,eAAe,IAAI,CAAC"}
|
|
@@ -19,6 +19,7 @@ const stub = {
|
|
|
19
19
|
requestPermissionsAsync: () => notSupported(),
|
|
20
20
|
enableEventLogging: () => notSupported(),
|
|
21
21
|
disableEventLogging: () => notSupported(),
|
|
22
|
+
isEventLoggingEnabled: () => false,
|
|
22
23
|
getEventLogs: (_options) => notSupported(),
|
|
23
24
|
clearEventLogs: () => notSupported(),
|
|
24
25
|
destroyEventLogs: () => notSupported(),
|
|
@@ -30,6 +31,7 @@ const stub = {
|
|
|
30
31
|
requestBatteryOptimizationExemption: () => Promise.resolve(true),
|
|
31
32
|
startCarPlayMonitoring: () => Promise.resolve(),
|
|
32
33
|
stopCarPlayMonitoring: () => Promise.resolve(),
|
|
34
|
+
isCarPlayMonitoringEnabled: () => false,
|
|
33
35
|
addListener: (_eventName, _listener) => ({
|
|
34
36
|
remove: () => { },
|
|
35
37
|
}),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ExpoBeaconModule.web.js","sourceRoot":"","sources":["../src/ExpoBeaconModule.web.ts"],"names":[],"mappings":"AAWA,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,mBAAmB,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IACzC,uBAAuB,EAAE,CAAC,WAAmB,EAA+B,EAAE,CAAC,YAAY,EAAE;IAC7F,wBAAwB,EAAE,GAA2B,EAAE,CAAC,YAAY,EAAE;IACtE,cAAc,EAAE,GAAqE,EAAE,CAAC,YAAY,EAAE;IACtG,2BAA2B,EAAE,GAAY,EAAE,CAAC,IAAI;IAChD,mCAAmC,EAAE,GAAqB,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;IAClF,sBAAsB,EAAE,GAAkB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;IAC9D,qBAAqB,EAAE,GAAkB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;IAC7D,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 MonitoredDeviceState,\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 getMonitoringConfig: () => notSupported(),\r\n getMonitoredDeviceState: (_identifier: string): MonitoredDeviceState | null => notSupported(),\r\n getMonitoredDeviceStates: (): MonitoredDeviceState[] => notSupported(),\r\n getApiEndpoint: (): { url: string | null; apiKey: string | null; id: string | null } => notSupported(),\r\n isBatteryOptimizationExempt: (): boolean => true,\r\n requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),\r\n startCarPlayMonitoring: (): Promise<void> => Promise.resolve(),\r\n stopCarPlayMonitoring: (): Promise<void> => Promise.resolve(),\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":"AAWA,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,qBAAqB,EAAE,GAAY,EAAE,CAAC,KAAK;IAC3C,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,mBAAmB,EAAE,GAAG,EAAE,CAAC,YAAY,EAAE;IACzC,uBAAuB,EAAE,CAAC,WAAmB,EAA+B,EAAE,CAAC,YAAY,EAAE;IAC7F,wBAAwB,EAAE,GAA2B,EAAE,CAAC,YAAY,EAAE;IACtE,cAAc,EAAE,GAAqE,EAAE,CAAC,YAAY,EAAE;IACtG,2BAA2B,EAAE,GAAY,EAAE,CAAC,IAAI;IAChD,mCAAmC,EAAE,GAAqB,EAAE,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC;IAClF,sBAAsB,EAAE,GAAkB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;IAC9D,qBAAqB,EAAE,GAAkB,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE;IAC7D,0BAA0B,EAAE,GAAY,EAAE,CAAC,KAAK;IAChD,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 MonitoredDeviceState,\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 isEventLoggingEnabled: (): boolean => false,\r\n getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),\r\n clearEventLogs: (): void => notSupported(),\r\n destroyEventLogs: (): void => notSupported(),\r\n getMonitoringConfig: () => notSupported(),\r\n getMonitoredDeviceState: (_identifier: string): MonitoredDeviceState | null => notSupported(),\r\n getMonitoredDeviceStates: (): MonitoredDeviceState[] => notSupported(),\r\n getApiEndpoint: (): { url: string | null; apiKey: string | null; id: string | null } => notSupported(),\r\n isBatteryOptimizationExempt: (): boolean => true,\r\n requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),\r\n startCarPlayMonitoring: (): Promise<void> => Promise.resolve(),\r\n stopCarPlayMonitoring: (): Promise<void> => Promise.resolve(),\r\n isCarPlayMonitoringEnabled: (): boolean => false,\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"]}
|
package/ios/CarPlayMonitor.swift
CHANGED
|
@@ -67,6 +67,27 @@ final class CarPlayMonitor {
|
|
|
67
67
|
}
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
+
/// Re-read the current audio route and emit a connect/disconnect event if
|
|
71
|
+
/// the state has changed since the last observed value. Cheap and idempotent;
|
|
72
|
+
/// safe to call from any background-wake hook (e.g. CLLocationManager region
|
|
73
|
+
/// callbacks) to reconcile CarPlay state changes that occurred while the app
|
|
74
|
+
/// was suspended and `AVAudioSession.routeChangeNotification` was not delivered.
|
|
75
|
+
func resyncIfNeeded() {
|
|
76
|
+
queue.async { [weak self] in
|
|
77
|
+
self?.handleRouteChange()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/// Whether `start(emit:)` has been called and `stop()` has not.
|
|
82
|
+
/// Use to decide whether to skip a redundant start during auto-start logic.
|
|
83
|
+
var isObserving: Bool {
|
|
84
|
+
// Synchronous read from main queue is safe — `observer` is only mutated on `queue`.
|
|
85
|
+
if Thread.isMainThread {
|
|
86
|
+
return observer != nil
|
|
87
|
+
}
|
|
88
|
+
return queue.sync { observer != nil }
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
private func handleRouteChange() {
|
|
71
92
|
let (connected, transport) = Self.currentCarPlayState()
|
|
72
93
|
if connected == isConnected { return }
|
|
@@ -14,6 +14,7 @@ private let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
|
|
|
14
14
|
private let MIN_RSSI_KEY = "expo.beacon.min_rssi"
|
|
15
15
|
private let EVENT_LEVEL_KEY = "expo.beacon.event_level"
|
|
16
16
|
private let EXIT_TIMEOUT_SECONDS_KEY = "expo.beacon.exit_timeout_seconds"
|
|
17
|
+
private let CARPLAY_MONITORING_ENABLED_KEY = "expo.beacon.carplay_monitoring_enabled"
|
|
17
18
|
|
|
18
19
|
/// Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable.
|
|
19
20
|
private let DEFAULT_MIN_RSSI: Int = -85
|
|
@@ -150,6 +151,14 @@ public class ExpoBeaconModule: Module {
|
|
|
150
151
|
|
|
151
152
|
OnCreate {
|
|
152
153
|
self.migrateUserDefaultsIfNeeded()
|
|
154
|
+
// If the user previously enabled CarPlay monitoring, restart it now —
|
|
155
|
+
// the module may have been recreated after the app was killed and
|
|
156
|
+
// background-launched (e.g. via a CLLocationManager region wake).
|
|
157
|
+
// Without this, CarPlay state changes that happened during suspension
|
|
158
|
+
// would be missed until JS calls startCarPlayMonitoring() again.
|
|
159
|
+
if self.defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
|
|
160
|
+
self.startCarPlayMonitoringInternal()
|
|
161
|
+
}
|
|
153
162
|
}
|
|
154
163
|
|
|
155
164
|
Events("onBeaconEnter", "onBeaconExit", "onBeaconDistance", "onBeaconTimeout", "onBeaconFound", "onEddystoneFound", "onEddystoneEnter", "onEddystoneExit", "onEddystoneDistance", "onEddystoneTimeout", "onBeaconError", "onCarPlayConnected", "onCarPlayDisconnected")
|
|
@@ -432,6 +441,11 @@ public class ExpoBeaconModule: Module {
|
|
|
432
441
|
}
|
|
433
442
|
self.requestNotificationPermission()
|
|
434
443
|
self.startRegionMonitoring()
|
|
444
|
+
// Auto-enable CarPlay monitoring when beacon monitoring starts so
|
|
445
|
+
// CarPlay events are captured for the same lifetime as beacons.
|
|
446
|
+
// Users can opt out at any time via stopCarPlayMonitoring().
|
|
447
|
+
self.defaults.set(true, forKey: CARPLAY_MONITORING_ENABLED_KEY)
|
|
448
|
+
self.startCarPlayMonitoringInternal()
|
|
435
449
|
promise.resolve(nil)
|
|
436
450
|
}
|
|
437
451
|
}
|
|
@@ -458,17 +472,21 @@ public class ExpoBeaconModule: Module {
|
|
|
458
472
|
// MARK: - CarPlay
|
|
459
473
|
|
|
460
474
|
AsyncFunction("startCarPlayMonitoring") { (promise: Promise) in
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
}
|
|
475
|
+
self.defaults.set(true, forKey: CARPLAY_MONITORING_ENABLED_KEY)
|
|
476
|
+
self.startCarPlayMonitoringInternal()
|
|
464
477
|
promise.resolve(nil)
|
|
465
478
|
}
|
|
466
479
|
|
|
467
480
|
AsyncFunction("stopCarPlayMonitoring") { (promise: Promise) in
|
|
481
|
+
self.defaults.set(false, forKey: CARPLAY_MONITORING_ENABLED_KEY)
|
|
468
482
|
CarPlayMonitor.shared.stop()
|
|
469
483
|
promise.resolve(nil)
|
|
470
484
|
}
|
|
471
485
|
|
|
486
|
+
Function("isCarPlayMonitoringEnabled") { () -> Bool in
|
|
487
|
+
return self.defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY)
|
|
488
|
+
}
|
|
489
|
+
|
|
472
490
|
// MARK: - Continuous Scan
|
|
473
491
|
|
|
474
492
|
Function("startContinuousScan") { () -> Void in
|
|
@@ -528,6 +546,10 @@ public class ExpoBeaconModule: Module {
|
|
|
528
546
|
self.loggingEnabled = false
|
|
529
547
|
}
|
|
530
548
|
|
|
549
|
+
Function("isEventLoggingEnabled") { () -> Bool in
|
|
550
|
+
return self.defaults.bool(forKey: EVENT_LOGGING_ENABLED_KEY)
|
|
551
|
+
}
|
|
552
|
+
|
|
531
553
|
Function("getEventLogs") { (options: [String: Any]?) -> [[String: Any]] in
|
|
532
554
|
let logger = self.getOrCreateEventLogger()
|
|
533
555
|
let limit = (options?["limit"] as? Int) ?? 1000
|
|
@@ -613,7 +635,13 @@ public class ExpoBeaconModule: Module {
|
|
|
613
635
|
self.eventLogger = nil
|
|
614
636
|
self.stopRegionMonitoring()
|
|
615
637
|
self.stopEddystoneMonitoring()
|
|
616
|
-
|
|
638
|
+
// Only tear down CarPlay observation when the user has explicitly
|
|
639
|
+
// disabled it. Otherwise leave `CarPlayMonitor.shared` running so
|
|
640
|
+
// that route changes continue to be observed across module recreations
|
|
641
|
+
// (e.g. background-launch wake → module re-init → OnDestroy on suspend).
|
|
642
|
+
if !self.defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
|
|
643
|
+
CarPlayMonitor.shared.stop()
|
|
644
|
+
}
|
|
617
645
|
self.centralManager?.stopScan()
|
|
618
646
|
self.centralManager = nil
|
|
619
647
|
self.scanTimer?.cancel()
|
|
@@ -1080,6 +1108,16 @@ public class ExpoBeaconModule: Module {
|
|
|
1080
1108
|
return distance
|
|
1081
1109
|
}
|
|
1082
1110
|
|
|
1111
|
+
/// Starts the shared `CarPlayMonitor` and routes its events through the
|
|
1112
|
+
/// standard `sendLoggedEvent` pipeline (JS bridge + SQLite + API forwarder
|
|
1113
|
+
/// + lifecycle plugin registry). Idempotent — safe to call multiple times
|
|
1114
|
+
/// (see `CarPlayMonitor.start(emit:)` semantics).
|
|
1115
|
+
private func startCarPlayMonitoringInternal() {
|
|
1116
|
+
CarPlayMonitor.shared.start { [weak self] eventName, payload in
|
|
1117
|
+
self?.sendLoggedEvent(eventName, payload)
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1083
1121
|
/// Sends an event to JS and logs it to SQLite if logging is enabled.
|
|
1084
1122
|
private func sendLoggedEvent(_ eventName: String, _ params: [String: Any]) {
|
|
1085
1123
|
if isEventLoggingEnabled() {
|
|
@@ -1739,12 +1777,22 @@ public class ExpoBeaconModule: Module {
|
|
|
1739
1777
|
}
|
|
1740
1778
|
|
|
1741
1779
|
fileprivate func handleDidEnterRegion(_ region: CLRegion) {
|
|
1780
|
+
// CLLocationManager region events are one of the few iOS background-wake
|
|
1781
|
+
// signals available to this app. Use this opportunity to reconcile
|
|
1782
|
+
// CarPlay state in case it changed during suspension.
|
|
1783
|
+
if defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
|
|
1784
|
+
CarPlayMonitor.shared.resyncIfNeeded()
|
|
1785
|
+
}
|
|
1742
1786
|
// Region callbacks are suppressed — all enter/exit logic goes through
|
|
1743
1787
|
// ranging-based hysteresis in handleDidRange for consistent behaviour
|
|
1744
1788
|
// with ENTER_HYSTERESIS_COUNT / EXIT_HYSTERESIS_COUNT, regardless of whether maxDistance is set.
|
|
1745
1789
|
}
|
|
1746
1790
|
|
|
1747
1791
|
fileprivate func handleDidExitRegion(_ region: CLRegion) {
|
|
1792
|
+
// Reconcile CarPlay state on background wake — see handleDidEnterRegion.
|
|
1793
|
+
if defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
|
|
1794
|
+
CarPlayMonitor.shared.resyncIfNeeded()
|
|
1795
|
+
}
|
|
1748
1796
|
guard let beaconRegion = region as? CLBeaconRegion else { return }
|
|
1749
1797
|
let identifier = beaconRegion.identifier
|
|
1750
1798
|
|
package/package.json
CHANGED
package/src/ExpoBeaconModule.ts
CHANGED
|
@@ -144,6 +144,12 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
144
144
|
/** Disable event logging. Previously logged events are retained. */
|
|
145
145
|
disableEventLogging(): void;
|
|
146
146
|
|
|
147
|
+
/**
|
|
148
|
+
* Returns whether SQLite event logging is currently enabled.
|
|
149
|
+
* Reads the persisted flag, so this stays accurate across app cold-starts.
|
|
150
|
+
*/
|
|
151
|
+
isEventLoggingEnabled(): boolean;
|
|
152
|
+
|
|
147
153
|
/**
|
|
148
154
|
* Retrieve logged beacon events from the SQLite database.
|
|
149
155
|
* @param options Optional filters (limit, eventType, sinceTimestamp).
|
|
@@ -198,7 +204,22 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
198
204
|
* endpoint, and dispatched to native lifecycle plugins (used by the auto-
|
|
199
205
|
* generated geolocation plugin to start/stop background-geolocation).
|
|
200
206
|
*
|
|
201
|
-
*
|
|
207
|
+
* **Persistent.** The enabled state is stored in native preferences and
|
|
208
|
+
* survives app kill / device reboot. Call `stopCarPlayMonitoring()` to
|
|
209
|
+
* disable. `startMonitoring()` also enables CarPlay observation
|
|
210
|
+
* automatically — calling this method explicitly is only required if you
|
|
211
|
+
* want CarPlay events without beacon monitoring.
|
|
212
|
+
*
|
|
213
|
+
* **Background behavior:**
|
|
214
|
+
* - **Android**: the foreground service hosts the observer and continues to
|
|
215
|
+
* receive `CarConnection` events even after the app process is killed and
|
|
216
|
+
* restarted via `BootReceiver`. Guaranteed background detection.
|
|
217
|
+
* - **iOS**: the observer auto-restarts in `OnCreate` whenever the module
|
|
218
|
+
* is recreated, including background-launches triggered by beacon region
|
|
219
|
+
* monitoring. **iOS cannot wake a terminated app on CarPlay alone** — for
|
|
220
|
+
* guaranteed wake-from-suspension, also call `startMonitoring()` with at
|
|
221
|
+
* least one paired beacon. Region wake events trigger a CarPlay state
|
|
222
|
+
* resync to reconcile any route changes that occurred during suspension.
|
|
202
223
|
*
|
|
203
224
|
* - iOS: observes `AVAudioSession.routeChangeNotification` for `.carAudio` ports.
|
|
204
225
|
* No CarPlay entitlement required.
|
|
@@ -208,8 +229,22 @@ declare class ExpoBeaconModule extends NativeModule<ExpoBeaconModuleEvents> {
|
|
|
208
229
|
*/
|
|
209
230
|
startCarPlayMonitoring(): Promise<void>;
|
|
210
231
|
|
|
211
|
-
/**
|
|
232
|
+
/**
|
|
233
|
+
* Stop CarPlay / Android Auto connection monitoring and clear the persisted
|
|
234
|
+
* "enabled" flag so the observer is not auto-restarted on next launch / boot.
|
|
235
|
+
*
|
|
236
|
+
* On Android, if no beacon monitoring is active, the foreground service
|
|
237
|
+
* stops itself.
|
|
238
|
+
*/
|
|
212
239
|
stopCarPlayMonitoring(): Promise<void>;
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Returns whether CarPlay / Android Auto observation is currently enabled.
|
|
243
|
+
* Reads the persisted flag, so the value survives app cold-starts and
|
|
244
|
+
* reflects the native source of truth (foreground service on Android,
|
|
245
|
+
* UserDefaults suite on iOS).
|
|
246
|
+
*/
|
|
247
|
+
isCarPlayMonitoringEnabled(): boolean;
|
|
213
248
|
}
|
|
214
249
|
|
|
215
250
|
export default requireNativeModule<ExpoBeaconModule>("ExpoBeacon");
|
|
@@ -45,6 +45,7 @@ const stub = {
|
|
|
45
45
|
requestPermissionsAsync: (): Promise<boolean> => notSupported(),
|
|
46
46
|
enableEventLogging: (): void => notSupported(),
|
|
47
47
|
disableEventLogging: (): void => notSupported(),
|
|
48
|
+
isEventLoggingEnabled: (): boolean => false,
|
|
48
49
|
getEventLogs: (_options?: EventLogQueryOptions): EventLogEntry[] => notSupported(),
|
|
49
50
|
clearEventLogs: (): void => notSupported(),
|
|
50
51
|
destroyEventLogs: (): void => notSupported(),
|
|
@@ -56,6 +57,7 @@ const stub = {
|
|
|
56
57
|
requestBatteryOptimizationExemption: (): Promise<boolean> => Promise.resolve(true),
|
|
57
58
|
startCarPlayMonitoring: (): Promise<void> => Promise.resolve(),
|
|
58
59
|
stopCarPlayMonitoring: (): Promise<void> => Promise.resolve(),
|
|
60
|
+
isCarPlayMonitoringEnabled: (): boolean => false,
|
|
59
61
|
addListener: (_eventName: keyof ExpoBeaconModuleEvents, _listener: any) => ({
|
|
60
62
|
remove: () => {},
|
|
61
63
|
}),
|