Merge remote-tracking branch 'refs/remotes/origin/master'

This commit is contained in:
Selim Mustafaev 2020-07-28 10:34:12 +03:00
commit 510c6c736d
8 changed files with 238 additions and 143 deletions

View File

@ -80,6 +80,7 @@
7A96AE2F246B2BCD00297C33 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7A96AE2E246B2BCD00297C33 /* WebKit.framework */; };
7A96AE31246B2FE400297C33 /* Constants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A96AE30246B2FE400297C33 /* Constants.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 */; };
7AB67E8C2435C38700258F61 /* CustomTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7AB67E8B2435C38700258F61 /* CustomTextField.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; };
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>"; };
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>"; };
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>"; };
@ -285,6 +287,7 @@
7A333813249A532400D878F1 /* Filter.swift */,
7AB562B9249C9E9B00473D53 /* Region.swift */,
7A659B5824A2B1BA0043A0F2 /* AudioRecord.swift */,
7AAE6AD224CDDF950023860B /* VehicleEvent.swift */,
);
path = Models;
sourceTree = "<group>";
@ -526,6 +529,7 @@
7A43F9F8246C8A6200BA5B49 /* JWT.swift in Sources */,
7A6DD903242BF4A5009DE740 /* PlateView.swift in Sources */,
7A488C3F24A74B990054D0B2 /* RealmBindObserver.swift in Sources */,
7AAE6AD324CDDF950023860B /* VehicleEvent.swift in Sources */,
7A11470323FDE7E500B424AF /* SceneDelegate.swift in Sources */,
7A530B7E24017FEE00CBFE6E /* VehicleCell.swift in Sources */,
7A11474423FF06CA00B424AF /* Api.swift in Sources */,

View File

@ -47,5 +47,53 @@
</Locations>
</BreakpointContent>
</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>
</Bucket>

View File

