@iternio/react-native-auto-play 0.5.13 → 0.5.14-beta.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/ios/hybrid/HybridAutoPlay.swift +75 -40
- package/ios/hybrid/HybridCarPlayDashboard.swift +38 -11
- package/ios/hybrid/HybridCluster.swift +103 -47
- package/ios/scenes/SceneStore.swift +22 -9
- package/ios/templates/TemplateStore.swift +51 -11
- package/ios/utils/SymbolFont.swift +7 -0
- package/ios/utils/VoiceInputManager.swift +22 -15
- package/package.json +1 -1
|
@@ -18,14 +18,26 @@ struct SafeAreaListener {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
class HybridAutoPlay: HybridAutoPlaySpec {
|
|
21
|
+
/// Guards the three listener dictionaries below. They are mutated from the
|
|
22
|
+
/// JS thread (add/remove) and read from the main thread (UIKit layout
|
|
23
|
+
/// driving `emitSafeAreaInsets`), so every access must be serialised.
|
|
24
|
+
private static let listenersLock = NSLock()
|
|
21
25
|
private static var listeners = [EventName: [StateListener]]()
|
|
22
26
|
private static var renderStateListeners = [String: [RenderStateListener]]()
|
|
23
27
|
private static var safeAreaInsetsListeners = [String: [SafeAreaListener]]()
|
|
24
28
|
|
|
29
|
+
private static func withListenersLock<T>(_ body: () -> T) -> T {
|
|
30
|
+
listenersLock.lock()
|
|
31
|
+
defer { listenersLock.unlock() }
|
|
32
|
+
return body()
|
|
33
|
+
}
|
|
34
|
+
|
|
25
35
|
override init() {
|
|
26
|
-
HybridAutoPlay.
|
|
27
|
-
|
|
28
|
-
|
|
36
|
+
HybridAutoPlay.withListenersLock {
|
|
37
|
+
HybridAutoPlay.listeners.removeAll()
|
|
38
|
+
HybridAutoPlay.renderStateListeners.removeAll()
|
|
39
|
+
HybridAutoPlay.safeAreaInsetsListeners.removeAll()
|
|
40
|
+
}
|
|
29
41
|
super.init()
|
|
30
42
|
}
|
|
31
43
|
|
|
@@ -34,18 +46,22 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
34
46
|
{
|
|
35
47
|
let listener = StateListener(id: UUID(), callback: callback)
|
|
36
48
|
|
|
37
|
-
HybridAutoPlay.
|
|
49
|
+
HybridAutoPlay.withListenersLock {
|
|
50
|
+
HybridAutoPlay.listeners[eventType, default: []].append(listener)
|
|
51
|
+
}
|
|
38
52
|
|
|
39
53
|
if eventType == .didconnect && SceneStore.isRootModuleConnected() {
|
|
40
54
|
callback()
|
|
41
55
|
}
|
|
42
56
|
|
|
43
57
|
return {
|
|
44
|
-
HybridAutoPlay.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
HybridAutoPlay.listeners
|
|
58
|
+
HybridAutoPlay.withListenersLock {
|
|
59
|
+
HybridAutoPlay.listeners[eventType]?.removeAll {
|
|
60
|
+
$0.id == listener.id
|
|
61
|
+
}
|
|
62
|
+
if HybridAutoPlay.listeners[eventType]?.isEmpty ?? false {
|
|
63
|
+
HybridAutoPlay.listeners.removeValue(forKey: eventType)
|
|
64
|
+
}
|
|
49
65
|
}
|
|
50
66
|
}
|
|
51
67
|
}
|
|
@@ -56,24 +72,28 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
56
72
|
) throws -> () -> Void {
|
|
57
73
|
let listener = RenderStateListener(id: UUID(), callback: callback)
|
|
58
74
|
|
|
59
|
-
HybridAutoPlay.
|
|
60
|
-
|
|
61
|
-
|
|
75
|
+
HybridAutoPlay.withListenersLock {
|
|
76
|
+
HybridAutoPlay.renderStateListeners[moduleName, default: []].append(
|
|
77
|
+
listener
|
|
78
|
+
)
|
|
79
|
+
}
|
|
62
80
|
|
|
63
81
|
if let state = SceneStore.getState(moduleName: moduleName) {
|
|
64
82
|
callback(state)
|
|
65
83
|
}
|
|
66
84
|
|
|
67
85
|
return {
|
|
68
|
-
HybridAutoPlay.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
86
|
+
HybridAutoPlay.withListenersLock {
|
|
87
|
+
HybridAutoPlay.renderStateListeners[moduleName]?.removeAll {
|
|
88
|
+
$0.id == listener.id
|
|
89
|
+
}
|
|
90
|
+
if HybridAutoPlay.renderStateListeners[moduleName]?.isEmpty
|
|
91
|
+
?? false
|
|
92
|
+
{
|
|
93
|
+
HybridAutoPlay.renderStateListeners.removeValue(
|
|
94
|
+
forKey: moduleName
|
|
95
|
+
)
|
|
96
|
+
}
|
|
77
97
|
}
|
|
78
98
|
}
|
|
79
99
|
}
|
|
@@ -92,9 +112,10 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
92
112
|
) throws -> () -> Void {
|
|
93
113
|
let listener = SafeAreaListener(id: UUID(), callback: callback)
|
|
94
114
|
|
|
95
|
-
HybridAutoPlay.
|
|
96
|
-
|
|
97
|
-
|
|
115
|
+
HybridAutoPlay.withListenersLock {
|
|
116
|
+
HybridAutoPlay.safeAreaInsetsListeners[moduleName, default: []]
|
|
117
|
+
.append(listener)
|
|
118
|
+
}
|
|
98
119
|
|
|
99
120
|
if let safeAreaInsets = SceneStore.getScene(moduleName: moduleName)?
|
|
100
121
|
.safeAreaInsets
|
|
@@ -106,15 +127,17 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
106
127
|
}
|
|
107
128
|
|
|
108
129
|
return {
|
|
109
|
-
HybridAutoPlay.
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
130
|
+
HybridAutoPlay.withListenersLock {
|
|
131
|
+
HybridAutoPlay.safeAreaInsetsListeners[moduleName]?.removeAll {
|
|
132
|
+
$0.id == listener.id
|
|
133
|
+
}
|
|
134
|
+
if HybridAutoPlay.safeAreaInsetsListeners[moduleName]?.isEmpty
|
|
135
|
+
?? false
|
|
136
|
+
{
|
|
137
|
+
HybridAutoPlay.safeAreaInsetsListeners.removeValue(
|
|
138
|
+
forKey: moduleName
|
|
139
|
+
)
|
|
140
|
+
}
|
|
118
141
|
}
|
|
119
142
|
}
|
|
120
143
|
}
|
|
@@ -298,14 +321,21 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
298
321
|
|
|
299
322
|
// MARK: events
|
|
300
323
|
static func emit(event: EventName) {
|
|
301
|
-
|
|
324
|
+
// Snapshot under the lock, then call out without holding it so a
|
|
325
|
+
// callback that adds or removes a listener cannot deadlock.
|
|
326
|
+
let listeners = HybridAutoPlay.withListenersLock {
|
|
327
|
+
HybridAutoPlay.listeners[event] ?? []
|
|
328
|
+
}
|
|
329
|
+
listeners.forEach { listener in
|
|
302
330
|
listener.callback()
|
|
303
331
|
}
|
|
304
332
|
}
|
|
305
333
|
|
|
306
334
|
static func emitRenderState(moduleName: String, state: VisibilityState) {
|
|
307
|
-
HybridAutoPlay.
|
|
308
|
-
|
|
335
|
+
let listeners = HybridAutoPlay.withListenersLock {
|
|
336
|
+
HybridAutoPlay.renderStateListeners[moduleName] ?? []
|
|
337
|
+
}
|
|
338
|
+
listeners.forEach { listener in
|
|
309
339
|
listener.callback(state)
|
|
310
340
|
}
|
|
311
341
|
}
|
|
@@ -317,14 +347,19 @@ class HybridAutoPlay: HybridAutoPlaySpec {
|
|
|
317
347
|
let insets = HybridAutoPlay.getSafeAreaInsets(
|
|
318
348
|
safeAreaInsets: safeAreaInsets
|
|
319
349
|
)
|
|
320
|
-
HybridAutoPlay.
|
|
321
|
-
|
|
350
|
+
let listeners = HybridAutoPlay.withListenersLock {
|
|
351
|
+
HybridAutoPlay.safeAreaInsetsListeners[moduleName] ?? []
|
|
322
352
|
}
|
|
353
|
+
listeners.forEach { listener in listener.callback(insets) }
|
|
323
354
|
}
|
|
324
355
|
|
|
325
356
|
static func removeListeners(templateId: String) {
|
|
326
|
-
HybridAutoPlay.
|
|
327
|
-
|
|
357
|
+
HybridAutoPlay.withListenersLock {
|
|
358
|
+
HybridAutoPlay.renderStateListeners.removeValue(forKey: templateId)
|
|
359
|
+
HybridAutoPlay.safeAreaInsetsListeners.removeValue(
|
|
360
|
+
forKey: templateId
|
|
361
|
+
)
|
|
362
|
+
}
|
|
328
363
|
}
|
|
329
364
|
|
|
330
365
|
static func getSafeAreaInsets(safeAreaInsets: UIEdgeInsets)
|
|
@@ -7,26 +7,40 @@
|
|
|
7
7
|
import NitroModules
|
|
8
8
|
|
|
9
9
|
class HybridCarPlayDashboard: HybridCarPlayDashboardSpec {
|
|
10
|
+
/// Guards the listener dictionaries below. `addListener*` mutate them from
|
|
11
|
+
/// the JS thread while `DashboardSceneDelegate` emits from main-thread
|
|
12
|
+
/// CarPlay callbacks, and Swift dictionaries are not thread-safe.
|
|
13
|
+
private static let listenersLock = NSLock()
|
|
10
14
|
private static var listeners = [EventName: [String: () -> Void]]()
|
|
11
15
|
private static var colorSchemeListeners = [
|
|
12
16
|
String: (_:ColorScheme) -> Void
|
|
13
17
|
]()
|
|
14
18
|
|
|
19
|
+
private static func withListenersLock<T>(_ body: () -> T) -> T {
|
|
20
|
+
listenersLock.lock()
|
|
21
|
+
defer { listenersLock.unlock() }
|
|
22
|
+
return body()
|
|
23
|
+
}
|
|
24
|
+
|
|
15
25
|
func addListener(eventType: EventName, callback: @escaping () -> Void)
|
|
16
26
|
throws -> () -> Void
|
|
17
27
|
{
|
|
18
28
|
let uuid = UUID().uuidString
|
|
19
|
-
HybridCarPlayDashboard.
|
|
20
|
-
|
|
29
|
+
HybridCarPlayDashboard.withListenersLock {
|
|
30
|
+
HybridCarPlayDashboard.listeners[eventType, default: [:]][uuid] =
|
|
31
|
+
callback
|
|
32
|
+
}
|
|
21
33
|
|
|
22
34
|
if eventType == .didconnect && SceneStore.isDashboardModuleConnected() {
|
|
23
35
|
callback()
|
|
24
36
|
}
|
|
25
37
|
|
|
26
38
|
return {
|
|
27
|
-
HybridCarPlayDashboard.
|
|
28
|
-
|
|
29
|
-
|
|
39
|
+
HybridCarPlayDashboard.withListenersLock {
|
|
40
|
+
_ = HybridCarPlayDashboard.listeners[eventType]?.removeValue(
|
|
41
|
+
forKey: uuid
|
|
42
|
+
)
|
|
43
|
+
}
|
|
30
44
|
}
|
|
31
45
|
}
|
|
32
46
|
|
|
@@ -54,23 +68,36 @@ class HybridCarPlayDashboard: HybridCarPlayDashboardSpec {
|
|
|
54
68
|
callback: @escaping (_ payload: ColorScheme) -> Void
|
|
55
69
|
) throws -> () -> Void {
|
|
56
70
|
let uuid = UUID().uuidString
|
|
57
|
-
HybridCarPlayDashboard.
|
|
71
|
+
HybridCarPlayDashboard.withListenersLock {
|
|
72
|
+
HybridCarPlayDashboard.colorSchemeListeners[uuid] = callback
|
|
73
|
+
}
|
|
58
74
|
|
|
59
75
|
return {
|
|
60
|
-
HybridCarPlayDashboard.
|
|
61
|
-
|
|
62
|
-
|
|
76
|
+
HybridCarPlayDashboard.withListenersLock {
|
|
77
|
+
_ = HybridCarPlayDashboard.colorSchemeListeners.removeValue(
|
|
78
|
+
forKey: uuid
|
|
79
|
+
)
|
|
80
|
+
}
|
|
63
81
|
}
|
|
64
82
|
}
|
|
65
83
|
|
|
66
84
|
static func emit(event: EventName) {
|
|
67
|
-
|
|
85
|
+
// Snapshot under the lock, then call out without holding it so a
|
|
86
|
+
// callback that adds or removes a listener cannot deadlock.
|
|
87
|
+
let listeners = HybridCarPlayDashboard.withListenersLock {
|
|
88
|
+
HybridCarPlayDashboard.listeners[event].map { Array($0.values) }
|
|
89
|
+
?? []
|
|
90
|
+
}
|
|
91
|
+
listeners.forEach {
|
|
68
92
|
$0()
|
|
69
93
|
}
|
|
70
94
|
}
|
|
71
95
|
|
|
72
96
|
static func emitColorScheme(colorScheme: ColorScheme) {
|
|
73
|
-
HybridCarPlayDashboard.
|
|
97
|
+
let listeners = HybridCarPlayDashboard.withListenersLock {
|
|
98
|
+
Array(HybridCarPlayDashboard.colorSchemeListeners.values)
|
|
99
|
+
}
|
|
100
|
+
listeners.forEach {
|
|
74
101
|
$0(colorScheme)
|
|
75
102
|
}
|
|
76
103
|
}
|
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
import NitroModules
|
|
8
8
|
|
|
9
9
|
class HybridCluster: HybridClusterSpec {
|
|
10
|
+
/// Guards every listener dictionary below. `addListener*` mutate them from
|
|
11
|
+
/// the JS thread while `ClusterSceneDelegate` emits from main-thread
|
|
12
|
+
/// CarPlay callbacks, and Swift dictionaries are not thread-safe.
|
|
13
|
+
private static let listenersLock = NSLock()
|
|
14
|
+
|
|
10
15
|
private static var listeners = [
|
|
11
16
|
ClusterEventName: [String: (_:String) -> Void]
|
|
12
17
|
]()
|
|
@@ -31,12 +36,20 @@ class HybridCluster: HybridClusterSpec {
|
|
|
31
36
|
String: (_: String, _: Bool) -> Void
|
|
32
37
|
]()
|
|
33
38
|
|
|
39
|
+
private static func withListenersLock<T>(_ body: () -> T) -> T {
|
|
40
|
+
listenersLock.lock()
|
|
41
|
+
defer { listenersLock.unlock() }
|
|
42
|
+
return body()
|
|
43
|
+
}
|
|
44
|
+
|
|
34
45
|
override init() {
|
|
35
|
-
HybridCluster.
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
HybridCluster.withListenersLock {
|
|
47
|
+
HybridCluster.listeners.removeAll()
|
|
48
|
+
HybridCluster.colorSchemeListeners.removeAll()
|
|
49
|
+
HybridCluster.zoomListeners.removeAll()
|
|
50
|
+
HybridCluster.compassListeners.removeAll()
|
|
51
|
+
HybridCluster.speedLimitListeners.removeAll()
|
|
52
|
+
}
|
|
40
53
|
super.init()
|
|
41
54
|
}
|
|
42
55
|
|
|
@@ -45,20 +58,22 @@ class HybridCluster: HybridClusterSpec {
|
|
|
45
58
|
callback: @escaping (_ clusterId: String) -> Void
|
|
46
59
|
) throws -> () -> Void {
|
|
47
60
|
let uuid = UUID().uuidString
|
|
48
|
-
HybridCluster.
|
|
49
|
-
|
|
61
|
+
let queuedClusterIds = HybridCluster.withListenersLock {
|
|
62
|
+
() -> [String]? in
|
|
63
|
+
HybridCluster.listeners[eventType, default: [:]][uuid] = callback
|
|
50
64
|
|
|
51
|
-
|
|
52
|
-
for clusterId in queuedClusterIds {
|
|
53
|
-
callback(clusterId)
|
|
54
|
-
}
|
|
55
|
-
HybridCluster.eventQueue[eventType] = nil
|
|
65
|
+
return HybridCluster.eventQueue.removeValue(forKey: eventType)
|
|
56
66
|
}
|
|
57
67
|
|
|
68
|
+
// Drain the queue outside the lock — the callback runs JS.
|
|
69
|
+
queuedClusterIds?.forEach { clusterId in callback(clusterId) }
|
|
70
|
+
|
|
58
71
|
return {
|
|
59
|
-
HybridCluster.
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
HybridCluster.withListenersLock {
|
|
73
|
+
_ = HybridCluster.listeners[eventType]?.removeValue(
|
|
74
|
+
forKey: uuid
|
|
75
|
+
)
|
|
76
|
+
}
|
|
62
77
|
}
|
|
63
78
|
}
|
|
64
79
|
|
|
@@ -90,7 +105,17 @@ class HybridCluster: HybridClusterSpec {
|
|
|
90
105
|
attributedInactiveDescriptionVariants:
|
|
91
106
|
[NitroAttributedString]
|
|
92
107
|
) throws {
|
|
93
|
-
|
|
108
|
+
guard #available(iOS 15.4, *) else {
|
|
109
|
+
throw AutoPlayError.unsupportedVersion(
|
|
110
|
+
"Cluster support only available on iOS >= 15.4"
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// This runs on the JS thread, but it ends up assigning
|
|
115
|
+
// CPInstrumentClusterController.attributedInactiveDescriptionVariants,
|
|
116
|
+
// which is main-thread only — and the stored variants are also read
|
|
117
|
+
// from main-thread trait/content-style callbacks.
|
|
118
|
+
Task { @MainActor in
|
|
94
119
|
guard
|
|
95
120
|
let scene = SceneStore.getClusterScene(
|
|
96
121
|
clusterId: clusterId
|
|
@@ -104,23 +129,22 @@ class HybridCluster: HybridClusterSpec {
|
|
|
104
129
|
attributedInactiveDescriptionVariants
|
|
105
130
|
)
|
|
106
131
|
}
|
|
107
|
-
else {
|
|
108
|
-
throw AutoPlayError.unsupportedVersion(
|
|
109
|
-
"Cluster support only available on iOS >= 15.4"
|
|
110
|
-
)
|
|
111
|
-
}
|
|
112
132
|
}
|
|
113
133
|
|
|
114
134
|
func addListenerColorScheme(
|
|
115
135
|
callback: @escaping (String, ColorScheme) -> Void
|
|
116
136
|
) throws -> () -> Void {
|
|
117
137
|
let uuid = UUID().uuidString
|
|
118
|
-
HybridCluster.
|
|
138
|
+
HybridCluster.withListenersLock {
|
|
139
|
+
HybridCluster.colorSchemeListeners[uuid] = callback
|
|
140
|
+
}
|
|
119
141
|
|
|
120
142
|
return {
|
|
121
|
-
HybridCluster.
|
|
122
|
-
|
|
123
|
-
|
|
143
|
+
HybridCluster.withListenersLock {
|
|
144
|
+
_ = HybridCluster.colorSchemeListeners.removeValue(
|
|
145
|
+
forKey: uuid
|
|
146
|
+
)
|
|
147
|
+
}
|
|
124
148
|
}
|
|
125
149
|
}
|
|
126
150
|
|
|
@@ -128,12 +152,16 @@ class HybridCluster: HybridClusterSpec {
|
|
|
128
152
|
callback: @escaping (_ clusterId: String, _ payload: ZoomEvent) -> Void
|
|
129
153
|
) throws -> () -> Void {
|
|
130
154
|
let uuid = UUID().uuidString
|
|
131
|
-
HybridCluster.
|
|
155
|
+
HybridCluster.withListenersLock {
|
|
156
|
+
HybridCluster.zoomListeners[uuid] = callback
|
|
157
|
+
}
|
|
132
158
|
|
|
133
159
|
return {
|
|
134
|
-
HybridCluster.
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
HybridCluster.withListenersLock {
|
|
161
|
+
_ = HybridCluster.zoomListeners.removeValue(
|
|
162
|
+
forKey: uuid
|
|
163
|
+
)
|
|
164
|
+
}
|
|
137
165
|
}
|
|
138
166
|
}
|
|
139
167
|
|
|
@@ -141,12 +169,16 @@ class HybridCluster: HybridClusterSpec {
|
|
|
141
169
|
callback: @escaping (_ clusterId: String, _ payload: Bool) -> Void
|
|
142
170
|
) throws -> () -> Void {
|
|
143
171
|
let uuid = UUID().uuidString
|
|
144
|
-
HybridCluster.
|
|
172
|
+
HybridCluster.withListenersLock {
|
|
173
|
+
HybridCluster.compassListeners[uuid] = callback
|
|
174
|
+
}
|
|
145
175
|
|
|
146
176
|
return {
|
|
147
|
-
HybridCluster.
|
|
148
|
-
|
|
149
|
-
|
|
177
|
+
HybridCluster.withListenersLock {
|
|
178
|
+
_ = HybridCluster.compassListeners.removeValue(
|
|
179
|
+
forKey: uuid
|
|
180
|
+
)
|
|
181
|
+
}
|
|
150
182
|
}
|
|
151
183
|
}
|
|
152
184
|
|
|
@@ -154,48 +186,72 @@ class HybridCluster: HybridClusterSpec {
|
|
|
154
186
|
callback: @escaping (_ clusterId: String, _ payload: Bool) -> Void
|
|
155
187
|
) throws -> () -> Void {
|
|
156
188
|
let uuid = UUID().uuidString
|
|
157
|
-
HybridCluster.
|
|
189
|
+
HybridCluster.withListenersLock {
|
|
190
|
+
HybridCluster.speedLimitListeners[uuid] = callback
|
|
191
|
+
}
|
|
158
192
|
|
|
159
193
|
return {
|
|
160
|
-
HybridCluster.
|
|
161
|
-
|
|
162
|
-
|
|
194
|
+
HybridCluster.withListenersLock {
|
|
195
|
+
_ = HybridCluster.speedLimitListeners.removeValue(
|
|
196
|
+
forKey: uuid
|
|
197
|
+
)
|
|
198
|
+
}
|
|
163
199
|
}
|
|
164
200
|
}
|
|
165
201
|
|
|
166
202
|
static func emit(event: ClusterEventName, clusterId: String) {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
203
|
+
// Snapshot under the lock, then call out without holding it so a
|
|
204
|
+
// callback that adds or removes a listener cannot deadlock.
|
|
205
|
+
let listeners = HybridCluster.withListenersLock {
|
|
206
|
+
() -> [(_: String) -> Void] in
|
|
207
|
+
guard let listeners = HybridCluster.listeners[event],
|
|
208
|
+
!listeners.isEmpty
|
|
209
|
+
else {
|
|
210
|
+
// no listeners -> queue the event
|
|
211
|
+
HybridCluster.eventQueue[event, default: []].append(clusterId)
|
|
212
|
+
return []
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return Array(listeners.values)
|
|
172
216
|
}
|
|
173
217
|
|
|
174
|
-
listeners.
|
|
218
|
+
listeners.forEach {
|
|
175
219
|
$0(clusterId)
|
|
176
220
|
}
|
|
177
221
|
}
|
|
178
222
|
|
|
179
223
|
static func emitColorScheme(clusterId: String, colorScheme: ColorScheme) {
|
|
180
|
-
HybridCluster.
|
|
224
|
+
let listeners = HybridCluster.withListenersLock {
|
|
225
|
+
Array(HybridCluster.colorSchemeListeners.values)
|
|
226
|
+
}
|
|
227
|
+
listeners.forEach {
|
|
181
228
|
$0(clusterId, colorScheme)
|
|
182
229
|
}
|
|
183
230
|
}
|
|
184
231
|
|
|
185
232
|
static func emitZoom(clusterId: String, payload: ZoomEvent) {
|
|
186
|
-
HybridCluster.
|
|
233
|
+
let listeners = HybridCluster.withListenersLock {
|
|
234
|
+
Array(HybridCluster.zoomListeners.values)
|
|
235
|
+
}
|
|
236
|
+
listeners.forEach {
|
|
187
237
|
$0(clusterId, payload)
|
|
188
238
|
}
|
|
189
239
|
}
|
|
190
240
|
|
|
191
241
|
static func emitCompass(clusterId: String, payload: Bool) {
|
|
192
|
-
HybridCluster.
|
|
242
|
+
let listeners = HybridCluster.withListenersLock {
|
|
243
|
+
Array(HybridCluster.compassListeners.values)
|
|
244
|
+
}
|
|
245
|
+
listeners.forEach {
|
|
193
246
|
$0(clusterId, payload)
|
|
194
247
|
}
|
|
195
248
|
}
|
|
196
249
|
|
|
197
250
|
static func emitSpeedLimit(clusterId: String, payload: Bool) {
|
|
198
|
-
HybridCluster.
|
|
251
|
+
let listeners = HybridCluster.withListenersLock {
|
|
252
|
+
Array(HybridCluster.speedLimitListeners.values)
|
|
253
|
+
}
|
|
254
|
+
listeners.forEach {
|
|
199
255
|
$0(clusterId, payload)
|
|
200
256
|
}
|
|
201
257
|
}
|
|
@@ -12,36 +12,49 @@ class SceneStore {
|
|
|
12
12
|
static let dashboardModuleName = "CarPlayDashboard"
|
|
13
13
|
static let windowSceneModuleName = "main"
|
|
14
14
|
|
|
15
|
+
/// Guards `renderState` and `store`. Scenes are added/removed on the main
|
|
16
|
+
/// thread while the dictionaries are read from the JS thread, and Swift
|
|
17
|
+
/// dictionaries are not thread-safe.
|
|
18
|
+
private static let lock = NSLock()
|
|
19
|
+
|
|
15
20
|
private static var renderState = [String: VisibilityState]()
|
|
16
21
|
|
|
17
22
|
private static var store: [String: AutoPlayScene] = [:]
|
|
18
23
|
|
|
24
|
+
private static func withLock<T>(_ body: () -> T) -> T {
|
|
25
|
+
lock.lock()
|
|
26
|
+
defer { lock.unlock() }
|
|
27
|
+
return body()
|
|
28
|
+
}
|
|
29
|
+
|
|
19
30
|
static func addScene(moduleName: String, scene: AutoPlayScene) {
|
|
20
|
-
store[moduleName] = scene
|
|
31
|
+
withLock { store[moduleName] = scene }
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
static func removeScene(moduleName: String) {
|
|
24
|
-
store.removeValue(forKey: moduleName)
|
|
35
|
+
let _ = withLock { store.removeValue(forKey: moduleName) }
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
static func getScene(moduleName: String) -> AutoPlayScene? {
|
|
28
|
-
return store[moduleName]
|
|
39
|
+
return withLock { store[moduleName] }
|
|
29
40
|
}
|
|
30
41
|
|
|
31
42
|
static func isRootModuleConnected() -> Bool {
|
|
32
|
-
return
|
|
43
|
+
return getScene(moduleName: SceneStore.rootModuleName)?.isConnected
|
|
44
|
+
?? false
|
|
33
45
|
}
|
|
34
46
|
|
|
35
47
|
static func isDashboardModuleConnected() -> Bool {
|
|
36
|
-
return
|
|
48
|
+
return getScene(moduleName: SceneStore.dashboardModuleName)?.isConnected
|
|
49
|
+
?? false
|
|
37
50
|
}
|
|
38
51
|
|
|
39
52
|
static func getState(moduleName: String) -> VisibilityState? {
|
|
40
|
-
return renderState[moduleName]
|
|
53
|
+
return withLock { renderState[moduleName] }
|
|
41
54
|
}
|
|
42
55
|
|
|
43
56
|
static func setState(moduleName: String, state: VisibilityState) {
|
|
44
|
-
renderState[moduleName] = state
|
|
57
|
+
withLock { renderState[moduleName] = state }
|
|
45
58
|
|
|
46
59
|
HybridAutoPlay.emitRenderState(
|
|
47
60
|
moduleName: moduleName,
|
|
@@ -77,10 +90,10 @@ class SceneStore {
|
|
|
77
90
|
}
|
|
78
91
|
|
|
79
92
|
static func getRootScene() -> AutoPlayScene? {
|
|
80
|
-
return
|
|
93
|
+
return getScene(moduleName: SceneStore.rootModuleName)
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
static func getRootTraitCollection() -> UITraitCollection? {
|
|
84
|
-
return
|
|
97
|
+
return getRootScene()?.traitCollection
|
|
85
98
|
}
|
|
86
99
|
}
|
|
@@ -7,47 +7,87 @@
|
|
|
7
7
|
import CarPlay
|
|
8
8
|
|
|
9
9
|
class TemplateStore {
|
|
10
|
+
/// Guards `store`. Templates are added from the JS thread (the
|
|
11
|
+
/// `createXTemplate` hybrid methods are synchronous and never hop actors)
|
|
12
|
+
/// while the CarPlay delegate callbacks remove/purge them on the main
|
|
13
|
+
/// thread, and Swift dictionaries are not thread-safe.
|
|
14
|
+
private let lock = NSLock()
|
|
10
15
|
private var store: [String: AutoPlayTemplate] = [:]
|
|
11
16
|
|
|
17
|
+
private func withLock<T>(_ body: () -> T) -> T {
|
|
18
|
+
lock.lock()
|
|
19
|
+
defer { lock.unlock() }
|
|
20
|
+
return body()
|
|
21
|
+
}
|
|
22
|
+
|
|
12
23
|
func getCPTemplate(templateId key: String) -> CPTemplate? {
|
|
13
|
-
return store[key]?.getTemplate()
|
|
24
|
+
return withLock { store[key] }?.getTemplate()
|
|
14
25
|
}
|
|
15
26
|
|
|
16
27
|
func getTemplate(templateId: String) throws -> AutoPlayTemplate {
|
|
17
|
-
if let template = store[templateId] {
|
|
28
|
+
if let template = withLock({ store[templateId] }) {
|
|
18
29
|
return template
|
|
19
30
|
}
|
|
20
31
|
throw AutoPlayError.templateNotFound(templateId)
|
|
21
32
|
}
|
|
22
33
|
|
|
23
34
|
func addTemplate(template: AutoPlayTemplate, templateId: String) {
|
|
24
|
-
store[templateId] = template
|
|
35
|
+
withLock { store[templateId] = template }
|
|
25
36
|
}
|
|
26
37
|
|
|
27
38
|
func removeTemplate(templateId: String) {
|
|
28
|
-
|
|
39
|
+
// Remove first, then notify outside the lock: `onPopped` runs a JS
|
|
40
|
+
// callback which can re-enter the store (see
|
|
41
|
+
// VoiceInputTemplate.onDidDisappear).
|
|
42
|
+
let removed = withLock { store.removeValue(forKey: templateId) }
|
|
29
43
|
|
|
30
|
-
|
|
44
|
+
removed?.onPopped()
|
|
31
45
|
}
|
|
32
46
|
|
|
33
47
|
func removeTemplates(templateIds: [String]) {
|
|
34
|
-
|
|
35
|
-
store
|
|
48
|
+
let removed = withLock {
|
|
49
|
+
templateIds.compactMap { store.removeValue(forKey: $0) }
|
|
36
50
|
}
|
|
37
51
|
|
|
38
|
-
|
|
52
|
+
removed.forEach { template in template.onPopped() }
|
|
39
53
|
}
|
|
40
54
|
|
|
41
55
|
func purge() {
|
|
42
|
-
|
|
56
|
+
// These templates were popped by a native CarPlay button we cannot
|
|
57
|
+
// intercept, so they need an `onPopped()` just like an explicit pop.
|
|
58
|
+
let removed = withLock {
|
|
59
|
+
let searchTemplates = store.filter {
|
|
60
|
+
$0.value.getTemplate() is CPSearchTemplate
|
|
61
|
+
}
|
|
62
|
+
searchTemplates.keys.forEach { store.removeValue(forKey: $0) }
|
|
63
|
+
|
|
64
|
+
return Array(searchTemplates.values)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
removed.forEach { template in template.onPopped() }
|
|
43
68
|
}
|
|
44
69
|
|
|
45
70
|
@MainActor
|
|
46
71
|
func traitCollectionDidChange() {
|
|
47
|
-
|
|
72
|
+
let templates = withLock { Array(store.values) }
|
|
73
|
+
|
|
74
|
+
templates.forEach { template in template.traitCollectionDidChange() }
|
|
48
75
|
}
|
|
49
76
|
|
|
50
77
|
func disconnect() {
|
|
51
|
-
|
|
78
|
+
// The session is gone, so these templates will never be popped
|
|
79
|
+
// individually. Without an `onPopped()` here anything the app tied to a
|
|
80
|
+
// template's lifetime (event listeners, timers, cached render state)
|
|
81
|
+
// leaks for the rest of the app lifetime, and the host app keeps
|
|
82
|
+
// running after a disconnect. Android does the same on
|
|
83
|
+
// `Lifecycle.Event.ON_DESTROY` (AndroidAutoScreen.kt).
|
|
84
|
+
let removed = withLock {
|
|
85
|
+
let templates = Array(store.values)
|
|
86
|
+
store = [:]
|
|
87
|
+
|
|
88
|
+
return templates
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
removed.forEach { template in template.onPopped() }
|
|
52
92
|
}
|
|
53
93
|
}
|
|
@@ -9,10 +9,17 @@ import CoreText
|
|
|
9
9
|
import UIKit
|
|
10
10
|
|
|
11
11
|
class SymbolFont {
|
|
12
|
+
/// Guards the font cache below. Glyphs are rendered both during template
|
|
13
|
+
/// construction on the JS thread and from `@MainActor` template
|
|
14
|
+
/// invalidation, so the cache is touched from more than one thread.
|
|
15
|
+
private static let cacheLock = NSLock()
|
|
12
16
|
private static var cachedFontName: String?
|
|
13
17
|
private static var cachedPSName: String?
|
|
14
18
|
|
|
15
19
|
private static func loadFont(named fontName: String) -> String? {
|
|
20
|
+
cacheLock.lock()
|
|
21
|
+
defer { cacheLock.unlock() }
|
|
22
|
+
|
|
16
23
|
if fontName == cachedFontName {
|
|
17
24
|
return cachedPSName
|
|
18
25
|
}
|
|
@@ -307,9 +307,11 @@ class VoiceInputManager {
|
|
|
307
307
|
throw VoiceInputError.converterUnavailable
|
|
308
308
|
}
|
|
309
309
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
310
|
+
stopLock.withLock {
|
|
311
|
+
recordingStart = Date()
|
|
312
|
+
silenceStart = nil
|
|
313
|
+
firstBufferContinuation = nil
|
|
314
|
+
}
|
|
313
315
|
|
|
314
316
|
inputNode.installTap(
|
|
315
317
|
onBus: 0,
|
|
@@ -381,18 +383,22 @@ class VoiceInputManager {
|
|
|
381
383
|
now.timeIntervalSince(start) * 1000 >= VoiceInputManager.warmupMs
|
|
382
384
|
{
|
|
383
385
|
let peak = newSamples.reduce(0) { max($0, abs(Int($1))) }
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
{
|
|
391
|
-
self.triggerAutoStop(interfaceController: interfaceController)
|
|
386
|
+
// `silenceStart` is also cleared from cleanup() on another
|
|
387
|
+
// thread, so the read-modify-write has to hold stopLock.
|
|
388
|
+
let silenceElapsedMs = self.stopLock.withLock { () -> Double? in
|
|
389
|
+
guard peak < VoiceInputManager.silenceAmplitudeThreshold else {
|
|
390
|
+
self.silenceStart = nil
|
|
391
|
+
return nil
|
|
392
392
|
}
|
|
393
|
+
|
|
394
|
+
let silenceBegin = self.silenceStart ?? now
|
|
395
|
+
self.silenceStart = silenceBegin
|
|
396
|
+
|
|
397
|
+
return now.timeIntervalSince(silenceBegin) * 1000
|
|
393
398
|
}
|
|
394
|
-
|
|
395
|
-
|
|
399
|
+
|
|
400
|
+
if let silenceElapsedMs, silenceElapsedMs >= silenceThresholdMs {
|
|
401
|
+
self.triggerAutoStop(interfaceController: interfaceController)
|
|
396
402
|
}
|
|
397
403
|
}
|
|
398
404
|
}
|
|
@@ -412,10 +418,11 @@ class VoiceInputManager {
|
|
|
412
418
|
audioEngine?.stop()
|
|
413
419
|
audioEngine = nil
|
|
414
420
|
recognitionRequest = nil
|
|
415
|
-
recordingStart = nil
|
|
416
|
-
silenceStart = nil
|
|
417
421
|
// Drain firstBufferContinuation so the template Task doesn't hang if stop() fired before the first buffer.
|
|
418
422
|
let pendingCont = stopLock.withLock { () -> CheckedContinuation<Void, Never>? in
|
|
423
|
+
recordingStart = nil
|
|
424
|
+
silenceStart = nil
|
|
425
|
+
|
|
419
426
|
let c = firstBufferContinuation
|
|
420
427
|
firstBufferContinuation = nil
|
|
421
428
|
return c
|