expo-beacon 0.8.1 → 0.8.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.
- package/android/src/main/java/expo/modules/beacon/BeaconApiForwarder.kt +0 -3
- package/ios/BeaconApiForwarder.swift +0 -5
- package/ios/BeaconCarPlaySceneDelegate.swift +78 -0
- package/ios/BluetoothDelegate.swift +43 -0
- package/ios/CarPlayMonitor.swift +80 -10
- package/ios/ExpoBeacon.podspec +5 -2
- package/ios/ExpoBeaconConstants.swift +41 -0
- package/ios/ExpoBeaconModule+CarPlay.swift +49 -0
- package/ios/ExpoBeaconModule+Eddystone.swift +365 -0
- package/ios/ExpoBeaconModule+EventLogging.swift +98 -0
- package/ios/ExpoBeaconModule+Monitoring.swift +383 -0
- package/ios/ExpoBeaconModule+Notifications.swift +59 -0
- package/ios/ExpoBeaconModule+Permissions.swift +55 -0
- package/ios/ExpoBeaconModule+Scanning.swift +54 -0
- package/ios/ExpoBeaconModule+Storage.swift +97 -0
- package/ios/ExpoBeaconModule+Timers.swift +100 -0
- package/ios/ExpoBeaconModule.swift +91 -1381
- package/ios/LocationDelegate.swift +48 -0
- package/package.json +1 -1
- package/plugin/build/index.d.ts +5 -1
- package/plugin/build/index.d.ts.map +1 -1
- package/plugin/build/index.js +3 -2
- package/plugin/build/withBeaconIOS.d.ts +19 -1
- package/plugin/build/withBeaconIOS.d.ts.map +1 -1
- package/plugin/build/withBeaconIOS.js +41 -1
|
@@ -28,9 +28,6 @@ internal class BeaconApiForwarder(private val context: Context) {
|
|
|
28
28
|
context.getSharedPreferences(API_PREFS, Context.MODE_PRIVATE)
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
val isConfigured: Boolean
|
|
32
|
-
get() = prefs.getString(API_URL_KEY, null)?.isNotEmpty() == true
|
|
33
|
-
|
|
34
31
|
fun configure(url: String, apiKey: String?, id: String? = null) {
|
|
35
32
|
prefs.edit().apply {
|
|
36
33
|
putString(API_URL_KEY, url)
|
|
@@ -14,11 +14,6 @@ final class BeaconApiForwarder {
|
|
|
14
14
|
private let defaults: UserDefaults
|
|
15
15
|
private let session: URLSession
|
|
16
16
|
|
|
17
|
-
var isConfigured: Bool {
|
|
18
|
-
guard let url = defaults.string(forKey: API_URL_KEY) else { return false }
|
|
19
|
-
return !url.isEmpty
|
|
20
|
-
}
|
|
21
|
-
|
|
22
17
|
init(defaults: UserDefaults? = nil) {
|
|
23
18
|
self.defaults = defaults ?? (UserDefaults(suiteName: "expo.modules.beacon") ?? .standard)
|
|
24
19
|
let config = URLSessionConfiguration.default
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import UIKit
|
|
3
|
+
import CarPlay
|
|
4
|
+
import os.log
|
|
5
|
+
|
|
6
|
+
/// CarPlay scene delegate used when the consumer app is provisioned with the
|
|
7
|
+
/// `com.apple.developer.carplay-driving-task` entitlement (driver / fleet
|
|
8
|
+
/// workflow apps).
|
|
9
|
+
///
|
|
10
|
+
/// Wiring is performed by the config plugin (`carplayDrivingTask: true`):
|
|
11
|
+
/// 1. The entitlement is added to the app's `*.entitlements`.
|
|
12
|
+
/// 2. `Info.plist` `UIApplicationSceneManifest` declares a
|
|
13
|
+
/// `CPTemplateApplicationSceneSessionRoleApplication` configuration with
|
|
14
|
+
/// `UISceneDelegateClassName = "ExpoBeacon.BeaconCarPlaySceneDelegate"`.
|
|
15
|
+
///
|
|
16
|
+
/// When CarPlay attaches, iOS launches (or wakes) the app and instantiates this
|
|
17
|
+
/// delegate. We notify the shared `CarPlayMonitor` which produces an
|
|
18
|
+
/// `onCarPlayConnected` event through the standard pipeline (JS bridge,
|
|
19
|
+
/// SQLite log, HTTP forwarder, lifecycle plugin registry).
|
|
20
|
+
///
|
|
21
|
+
/// Apple requires that a CarPlay-entitled app present a meaningful CarPlay UI.
|
|
22
|
+
/// We install a minimal `CPInformationTemplate` titled "Driver tracking active"
|
|
23
|
+
/// as the root template so the app passes review without forcing every consumer
|
|
24
|
+
/// to build a full CarPlay UX. Consumers that need richer UI can subclass this
|
|
25
|
+
/// delegate (or replace the scene class) and override `makeRootTemplate()`.
|
|
26
|
+
///
|
|
27
|
+
/// IMPORTANT: This class must be accessible to the Objective-C runtime so that
|
|
28
|
+
/// `Info.plist`'s `UISceneDelegateClassName` string can resolve via
|
|
29
|
+
/// `NSClassFromString`. Hence `@objc(...)` and `: NSObject`.
|
|
30
|
+
@objc(BeaconCarPlaySceneDelegate)
|
|
31
|
+
open class BeaconCarPlaySceneDelegate: NSObject, CPTemplateApplicationSceneDelegate {
|
|
32
|
+
|
|
33
|
+
private static let log = OSLog(subsystem: "expo.modules.beacon", category: "CarPlayScene")
|
|
34
|
+
|
|
35
|
+
private var interfaceController: CPInterfaceController?
|
|
36
|
+
|
|
37
|
+
public override init() {
|
|
38
|
+
super.init()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// MARK: - CPTemplateApplicationSceneDelegate
|
|
42
|
+
|
|
43
|
+
public func templateApplicationScene(
|
|
44
|
+
_ templateApplicationScene: CPTemplateApplicationScene,
|
|
45
|
+
didConnect interfaceController: CPInterfaceController
|
|
46
|
+
) {
|
|
47
|
+
os_log("CarPlay templateApplicationScene didConnect", log: Self.log, type: .info)
|
|
48
|
+
self.interfaceController = interfaceController
|
|
49
|
+
let template = makeRootTemplate()
|
|
50
|
+
interfaceController.setRootTemplate(template, animated: false, completion: nil)
|
|
51
|
+
CarPlayMonitor.shared.notifyEntitledConnect()
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
public func templateApplicationScene(
|
|
55
|
+
_ templateApplicationScene: CPTemplateApplicationScene,
|
|
56
|
+
didDisconnectInterfaceController interfaceController: CPInterfaceController
|
|
57
|
+
) {
|
|
58
|
+
os_log("CarPlay templateApplicationScene didDisconnect", log: Self.log, type: .info)
|
|
59
|
+
self.interfaceController = nil
|
|
60
|
+
CarPlayMonitor.shared.notifyEntitledDisconnect()
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// MARK: - Override hooks
|
|
64
|
+
|
|
65
|
+
/// Build the root template shown when CarPlay attaches. Override in a
|
|
66
|
+
/// consumer subclass to install a richer template (e.g. `CPListTemplate`,
|
|
67
|
+
/// `CPMapTemplate`, `CPNowPlayingTemplate`). Default is a minimal
|
|
68
|
+
/// `CPInformationTemplate` confirming the app is tracking.
|
|
69
|
+
open func makeRootTemplate() -> CPTemplate {
|
|
70
|
+
let item = CPInformationItem(title: "Status", detail: "Tracking active")
|
|
71
|
+
return CPInformationTemplate(
|
|
72
|
+
title: "Driver tracking",
|
|
73
|
+
layout: .leading,
|
|
74
|
+
items: [item],
|
|
75
|
+
actions: []
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import CoreBluetooth
|
|
2
|
+
|
|
3
|
+
// MARK: - CBCentralManagerDelegate (Eddystone BLE scanning)
|
|
4
|
+
|
|
5
|
+
internal final class BluetoothDelegate: NSObject, CBCentralManagerDelegate {
|
|
6
|
+
private weak var module: ExpoBeaconModule?
|
|
7
|
+
|
|
8
|
+
init(module: ExpoBeaconModule) {
|
|
9
|
+
self.module = module
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
func centralManagerDidUpdateState(_ central: CBCentralManager) {
|
|
13
|
+
switch central.state {
|
|
14
|
+
case .poweredOn:
|
|
15
|
+
module?.ensureBleScanRunning()
|
|
16
|
+
case .unauthorized:
|
|
17
|
+
print("[ExpoBeacon] Bluetooth authorization denied — Eddystone scanning/monitoring unavailable. " +
|
|
18
|
+
"Ensure NSBluetoothAlwaysUsageDescription is set in Info.plist.")
|
|
19
|
+
module?.handleBluetoothStateError(code: "BLUETOOTH_UNAUTHORIZED", message: "Bluetooth authorization denied — Eddystone scanning/monitoring unavailable")
|
|
20
|
+
module?.eddystoneScanPromise?.reject("BLUETOOTH_UNAUTHORIZED", "Bluetooth permission denied")
|
|
21
|
+
module?.eddystoneScanPromise = nil
|
|
22
|
+
case .poweredOff:
|
|
23
|
+
print("[ExpoBeacon] Bluetooth is powered off — Eddystone scanning/monitoring unavailable.")
|
|
24
|
+
module?.handleBluetoothStateError(code: "BLUETOOTH_OFF", message: "Bluetooth is powered off — Eddystone scanning/monitoring unavailable")
|
|
25
|
+
module?.eddystoneScanPromise?.reject("BLUETOOTH_OFF", "Bluetooth is powered off")
|
|
26
|
+
module?.eddystoneScanPromise = nil
|
|
27
|
+
default:
|
|
28
|
+
break
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
func centralManager(_ central: CBCentralManager,
|
|
33
|
+
didDiscover peripheral: CBPeripheral,
|
|
34
|
+
advertisementData: [String: Any],
|
|
35
|
+
rssi RSSI: NSNumber) {
|
|
36
|
+
module?.handleEddystoneDiscovery(advertisementData: advertisementData, rssi: RSSI)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func centralManager(_ central: CBCentralManager, willRestoreState dict: [String: Any]) {
|
|
40
|
+
// State restoration: CBCentralManager was recreated by iOS after app was killed.
|
|
41
|
+
// Scanning will be re-started in centralManagerDidUpdateState when state is .poweredOn.
|
|
42
|
+
}
|
|
43
|
+
}
|
package/ios/CarPlayMonitor.swift
CHANGED
|
@@ -23,6 +23,12 @@ final class CarPlayMonitor {
|
|
|
23
23
|
private var observer: NSObjectProtocol?
|
|
24
24
|
private var emit: Emit?
|
|
25
25
|
private var isConnected: Bool = false
|
|
26
|
+
/// When `true`, an authoritative source (CarPlay scene delegate, granted via the
|
|
27
|
+
/// `com.apple.developer.carplay-driving-task` entitlement) is providing
|
|
28
|
+
/// connect/disconnect events. The audio-session observer becomes a passive
|
|
29
|
+
/// secondary signal only — it will not emit events, to avoid duplicate
|
|
30
|
+
/// connect/disconnect notifications.
|
|
31
|
+
private var isEntitledMode: Bool = false
|
|
26
32
|
|
|
27
33
|
private init() {}
|
|
28
34
|
|
|
@@ -39,8 +45,8 @@ final class CarPlayMonitor {
|
|
|
39
45
|
forName: AVAudioSession.routeChangeNotification,
|
|
40
46
|
object: nil,
|
|
41
47
|
queue: .main
|
|
42
|
-
) { [weak self]
|
|
43
|
-
self?.handleRouteChange()
|
|
48
|
+
) { [weak self] note in
|
|
49
|
+
self?.handleRouteChange(notification: note)
|
|
44
50
|
}
|
|
45
51
|
os_log("CarPlay monitoring started", log: self.log, type: .info)
|
|
46
52
|
}
|
|
@@ -74,21 +80,85 @@ final class CarPlayMonitor {
|
|
|
74
80
|
/// was suspended and `AVAudioSession.routeChangeNotification` was not delivered.
|
|
75
81
|
func resyncIfNeeded() {
|
|
76
82
|
queue.async { [weak self] in
|
|
77
|
-
|
|
83
|
+
// Snapshot reads (initial start, region wake, explicit resync) are
|
|
84
|
+
// trusted: they reflect the actual current route, not a transient
|
|
85
|
+
// category/configuration change. Pass `nil` notification to bypass
|
|
86
|
+
// the route-change-reason filter.
|
|
87
|
+
self?.handleRouteChange(notification: nil)
|
|
78
88
|
}
|
|
79
89
|
}
|
|
80
90
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
91
|
+
// MARK: - Entitled (Driving Task) source
|
|
92
|
+
|
|
93
|
+
/// Called by `BeaconCarPlaySceneDelegate.templateApplicationScene(_:didConnect:)`.
|
|
94
|
+
/// Marks the entitled path as the authoritative source and emits an immediate
|
|
95
|
+
/// `onCarPlayConnected` event (if not already connected from this source).
|
|
96
|
+
/// Subsequent route-change notifications from `AVAudioSession` are suppressed
|
|
97
|
+
/// for emission purposes to prevent duplicate events.
|
|
98
|
+
func notifyEntitledConnect(transport: String = "carplay-scene") {
|
|
99
|
+
queue.async { [weak self] in
|
|
100
|
+
guard let self = self else { return }
|
|
101
|
+
self.isEntitledMode = true
|
|
102
|
+
os_log("CarPlay scene connected (entitled source)", log: self.log, type: .info)
|
|
103
|
+
if !self.isConnected {
|
|
104
|
+
self.isConnected = true
|
|
105
|
+
self.emitConnected(transport: transport)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Called by `BeaconCarPlaySceneDelegate.templateApplicationScene(_:didDisconnect:)`.
|
|
111
|
+
/// Emits `onCarPlayDisconnected` and keeps the entitled-mode flag set so
|
|
112
|
+
/// subsequent audio-session events remain suppressed (the scene delegate is
|
|
113
|
+
/// the source of truth for the lifetime of the process).
|
|
114
|
+
func notifyEntitledDisconnect() {
|
|
115
|
+
queue.async { [weak self] in
|
|
116
|
+
guard let self = self else { return }
|
|
117
|
+
os_log("CarPlay scene disconnected (entitled source)", log: self.log, type: .info)
|
|
118
|
+
if self.isConnected {
|
|
119
|
+
self.isConnected = false
|
|
120
|
+
self.emitDisconnected()
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Whether an entitled CarPlay scene source has notified us at least once.
|
|
126
|
+
/// Consumers (e.g. SLC/Visit auto-start logic) can skip cheap fallbacks
|
|
127
|
+
/// when the entitled real-time path is active.
|
|
128
|
+
var isUsingEntitledSource: Bool {
|
|
85
129
|
if Thread.isMainThread {
|
|
86
|
-
return
|
|
130
|
+
return isEntitledMode
|
|
87
131
|
}
|
|
88
|
-
return queue.sync {
|
|
132
|
+
return queue.sync { isEntitledMode }
|
|
89
133
|
}
|
|
90
134
|
|
|
91
|
-
|
|
135
|
+
/// Process a route change. When invoked from a system notification the
|
|
136
|
+
/// reason is checked: only `.newDeviceAvailable` and `.oldDeviceUnavailable`
|
|
137
|
+
/// indicate a real device connect/disconnect. Other reasons (category change,
|
|
138
|
+
/// configuration change, override, etc.) are filtered to avoid spurious
|
|
139
|
+
/// disconnect events when the app suspends/resumes and iOS re-evaluates
|
|
140
|
+
/// the audio session category without the physical CarPlay link changing.
|
|
141
|
+
/// When `notification` is nil (initial start, explicit resync), the snapshot
|
|
142
|
+
/// is always trusted.
|
|
143
|
+
private func handleRouteChange(notification: Notification?) {
|
|
144
|
+
if let userInfo = notification?.userInfo,
|
|
145
|
+
let reasonRaw = userInfo[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
146
|
+
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonRaw) {
|
|
147
|
+
switch reason {
|
|
148
|
+
case .newDeviceAvailable, .oldDeviceUnavailable:
|
|
149
|
+
break // real device change — proceed
|
|
150
|
+
default:
|
|
151
|
+
// Category/override/configuration changes etc. don't represent
|
|
152
|
+
// a CarPlay connect/disconnect. Skip to avoid spurious events.
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
// When an entitled CarPlay scene source is active it is authoritative.
|
|
157
|
+
// The audio-session signal is kept as a redundant secondary check but
|
|
158
|
+
// must NOT emit events — the scene delegate already did, or will.
|
|
159
|
+
if isEntitledMode {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
92
162
|
let (connected, transport) = Self.currentCarPlayState()
|
|
93
163
|
if connected == isConnected { return }
|
|
94
164
|
isConnected = connected
|
package/ios/ExpoBeacon.podspec
CHANGED
|
@@ -19,8 +19,11 @@ Pod::Spec.new do |s|
|
|
|
19
19
|
|
|
20
20
|
s.dependency 'ExpoModulesCore'
|
|
21
21
|
|
|
22
|
-
# Required system frameworks for iBeacon monitoring + wildcard BLE scanning
|
|
23
|
-
|
|
22
|
+
# Required system frameworks for iBeacon monitoring + wildcard BLE scanning.
|
|
23
|
+
# CarPlay is used by `BeaconCarPlaySceneDelegate` for the optional "Driving
|
|
24
|
+
# Task" entitlement integration; linking is harmless when the entitlement is
|
|
25
|
+
# not granted.
|
|
26
|
+
s.frameworks = 'CoreLocation', 'CoreBluetooth', 'UserNotifications', 'CarPlay', 'AVFoundation'
|
|
24
27
|
|
|
25
28
|
# Swift/Objective-C compatibility
|
|
26
29
|
s.pod_target_xcconfig = {
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
// MARK: - UserDefaults keys
|
|
4
|
+
|
|
5
|
+
internal let PAIRED_BEACONS_KEY = "expo.beacon.paired"
|
|
6
|
+
internal let PAIRED_EDDYSTONES_KEY = "expo.beacon.paired_eddystones"
|
|
7
|
+
internal let IS_MONITORING_KEY = "expo.beacon.is_monitoring"
|
|
8
|
+
internal let MAX_DISTANCE_KEY = "expo.beacon.max_distance"
|
|
9
|
+
internal let EXIT_DISTANCE_KEY = "expo.beacon.exit_distance"
|
|
10
|
+
internal let NOTIFICATION_CONFIG_KEY = "expo.beacon.notification_config"
|
|
11
|
+
internal let EVENT_LOGGING_ENABLED_KEY = "expo.beacon.event_logging_enabled"
|
|
12
|
+
internal let MIN_RSSI_KEY = "expo.beacon.min_rssi"
|
|
13
|
+
internal let EVENT_LEVEL_KEY = "expo.beacon.event_level"
|
|
14
|
+
internal let EXIT_TIMEOUT_SECONDS_KEY = "expo.beacon.exit_timeout_seconds"
|
|
15
|
+
internal let CARPLAY_MONITORING_ENABLED_KEY = "expo.beacon.carplay_monitoring_enabled"
|
|
16
|
+
|
|
17
|
+
// MARK: - Tuning thresholds
|
|
18
|
+
|
|
19
|
+
/// Default minimum RSSI (dBm) below which beacon readings are discarded as unreliable.
|
|
20
|
+
internal let DEFAULT_MIN_RSSI: Int = -85
|
|
21
|
+
/// Default seconds of silence after last beacon sighting before a disappearance-based exit fires.
|
|
22
|
+
internal let DEFAULT_EXIT_TIMEOUT_SECONDS: TimeInterval = 300.0
|
|
23
|
+
|
|
24
|
+
/// Number of consecutive in-range readings required before an enter event is emitted.
|
|
25
|
+
/// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
|
|
26
|
+
internal let ENTER_HYSTERESIS_COUNT = 1
|
|
27
|
+
/// Number of consecutive out-of-range readings required before an exit event is emitted.
|
|
28
|
+
/// IMPORTANT: Keep in sync with BeaconConstants.kt (Android).
|
|
29
|
+
internal let EXIT_HYSTERESIS_COUNT = 3
|
|
30
|
+
|
|
31
|
+
/// Eddystone monitoring timer interval in seconds.
|
|
32
|
+
internal let EDDYSTONE_MONITORING_TICK_INTERVAL: TimeInterval = 2.0
|
|
33
|
+
/// Maximum age (in seconds) before a beacon is considered "not recently seen".
|
|
34
|
+
/// Set high enough to tolerate iOS background CoreBluetooth throttling which
|
|
35
|
+
/// can cause 10-12 s gaps between Eddystone advertisements.
|
|
36
|
+
internal let EDDYSTONE_RECENTLY_SEEN_THRESHOLD: TimeInterval = 15.0
|
|
37
|
+
/// Minimum interval between consecutive distance event emissions per identifier.
|
|
38
|
+
internal let DISTANCE_EVENT_THROTTLE_INTERVAL: TimeInterval = 1.0
|
|
39
|
+
/// Seconds of no valid BLE readings before starting the timeout countdown.
|
|
40
|
+
/// Acts as a safety net when ranging cycles stop entirely (e.g. Doze mode).
|
|
41
|
+
internal let DISTANCE_INACTIVITY_SECONDS: TimeInterval = 60.0
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import CoreLocation
|
|
2
|
+
|
|
3
|
+
extension ExpoBeaconModule {
|
|
4
|
+
/// Starts the shared `CarPlayMonitor` and routes its events through the
|
|
5
|
+
/// standard `sendLoggedEvent` pipeline (JS bridge + SQLite + API forwarder
|
|
6
|
+
/// + lifecycle plugin registry). Idempotent — safe to call multiple times
|
|
7
|
+
/// (see `CarPlayMonitor.start(emit:)` semantics).
|
|
8
|
+
func startCarPlayMonitoringInternal() {
|
|
9
|
+
CarPlayMonitor.shared.start { [weak self] eventName, payload in
|
|
10
|
+
self?.sendLoggedEvent(eventName, payload)
|
|
11
|
+
}
|
|
12
|
+
// Tier 2 fallback: subscribe to background-wake signals so suspended
|
|
13
|
+
// apps still notice CarPlay route changes that happened off-process.
|
|
14
|
+
// Skipped when the entitled CarPlay scene path is providing real-time
|
|
15
|
+
// events — that source keeps the app awake for the entire CarPlay
|
|
16
|
+
// session and renders SLC/Visit redundant.
|
|
17
|
+
if !CarPlayMonitor.shared.isUsingEntitledSource {
|
|
18
|
+
startCarPlayBackgroundWakes()
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Start Significant Location Change + Visit monitoring as background-wake
|
|
23
|
+
/// hooks for CarPlay state reconciliation. Both are extremely low cost
|
|
24
|
+
/// (no continuous GPS) and reuse the existing `CLLocationManager` /
|
|
25
|
+
/// `LocationDelegate`. Idempotent.
|
|
26
|
+
func startCarPlayBackgroundWakes() {
|
|
27
|
+
if !CLLocationManager.significantLocationChangeMonitoringAvailable() {
|
|
28
|
+
return
|
|
29
|
+
}
|
|
30
|
+
locationManager.startMonitoringSignificantLocationChanges()
|
|
31
|
+
locationManager.startMonitoringVisits()
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/// Stop the SLC + Visit hooks. Called from `stopCarPlayMonitoring`.
|
|
35
|
+
func stopCarPlayBackgroundWakes() {
|
|
36
|
+
locationManager.stopMonitoringSignificantLocationChanges()
|
|
37
|
+
locationManager.stopMonitoringVisits()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Forwarded from `LocationDelegate` for SLC and Visit callbacks.
|
|
41
|
+
/// Cheap reconciliation: just snapshot the audio route. The entitled
|
|
42
|
+
/// scene-delegate path (when active) takes precedence in `CarPlayMonitor`
|
|
43
|
+
/// itself.
|
|
44
|
+
func handleBackgroundWakeForCarPlay() {
|
|
45
|
+
if defaults.bool(forKey: CARPLAY_MONITORING_ENABLED_KEY) {
|
|
46
|
+
CarPlayMonitor.shared.resyncIfNeeded()
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|