react-native-bg-geolocation 0.2.0
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/BgGeolocation.podspec +39 -0
- package/LICENSE +20 -0
- package/README.md +366 -0
- package/android/build.gradle +69 -0
- package/android/src/main/AndroidManifest.xml +53 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationActivityRecognitionReceiver.kt +116 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationBootReceiver.kt +44 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationForegroundService.kt +373 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationGeofenceReceiver.kt +55 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationHeadlessTask.kt +138 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationModule.kt +1030 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationMotionStateMachine.kt +159 -0
- package/android/src/main/java/com/bggeolocation/BgGeolocationPackage.kt +31 -0
- package/android/src/main/res/drawable/bg_geo_notification.xml +9 -0
- package/ios/BgGeolocation.h +14 -0
- package/ios/BgGeolocation.mm +709 -0
- package/ios/engine/AtomicBoolean.swift +48 -0
- package/ios/engine/BGActivityChangeEvent.swift +20 -0
- package/ios/engine/BGActivityConfig.swift +71 -0
- package/ios/engine/BGAppConfig.swift +92 -0
- package/ios/engine/BGAppState.swift +147 -0
- package/ios/engine/BGAuthorization.swift +85 -0
- package/ios/engine/BGAuthorizationAlertPresenter.swift +39 -0
- package/ios/engine/BGAuthorizationConfig.swift +50 -0
- package/ios/engine/BGAuthorizationEvent.swift +40 -0
- package/ios/engine/BGBackgroundTaskManager.swift +143 -0
- package/ios/engine/BGCLRouter.swift +101 -0
- package/ios/engine/BGCallback.swift +19 -0
- package/ios/engine/BGConfig.swift +440 -0
- package/ios/engine/BGConfigModuleBase.swift +180 -0
- package/ios/engine/BGConfigOLD.swift +582 -0
- package/ios/engine/BGConnectivityChangeEvent.swift +15 -0
- package/ios/engine/BGCrashDetector.swift +122 -0
- package/ios/engine/BGCurrentPositionRequest.swift +87 -0
- package/ios/engine/BGDataStore.swift +75 -0
- package/ios/engine/BGDatabase.swift +677 -0
- package/ios/engine/BGDatabasePool.swift +220 -0
- package/ios/engine/BGDatabaseQueue.swift +215 -0
- package/ios/engine/BGDateUtils.swift +26 -0
- package/ios/engine/BGDeviceInfo.swift +54 -0
- package/ios/engine/BGDeviceManager.swift +65 -0
- package/ios/engine/BGEnabledChangeEvent.swift +11 -0
- package/ios/engine/BGEnv.swift +17 -0
- package/ios/engine/BGEventBus.swift +83 -0
- package/ios/engine/BGEventManager.swift +169 -0
- package/ios/engine/BGEventNames.swift +51 -0
- package/ios/engine/BGGeofence.swift +233 -0
- package/ios/engine/BGGeofenceDAO.swift +152 -0
- package/ios/engine/BGGeofenceEvent.swift +42 -0
- package/ios/engine/BGGeofenceLocationRequest.swift +94 -0
- package/ios/engine/BGGeofenceManager.swift +315 -0
- package/ios/engine/BGGeofenceTransition.swift +97 -0
- package/ios/engine/BGGeofencesChangeEvent.swift +26 -0
- package/ios/engine/BGGeolocationConfig.swift +136 -0
- package/ios/engine/BGHeartbeatEvent.swift +31 -0
- package/ios/engine/BGHeartbeatService.swift +51 -0
- package/ios/engine/BGHttpConfig.swift +105 -0
- package/ios/engine/BGHttpErrorCodes.swift +63 -0
- package/ios/engine/BGHttpEvent.swift +34 -0
- package/ios/engine/BGHttpRequest.swift +126 -0
- package/ios/engine/BGHttpResponse.swift +93 -0
- package/ios/engine/BGHttpService.swift +428 -0
- package/ios/engine/BGKalmanFilter.swift +105 -0
- package/ios/engine/BGLMActionNames.swift +55 -0
- package/ios/engine/BGLicenseManager.swift +26 -0
- package/ios/engine/BGLiveActivityManager.swift +327 -0
- package/ios/engine/BGLocation.swift +311 -0
- package/ios/engine/BGLocationAuthorization.swift +427 -0
- package/ios/engine/BGLocationDAO.swift +252 -0
- package/ios/engine/BGLocationErrors.swift +28 -0
- package/ios/engine/BGLocationEvent.swift +43 -0
- package/ios/engine/BGLocationFilter.swift +82 -0
- package/ios/engine/BGLocationFilterConfig.swift +57 -0
- package/ios/engine/BGLocationHelper.swift +54 -0
- package/ios/engine/BGLocationManager.swift +662 -0
- package/ios/engine/BGLocationMetricsEngine.swift +116 -0
- package/ios/engine/BGLocationRequestService.swift +459 -0
- package/ios/engine/BGLocationSatisfier.swift +14 -0
- package/ios/engine/BGLocationStreamEvent.swift +27 -0
- package/ios/engine/BGLog.swift +337 -0
- package/ios/engine/BGLogLevel.swift +26 -0
- package/ios/engine/BGLoggerConfig.swift +60 -0
- package/ios/engine/BGMotionActivity.swift +31 -0
- package/ios/engine/BGMotionActivityClassifier.swift +108 -0
- package/ios/engine/BGMotionActivityManagerAdapter.swift +40 -0
- package/ios/engine/BGMotionActivitySource.swift +46 -0
- package/ios/engine/BGMotionDetector.swift +377 -0
- package/ios/engine/BGMotionPermissionManager.swift +50 -0
- package/ios/engine/BGNativeLogger.swift +48 -0
- package/ios/engine/BGNotificaitons.swift +37 -0
- package/ios/engine/BGOdometer.swift +66 -0
- package/ios/engine/BGPersistenceConfig.swift +29 -0
- package/ios/engine/BGPolygonStreamRequest.swift +48 -0
- package/ios/engine/BGPowerSaveChangeEvent.swift +12 -0
- package/ios/engine/BGPropertySpec.swift +29 -0
- package/ios/engine/BGProviderChangeEvent.swift +31 -0
- package/ios/engine/BGQueue.swift +50 -0
- package/ios/engine/BGRPC.swift +194 -0
- package/ios/engine/BGReachability.swift +58 -0
- package/ios/engine/BGResultSet.swift +157 -0
- package/ios/engine/BGSchedule.swift +228 -0
- package/ios/engine/BGScheduleEvent.swift +13 -0
- package/ios/engine/BGScheduler.swift +116 -0
- package/ios/engine/BGSingleLocationRequest.swift +49 -0
- package/ios/engine/BGStreamLocationRequest.swift +42 -0
- package/ios/engine/BGTemplate.swift +54 -0
- package/ios/engine/BGTimerService.swift +46 -0
- package/ios/engine/BGTrackingAudioManager.swift +286 -0
- package/ios/engine/BGTrackingService.swift +879 -0
- package/ios/engine/BGWatchPositionRequest.swift +63 -0
- package/ios/engine/DatabaseQueue.swift +47 -0
- package/ios/engine/LogQuery.swift +10 -0
- package/ios/engine/SQLQuery.swift +65 -0
- package/ios/engine/TransistorAuthorizationToken.swift +182 -0
- package/ios/liveactivity/BGLiveTrackingAttributes.swift +52 -0
- package/ios/locationpush/BGLocationPushDeliverer.swift +260 -0
- package/ios/locationpush/BGLocationPushService.swift +161 -0
- package/ios/locationpush/BGLocationPushShared.swift +98 -0
- package/ios/locationpush/BGLocationPushSocketClient.swift +198 -0
- package/lib/module/NativeBgGeolocation.js +5 -0
- package/lib/module/NativeBgGeolocation.js.map +1 -0
- package/lib/module/events.js +20 -0
- package/lib/module/events.js.map +1 -0
- package/lib/module/index.js +706 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeBgGeolocation.d.ts +57 -0
- package/lib/typescript/src/NativeBgGeolocation.d.ts.map +1 -0
- package/lib/typescript/src/events.d.ts +18 -0
- package/lib/typescript/src/events.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +238 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +229 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +141 -0
- package/src/NativeBgGeolocation.ts +236 -0
- package/src/events.ts +17 -0
- package/src/index.tsx +935 -0
- package/src/types.ts +254 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import MediaPlayer
|
|
4
|
+
import UIKit
|
|
5
|
+
|
|
6
|
+
/// Owns the user-enabled audible background audio session for an active
|
|
7
|
+
/// tracking session and publishes user controls through Now Playing.
|
|
8
|
+
@objc public final class BGTrackingAudioManager: NSObject {
|
|
9
|
+
@objc public static let shared = BGTrackingAudioManager()
|
|
10
|
+
|
|
11
|
+
private let audioQueue = DispatchQueue(label: "BGTrackingAudioManager.audio")
|
|
12
|
+
private var engine: AVAudioEngine?
|
|
13
|
+
private var sourceNode: AVAudioSourceNode?
|
|
14
|
+
private var shouldBeRunning = false
|
|
15
|
+
private var active = false
|
|
16
|
+
private var lastError: String?
|
|
17
|
+
private var remoteCommandsConfigured = false
|
|
18
|
+
|
|
19
|
+
private override init() {
|
|
20
|
+
super.init()
|
|
21
|
+
NotificationCenter.default.addObserver(
|
|
22
|
+
self,
|
|
23
|
+
selector: #selector(applicationDidFinishLaunching),
|
|
24
|
+
name: UIApplication.didFinishLaunchingNotification,
|
|
25
|
+
object: nil
|
|
26
|
+
)
|
|
27
|
+
NotificationCenter.default.addObserver(
|
|
28
|
+
self,
|
|
29
|
+
selector: #selector(handleInterruption(_:)),
|
|
30
|
+
name: AVAudioSession.interruptionNotification,
|
|
31
|
+
object: AVAudioSession.sharedInstance()
|
|
32
|
+
)
|
|
33
|
+
NotificationCenter.default.addObserver(
|
|
34
|
+
self,
|
|
35
|
+
selector: #selector(handleMediaServicesReset),
|
|
36
|
+
name: AVAudioSession.mediaServicesWereResetNotification,
|
|
37
|
+
object: AVAudioSession.sharedInstance()
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@objc public func startIfNeeded() {
|
|
42
|
+
guard BGConfig.sharedInstance().app.trackingAudioEnabled else { return }
|
|
43
|
+
audioQueue.sync {
|
|
44
|
+
self.shouldBeRunning = true
|
|
45
|
+
self.startLocked()
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@objc public func stop() {
|
|
50
|
+
audioQueue.sync {
|
|
51
|
+
self.shouldBeRunning = false
|
|
52
|
+
self.stopLocked(deactivateSession: true)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
@objc public func stateDictionary() -> [String: Any] {
|
|
57
|
+
audioQueue.sync {
|
|
58
|
+
var state: [String: Any] = [
|
|
59
|
+
"enabled": BGConfig.sharedInstance().app.trackingAudioEnabled,
|
|
60
|
+
"requested": shouldBeRunning,
|
|
61
|
+
"active": active,
|
|
62
|
+
"audible": true,
|
|
63
|
+
"volume": BGConfig.sharedInstance().app.trackingAudioVolume,
|
|
64
|
+
// iOS has no runtime permission for audio playback. The app
|
|
65
|
+
// must obtain user consent in its own UI and declare the audio
|
|
66
|
+
// background mode; microphone permission is unrelated.
|
|
67
|
+
"permissionRequired": false,
|
|
68
|
+
"authorizationStatus": "notRequired",
|
|
69
|
+
"backgroundModeDeclared": hasAudioBackgroundMode(),
|
|
70
|
+
"nowPlayingActive": MPNowPlayingInfoCenter.default().nowPlayingInfo != nil
|
|
71
|
+
]
|
|
72
|
+
if let lastError {
|
|
73
|
+
state["error"] = lastError
|
|
74
|
+
}
|
|
75
|
+
return state
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@objc private func applicationDidFinishLaunching() {
|
|
80
|
+
audioQueue.async {
|
|
81
|
+
if self.shouldBeRunning || BGConfig.sharedInstance().enabled {
|
|
82
|
+
self.startLocked()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@objc private func handleInterruption(_ notification: Notification) {
|
|
88
|
+
guard
|
|
89
|
+
let info = notification.userInfo,
|
|
90
|
+
let rawType = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
91
|
+
let type = AVAudioSession.InterruptionType(rawValue: rawType)
|
|
92
|
+
else { return }
|
|
93
|
+
|
|
94
|
+
audioQueue.async {
|
|
95
|
+
switch type {
|
|
96
|
+
case .began:
|
|
97
|
+
self.stopLocked(deactivateSession: false)
|
|
98
|
+
case .ended:
|
|
99
|
+
let rawOptions = info[AVAudioSessionInterruptionOptionKey] as? UInt ?? 0
|
|
100
|
+
let options = AVAudioSession.InterruptionOptions(rawValue: rawOptions)
|
|
101
|
+
if self.shouldBeRunning && options.contains(.shouldResume) {
|
|
102
|
+
self.startLocked()
|
|
103
|
+
}
|
|
104
|
+
@unknown default:
|
|
105
|
+
break
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
@objc private func handleMediaServicesReset() {
|
|
111
|
+
audioQueue.async {
|
|
112
|
+
self.stopLocked(deactivateSession: false)
|
|
113
|
+
if self.shouldBeRunning {
|
|
114
|
+
self.startLocked()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private func startLocked() {
|
|
120
|
+
guard shouldBeRunning || BGConfig.sharedInstance().enabled else { return }
|
|
121
|
+
guard BGConfig.sharedInstance().app.trackingAudioEnabled else { return }
|
|
122
|
+
guard !active else { return }
|
|
123
|
+
|
|
124
|
+
do {
|
|
125
|
+
let config = BGConfig.sharedInstance().app
|
|
126
|
+
let session = AVAudioSession.sharedInstance()
|
|
127
|
+
let options: AVAudioSession.CategoryOptions =
|
|
128
|
+
config.trackingAudioMixWithOthers ? [.mixWithOthers] : []
|
|
129
|
+
try session.setCategory(.playback, mode: .default, options: options)
|
|
130
|
+
try session.setActive(true)
|
|
131
|
+
|
|
132
|
+
let audioEngine = AVAudioEngine()
|
|
133
|
+
let sampleRate = 44_100.0
|
|
134
|
+
let frequency = 196.0
|
|
135
|
+
let amplitude = Float(min(max(config.trackingAudioVolume, 0.01), 1.0))
|
|
136
|
+
var phase = 0.0
|
|
137
|
+
let phaseIncrement = 2.0 * Double.pi * frequency / sampleRate
|
|
138
|
+
|
|
139
|
+
let node = AVAudioSourceNode { _, _, frameCount, audioBufferList -> OSStatus in
|
|
140
|
+
let buffers = UnsafeMutableAudioBufferListPointer(audioBufferList)
|
|
141
|
+
for frame in 0..<Int(frameCount) {
|
|
142
|
+
// A soft two-harmonic tracking tone. This is intentionally
|
|
143
|
+
// audible; the audio background mode must not be driven by
|
|
144
|
+
// silent playback.
|
|
145
|
+
let fundamental = sin(phase)
|
|
146
|
+
let harmonic = sin(phase * 2.0) * 0.18
|
|
147
|
+
let sample = Float(fundamental + harmonic) * amplitude
|
|
148
|
+
phase += phaseIncrement
|
|
149
|
+
if phase >= 2.0 * Double.pi {
|
|
150
|
+
phase -= 2.0 * Double.pi
|
|
151
|
+
}
|
|
152
|
+
for buffer in buffers {
|
|
153
|
+
guard let data = buffer.mData else { continue }
|
|
154
|
+
data.assumingMemoryBound(to: Float.self)[frame] = sample
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return noErr
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
guard let format = AVAudioFormat(
|
|
161
|
+
standardFormatWithSampleRate: sampleRate,
|
|
162
|
+
channels: 1
|
|
163
|
+
) else {
|
|
164
|
+
throw NSError(
|
|
165
|
+
domain: "BGTrackingAudioManager",
|
|
166
|
+
code: 1,
|
|
167
|
+
userInfo: [NSLocalizedDescriptionKey: "Unable to create audio format"]
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
audioEngine.attach(node)
|
|
172
|
+
audioEngine.connect(node, to: audioEngine.mainMixerNode, format: format)
|
|
173
|
+
audioEngine.prepare()
|
|
174
|
+
try audioEngine.start()
|
|
175
|
+
|
|
176
|
+
engine = audioEngine
|
|
177
|
+
sourceNode = node
|
|
178
|
+
active = true
|
|
179
|
+
lastError = nil
|
|
180
|
+
publishNowPlaying()
|
|
181
|
+
log("Tracking audio started")
|
|
182
|
+
} catch {
|
|
183
|
+
active = false
|
|
184
|
+
lastError = error.localizedDescription
|
|
185
|
+
log("Unable to start tracking audio: \(error.localizedDescription)")
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
private func stopLocked(deactivateSession: Bool) {
|
|
190
|
+
engine?.stop()
|
|
191
|
+
if let node = sourceNode {
|
|
192
|
+
engine?.disconnectNodeOutput(node)
|
|
193
|
+
engine?.detach(node)
|
|
194
|
+
}
|
|
195
|
+
sourceNode = nil
|
|
196
|
+
engine = nil
|
|
197
|
+
active = false
|
|
198
|
+
clearNowPlaying()
|
|
199
|
+
|
|
200
|
+
if deactivateSession {
|
|
201
|
+
do {
|
|
202
|
+
try AVAudioSession.sharedInstance().setActive(
|
|
203
|
+
false,
|
|
204
|
+
options: [.notifyOthersOnDeactivation]
|
|
205
|
+
)
|
|
206
|
+
} catch {
|
|
207
|
+
lastError = error.localizedDescription
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
log("Tracking audio stopped")
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private func publishNowPlaying() {
|
|
214
|
+
let app = BGConfig.sharedInstance().app
|
|
215
|
+
let appName =
|
|
216
|
+
Bundle.main.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
|
|
217
|
+
?? Bundle.main.object(forInfoDictionaryKey: "CFBundleName") as? String
|
|
218
|
+
?? "Background Geolocation"
|
|
219
|
+
|
|
220
|
+
DispatchQueue.main.async {
|
|
221
|
+
self.configureRemoteCommandsIfNeeded()
|
|
222
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = [
|
|
223
|
+
MPMediaItemPropertyTitle: app.liveActivityTitle,
|
|
224
|
+
MPMediaItemPropertyArtist: appName,
|
|
225
|
+
MPMediaItemPropertyAlbumTitle: app.liveActivitySubtitle,
|
|
226
|
+
MPNowPlayingInfoPropertyPlaybackRate: 1.0,
|
|
227
|
+
MPNowPlayingInfoPropertyElapsedPlaybackTime: 0.0,
|
|
228
|
+
MPNowPlayingInfoPropertyIsLiveStream: true,
|
|
229
|
+
MPNowPlayingInfoPropertyMediaType: MPNowPlayingInfoMediaType.audio.rawValue
|
|
230
|
+
]
|
|
231
|
+
MPNowPlayingInfoCenter.default().playbackState = .playing
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private func clearNowPlaying() {
|
|
236
|
+
DispatchQueue.main.async {
|
|
237
|
+
MPNowPlayingInfoCenter.default().playbackState = .stopped
|
|
238
|
+
MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private func configureRemoteCommandsIfNeeded() {
|
|
243
|
+
guard !remoteCommandsConfigured else { return }
|
|
244
|
+
remoteCommandsConfigured = true
|
|
245
|
+
|
|
246
|
+
let commands = MPRemoteCommandCenter.shared()
|
|
247
|
+
commands.playCommand.isEnabled = false
|
|
248
|
+
commands.nextTrackCommand.isEnabled = false
|
|
249
|
+
commands.previousTrackCommand.isEnabled = false
|
|
250
|
+
commands.changePlaybackPositionCommand.isEnabled = false
|
|
251
|
+
|
|
252
|
+
commands.pauseCommand.isEnabled = true
|
|
253
|
+
commands.pauseCommand.addTarget { [weak self] _ in
|
|
254
|
+
self?.endTrackingFromRemoteCommand()
|
|
255
|
+
return .success
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
commands.stopCommand.isEnabled = true
|
|
259
|
+
commands.stopCommand.addTarget { [weak self] _ in
|
|
260
|
+
self?.endTrackingFromRemoteCommand()
|
|
261
|
+
return .success
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
commands.togglePlayPauseCommand.isEnabled = true
|
|
265
|
+
commands.togglePlayPauseCommand.addTarget { [weak self] _ in
|
|
266
|
+
self?.endTrackingFromRemoteCommand()
|
|
267
|
+
return .success
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
private func endTrackingFromRemoteCommand() {
|
|
272
|
+
DispatchQueue.main.async {
|
|
273
|
+
BGLocationManager.sharedInstance().stop()
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private func hasAudioBackgroundMode() -> Bool {
|
|
278
|
+
let modes = Bundle.main.object(forInfoDictionaryKey: "UIBackgroundModes") as? [String]
|
|
279
|
+
return modes?.contains("audio") == true
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
private func log(_ message: String) {
|
|
283
|
+
NSLog("[BGGEO][TrackingAudio] \(message)")
|
|
284
|
+
BGLog.sharedInstance().notify(message, debug: true)
|
|
285
|
+
}
|
|
286
|
+
}
|