@npaw/forge-react-native 9.7.1 → 9.7.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/android/build.gradle +1 -1
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeDtoMapper.java +10 -0
- package/android/src/main/java/com/npaw/forge/reactnative/ForgeInstanceRegistry.java +59 -1
- package/android/src/main/java/com/npaw/forge/reactnative/NPAWForgeModule.java +33 -0
- package/android/src/test/java/com/npaw/forge/reactnative/ForgeInstanceRegistryTest.java +65 -0
- package/app.plugin.js +17 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/Info.plist +8 -8
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/ios-arm64_x86_64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/Headers/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/ForgeKernelFFI.xcframework/tvos-arm64-simulator/libForgeKernelFFI.a +0 -0
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernel.swift +198 -1
- package/ios/ForgeApple/Sources/ForgeKernelUniFFI/forge_kernelFFI.h +71 -0
- package/ios/ForgeApple/Sources/NPAWForgeAVPlayerAdapter/AVPlayerAdapter.swift +12 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ConnectionType.swift +125 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeApple.swift +67 -1
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeCapturePlan.swift +4 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeLogging.swift +80 -3
- package/ios/ForgeApple/Sources/NPAWForgeCore/ForgeRuntime.swift +44 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/LocationObserver.swift +183 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/NPAWForge.swift +123 -2
- package/ios/ForgeApple/Sources/NPAWForgeCore/NativeUniFFIRuntime.swift +59 -0
- package/ios/ForgeApple/Sources/NPAWForgeCore/PlayerAdapter.swift +17 -1
- package/ios/ForgeInstanceRegistry.swift +69 -0
- package/ios/ForgeModuleImpl.swift +53 -6
- package/ios/NPAWForgeTurboModule.mm +6 -1
- package/package.json +3 -1
- package/plugin/index.js +82 -0
- package/plugin/mutations.js +140 -0
- package/src/NPAWForge.ts +12 -1
- package/src/events.ts +21 -1
- package/src/index.ts +11 -1
- package/src/sections.ts +292 -0
- package/src/types.ts +23 -2
|
@@ -30,6 +30,10 @@ public struct ForgeAppleConfig: Equatable {
|
|
|
30
30
|
public var logLevel: ForgeLogLevel
|
|
31
31
|
public var deviceUuid: String?
|
|
32
32
|
public var deviceInfo: ForgeDeviceInfo?
|
|
33
|
+
/// Framework layered on top of this facade, when one applies. Set by
|
|
34
|
+
/// first-party bindings only - React Native declares itself here so its
|
|
35
|
+
/// traffic is distinguishable in `pluginInfo`. The platform stays `apple`.
|
|
36
|
+
public var framework: String?
|
|
33
37
|
|
|
34
38
|
public init(
|
|
35
39
|
accountCode: String,
|
|
@@ -46,7 +50,8 @@ public struct ForgeAppleConfig: Equatable {
|
|
|
46
50
|
preSessionEventQueueMaxAgeMs: Int64 = 30_000,
|
|
47
51
|
logLevel: ForgeLogLevel = .warning,
|
|
48
52
|
deviceUuid: String? = nil,
|
|
49
|
-
deviceInfo: ForgeDeviceInfo? = nil
|
|
53
|
+
deviceInfo: ForgeDeviceInfo? = nil,
|
|
54
|
+
framework: String? = nil
|
|
50
55
|
) {
|
|
51
56
|
self.accountCode = accountCode
|
|
52
57
|
self.pluginVersion = pluginVersion
|
|
@@ -63,6 +68,7 @@ public struct ForgeAppleConfig: Equatable {
|
|
|
63
68
|
self.logLevel = logLevel
|
|
64
69
|
self.deviceUuid = deviceUuid
|
|
65
70
|
self.deviceInfo = deviceInfo
|
|
71
|
+
self.framework = framework
|
|
66
72
|
}
|
|
67
73
|
}
|
|
68
74
|
|
|
@@ -140,6 +146,7 @@ public final class ForgeApple {
|
|
|
140
146
|
private var onSendRequestListeners: [UUID: (String, inout [String: String]) -> Void] = [:]
|
|
141
147
|
private var capturePlanObservers: [UUID: (ForgeCapturePlan) -> Void] = [:]
|
|
142
148
|
private var latestCapturePlan: ForgeCapturePlan?
|
|
149
|
+
private var logObservers: [UUID: (ForgeRenderedLog) -> Void] = [:]
|
|
143
150
|
|
|
144
151
|
public init(config: ForgeAppleConfig, services: ForgeAppleServices) {
|
|
145
152
|
let safeConfig = config.withPluginVersion(NPAWForge.defaultPluginVersion)
|
|
@@ -150,6 +157,11 @@ public final class ForgeApple {
|
|
|
150
157
|
_ = runtime.setCapturePlanListener { [weak self] plan in
|
|
151
158
|
self?.dispatchCapturePlan(plan)
|
|
152
159
|
}
|
|
160
|
+
// Registered before start() so the kernel's own start-up decisions are
|
|
161
|
+
// the first thing a `logLevel: trace` host sees.
|
|
162
|
+
_ = runtime.setLogListener { [weak self] log in
|
|
163
|
+
self?.dispatchRenderedLog(log)
|
|
164
|
+
}
|
|
153
165
|
_ = runtime.start()
|
|
154
166
|
if safeConfig.startSessionOnInitialize {
|
|
155
167
|
_ = startSession()
|
|
@@ -372,6 +384,20 @@ public final class ForgeApple {
|
|
|
372
384
|
return runtime.setLogLevel(level)
|
|
373
385
|
}
|
|
374
386
|
|
|
387
|
+
/// Forwards the observed device connection class to the kernel, which
|
|
388
|
+
/// applies it only when the integrator supplied no `connectionType` of
|
|
389
|
+
/// their own.
|
|
390
|
+
@discardableResult
|
|
391
|
+
public func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome {
|
|
392
|
+
runtime.setAutoConnectionType(connectionType)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/// Forwards a location fix to the kernel, which owns every threshold.
|
|
396
|
+
@discardableResult
|
|
397
|
+
public func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome {
|
|
398
|
+
runtime.recordLocationFix(fix)
|
|
399
|
+
}
|
|
400
|
+
|
|
375
401
|
public func effectiveLogLevel() -> ForgeLogLevel {
|
|
376
402
|
logger.effectiveLevel()
|
|
377
403
|
}
|
|
@@ -483,6 +509,44 @@ public final class ForgeApple {
|
|
|
483
509
|
return latest ?? runtime.capturePlan()
|
|
484
510
|
}
|
|
485
511
|
|
|
512
|
+
/// Observes the kernel's rendered decision log. Console output happens
|
|
513
|
+
/// regardless of whether anything is registered here; listeners are for
|
|
514
|
+
/// hosts that want to forward the stream (the React Native binding does).
|
|
515
|
+
public func addLogListener(_ listener: @escaping (ForgeRenderedLog) -> Void) -> ForgeEventSubscription {
|
|
516
|
+
let id = UUID()
|
|
517
|
+
lock.lock()
|
|
518
|
+
logObservers[id] = listener
|
|
519
|
+
lock.unlock()
|
|
520
|
+
return ForgeEventSubscription { [weak self] in
|
|
521
|
+
self?.lock.lock()
|
|
522
|
+
self?.logObservers.removeValue(forKey: id)
|
|
523
|
+
self?.lock.unlock()
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/// Sends the kernel line to the platform console through the logger, then
|
|
528
|
+
/// to every registered observer. The logger owns formatting, so observers
|
|
529
|
+
/// and the console always agree on what a line says.
|
|
530
|
+
private func dispatchRenderedLog(_ log: ForgeRenderedLog) {
|
|
531
|
+
lock.lock()
|
|
532
|
+
guard !destroyed else {
|
|
533
|
+
lock.unlock()
|
|
534
|
+
return
|
|
535
|
+
}
|
|
536
|
+
lock.unlock()
|
|
537
|
+
guard let emitted = logger.emitRendered(
|
|
538
|
+
tag: log.tag,
|
|
539
|
+
levelName: log.level.wireName,
|
|
540
|
+
message: log.message
|
|
541
|
+
) else {
|
|
542
|
+
return
|
|
543
|
+
}
|
|
544
|
+
lock.lock()
|
|
545
|
+
let observers = Array(logObservers.values)
|
|
546
|
+
lock.unlock()
|
|
547
|
+
observers.forEach { $0(emitted) }
|
|
548
|
+
}
|
|
549
|
+
|
|
486
550
|
private func dispatchCapturePlan(_ plan: ForgeCapturePlan) {
|
|
487
551
|
lock.lock()
|
|
488
552
|
guard !destroyed else {
|
|
@@ -515,9 +579,11 @@ public final class ForgeApple {
|
|
|
515
579
|
eventObservers.removeAll()
|
|
516
580
|
onSendRequestListeners.removeAll()
|
|
517
581
|
capturePlanObservers.removeAll()
|
|
582
|
+
logObservers.removeAll()
|
|
518
583
|
recordedEvents.removeAll()
|
|
519
584
|
_ = runtime.registerSessionPreSendHook(nil)
|
|
520
585
|
_ = runtime.setCapturePlanListener(nil)
|
|
586
|
+
_ = runtime.setLogListener(nil)
|
|
521
587
|
return runtime.destroy()
|
|
522
588
|
}
|
|
523
589
|
|
|
@@ -207,6 +207,10 @@ public struct ForgeCapturePlan: Equatable {
|
|
|
207
207
|
// positions stay unassigned so existing bits never renumber.
|
|
208
208
|
public static let sections: UInt32 = 1 << 13
|
|
209
209
|
public static let attribution: UInt32 = 1 << 14
|
|
210
|
+
/// The operator enabled at least one `mo.*` switch, so the host may
|
|
211
|
+
/// observe location. Without this bit Forge reads no location at all,
|
|
212
|
+
/// not even the authorization status.
|
|
213
|
+
public static let location: UInt32 = 1 << 15
|
|
210
214
|
}
|
|
211
215
|
}
|
|
212
216
|
|
|
@@ -38,9 +38,21 @@ public enum ForgeLogLevel: Int {
|
|
|
38
38
|
if let level = value as? ForgeLogLevel {
|
|
39
39
|
return level
|
|
40
40
|
}
|
|
41
|
-
guard let raw = value.map({ String(describing: $0)
|
|
41
|
+
guard let raw = value.map({ String(describing: $0) }) else {
|
|
42
42
|
return defaultLevel
|
|
43
43
|
}
|
|
44
|
+
return parse(raw) ?? defaultLevel
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Strict counterpart to `from(_:default:)`: an unknown or blank name
|
|
48
|
+
/// yields `nil` instead of a default. Kernel-rendered lines carry their
|
|
49
|
+
/// level as a wire string, and a level Forge does not recognize must be
|
|
50
|
+
/// dropped rather than silently relabelled.
|
|
51
|
+
public static func parse(_ value: String?) -> ForgeLogLevel? {
|
|
52
|
+
guard let raw = value?.trimmingCharacters(in: .whitespacesAndNewlines).lowercased(),
|
|
53
|
+
!raw.isEmpty else {
|
|
54
|
+
return nil
|
|
55
|
+
}
|
|
44
56
|
switch raw {
|
|
45
57
|
case "off": return .off
|
|
46
58
|
case "error": return .error
|
|
@@ -48,11 +60,26 @@ public enum ForgeLogLevel: Int {
|
|
|
48
60
|
case "info": return .info
|
|
49
61
|
case "debug": return .debug
|
|
50
62
|
case "trace": return .trace
|
|
51
|
-
default: return
|
|
63
|
+
default: return nil
|
|
52
64
|
}
|
|
53
65
|
}
|
|
54
66
|
}
|
|
55
67
|
|
|
68
|
+
/// A log line the kernel already rendered, level-gated and redacted. Mirrors
|
|
69
|
+
/// the Android binding's `ForgeRenderedLog` so both platforms hand hosts the
|
|
70
|
+
/// same shape.
|
|
71
|
+
public struct ForgeRenderedLog: Equatable {
|
|
72
|
+
public let level: ForgeLogLevel
|
|
73
|
+
public let tag: String
|
|
74
|
+
public let message: String
|
|
75
|
+
|
|
76
|
+
public init(level: ForgeLogLevel, tag: String, message: String) {
|
|
77
|
+
self.level = level
|
|
78
|
+
self.tag = tag
|
|
79
|
+
self.message = message
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
56
83
|
public enum ForgeLogFamily: String {
|
|
57
84
|
case video = "Video"
|
|
58
85
|
case ad = "Ad"
|
|
@@ -68,15 +95,40 @@ public enum ForgeLogFamily: String {
|
|
|
68
95
|
|
|
69
96
|
public protocol ForgeLogSink: AnyObject {
|
|
70
97
|
func log(level: ForgeLogLevel, family: ForgeLogFamily, message: String, attributes: [String: String])
|
|
98
|
+
/// Emits a line the kernel already rendered. The kernel owns the family
|
|
99
|
+
/// label, the attribute rendering and the level gate for this stream, so
|
|
100
|
+
/// the sink receives finished text under the kernel's own tag.
|
|
101
|
+
func log(level: ForgeLogLevel, tag: String, message: String)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Additive default so sinks written before the kernel log stream existed keep
|
|
105
|
+
/// compiling and still receive it, through the entry point they already
|
|
106
|
+
/// implement.
|
|
107
|
+
public extension ForgeLogSink {
|
|
108
|
+
func log(level: ForgeLogLevel, tag: String, message: String) {
|
|
109
|
+
log(level: level, family: .diagnostics, message: message, attributes: ["tag": tag])
|
|
110
|
+
}
|
|
71
111
|
}
|
|
72
112
|
|
|
73
113
|
public final class OSLogForgeLogSink: ForgeLogSink {
|
|
114
|
+
/// Prefix on every Forge Apple line, facade and kernel alike, so one grep
|
|
115
|
+
/// token finds the whole stream.
|
|
116
|
+
public static let platformTag = "NPAW-Forge-Apple"
|
|
117
|
+
|
|
74
118
|
public init() {}
|
|
75
119
|
|
|
76
120
|
public func log(level: ForgeLogLevel, family: ForgeLogFamily, message: String, attributes: [String: String]) {
|
|
77
121
|
let rendered = "[\(family.rawValue)] \(message) \(attributes)"
|
|
78
122
|
guard level != .off else { return }
|
|
79
|
-
NSLog("[
|
|
123
|
+
NSLog("[\(Self.platformTag)] %@", rendered)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
public func log(level: ForgeLogLevel, tag: String, message: String) {
|
|
127
|
+
guard level != .off else { return }
|
|
128
|
+
// The kernel's own tag is kept, except when it would simply repeat the
|
|
129
|
+
// platform prefix.
|
|
130
|
+
let scope = tag.isEmpty || tag == Self.platformTag ? "" : "[\(tag)] "
|
|
131
|
+
NSLog("[\(Self.platformTag)] %@", "\(scope)\(message)")
|
|
80
132
|
}
|
|
81
133
|
}
|
|
82
134
|
|
|
@@ -106,4 +158,29 @@ public final class ForgeLogger {
|
|
|
106
158
|
guard level.rawValue >= ForgeLogLevel.error.rawValue else { return }
|
|
107
159
|
sink?.log(level: .error, family: family, message: action, attributes: details)
|
|
108
160
|
}
|
|
161
|
+
|
|
162
|
+
/// Emits a line the kernel rendered, and returns it so the facade can fan
|
|
163
|
+
/// it out to host listeners. Malformed input (unknown level, blank
|
|
164
|
+
/// message) is dropped and reported as `nil`.
|
|
165
|
+
///
|
|
166
|
+
/// Deliberately not re-gated against `level`: the kernel already applied
|
|
167
|
+
/// the effective level *and* the remote `lg.lvl` floor, which exists so
|
|
168
|
+
/// support can raise verbosity remotely. Re-checking the host level here
|
|
169
|
+
/// would silently defeat that floor on Apple.
|
|
170
|
+
@discardableResult
|
|
171
|
+
public func emitRendered(tag: String?, levelName: String?, message: String?) -> ForgeRenderedLog? {
|
|
172
|
+
guard let level = ForgeLogLevel.parse(levelName), level != .off else { return nil }
|
|
173
|
+
guard let message = message?.trimmingCharacters(in: .whitespacesAndNewlines),
|
|
174
|
+
!message.isEmpty else {
|
|
175
|
+
return nil
|
|
176
|
+
}
|
|
177
|
+
let resolvedTag = tag?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
178
|
+
.nonEmpty ?? OSLogForgeLogSink.platformTag
|
|
179
|
+
sink?.log(level: level, tag: resolvedTag, message: message)
|
|
180
|
+
return ForgeRenderedLog(level: level, tag: resolvedTag, message: message)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private extension String {
|
|
185
|
+
var nonEmpty: String? { isEmpty ? nil : self }
|
|
109
186
|
}
|
|
@@ -64,6 +64,13 @@ public protocol RuntimeSessionPreSendHook: AnyObject {
|
|
|
64
64
|
public protocol ForgeRuntime: AnyObject {
|
|
65
65
|
func start() -> ForgeOutcome
|
|
66
66
|
func setLogLevel(_ level: ForgeLogLevel) -> ForgeOutcome
|
|
67
|
+
/// Publishes the connection class the device is currently on, or `nil` to
|
|
68
|
+
/// clear it. The kernel keeps the last value and puts it on the wire only
|
|
69
|
+
/// when the integrator supplied no `connectionType` of their own.
|
|
70
|
+
func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome
|
|
71
|
+
/// Forwards a location fix. Fixes only - the kernel owns every threshold,
|
|
72
|
+
/// so the three platforms cannot drift into different definitions of moving.
|
|
73
|
+
func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome
|
|
67
74
|
func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView
|
|
68
75
|
func recordEvent(_ event: ForgeRecordedEvent) -> ForgeOutcome
|
|
69
76
|
/// Records a typed identity transition (`loginSuccess`, `loginFailed`,
|
|
@@ -102,6 +109,9 @@ public protocol ForgeRuntime: AnyObject {
|
|
|
102
109
|
func setCapturePlanListener(_ listener: ((ForgeCapturePlan) -> Void)?) -> ForgeOutcome
|
|
103
110
|
/// Latest kernel-emitted capture plan, or `nil` before the first emission.
|
|
104
111
|
func capturePlan() -> ForgeCapturePlan?
|
|
112
|
+
/// Registers (or clears with `nil`) the kernel rendered-log observer. Lines
|
|
113
|
+
/// the kernel rendered before registration are replayed on registration.
|
|
114
|
+
func setLogListener(_ listener: ((ForgeRenderedLog) -> Void)?) -> ForgeOutcome
|
|
105
115
|
}
|
|
106
116
|
|
|
107
117
|
/// Additive defaults so existing `ForgeRuntime` conformances (including the
|
|
@@ -110,9 +120,17 @@ public protocol ForgeRuntime: AnyObject {
|
|
|
110
120
|
public extension ForgeRuntime {
|
|
111
121
|
func setCapturePlanListener(_ listener: ((ForgeCapturePlan) -> Void)?) -> ForgeOutcome { .accepted }
|
|
112
122
|
func capturePlan() -> ForgeCapturePlan? { nil }
|
|
123
|
+
/// Additive default: runtimes with no kernel behind them render no logs,
|
|
124
|
+
/// so registration succeeds and simply never delivers.
|
|
125
|
+
func setLogListener(_ listener: ((ForgeRenderedLog) -> Void)?) -> ForgeOutcome { .accepted }
|
|
113
126
|
/// Additive default: runtimes without kernel identity support report the
|
|
114
127
|
/// capability gap instead of silently accepting the transition.
|
|
115
128
|
func recordIdentityEvent(kind: String, timemarkMs: Int64, fields: [String: String]) -> ForgeOutcome { .unsupportedCapability }
|
|
129
|
+
/// Additive default: a runtime with no kernel behind it has nowhere to keep
|
|
130
|
+
/// the observed connection class, so it accepts and forgets.
|
|
131
|
+
func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome { .accepted }
|
|
132
|
+
/// Additive default: no kernel behind it means nothing to derive motion with.
|
|
133
|
+
func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome { .accepted }
|
|
116
134
|
}
|
|
117
135
|
|
|
118
136
|
public protocol ForgeRuntimeFactory: AnyObject {
|
|
@@ -128,6 +146,8 @@ public final class UnavailableForgeRuntime: ForgeRuntime {
|
|
|
128
146
|
|
|
129
147
|
public func start() -> ForgeOutcome { failed() }
|
|
130
148
|
public func setLogLevel(_ level: ForgeLogLevel) -> ForgeOutcome { .accepted }
|
|
149
|
+
public func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome { .accepted }
|
|
150
|
+
public func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome { .accepted }
|
|
131
151
|
public func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView { RuntimeVideoView(id: -1) }
|
|
132
152
|
public func recordEvent(_ event: ForgeRecordedEvent) -> ForgeOutcome { failed() }
|
|
133
153
|
public func recordIdentityEvent(kind: String, timemarkMs: Int64, fields: [String: String]) -> ForgeOutcome { failed() }
|
|
@@ -189,12 +209,26 @@ public final class RecordingForgeRuntime: ForgeRuntime {
|
|
|
189
209
|
public private(set) var destroyedViewIds: [Int64] = []
|
|
190
210
|
public private(set) var capturePlanListener: ((ForgeCapturePlan) -> Void)?
|
|
191
211
|
public private(set) var latestCapturePlan: ForgeCapturePlan?
|
|
212
|
+
public private(set) var logListener: ((ForgeRenderedLog) -> Void)?
|
|
213
|
+
/// Every connection class the observer published, in order, including the
|
|
214
|
+
/// `nil` that clears it on teardown.
|
|
215
|
+
public private(set) var publishedConnectionTypes: [String?] = []
|
|
216
|
+
/// Every location fix forwarded, in order.
|
|
217
|
+
public private(set) var recordedLocationFixes: [ForgeLocationFix] = []
|
|
192
218
|
private var nextViewId: Int64 = 1
|
|
193
219
|
|
|
194
220
|
public init() {}
|
|
195
221
|
|
|
196
222
|
public func start() -> ForgeOutcome { .accepted }
|
|
197
223
|
public func setLogLevel(_ level: ForgeLogLevel) -> ForgeOutcome { .accepted }
|
|
224
|
+
public func setAutoConnectionType(_ connectionType: String?) -> ForgeOutcome {
|
|
225
|
+
publishedConnectionTypes.append(connectionType)
|
|
226
|
+
return .accepted
|
|
227
|
+
}
|
|
228
|
+
public func recordLocationFix(_ fix: ForgeLocationFix) -> ForgeOutcome {
|
|
229
|
+
recordedLocationFixes.append(fix)
|
|
230
|
+
return .accepted
|
|
231
|
+
}
|
|
198
232
|
public func createVideoView(timestampMs: Int64, options: VideoOptions) -> RuntimeVideoView {
|
|
199
233
|
createdVideoTimestamps.append(timestampMs)
|
|
200
234
|
defer { nextViewId += 1 }
|
|
@@ -288,4 +322,14 @@ public final class RecordingForgeRuntime: ForgeRuntime {
|
|
|
288
322
|
latestCapturePlan = plan
|
|
289
323
|
capturePlanListener?(plan)
|
|
290
324
|
}
|
|
325
|
+
|
|
326
|
+
public func setLogListener(_ listener: ((ForgeRenderedLog) -> Void)?) -> ForgeOutcome {
|
|
327
|
+
logListener = listener
|
|
328
|
+
return .accepted
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/// Test hook mirroring the kernel's rendered-log push path.
|
|
332
|
+
public func emitRenderedLog(_ log: ForgeRenderedLog) {
|
|
333
|
+
logListener?(log)
|
|
334
|
+
}
|
|
291
335
|
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (C) NPAW - All Rights Reserved
|
|
3
|
+
*
|
|
4
|
+
* This source code is protected under international copyright law. All rights
|
|
5
|
+
* reserved and protected by the copyright holders.
|
|
6
|
+
* This file is confidential and only available to authorized individuals with the
|
|
7
|
+
* permission of the copyright holders. If you encounter this file and do not have
|
|
8
|
+
* permission, please contact the copyright holders and delete this file.
|
|
9
|
+
*
|
|
10
|
+
*
|
|
11
|
+
* @author: Victor Soto <victor.soto@nicepeopleatwork.com>
|
|
12
|
+
* @description: Apple location observation for viewer motion, reading only an authorization the host app already holds.
|
|
13
|
+
*/
|
|
14
|
+
import CoreLocation
|
|
15
|
+
import Foundation
|
|
16
|
+
|
|
17
|
+
/// A location fix forwarded to the kernel.
|
|
18
|
+
///
|
|
19
|
+
/// The binding forwards fixes and never verdicts: the thresholds, hysteresis
|
|
20
|
+
/// and staleness rules all live in the kernel so Apple, Android and Web cannot
|
|
21
|
+
/// drift into three different definitions of "moving".
|
|
22
|
+
public struct ForgeLocationFix: Equatable {
|
|
23
|
+
public let latitude: Double
|
|
24
|
+
public let longitude: Double
|
|
25
|
+
/// Horizontal accuracy radius in metres. Negative means unknown, which the
|
|
26
|
+
/// kernel discards rather than guessing.
|
|
27
|
+
public let accuracyMetres: Double
|
|
28
|
+
public let timestampMs: Int64
|
|
29
|
+
|
|
30
|
+
public init(latitude: Double, longitude: Double, accuracyMetres: Double, timestampMs: Int64) {
|
|
31
|
+
self.latitude = latitude
|
|
32
|
+
self.longitude = longitude
|
|
33
|
+
self.accuracyMetres = accuracyMetres
|
|
34
|
+
self.timestampMs = timestampMs
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Observes location while the host app already holds an authorization.
|
|
39
|
+
///
|
|
40
|
+
/// **Forge never requests location authorization.** It reads
|
|
41
|
+
/// `CLLocationManager.authorizationStatus` and acts only on a grant the app
|
|
42
|
+
/// obtained for its own reasons. `requestWhenInUseAuthorization` and
|
|
43
|
+
/// `requestAlwaysAuthorization` appear nowhere in Forge, which is what keeps a
|
|
44
|
+
/// customer's permission dialogs and App Store disclosure unchanged by
|
|
45
|
+
/// integrating the SDK.
|
|
46
|
+
public final class ForgeLocationObserver: NSObject {
|
|
47
|
+
private let manager: CLLocationManager
|
|
48
|
+
private let publish: (ForgeLocationFix) -> Void
|
|
49
|
+
private let lock = NSLock()
|
|
50
|
+
private var started = false
|
|
51
|
+
|
|
52
|
+
public init(
|
|
53
|
+
manager: CLLocationManager = CLLocationManager(),
|
|
54
|
+
publish: @escaping (ForgeLocationFix) -> Void
|
|
55
|
+
) {
|
|
56
|
+
self.manager = manager
|
|
57
|
+
self.publish = publish
|
|
58
|
+
super.init()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Whether the host app already holds a usable authorization.
|
|
62
|
+
///
|
|
63
|
+
/// Both `authorizedWhenInUse` and `authorizedAlways` qualify. Reduced
|
|
64
|
+
/// accuracy is deliberately accepted: the kernel's accuracy gate is what
|
|
65
|
+
/// prevents a coarse fix inventing movement, so refusing coarse grants
|
|
66
|
+
/// would lose real coverage for no privacy gain.
|
|
67
|
+
public var isAuthorized: Bool {
|
|
68
|
+
// The instance property is iOS 14 / macOS 11; the package still
|
|
69
|
+
// supports iOS 13, so the deprecated class accessor is the fallback.
|
|
70
|
+
// Both only READ the status - neither prompts.
|
|
71
|
+
let status: CLAuthorizationStatus
|
|
72
|
+
if #available(iOS 14.0, tvOS 14.0, macOS 11.0, watchOS 7.0, *) {
|
|
73
|
+
status = manager.authorizationStatus
|
|
74
|
+
} else {
|
|
75
|
+
status = CLLocationManager.authorizationStatus()
|
|
76
|
+
}
|
|
77
|
+
switch status {
|
|
78
|
+
case .authorizedAlways, .authorizedWhenInUse:
|
|
79
|
+
return true
|
|
80
|
+
case .notDetermined, .restricted, .denied:
|
|
81
|
+
return false
|
|
82
|
+
@unknown default:
|
|
83
|
+
// A status Forge does not recognise is treated as "no grant".
|
|
84
|
+
return false
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/// Starts observing if an authorization is already held. Starting twice is
|
|
89
|
+
/// a no-op, so a facade that rebuilds does not stack delegates.
|
|
90
|
+
///
|
|
91
|
+
/// Inert on tvOS. Apple does not allow continuous location updates there —
|
|
92
|
+
/// `startUpdatingLocation()` is unavailable and only the one-shot
|
|
93
|
+
/// `requestLocation()` exists — and a set-top box does not move anyway, so
|
|
94
|
+
/// deriving viewer motion on it would answer a question nobody asked. The
|
|
95
|
+
/// dimension reports `unknown` on tvOS, which is the honest value.
|
|
96
|
+
public func start() {
|
|
97
|
+
#if os(tvOS)
|
|
98
|
+
return
|
|
99
|
+
#else
|
|
100
|
+
lock.lock()
|
|
101
|
+
if started {
|
|
102
|
+
lock.unlock()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
lock.unlock()
|
|
106
|
+
|
|
107
|
+
guard isAuthorized else {
|
|
108
|
+
// No grant: stay completely inert. Not an error - the common case
|
|
109
|
+
// is an app that never asked for location. `started` deliberately
|
|
110
|
+
// stays false so a later grant, with the plan re-firing, can still
|
|
111
|
+
// start the observer; marking it started here would wedge the
|
|
112
|
+
// feature off for the rest of the session.
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
lock.lock()
|
|
117
|
+
started = true
|
|
118
|
+
lock.unlock()
|
|
119
|
+
manager.delegate = self
|
|
120
|
+
// Coarse cadence by design: this feature answers "moving or not",
|
|
121
|
+
// which needs neither navigation-grade accuracy nor its battery cost.
|
|
122
|
+
manager.desiredAccuracy = kCLLocationAccuracyHundredMeters
|
|
123
|
+
manager.distanceFilter = 50
|
|
124
|
+
#if os(iOS) || os(watchOS)
|
|
125
|
+
manager.pausesLocationUpdatesAutomatically = true
|
|
126
|
+
#endif
|
|
127
|
+
manager.startUpdatingLocation()
|
|
128
|
+
#endif
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Stops observing. Safe to call when never started.
|
|
132
|
+
public func stop() {
|
|
133
|
+
lock.lock()
|
|
134
|
+
let wasStarted = started
|
|
135
|
+
started = false
|
|
136
|
+
lock.unlock()
|
|
137
|
+
|
|
138
|
+
guard wasStarted else { return }
|
|
139
|
+
#if !os(tvOS)
|
|
140
|
+
manager.stopUpdatingLocation()
|
|
141
|
+
#endif
|
|
142
|
+
manager.delegate = nil
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Converts and forwards a fix. Separate from the delegate callback
|
|
146
|
+
/// because a `CLLocation` is constructible in a test while a delegate
|
|
147
|
+
/// invocation from CoreLocation is not.
|
|
148
|
+
func report(_ location: CLLocation) {
|
|
149
|
+
guard location.horizontalAccuracy >= 0 else {
|
|
150
|
+
// CoreLocation reports a negative radius when the fix is invalid.
|
|
151
|
+
return
|
|
152
|
+
}
|
|
153
|
+
publish(
|
|
154
|
+
ForgeLocationFix(
|
|
155
|
+
latitude: location.coordinate.latitude,
|
|
156
|
+
longitude: location.coordinate.longitude,
|
|
157
|
+
accuracyMetres: location.horizontalAccuracy,
|
|
158
|
+
timestampMs: Int64(location.timestamp.timeIntervalSince1970 * 1000)
|
|
159
|
+
)
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
extension ForgeLocationObserver: CLLocationManagerDelegate {
|
|
165
|
+
public func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) {
|
|
166
|
+
guard let latest = locations.last else { return }
|
|
167
|
+
report(latest)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public func locationManager(_ manager: CLLocationManager, didFailWithError error: Error) {
|
|
171
|
+
// A failed fix is not evidence of anything; the kernel's staleness rule
|
|
172
|
+
// decides what happens if they keep failing.
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
|
|
176
|
+
// Authorization revoked mid-session: stop observing rather than let
|
|
177
|
+
// CoreLocation keep a delegate on a manager that can no longer report.
|
|
178
|
+
// The kernel decays the dimension to `unknown` on its own.
|
|
179
|
+
if !isAuthorized {
|
|
180
|
+
stop()
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|