Merge remote-tracking branch 'refs/remotes/origin/master'
This commit is contained in:
commit
510c6c736d
@ -80,6 +80,7 @@
|
|||||||
7A96AE2F246B2BCD00297C33 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A96AE2E246B2BCD00297C33 /* WebKit.framework */; };
|
7A96AE2F246B2BCD00297C33 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A96AE2E246B2BCD00297C33 /* WebKit.framework */; };
|
||||||
7A96AE31246B2FE400297C33 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE30246B2FE400297C33 /* Constants.swift */; };
|
7A96AE31246B2FE400297C33 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE30246B2FE400297C33 /* Constants.swift */; };
|
||||||
7A96AE33246C095700297C33 /* Base64FS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE32246C095700297C33 /* Base64FS.swift */; };
|
7A96AE33246C095700297C33 /* Base64FS.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE32246C095700297C33 /* Base64FS.swift */; };
|
||||||
|
7AAE6AD324CDDF950023860B /* VehicleEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AAE6AD224CDDF950023860B /* VehicleEvent.swift */; };
|
||||||
7AB562BA249C9E9B00473D53 /* Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB562B9249C9E9B00473D53 /* Region.swift */; };
|
7AB562BA249C9E9B00473D53 /* Region.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB562B9249C9E9B00473D53 /* Region.swift */; };
|
||||||
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; };
|
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.swift */; };
|
||||||
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; };
|
7AB67E8E2435D1A000258F61 /* CustomButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8D2435D1A000258F61 /* CustomButton.swift */; };
|
||||||
@ -157,6 +158,7 @@
|
|||||||
7A96AE2E246B2BCD00297C33 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
|
7A96AE2E246B2BCD00297C33 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/MacOSX.platform/Developer/SDKs/MacOSX10.15.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; };
|
||||||
7A96AE30246B2FE400297C33 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
7A96AE30246B2FE400297C33 /* Constants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Constants.swift; sourceTree = "<group>"; };
|
||||||
7A96AE32246C095700297C33 /* Base64FS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64FS.swift; sourceTree = "<group>"; };
|
7A96AE32246C095700297C33 /* Base64FS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Base64FS.swift; sourceTree = "<group>"; };
|
||||||
|
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VehicleEvent.swift; sourceTree = "<group>"; };
|
||||||
7AB562B9249C9E9B00473D53 /* Region.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Region.swift; sourceTree = "<group>"; };
|
7AB562B9249C9E9B00473D53 /* Region.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Region.swift; sourceTree = "<group>"; };
|
||||||
7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
|
7AB67E8B2435C38700258F61 /* CustomTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomTextField.swift; sourceTree = "<group>"; };
|
||||||
7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; };
|
7AB67E8D2435D1A000258F61 /* CustomButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomButton.swift; sourceTree = "<group>"; };
|
||||||
@ -285,6 +287,7 @@
|
|||||||
7A333813249A532400D878F1 /* Filter.swift */,
|
7A333813249A532400D878F1 /* Filter.swift */,
|
||||||
7AB562B9249C9E9B00473D53 /* Region.swift */,
|
7AB562B9249C9E9B00473D53 /* Region.swift */,
|
||||||
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */,
|
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */,
|
||||||
|
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */,
|
||||||
);
|
);
|
||||||
path = Models;
|
path = Models;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -526,6 +529,7 @@
|
|||||||
7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */,
|
7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */,
|
||||||
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */,
|
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */,
|
||||||
7A488C3F24A74B990054D0B2 /* RealmBindObserver.swift in Sources */,
|
7A488C3F24A74B990054D0B2 /* RealmBindObserver.swift in Sources */,
|
||||||
|
7AAE6AD324CDDF950023860B /* VehicleEvent.swift in Sources */,
|
||||||
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */,
|
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */,
|
||||||
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */,
|
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */,
|
||||||
7A11474423FF06CA00B424AF /* Api.swift in Sources */,
|
7A11474423FF06CA00B424AF /* Api.swift in Sources */,
|
||||||
|
|||||||
@ -47,5 +47,53 @@
|
|||||||
</Locations>
|
</Locations>
|
||||||
</BreakpointContent>
|
</BreakpointContent>
|
||||||
</BreakpointProxy>
|
</BreakpointProxy>
|
||||||
|
<BreakpointProxy
|
||||||
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
|
<BreakpointContent
|
||||||
|
uuid = "0F1972A7-94E8-40E5-834C-B873D2578DC7"
|
||||||
|
shouldBeEnabled = "Yes"
|
||||||
|
ignoreCount = "0"
|
||||||
|
continueAfterRunningActions = "No"
|
||||||
|
filePath = "AutoCat/Controllers/RecordsController.swift"
|
||||||
|
startingColumnNumber = "9223372036854775807"
|
||||||
|
endingColumnNumber = "9223372036854775807"
|
||||||
|
startingLineNumber = "150"
|
||||||
|
endingLineNumber = "150"
|
||||||
|
landmarkName = "onAddVoiceRecord(_:)"
|
||||||
|
landmarkType = "7">
|
||||||
|
</BreakpointContent>
|
||||||
|
</BreakpointProxy>
|
||||||
|
<BreakpointProxy
|
||||||
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
|
<BreakpointContent
|
||||||
|
uuid = "79DDC3DB-613E-40E9-AD33-AEBAA5775C62"
|
||||||
|
shouldBeEnabled = "Yes"
|
||||||
|
ignoreCount = "0"
|
||||||
|
continueAfterRunningActions = "No"
|
||||||
|
filePath = "AutoCat/Controllers/RecordsController.swift"
|
||||||
|
startingColumnNumber = "9223372036854775807"
|
||||||
|
endingColumnNumber = "9223372036854775807"
|
||||||
|
startingLineNumber = "144"
|
||||||
|
endingLineNumber = "144"
|
||||||
|
landmarkName = "onAddVoiceRecord(_:)"
|
||||||
|
landmarkType = "7">
|
||||||
|
</BreakpointContent>
|
||||||
|
</BreakpointProxy>
|
||||||
|
<BreakpointProxy
|
||||||
|
BreakpointExtensionID = "Xcode.Breakpoint.FileBreakpoint">
|
||||||
|
<BreakpointContent
|
||||||
|
uuid = "DBCA971D-D424-4108-BA32-882EEF44B5A8"
|
||||||
|
shouldBeEnabled = "Yes"
|
||||||
|
ignoreCount = "0"
|
||||||
|
continueAfterRunningActions = "No"
|
||||||
|
filePath = "AutoCat/Utils/Location.swift"
|
||||||
|
startingColumnNumber = "9223372036854775807"
|
||||||
|
endingColumnNumber = "9223372036854775807"
|
||||||
|
startingLineNumber = "86"
|
||||||
|
endingLineNumber = "86"
|
||||||
|
landmarkName = "requestLocation()"
|
||||||
|
landmarkType = "7">
|
||||||
|
</BreakpointContent>
|
||||||
|
</BreakpointProxy>
|
||||||
</Breakpoints>
|
</Breakpoints>
|
||||||
</Bucket>
|
</Bucket>
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||||
|
|
||||||
let config = Realm.Configuration(
|
let config = Realm.Configuration(
|
||||||
schemaVersion: 9,
|
schemaVersion: 10,
|
||||||
migrationBlock: { migration, oldSchemaVersion in
|
migrationBlock: { migration, oldSchemaVersion in
|
||||||
if oldSchemaVersion <= 3 {
|
if oldSchemaVersion <= 3 {
|
||||||
var numbers: [String] = []
|
var numbers: [String] = []
|
||||||
|
|||||||
@ -15,6 +15,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
var recorder: Recorder?
|
var recorder: Recorder?
|
||||||
var addButton: UIBarButtonItem!
|
var addButton: UIBarButtonItem!
|
||||||
let bag = DisposeBag()
|
let bag = DisposeBag()
|
||||||
|
var recordDisposable: Disposable?
|
||||||
|
|
||||||
let validLetters = ["А", "В", "Е", "К", "М", "Н", "О", "Р", "С", "Т", "У", "Х"]
|
let validLetters = ["А", "В", "Е", "К", "М", "Н", "О", "Р", "С", "Т", "У", "Х"]
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
self.addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(onAddVoiceRecord(_:)))
|
self.addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(onAddVoiceRecord(_:)))
|
||||||
self.navigationItem.rightBarButtonItem = self.addButton
|
self.navigationItem.rightBarButtonItem = self.addButton
|
||||||
|
|
||||||
self.recorder = try? Recorder()
|
self.recorder = Recorder()
|
||||||
|
|
||||||
let ds = RxTableViewSectionedAnimatedDataSource<DateSection<AudioRecord>>(configureCell: { dataSource, tableView, indexPath, item in
|
let ds = RxTableViewSectionedAnimatedDataSource<DateSection<AudioRecord>>(configureCell: { dataSource, tableView, indexPath, item in
|
||||||
if let cell = tableView.dequeueReusableCell(withIdentifier: "AudioRecordCell", for: indexPath) as? AudioRecordCell {
|
if let cell = tableView.dequeueReusableCell(withIdentifier: "AudioRecordCell", for: indexPath) as? AudioRecordCell {
|
||||||
@ -59,8 +60,6 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.tableView.rx.setDelegate(self).disposed(by: self.bag)
|
self.tableView.rx.setDelegate(self).disposed(by: self.bag)
|
||||||
|
|
||||||
LocationManager.requestCurrentLocation().subscribe().disposed(by: self.bag)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewDidAppear(_ animated: Bool) {
|
override func viewDidAppear(_ animated: Bool) {
|
||||||
@ -113,41 +112,44 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
recorder.requestPermissions { error in
|
var alert: UIAlertController?
|
||||||
DispatchQueue.main.async {
|
var url: URL!
|
||||||
if let error = error {
|
|
||||||
self.show(error: error)
|
let locationObservable = LocationManager.requestCurrentLocation()
|
||||||
} else {
|
.map(Optional.init)
|
||||||
do {
|
.catchErrorJustReturn(nil)
|
||||||
let alert = UIAlertController(title: "Recording...", message: nil, preferredStyle: .alert)
|
|
||||||
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in self.recorder?.cancelRecording() }))
|
let recordObservable: Single<String> = recorder.requestPermissions()
|
||||||
alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in self.recorder?.stopRecording() }))
|
.observeOn(MainScheduler.instance)
|
||||||
self.present(alert, animated: true)
|
.flatMap(self.makeStartSoundIfNeeded)
|
||||||
|
.flatMap {
|
||||||
|
alert = UIAlertController(title: "Recording...", message: nil, preferredStyle: .alert)
|
||||||
|
alert!.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in self.recordDisposable?.dispose() }))
|
||||||
|
alert!.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in self.recorder?.stopRecording() }))
|
||||||
|
self.present(alert!, animated: true)
|
||||||
|
|
||||||
let date = Date()
|
let date = Date()
|
||||||
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
|
let fileName = "recording-\(date.timeIntervalSince1970).m4a"
|
||||||
let url = try FileManager.default.url(for: fileName, in: "recordings")
|
url = try FileManager.default.url(for: fileName, in: "recordings")
|
||||||
try self.makeStartSoundIfNeeded {
|
|
||||||
try recorder.startRecording(to: url) { result in
|
return recorder.startRecording(to: url)
|
||||||
|
}
|
||||||
|
|
||||||
|
self.recordDisposable = Single.zip(locationObservable, recordObservable) { event, text -> AudioRecord in
|
||||||
let asset = AVURLAsset(url: url)
|
let asset = AVURLAsset(url: url)
|
||||||
let duration = TimeInterval(CMTimeGetSeconds(asset.duration))
|
let duration = TimeInterval(CMTimeGetSeconds(asset.duration))
|
||||||
let record = AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: result), raw: result, duration: duration)
|
return AudioRecord(path: url.lastPathComponent, number: self.getPlateNumber(from: text), raw: text, duration: duration, event: event)
|
||||||
|
}
|
||||||
|
.subscribe(onSuccess: { record in
|
||||||
let realm = try? Realm()
|
let realm = try? Realm()
|
||||||
try? realm?.write {
|
try? realm?.write {
|
||||||
realm?.add(record)
|
realm?.add(record)
|
||||||
}
|
}
|
||||||
alert.dismiss(animated: true)
|
alert?.dismiss(animated: true)
|
||||||
print("New record saved to: \(url.path)")
|
}) { error in
|
||||||
}
|
|
||||||
}
|
|
||||||
self.donateUserActivity()
|
|
||||||
} catch {
|
|
||||||
IHProgressHUD.showError(withStatus: error.localizedDescription)
|
IHProgressHUD.showError(withStatus: error.localizedDescription)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Processing
|
// MARK: - Processing
|
||||||
|
|
||||||
@ -195,28 +197,18 @@ class RecordsController: UIViewController, UITableViewDelegate {
|
|||||||
&& region! < 1000
|
&& region! < 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeStartSoundIfNeeded(completion: @escaping () throws -> Void) throws {
|
func makeStartSoundIfNeeded() -> Single<Void> {
|
||||||
if !Settings.shared.recordBeep {
|
if !Settings.shared.recordBeep {
|
||||||
try completion()
|
return .just(())
|
||||||
} else {
|
} else {
|
||||||
//let session = AVAudioSession.sharedInstance()
|
return Single<Void>.create { observer in
|
||||||
//try session.setCategory(.playback, mode: .default, options: [.defaultToSpeaker])
|
|
||||||
//try session.setActive(true)
|
|
||||||
var err: Error?
|
|
||||||
var soundId = SystemSoundID()
|
var soundId = SystemSoundID()
|
||||||
let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/short_double_high.caf")
|
let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/short_double_high.caf")
|
||||||
AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
|
AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
|
||||||
AudioServicesPlaySystemSoundWithCompletion(soundId) {
|
AudioServicesPlaySystemSoundWithCompletion(soundId) {
|
||||||
do {
|
observer(.success(()))
|
||||||
//try session.setActive(false)
|
|
||||||
try completion()
|
|
||||||
} catch {
|
|
||||||
err = error
|
|
||||||
}
|
}
|
||||||
}
|
return Disposables.create()
|
||||||
|
|
||||||
if let error = err {
|
|
||||||
throw error
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,6 +9,7 @@ class AudioRecord: Object, IdentifiableType {
|
|||||||
@objc dynamic var rawText: String = ""
|
@objc dynamic var rawText: String = ""
|
||||||
@objc dynamic var addedDate: TimeInterval = Date().timeIntervalSince1970
|
@objc dynamic var addedDate: TimeInterval = Date().timeIntervalSince1970
|
||||||
@objc dynamic var duration: TimeInterval = 0
|
@objc dynamic var duration: TimeInterval = 0
|
||||||
|
@objc dynamic var event: VehicleEvent?
|
||||||
|
|
||||||
var identifier: TimeInterval = 0
|
var identifier: TimeInterval = 0
|
||||||
var identity: TimeInterval {
|
var identity: TimeInterval {
|
||||||
@ -18,11 +19,12 @@ class AudioRecord: Object, IdentifiableType {
|
|||||||
return self.identifier
|
return self.identifier
|
||||||
}
|
}
|
||||||
|
|
||||||
init(path: String, number: String?, raw: String, duration: TimeInterval) {
|
init(path: String, number: String?, raw: String, duration: TimeInterval, event: VehicleEvent?) {
|
||||||
self.path = path
|
self.path = path
|
||||||
self.number = number
|
self.number = number
|
||||||
self.duration = duration
|
self.duration = duration
|
||||||
self.rawText = raw
|
self.rawText = raw
|
||||||
|
self.event = event
|
||||||
}
|
}
|
||||||
|
|
||||||
required init() {
|
required init() {
|
||||||
|
|||||||
21
AutoCat/Models/VehicleEvent.swift
Normal file
21
AutoCat/Models/VehicleEvent.swift
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
import Foundation
|
||||||
|
import RealmSwift
|
||||||
|
|
||||||
|
class VehicleEvent: Object {
|
||||||
|
@objc dynamic var date: Date = Date()
|
||||||
|
@objc dynamic var latitude: Double = 0
|
||||||
|
@objc dynamic var longitude: Double = 0
|
||||||
|
@objc dynamic var speed: Double = 0
|
||||||
|
@objc dynamic var direction: Double = 0
|
||||||
|
|
||||||
|
init(lat: Double, lon: Double, speed: Double, dir: Double) {
|
||||||
|
self.latitude = lat
|
||||||
|
self.longitude = lon
|
||||||
|
self.speed = speed
|
||||||
|
self.direction = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
required init() {
|
||||||
|
super.init()
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -3,14 +3,6 @@ import RxSwift
|
|||||||
import RxCocoa
|
import RxCocoa
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
|
||||||
struct VehicleEvent {
|
|
||||||
var date: Date
|
|
||||||
var latitude: Double
|
|
||||||
var longitude: Double
|
|
||||||
var speed: Double
|
|
||||||
var direction: Double
|
|
||||||
}
|
|
||||||
|
|
||||||
class RxLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate {
|
class RxLocationManagerDelegateProxy: DelegateProxy<CLLocationManager, CLLocationManagerDelegate>, DelegateProxyType, CLLocationManagerDelegate {
|
||||||
|
|
||||||
init(locationManager: ParentObject) {
|
init(locationManager: ParentObject) {
|
||||||
@ -41,20 +33,30 @@ extension Reactive where Base: CLLocationManager {
|
|||||||
let sel = #selector((CLLocationManagerDelegate.locationManager(_:didChangeAuthorization:)! as (CLLocationManagerDelegate) -> (CLLocationManager, CLAuthorizationStatus) -> Void))
|
let sel = #selector((CLLocationManagerDelegate.locationManager(_:didChangeAuthorization:)! as (CLLocationManagerDelegate) -> (CLLocationManager, CLAuthorizationStatus) -> Void))
|
||||||
let source: Observable<CLAuthorizationStatus> = delegate.methodInvoked(sel)
|
let source: Observable<CLAuthorizationStatus> = delegate.methodInvoked(sel)
|
||||||
.map { arg in
|
.map { arg in
|
||||||
let status = arg[1] as! CLAuthorizationStatus
|
let status = CLAuthorizationStatus(rawValue: arg[1] as! Int32)
|
||||||
return status
|
return status!
|
||||||
}
|
}
|
||||||
return ControlEvent(events: source)
|
return ControlEvent(events: source)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var didUpdateLocations: Observable<VehicleEvent> {
|
||||||
|
let sel = #selector((CLLocationManagerDelegate.locationManager(_:didUpdateLocations:)! as (CLLocationManagerDelegate) -> (CLLocationManager, [CLLocation]) -> Void))
|
||||||
|
return delegate.methodInvoked(sel)
|
||||||
|
.map { args in
|
||||||
|
if let locations = args[1] as? [CLLocation], let location = locations.first {
|
||||||
|
return VehicleEvent(lat: location.coordinate.latitude, lon: location.coordinate.longitude, speed: location.speed, dir: location.course)
|
||||||
|
} else {
|
||||||
|
throw NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Update location error"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class LocationManager: {
|
class LocationManager {
|
||||||
static let shared = LocationManager()
|
private static let manager = CLLocationManager()
|
||||||
|
private static let bag = DisposeBag()
|
||||||
|
|
||||||
private let manager = CLLocationManager()
|
private static func checkPermissions() -> Single<Void> {
|
||||||
private let bag = DisposeBag()
|
|
||||||
|
|
||||||
private func checkPermissions() -> Single<Void> {
|
|
||||||
return Single<Void>.create { observer in
|
return Single<Void>.create { observer in
|
||||||
switch CLLocationManager.authorizationStatus() {
|
switch CLLocationManager.authorizationStatus() {
|
||||||
case .authorizedWhenInUse:
|
case .authorizedWhenInUse:
|
||||||
@ -62,21 +64,31 @@ class LocationManager: {
|
|||||||
break
|
break
|
||||||
case .notDetermined:
|
case .notDetermined:
|
||||||
self.manager.requestWhenInUseAuthorization()
|
self.manager.requestWhenInUseAuthorization()
|
||||||
_ = self.manager.rx.didChangeAuthorization.first().subscribe(onSuccess: { status in
|
_ = self.manager.rx.didChangeAuthorization.skip(1).first().subscribe(onSuccess: { result in
|
||||||
|
if let status = result, status == .authorizedWhenInUse {
|
||||||
|
observer(.success(()))
|
||||||
|
} else {
|
||||||
|
observer(.error(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Location permission error"])))
|
||||||
|
}
|
||||||
}, onError: { observer(.error($0)) })
|
}, onError: { observer(.error($0)) })
|
||||||
default:
|
default:
|
||||||
observer(.error(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Location permission error"])))
|
observer(.error(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Location permission error"])))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return Disposables.create { }
|
return Disposables.create()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestCurrentLocation() -> Single<VehicleEvent> {
|
private static func requestLocation() -> Single<VehicleEvent> {
|
||||||
return self.checkPermissions().map {
|
return self.manager.rx.didUpdateLocations.take(1).asSingle().do(onSubscribed: {
|
||||||
return VehicleEvent()
|
DispatchQueue.main.async {
|
||||||
}
|
self.manager.requestLocation()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static func requestCurrentLocation() -> Single<VehicleEvent> {
|
||||||
|
return self.checkPermissions().flatMap(self.requestLocation)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import Foundation
|
|||||||
import Speech
|
import Speech
|
||||||
import AVFoundation
|
import AVFoundation
|
||||||
import AudioToolbox
|
import AudioToolbox
|
||||||
|
import RxSwift
|
||||||
|
|
||||||
class Recorder {
|
class Recorder {
|
||||||
|
|
||||||
@ -25,49 +26,57 @@ class Recorder {
|
|||||||
init() {
|
init() {
|
||||||
}
|
}
|
||||||
|
|
||||||
func requestPermissions(completion: @escaping (NSError?) -> Void) {
|
func requestPermissions() -> Single<Void> {
|
||||||
|
return Single<Void>.create { observer in
|
||||||
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
|
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
|
||||||
if allowed {
|
if allowed {
|
||||||
SFSpeechRecognizer.requestAuthorization { status in
|
SFSpeechRecognizer.requestAuthorization { status in
|
||||||
switch status {
|
switch status {
|
||||||
case .authorized:
|
case .authorized:
|
||||||
completion(nil)
|
observer(.success(()))
|
||||||
break
|
break
|
||||||
case .denied:
|
case .denied:
|
||||||
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use speech recognition in system settings")
|
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use speech recognition in system settings")
|
||||||
completion(error)
|
observer(.error(error))
|
||||||
break
|
break
|
||||||
case .restricted:
|
case .restricted:
|
||||||
let error = CocoaError.error("Access restricted", suggestion: "Speech recognition is restricted on this device")
|
let error = CocoaError.error("Access restricted", suggestion: "Speech recognition is restricted on this device")
|
||||||
completion(error)
|
observer(.error(error))
|
||||||
break
|
break
|
||||||
case .notDetermined:
|
case .notDetermined:
|
||||||
let error = CocoaError.error("Access error", suggestion: "Speech recognition status is not yet determined")
|
let error = CocoaError.error("Access error", suggestion: "Speech recognition status is not yet determined")
|
||||||
completion(error)
|
observer(.error(error))
|
||||||
break
|
break
|
||||||
@unknown default:
|
@unknown default:
|
||||||
let error = CocoaError.error("Access error", suggestion: "Unknown error accessing speech recognizer")
|
let error = CocoaError.error("Access error", suggestion: "Unknown error accessing speech recognizer")
|
||||||
completion(error)
|
observer(.error(error))
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use microphone in system settings")
|
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use microphone in system settings")
|
||||||
completion(error)
|
observer(.error(error))
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startRecording(to file: URL, completion: @escaping (String) -> Void) throws {
|
return Disposables.create()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func startRecording(to file: URL) -> Single<String> {
|
||||||
|
return Single<String>.create { observer in
|
||||||
guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
|
guard let aac = AVAudioFormat(settings: self.recordingSettings) else {
|
||||||
throw CocoaError.error("Recording error", suggestion: "Format not supported")
|
observer(.error(CocoaError.error("Recording error", suggestion: "Format not supported")))
|
||||||
|
return Disposables.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &fileRef)
|
ExtAudioFileCreateWithURL(file as CFURL, kAudioFileM4AType, aac.streamDescription, nil, AudioFileFlags.eraseFile.rawValue, &self.fileRef)
|
||||||
guard let fileRef = self.fileRef else {
|
guard let fileRef = self.fileRef else {
|
||||||
throw CocoaError.error(CocoaError.Code.fileWriteUnknown)
|
observer(.error(CocoaError.error(CocoaError.Code.fileWriteUnknown)))
|
||||||
|
return Disposables.create()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
do {
|
||||||
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [])
|
try AVAudioSession.sharedInstance().setCategory(.playAndRecord, mode: .default, options: [])
|
||||||
try AVAudioSession.sharedInstance().setActive(true)
|
try AVAudioSession.sharedInstance().setActive(true)
|
||||||
|
|
||||||
@ -76,7 +85,6 @@ class Recorder {
|
|||||||
|
|
||||||
self.engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: inFormat) { buffer, time in
|
self.engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: inFormat) { buffer, time in
|
||||||
self.request.append(buffer)
|
self.request.append(buffer)
|
||||||
//print(self.recognitionTask?.state.rawValue)
|
|
||||||
ExtAudioFileWrite(fileRef, buffer.frameLength, buffer.audioBufferList)
|
ExtAudioFileWrite(fileRef, buffer.frameLength, buffer.audioBufferList)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,18 +94,26 @@ class Recorder {
|
|||||||
self.endRecognitionTimer?.invalidate()
|
self.endRecognitionTimer?.invalidate()
|
||||||
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { timer in
|
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { timer in
|
||||||
self.finishRecording()
|
self.finishRecording()
|
||||||
completion(self.result)
|
observer(.success(self.result))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
|
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
|
||||||
self.finishRecording()
|
self.finishRecording()
|
||||||
completion(self.result)
|
observer(.success(self.result))
|
||||||
}
|
}
|
||||||
|
|
||||||
self.engine.prepare()
|
self.engine.prepare()
|
||||||
try self.engine.start()
|
try self.engine.start()
|
||||||
|
} catch {
|
||||||
|
observer(.error(error))
|
||||||
|
}
|
||||||
|
|
||||||
|
return Disposables.create {
|
||||||
|
self.cancelRecording()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func cancelRecording() {
|
func cancelRecording() {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user