@ -23,7 +23,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
let config = Realm.Configuration(
schemaVersion: 9,
schemaVersion: 10,
migrationBlock: { migration, oldSchemaVersion in
if oldSchemaVersion <= 3 {
var numbers: [String] = []

View File

@ -15,6 +15,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
var recorder: Recorder?
var addButton: UIBarButtonItem!
let bag = DisposeBag()
var recordDisposable: Disposable?
let validLetters = ["А", "В", "Е", "К", "М", "Н", "О", "Р", "С", "Т", "У", "Х"]
@ -26,7 +27,7 @@ class RecordsController: UIViewController, UITableViewDelegate {
self.addButton = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(onAddVoiceRecord(_:)))
self.navigationItem.rightBarButtonItem = self.addButton
self.recorder = try? Recorder()
self.recorder = Recorder()
let ds = RxTableViewSectionedAnimatedDataSource<DateSection<AudioRecord>>(configureCell: { dataSource, tableView, indexPath, item in
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)
LocationManager.requestCurrentLocation().subscribe().disposed(by: self.bag)
}
override func viewDidAppear(_ animated: Bool) {
@ -113,41 +112,44 @@ class RecordsController: UIViewController, UITableViewDelegate {
return
}
recorder.requestPermissions { error in
DispatchQueue.main.async {
if let error = error {
self.show(error: error)
} else {
do {
let alert = UIAlertController(title: "Recording...", message: nil, preferredStyle: .alert)
alert.addAction(UIAlertAction(title: "Cancel", style: .cancel, handler: { _ in self.recorder?.cancelRecording() }))
alert.addAction(UIAlertAction(title: "Done", style: .default, handler: { _ in self.recorder?.stopRecording() }))
self.present(alert, animated: true)
var alert: UIAlertController?
var url: URL!
let locationObservable = LocationManager.requestCurrentLocation()
.map(Optional.init)
.catchErrorJustReturn(nil)
let recordObservable: Single<String> = recorder.requestPermissions()
.observeOn(MainScheduler.instance)
.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 fileName = "recording-\(date.timeIntervalSince1970).m4a"
let url = try FileManager.default.url(for: fileName, in: "recordings")
try self.makeStartSoundIfNeeded {
try recorder.startRecording(to: url) { result in
url = try FileManager.default.url(for: fileName, in: "recordings")
return recorder.startRecording(to: url)
}
self.recordDisposable = Single.zip(locationObservable, recordObservable) { event, text -> AudioRecord in
let asset = AVURLAsset(url: url)
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()
try? realm?.write {
realm?.add(record)
}
alert.dismiss(animated: true)
print("New record saved to: \(url.path)")
}
}
self.donateUserActivity()
} catch {
alert?.dismiss(animated: true)
}) { error in
IHProgressHUD.showError(withStatus: error.localizedDescription)
}
}
}
}
}
// MARK: - Processing
@ -195,28 +197,18 @@ class RecordsController: UIViewController, UITableViewDelegate {
&& region! < 1000
}
func makeStartSoundIfNeeded(completion: @escaping () throws -> Void) throws {
func makeStartSoundIfNeeded() -> Single<Void> {
if !Settings.shared.recordBeep {
try completion()
return .just(())
} else {
//let session = AVAudioSession.sharedInstance()
//try session.setCategory(.playback, mode: .default, options: [.defaultToSpeaker])
//try session.setActive(true)
var err: Error?
return Single<Void>.create { observer in
var soundId = SystemSoundID()
let url = URL(fileURLWithPath: "/System/Library/Audio/UISounds/short_double_high.caf")
AudioServicesCreateSystemSoundID(url as CFURL, &soundId)
AudioServicesPlaySystemSoundWithCompletion(soundId) {
do {
//try session.setActive(false)
try completion()
} catch {
err = error
observer(.success(()))
}
}
if let error = err {
throw error
return Disposables.create()
}
}
}

View File

@ -9,6 +9,7 @@ class AudioRecord: Object, IdentifiableType {
@objc dynamic var rawText: String = ""
@objc dynamic var addedDate: TimeInterval = Date().timeIntervalSince1970
@objc dynamic var duration: TimeInterval = 0
@objc dynamic var event: VehicleEvent?
var identifier: TimeInterval = 0
var identity: TimeInterval {
@ -18,11 +19,12 @@ class AudioRecord: Object, IdentifiableType {
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.number = number
self.duration = duration
self.rawText = raw
self.event = event
}
required init() {

View 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()
}
}

View File

@ -3,14 +3,6 @@ import RxSwift
import RxCocoa
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 {
init(locationManager: ParentObject) {
@ -41,20 +33,30 @@ extension Reactive where Base: CLLocationManager {
let sel = #selector((CLLocationManagerDelegate.locationManager(_:didChangeAuthorization:)! as (CLLocationManagerDelegate) -> (CLLocationManager, CLAuthorizationStatus) -> Void))
let source: Observable<CLAuthorizationStatus> = delegate.methodInvoked(sel)
.map { arg in
let status = arg[1] as! CLAuthorizationStatus
return status
let status = CLAuthorizationStatus(rawValue: arg[1] as! Int32)
return status!
}
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: {
static let shared = LocationManager()
class LocationManager {
private static let manager = CLLocationManager()
private static let bag = DisposeBag()
private let manager = CLLocationManager()
private let bag = DisposeBag()
private func checkPermissions() -> Single<Void> {
private static func checkPermissions() -> Single<Void> {
return Single<Void>.create { observer in
switch CLLocationManager.authorizationStatus() {
case .authorizedWhenInUse:
@ -62,21 +64,31 @@ class LocationManager: {
break
case .notDetermined:
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)) })
default:
observer(.error(NSError(domain: "", code: 0, userInfo: [NSLocalizedDescriptionKey: "Location permission error"])))
break
}
return Disposables.create { }
return Disposables.create()
}
}
func requestCurrentLocation() -> Single<VehicleEvent> {
return self.checkPermissions().map {
return VehicleEvent()
private static func requestLocation() -> Single<VehicleEvent> {
return self.manager.rx.didUpdateLocations.take(1).asSingle().do(onSubscribed: {
DispatchQueue.main.async {
self.manager.requestLocation()
}
})
}
static func requestCurrentLocation() -> Single<VehicleEvent> {
return self.checkPermissions().flatMap(self.requestLocation)
}
}

View File

@ -2,6 +2,7 @@ import Foundation
import Speech
import AVFoundation
import AudioToolbox
import RxSwift
class Recorder {
@ -25,49 +26,57 @@ class Recorder {
init() {
}
func requestPermissions(completion: @escaping (NSError?) -> Void) {
func requestPermissions() -> Single<Void> {
return Single<Void>.create { observer in
AVAudioSession.sharedInstance().requestRecordPermission { allowed in
if allowed {
SFSpeechRecognizer.requestAuthorization { status in
switch status {
case .authorized:
completion(nil)
observer(.success(()))
break
case .denied:
let error = CocoaError.error("Access denied", suggestion: "Please give permission to use speech recognition in system settings")
completion(error)
observer(.error(error))
break
case .restricted:
let error = CocoaError.error("Access restricted", suggestion: "Speech recognition is restricted on this device")
completion(error)
observer(.error(error))
break
case .notDetermined:
let error = CocoaError.error("Access error", suggestion: "Speech recognition status is not yet determined")
completion(error)
observer(.error(error))
break
@unknown default:
let error = CocoaError.error("Access error", suggestion: "Unknown error accessing speech recognizer")
completion(error)
observer(.error(error))
break
}
}
} else {
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 {
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 {
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().setActive(true)
@ -76,7 +85,6 @@ class Recorder {
self.engine.inputNode.installTap(onBus: 0, bufferSize: 1024, format: inFormat) { buffer, time in
self.request.append(buffer)
//print(self.recognitionTask?.state.rawValue)
ExtAudioFileWrite(fileRef, buffer.frameLength, buffer.audioBufferList)
}
@ -86,18 +94,26 @@ class Recorder {
self.endRecognitionTimer?.invalidate()
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 2, repeats: false) { timer in
self.finishRecording()
completion(self.result)
observer(.success(self.result))
}
}
}
self.endRecognitionTimer = Timer.scheduledTimer(withTimeInterval: 5, repeats: false) { timer in
self.finishRecording()
completion(self.result)
observer(.success(self.result))
}
self.engine.prepare()
try self.engine.start()
} catch {
observer(.error(error))
}
return Disposables.create {
self.cancelRecording()
}
}
}
func cancelRecording() {