expo-beacon 0.8.4 → 0.8.5
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/ios/CarPlayMonitor.swift +44 -43
- package/package.json +1 -1
package/ios/CarPlayMonitor.swift
CHANGED
|
@@ -36,13 +36,14 @@ final class CarPlayMonitor {
|
|
|
36
36
|
/// Optional persistence target for last-known connection state. Injected via
|
|
37
37
|
/// `start(emit:defaults:)`. When set, every connect/disconnect emission is
|
|
38
38
|
/// mirrored to `CARPLAY_LAST_CONNECTED_KEY` so a freshly relaunched process
|
|
39
|
-
/// can detect a missed disconnect via `reconcileOnProcessStart(
|
|
39
|
+
/// can detect a missed disconnect via `reconcileOnProcessStart()`.
|
|
40
40
|
private var defaults: UserDefaults?
|
|
41
41
|
/// When `true`, an authoritative source (CarPlay scene delegate, granted via the
|
|
42
42
|
/// `com.apple.developer.carplay-driving-task` entitlement) is providing
|
|
43
43
|
/// connect/disconnect events. The audio-session observer becomes a passive
|
|
44
44
|
/// secondary signal only — it will not emit events, to avoid duplicate
|
|
45
|
-
/// connect/disconnect notifications.
|
|
45
|
+
/// connect/disconnect notifications. Reset to `false` on entitled disconnect
|
|
46
|
+
/// so the audio-session fallback can take over for any subsequent events.
|
|
46
47
|
private var isEntitledMode: Bool = false
|
|
47
48
|
|
|
48
49
|
private init() {}
|
|
@@ -55,7 +56,7 @@ final class CarPlayMonitor {
|
|
|
55
56
|
/// - Parameter defaults: Optional `UserDefaults` suite used to persist the
|
|
56
57
|
/// last-known connection state for cross-process reconciliation. When the
|
|
57
58
|
/// module is recreated in a new process (e.g. after a background-wake)
|
|
58
|
-
/// the persisted value is used by `reconcileOnProcessStart(
|
|
59
|
+
/// the persisted value is used by `reconcileOnProcessStart()` to
|
|
59
60
|
/// synthesize a missed disconnect. Pass `nil` to disable persistence.
|
|
60
61
|
func start(emit: @escaping Emit, defaults: UserDefaults? = nil) {
|
|
61
62
|
queue.async { [weak self] in
|
|
@@ -128,7 +129,14 @@ final class CarPlayMonitor {
|
|
|
128
129
|
self.isEntitledMode = true
|
|
129
130
|
os_log("CarPlay scene connected (entitled source)", log: self.log, type: .info)
|
|
130
131
|
if !self.isConnected {
|
|
131
|
-
self.isConnected =
|
|
132
|
+
self.isConnected = true
|
|
133
|
+
self.emitConnected(transport: transport)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/// Called by `BeaconCarPlaySceneDelegate.templateApplicationScene(_:didDisconnect:)`.
|
|
139
|
+
/// Emits `onCarPlayDisconnected` and clears the entitled-mode flag so the
|
|
132
140
|
/// audio-session fallback path becomes authoritative again until the next
|
|
133
141
|
/// entitled connect. Without this reset, a single missed scene-delegate
|
|
134
142
|
/// disconnect (force-quit, OS reclaim, abrupt cable yank between connect
|
|
@@ -139,13 +147,6 @@ final class CarPlayMonitor {
|
|
|
139
147
|
guard let self = self else { return }
|
|
140
148
|
os_log("CarPlay scene disconnected (entitled source)", log: self.log, type: .info)
|
|
141
149
|
self.isEntitledMode = false
|
|
142
|
-
/// Emits `onCarPlayDisconnected` and keeps the entitled-mode flag set so
|
|
143
|
-
/// subsequent audio-session events remain suppressed (the scene delegate is
|
|
144
|
-
/// the source of truth for the lifetime of the process).
|
|
145
|
-
func notifyEntitledDisconnect() {
|
|
146
|
-
queue.async { [weak self] in
|
|
147
|
-
guard let self = self else { return }
|
|
148
|
-
os_log("CarPlay scene disconnected (entitled source)", log: self.log, type: .info)
|
|
149
150
|
if self.isConnected {
|
|
150
151
|
self.isConnected = false
|
|
151
152
|
self.emitDisconnected()
|
|
@@ -173,6 +174,17 @@ final class CarPlayMonitor {
|
|
|
173
174
|
/// is always trusted.
|
|
174
175
|
private func handleRouteChange(notification: Notification?) {
|
|
175
176
|
if let userInfo = notification?.userInfo,
|
|
177
|
+
let reasonRaw = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
178
|
+
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonRaw) {
|
|
179
|
+
switch reason {
|
|
180
|
+
case .newDeviceAvailable, .oldDeviceUnavailable:
|
|
181
|
+
break // real device change — proceed
|
|
182
|
+
default:
|
|
183
|
+
// Category/override/configuration changes etc. don't represent
|
|
184
|
+
// a CarPlay connect/disconnect. Skip to avoid spurious events.
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
}
|
|
176
188
|
let (connected, transport) = Self.currentCarPlayState()
|
|
177
189
|
// When an entitled CarPlay scene source is active it is authoritative.
|
|
178
190
|
// The audio-session signal is kept as a redundant secondary check but
|
|
@@ -203,24 +215,15 @@ final class CarPlayMonitor {
|
|
|
203
215
|
/// route is no longer CarPlay. Use case: the previous process was killed or
|
|
204
216
|
/// suspended-then-OS-reclaimed while CarPlay was connected, and the disconnect
|
|
205
217
|
/// fired off-process. JS listeners attached to the freshly recreated module
|
|
206
|
-
///
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
let now = Date()
|
|
216
|
-
var payload: [String: Any] = [
|
|
217
|
-
"timestamp": now.timeIntervalSince1970 * 1000.0,
|
|
218
|
-
"timestampIso": Self.isoFormatter.string(from: now),
|
|
219
|
-
]
|
|
220
|
-
if let reason = reason {
|
|
221
|
-
payload["reason"] = reason
|
|
222
|
-
}
|
|
223
|
-
persistConnectionState(false) let persistedConnected = defaults.bool(forKey: CARPLAY_LAST_CONNECTED_KEY)
|
|
218
|
+
/// would otherwise never learn the session ended.
|
|
219
|
+
///
|
|
220
|
+
/// Must be called AFTER `start(emit:defaults:)` so the emit callback is
|
|
221
|
+
/// installed and persistence target is known. Idempotent.
|
|
222
|
+
func reconcileOnProcessStart() {
|
|
223
|
+
queue.async { [weak self] in
|
|
224
|
+
guard let self = self else { return }
|
|
225
|
+
guard let defaults = self.defaults else { return }
|
|
226
|
+
let persistedConnected = defaults.bool(forKey: CARPLAY_LAST_CONNECTED_KEY)
|
|
224
227
|
let (currentConnected, _) = Self.currentCarPlayState()
|
|
225
228
|
if persistedConnected && !currentConnected {
|
|
226
229
|
os_log("CarPlay reconcile: persisted=connected, current=disconnected — emitting synthetic disconnect", log: self.log, type: .info)
|
|
@@ -233,18 +236,7 @@ final class CarPlayMonitor {
|
|
|
233
236
|
/// Write the current connection state to the injected `UserDefaults` suite
|
|
234
237
|
/// (when available). No-op when persistence wasn't configured.
|
|
235
238
|
private func persistConnectionState(_ connected: Bool) {
|
|
236
|
-
defaults?.set(connected, forKey: CARPLAY_LAST_CONNECTED_KEY)
|
|
237
|
-
if isEntitledMode {
|
|
238
|
-
return
|
|
239
|
-
}
|
|
240
|
-
let (connected, transport) = Self.currentCarPlayState()
|
|
241
|
-
if connected == isConnected { return }
|
|
242
|
-
isConnected = connected
|
|
243
|
-
if connected {
|
|
244
|
-
emitConnected(transport: transport)
|
|
245
|
-
} else {
|
|
246
|
-
emitDisconnected()
|
|
247
|
-
}
|
|
239
|
+
defaults?.set(connected, forKey: CARPLAY_LAST_CONNECTED_KEY)
|
|
248
240
|
}
|
|
249
241
|
|
|
250
242
|
private func emitConnected(transport: String) {
|
|
@@ -254,15 +246,24 @@ final class CarPlayMonitor {
|
|
|
254
246
|
"timestamp": now.timeIntervalSince1970 * 1000.0,
|
|
255
247
|
"timestampIso": Self.isoFormatter.string(from: now),
|
|
256
248
|
]
|
|
249
|
+
persistConnectionState(true)
|
|
257
250
|
emit?("onCarPlayConnected", payload)
|
|
258
251
|
}
|
|
259
252
|
|
|
260
|
-
|
|
253
|
+
/// Emit a disconnect event. When `reason` is non-nil it is included in the
|
|
254
|
+
/// payload so consumers can distinguish real-time disconnects from
|
|
255
|
+
/// post-hoc reconciled ones (currently `"reconciled"` from
|
|
256
|
+
/// `reconcileOnProcessStart`). Additive, non-breaking.
|
|
257
|
+
private func emitDisconnected(reason: String? = nil) {
|
|
261
258
|
let now = Date()
|
|
262
|
-
|
|
259
|
+
var payload: [String: Any] = [
|
|
263
260
|
"timestamp": now.timeIntervalSince1970 * 1000.0,
|
|
264
261
|
"timestampIso": Self.isoFormatter.string(from: now),
|
|
265
262
|
]
|
|
263
|
+
if let reason = reason {
|
|
264
|
+
payload["reason"] = reason
|
|
265
|
+
}
|
|
266
|
+
persistConnectionState(false)
|
|
266
267
|
emit?("onCarPlayDisconnected", payload)
|
|
267
268
|
}
|
|
268
269
|
|