diff --git a/.gitignore b/.gitignore index 16931720..965c5408 100644 --- a/.gitignore +++ b/.gitignore @@ -65,7 +65,9 @@ fastlane/test_output # SPM Generated /*.xcodeproj +.swiftpm/* # R.swift *.generated.swift iOS/rswift + diff --git a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/.swiftpm/xcode/package.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/CoreLock.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/CoreLock.xcscheme deleted file mode 100644 index e4570b78..00000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/CoreLock.xcscheme +++ /dev/null @@ -1,67 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Lock-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Lock-Package.xcscheme deleted file mode 100644 index f52c10bf..00000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/Lock-Package.xcscheme +++ /dev/null @@ -1,152 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/lockd.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/lockd.xcscheme deleted file mode 100644 index 25193a40..00000000 --- a/.swiftpm/xcode/xcshareddata/xcschemes/lockd.xcscheme +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Package.resolved b/Package.resolved index 4c1abafb..786fb0f4 100644 --- a/Package.resolved +++ b/Package.resolved @@ -1,6 +1,42 @@ { "object": { "pins": [ + { + "package": "BigInt", + "repositoryURL": "https://github.com/Boilertalk/BigInt.swift.git", + "state": { + "branch": null, + "revision": "fd9b2ebff53e7fd211a7d2b2ea682db300a8571b", + "version": "1.0.0" + } + }, + { + "package": "Cryptor", + "repositoryURL": "https://github.com/IBM-Swift/BlueCryptor.git", + "state": { + "branch": null, + "revision": "12d2bf3ec7207ec3cd004b9582f69ef5fae1da3b", + "version": "1.0.32" + } + }, + { + "package": "Socket", + "repositoryURL": "https://github.com/IBM-Swift/BlueSocket.git", + "state": { + "branch": null, + "revision": "f82e1401f04f62b2e8ce4670456d104466957810", + "version": "1.0.50" + } + }, + { + "package": "SSLService", + "repositoryURL": "https://github.com/IBM-Swift/BlueSSLService.git", + "state": { + "branch": null, + "revision": "d6616def31a12088034f0a63ed4646772eff5bce", + "version": "1.0.50" + } + }, { "package": "Bluetooth", "repositoryURL": "https://github.com/PureSwift/Bluetooth.git", @@ -28,15 +64,42 @@ "version": null } }, + { + "package": "Bonjour", + "repositoryURL": "https://github.com/PureSwift/Bonjour.git", + "state": { + "branch": "master", + "revision": "0907af223d72b1a4be14b805a8b5edd586f28721", + "version": null + } + }, + { + "package": "Cdns_sd", + "repositoryURL": "https://github.com/Bouke/Cdns_sd.git", + "state": { + "branch": null, + "revision": "b6dce342895400d0cd86726ad0fcdcad7abc6137", + "version": "2.0.0" + } + }, { "package": "CryptoSwift", "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift", "state": { "branch": "master", - "revision": "4ccfe9034a07af4b153657ba1cdac702ffcf8f2f", + "revision": "7c94c0bc9d222f9c88d9f6ed8f71926f372d30a8", "version": null } }, + { + "package": "Evergreen", + "repositoryURL": "https://github.com/Bouke/Evergreen.git", + "state": { + "branch": null, + "revision": "4d9d17d6efca12398de2b0a4c9c5a4eacc556e03", + "version": "2.0.0" + } + }, { "package": "GATT", "repositoryURL": "https://github.com/PureSwift/GATT.git", @@ -46,6 +109,123 @@ "version": null } }, + { + "package": "HAP", + "repositoryURL": "https://github.com/Bouke/HAP.git", + "state": { + "branch": "master", + "revision": "4104c73e94f9a9657ed5bcb660fdd7e02641cca4", + "version": null + } + }, + { + "package": "HKDF", + "repositoryURL": "https://github.com/Bouke/HKDF.git", + "state": { + "branch": null, + "revision": "9b9e521b49e5b529c33f4abe35a00e144af51e00", + "version": "3.1.0" + } + }, + { + "package": "Kitura", + "repositoryURL": "https://github.com/IBM-Swift/Kitura.git", + "state": { + "branch": null, + "revision": "0777165ed5b966ad2e62f9f9101cfa8b283e7684", + "version": "2.8.1" + } + }, + { + "package": "Kitura-net", + "repositoryURL": "https://github.com/IBM-Swift/Kitura-net.git", + "state": { + "branch": null, + "revision": "00985729329b73f3e20e40ec43071e4c294dfea3", + "version": "2.4.0" + } + }, + { + "package": "Kitura-TemplateEngine", + "repositoryURL": "https://github.com/IBM-Swift/Kitura-TemplateEngine.git", + "state": { + "branch": null, + "revision": "d62d74bca48c6fb76f9fc1f48eeb2d7af415e80b", + "version": "2.0.1" + } + }, + { + "package": "KituraContracts", + "repositoryURL": "https://github.com/IBM-Swift/KituraContracts.git", + "state": { + "branch": null, + "revision": "a30e2fb79e926672776a05ec6b919c239870a221", + "version": "1.2.1" + } + }, + { + "package": "LoggerAPI", + "repositoryURL": "https://github.com/IBM-Swift/LoggerAPI.git", + "state": { + "branch": null, + "revision": "3357dd9526cdf9436fa63bb792b669e6efdc43da", + "version": "1.9.0" + } + }, + { + "package": "NetService", + "repositoryURL": "https://github.com/Bouke/NetService.git", + "state": { + "branch": null, + "revision": "ade5b7bac667f91da62dfbbdb918aa748ce06bc0", + "version": "0.7.0" + } + }, + { + "package": "Regex", + "repositoryURL": "https://github.com/crossroadlabs/Regex.git", + "state": { + "branch": null, + "revision": "166728756082a9cac6e4aed3ebbce8e41cb3a945", + "version": "1.2.0" + } + }, + { + "package": "SRP", + "repositoryURL": "https://github.com/Bouke/SRP.git", + "state": { + "branch": null, + "revision": "9f3d77de5dbaac52d30b1d51d75e853fabb8fa8b", + "version": "3.1.0" + } + }, + { + "package": "swift-log", + "repositoryURL": "https://github.com/apple/swift-log.git", + "state": { + "branch": null, + "revision": "e8aabbe95db22e064ad42f1a4a9f8982664c70ed", + "version": "1.1.1" + } + }, + { + "package": "swift-nio", + "repositoryURL": "https://github.com/apple/swift-nio.git", + "state": { + "branch": null, + "revision": "ba7970fe396e8198b84c6c1b44b38a1d4e2eb6bd", + "version": "1.14.1" + } + }, + { + "package": "swift-nio-zlib-support", + "repositoryURL": "https://github.com/apple/swift-nio-zlib-support.git", + "state": { + "branch": null, + "revision": "37760e9a52030bb9011972c5213c3350fa9d41fd", + "version": "1.0.0" + } + }, { "package": "SwiftyGPIO", "repositoryURL": "https://github.com/uraimo/SwiftyGPIO.git", @@ -63,6 +243,15 @@ "revision": "8c41d0fd48f6fc00c429aad025c114a941aad764", "version": null } + }, + { + "package": "TypeDecoder", + "repositoryURL": "https://github.com/IBM-Swift/TypeDecoder.git", + "state": { + "branch": null, + "revision": "a5978582981f7151594bdaf5fe0d3cd9b1d2d0c0", + "version": "1.3.4" + } } ] }, diff --git a/Package.swift b/Package.swift index b1beedd6..18bed1a7 100644 --- a/Package.swift +++ b/Package.swift @@ -45,6 +45,22 @@ let package = Package( .package( url: "https://github.com/PureSwift/BluetoothDarwin.git", .branch("master") + ), + .package( + url: "https://github.com/IBM-Swift/Kitura.git", + from: "2.8.1" + ), + .package( + url: "https://github.com/Bouke/HAP.git", + .branch("master") + ), + .package( + url: "https://github.com/Bouke/NetService.git", + from: "0.7.0" + ), + .package( + url: "https://github.com/PureSwift/Bonjour.git", + .branch("master") ) ], targets: [ @@ -54,7 +70,9 @@ let package = Package( nativeBluetooth, nativeGATT, "CoreLockGATTServer", - "SwiftyGPIO" + "SwiftyGPIO", + "HAP", + "CoreLockWebServer" ] ), .target( @@ -62,13 +80,21 @@ let package = Package( dependencies: [ nativeGATT, "TLVCoding", - "CryptoSwift" + "CryptoSwift", + "Bonjour" ] ), .target( name: "CoreLockGATTServer", dependencies: ["CoreLock"] ), + .target( + name: "CoreLockWebServer", + dependencies: [ + "CoreLock", + "Kitura" + ] + ), .testTarget( name: "CoreLockTests", dependencies: ["CoreLock"] @@ -79,3 +105,7 @@ let package = Package( ) ] ) + +#if os(Linux) +package.targets.first(where: { $0.name == "CoreLockWebServer" })?.dependencies.append("NetService") +#endif diff --git a/Sources/CoreLock/Authentication.swift b/Sources/CoreLock/Authentication.swift index a030cb67..9cd76826 100644 --- a/Sources/CoreLock/Authentication.swift +++ b/Sources/CoreLock/Authentication.swift @@ -21,7 +21,6 @@ public struct Authentication: Equatable, Codable { } public func isAuthenticated(with key: KeyData) -> Bool { - return signedData.isAuthenticated(with: key, message: message) } } diff --git a/Sources/CoreLock/CreateNewKeyRequest.swift b/Sources/CoreLock/CreateNewKeyRequest.swift new file mode 100644 index 00000000..cf7ba36e --- /dev/null +++ b/Sources/CoreLock/CreateNewKeyRequest.swift @@ -0,0 +1,91 @@ +// +// CreateNewKeyRequest.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/16/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct CreateNewKeyNetServiceRequest: Equatable { + + /// Lock server + public let server: URL + + /// Authorization header + public let authorization: LockNetService.Authorization + + /// Encrypted request + public let encryptedData: LockNetService.EncryptedData +} + +// MARK: - URL Request + +public extension CreateNewKeyNetServiceRequest { + + func urlRequest(encoder: JSONEncoder = JSONEncoder()) -> URLRequest { + + // http://localhost:8080/keys + let url = server.appendingPathComponent("key") + var urlRequest = URLRequest(url: url) + urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField) + urlRequest.httpMethod = "POST" + urlRequest.httpBody = try! encoder.encode(encryptedData) + return urlRequest + } +} + +// MARK: - Encryption + +public extension CreateNewKeyNetServiceRequest { + + init(server: URL, + encrypt value: CreateNewKeyRequest, + with key: KeyCredentials, + encoder: JSONEncoder = JSONEncoder()) throws { + + self.server = server + self.authorization = LockNetService.Authorization(key: key) + let data = try encoder.encode(value) + self.encryptedData = try .init(encrypt: data, with: key.secret) + } + + static func decrypt(_ encryptedData: LockNetService.EncryptedData, + with key: KeyData, + decoder: JSONDecoder = JSONDecoder()) throws -> CreateNewKeyRequest { + + let jsonData = try encryptedData.decrypt(with: key) + return try decoder.decode(CreateNewKeyRequest.self, from: jsonData) + } +} + +// MARK: - Client + +public extension LockNetService.Client { + + /// Create new key. + func createKey(_ newKey: CreateNewKeyRequest, + for server: LockNetService, + with key: KeyCredentials, + timeout: TimeInterval = LockNetService.defaultTimeout) throws { + + log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier) for \(server.url.absoluteString)") + + let request = try CreateNewKeyNetServiceRequest( + server: server.url, + encrypt: newKey, + with: key, + encoder: jsonEncoder + ).urlRequest(encoder: jsonEncoder) + + let (httpResponse, _) = try urlSession.synchronousDataTask(with: request) + + guard httpResponse.statusCode == 201 + else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } + } +} diff --git a/Sources/CoreLock/DeleteKeyRequest.swift b/Sources/CoreLock/DeleteKeyRequest.swift new file mode 100644 index 00000000..8f60aff0 --- /dev/null +++ b/Sources/CoreLock/DeleteKeyRequest.swift @@ -0,0 +1,73 @@ +// +// DeleteKeyRequest.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/19/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// Lock Software Update HTTP Request +public struct DeleteKeyRequest: Equatable { + + /// Lock server + public let server: URL + + /// Authorization header + public let authorization: LockNetService.Authorization + + /// Key to delete + public let key: UUID + + /// Type of Key to delete + public let type: KeyType +} + +// MARK: - URL Request + +public extension DeleteKeyRequest { + + func urlRequest() -> URLRequest { + + // http://localhost:8080/key/A6E3EC9B-FD6E-4A50-B459-51CFDA2A21DD + let url = server + .appendingPathComponent(type.stringValue) + .appendingPathComponent(key.uuidString) + var urlRequest = URLRequest(url: url) + urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField) + urlRequest.httpMethod = "DELETE" + return urlRequest + } +} + +// MARK: - Client + +public extension LockNetService.Client { + + /// Remove the specified key. + func removeKey(_ identifier: UUID, + type: KeyType = .key, + for server: LockNetService, + with key: KeyCredentials, + timeout: TimeInterval = LockNetService.defaultTimeout) throws { + + log?("Remove \(type) \(identifier) for \(server.url.absoluteString)") + + let request = DeleteKeyRequest( + server: server.url, + authorization: .init(key: key), + key: identifier, + type: type + ).urlRequest() + + let (httpResponse, _) = try urlSession.synchronousDataTask(with: request) + + guard httpResponse.statusCode == 200 + else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } + } +} diff --git a/Sources/CoreLock/DeviceManager.swift b/Sources/CoreLock/DeviceManager.swift index e913893b..2675e07c 100644 --- a/Sources/CoreLock/DeviceManager.swift +++ b/Sources/CoreLock/DeviceManager.swift @@ -19,7 +19,6 @@ public final class LockManager { // MARK: - Initialization public init(central: Central) { - self.central = central } @@ -149,7 +148,7 @@ public final class LockManager { with key: KeyCredentials, timeout: TimeInterval = .gattDefaultTimeout) throws { - log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier)") + log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier) for \(peripheral)") let timeout = Timeout(timeout: timeout) @@ -172,7 +171,7 @@ public final class LockManager { with key: KeyCredentials, timeout: TimeInterval = .gattDefaultTimeout) throws { - log?("Confirm key \(key.identifier)") + log?("Confirm key \(key.identifier) for \(peripheral)") let timeout = Timeout(timeout: timeout) @@ -196,7 +195,7 @@ public final class LockManager { with key: KeyCredentials, timeout: TimeInterval = .gattDefaultTimeout) throws { - log?("Remove \(type) \(identifier)") + log?("Remove \(type) \(identifier) for \(peripheral)") let timeout = Timeout(timeout: timeout) @@ -311,7 +310,7 @@ public final class LockManager { } /// Retreive a list of events on device. - public func listEvents(fetchRequest: ListEventsCharacteristic.FetchRequest? = nil, + public func listEvents(fetchRequest: LockEvent.FetchRequest? = nil, for peripheral: Peripheral, with key: KeyCredentials, timeout: TimeInterval = .gattDefaultTimeout) throws -> EventsList { @@ -326,7 +325,7 @@ public final class LockManager { } /// Retreive a list of events on device. - public func listEvents(fetchRequest: ListEventsCharacteristic.FetchRequest? = nil, + public func listEvents(fetchRequest: LockEvent.FetchRequest? = nil, for peripheral: Peripheral, with key: KeyCredentials, timeout: TimeInterval = .gattDefaultTimeout, diff --git a/Sources/CoreLock/EncryptedData.swift b/Sources/CoreLock/EncryptedData.swift index 3575a911..770586c9 100644 --- a/Sources/CoreLock/EncryptedData.swift +++ b/Sources/CoreLock/EncryptedData.swift @@ -40,7 +40,6 @@ public extension EncryptedData { // attempt to decrypt do { return try CoreLock.decrypt(key: key.data, iv: initializationVector, data: encryptedData) } - catch { throw AuthenticationError.decryptionError(error) } } } diff --git a/Sources/CoreLock/Event.swift b/Sources/CoreLock/Event.swift index 14c78840..3c44c190 100644 --- a/Sources/CoreLock/Event.swift +++ b/Sources/CoreLock/Event.swift @@ -275,3 +275,4 @@ public extension LockEvent { } } } + diff --git a/Sources/CoreLock/EventStore.swift b/Sources/CoreLock/EventStore.swift new file mode 100644 index 00000000..7ddf9e40 --- /dev/null +++ b/Sources/CoreLock/EventStore.swift @@ -0,0 +1,119 @@ +// +// EventStore.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/16/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +public protocol LockEventStore: class { + + func fetch(_ fetchRequest: LockEvent.FetchRequest) throws -> [LockEvent] + + func save(_ event: LockEvent) throws +} + +// MARK: - Supporting Types + +public final class InMemoryLockEvents: LockEventStore { + + public init() { } + + public private(set) var events = [LockEvent]() + + public func fetch(_ fetchRequest: LockEvent.FetchRequest) throws -> [LockEvent] { + return events.fetch(fetchRequest) + } + + public func save(_ event: LockEvent) throws { + events.append(event) + } +} + +// MARK: - Fetch Request + +public extension LockEvent { + + struct FetchRequest: Codable, Equatable { + + /// The fetch offset of the fetch request. + public var offset: UInt8 + + /// The fetch limit of the fetch request. + public var limit: UInt8? + + /// The predicate of the fetch request. + public var predicate: Predicate? + + public init(offset: UInt8 = 0, + limit: UInt8? = nil, + predicate: Predicate? = nil) { + + self.offset = offset + self.limit = limit + self.predicate = predicate + } + } + + struct Predicate: Codable, Equatable { + + public var keys: [UUID]? + + public var start: Date? + + public var end: Date? + + public init(keys: [UUID]?, + start: Date? = nil, + end: Date? = nil) { + + self.keys = keys + self.start = start + self.end = end + } + } +} + +public extension LockEvent.Predicate { + + static var empty: LockEvent.Predicate { return .init(keys: nil, start: nil, end: nil) } +} + +public extension Collection where Self.Element == LockEvent { + + func fetch(_ fetchRequest: LockEvent.FetchRequest) -> [LockEvent] { + + guard isEmpty == false else { return [] } + let limit = Int(fetchRequest.limit ?? .max) + let offset = Int(fetchRequest.offset) + let predicate = fetchRequest.predicate ?? .empty + return sorted(by: { $0.date > $1.date }) + .lazy + .suffix(from: offset) + .lazy + .prefix(limit) + .filter(predicate) + } + + func filter(_ predicate: LockEvent.Predicate) -> [LockEvent] { + + // don't filter + guard predicate != .empty else { return Array(self) } + var value = Array(self) + // filter by keys + if let keys = predicate.keys, + keys.isEmpty == false { + value.removeAll(where: { keys.contains($0.key) == false }) + } + // filter by date + if let startDate = predicate.start { + value.removeAll(where: { $0.date < startDate }) + } + if let endDate = predicate.end { + value.removeAll(where: { $0.date > endDate }) + } + return value + } +} diff --git a/Sources/CoreLock/EventsRequest.swift b/Sources/CoreLock/EventsRequest.swift new file mode 100644 index 00000000..c93de29d --- /dev/null +++ b/Sources/CoreLock/EventsRequest.swift @@ -0,0 +1,149 @@ +// +// EventsRequest.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/20/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct EventsNetServiceRequest: Equatable { + + /// Lock server + public let server: URL + + /// Authorization header + public let authorization: LockNetService.Authorization + + /// Encrypted request + public let fetchRequest: LockEvent.FetchRequest? +} + +// MARK: - URL Request + +public extension EventsNetServiceRequest { + + func urlRequest() -> URLRequest { + + // http://localhost:8080/event + guard var urlComponents = URLComponents(url: server.appendingPathComponent("event"), resolvingAgainstBaseURL: false) + else { fatalError() } + urlComponents.queryItems = fetchRequest?.queryItems + guard let url = urlComponents.url + else { fatalError() } + var urlRequest = URLRequest(url: url) + urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField) + urlRequest.httpMethod = "GET" + return urlRequest + } +} + +public extension LockEvent.FetchRequest { + + init?(queryItems: [URLQueryItem]) { + guard let offset = queryItems.first(.offset).flatMap(UInt8.init) + else { return nil } + self.offset = offset + self.limit = queryItems.first(.limit).flatMap(UInt8.init) + var predicate = LockEvent.Predicate.empty + predicate.start = queryItems.firstDate(.start) + predicate.end = queryItems.firstDate(.end) + predicate.keys = queryItems + .compactMap(.key) + .compactMap { UUID(uuidString: $0) } + self.predicate = predicate != .empty ? predicate : nil + } + + var queryItems: [URLQueryItem] { + var queryItems = [URLQueryItem]() + queryItems.reserveCapacity(3) + queryItems.append(.init(name: .offset, value: offset.description)) + limit.flatMap { queryItems.append(.init(name: .limit, value: $0.description)) } + if let predicate = predicate { + predicate.keys.flatMap { + queryItems += $0.map { .init(name: .key, value: $0.uuidString) } + } + predicate.start.flatMap { queryItems.append(.init(name: .start, value: $0)) } + predicate.end.flatMap { queryItems.append(.init(name: .end, value: $0)) } + } + return queryItems + } +} + +internal extension EventsNetServiceRequest { + + enum QueryItem: String { + case offset + case limit + case key + case start + case end + } +} + +internal extension URLQueryItem { + + init(name: EventsNetServiceRequest.QueryItem, value: String? = nil) { + self.init(name: name.rawValue, value: value) + } + + init(name: EventsNetServiceRequest.QueryItem, value date: Date) { + let value = Int(date.timeIntervalSince1970).description + self.init(name: name, value: value) + } +} + +internal extension Sequence where Self.Element == URLQueryItem { + + func first(_ name: EventsNetServiceRequest.QueryItem) -> String? { + return first(where: { $0.name == name.rawValue })?.value + } + + func compactMap(_ name: EventsNetServiceRequest.QueryItem) -> [String] { + return compactMap { $0.name == name.rawValue ? $0.value : nil } + } + + func firstDate(_ name: EventsNetServiceRequest.QueryItem) -> Date? { + guard let value = first(name), + let timeInterval = TimeInterval(value) + else { return nil } + return Date(timeIntervalSince1970: timeInterval) + } +} + +// MARK: - Client + +public extension LockNetService.Client { + + /// Retreive a list of events on device. + func listEvents(fetchRequest: LockEvent.FetchRequest? = nil, + for server: LockNetService, + with key: KeyCredentials, + timeout: TimeInterval = LockNetService.defaultTimeout) throws -> EventsList { + + log?("List events for \(server.url.absoluteString)") + + let request = EventsNetServiceRequest( + server: server.url, + authorization: LockNetService.Authorization(key: key), + fetchRequest: fetchRequest + ) + + let (httpResponse, data) = try urlSession.synchronousDataTask(with: request.urlRequest()) + + guard httpResponse.statusCode == 200 + else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } + + guard let jsonData = data, + let response = try? jsonDecoder.decode(EventsResponse.self, from: jsonData) + else { throw LockNetService.Error.invalidResponse } + + let keys = try response.decrypt(with: key.secret, decoder: jsonDecoder) + return keys + } +} diff --git a/Sources/CoreLock/EventsResponse.swift b/Sources/CoreLock/EventsResponse.swift new file mode 100644 index 00000000..3213698c --- /dev/null +++ b/Sources/CoreLock/EventsResponse.swift @@ -0,0 +1,47 @@ +// +// EventsResponse.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/21/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +public struct EventsResponse: Equatable { + + public let encryptedData: LockNetService.EncryptedData +} + +// MARK: - Codable + +extension EventsResponse: Codable { + + public init(from decoder: Decoder) throws { + self.encryptedData = try LockNetService.EncryptedData(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try encryptedData.encode(to: encoder) + } +} + +// MARK: - Encryption + +public extension EventsResponse { + + init(encrypt value: EventsList, + with key: KeyData, + encoder: JSONEncoder = JSONEncoder()) throws { + + let data = try encoder.encode(value) + self.encryptedData = try .init(encrypt: data, with: key) + } + + func decrypt(with key: KeyData, + decoder: JSONDecoder = JSONDecoder()) throws -> EventsList { + + let data = try encryptedData.decrypt(with: key) + return try decoder.decode(EventsList.self, from: data) + } +} diff --git a/Sources/CoreLock/KeysRequest.swift b/Sources/CoreLock/KeysRequest.swift new file mode 100644 index 00000000..61237d80 --- /dev/null +++ b/Sources/CoreLock/KeysRequest.swift @@ -0,0 +1,70 @@ +// +// KeysRequest.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/16/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CryptoSwift + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct KeysNetServiceRequest: Equatable { + + /// Lock server + public let server: URL + + /// Authorization header + public let authorization: LockNetService.Authorization +} + +// MARK: - URL Request + +public extension KeysNetServiceRequest { + + func urlRequest() -> URLRequest { + + // http://localhost:8080/keys + let url = server.appendingPathComponent("key") + var urlRequest = URLRequest(url: url) + urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField) + return urlRequest + } +} + +// MARK: - Client + +public extension LockNetService.Client { + + /// Retreive a list of all keys on device. + func listKeys(for server: LockNetService, + with key: KeyCredentials, + timeout: TimeInterval = LockNetService.defaultTimeout) throws -> KeysList { + + log?("List keys for \(server.url.absoluteString)") + + let request = KeysNetServiceRequest( + server: server.url, + authorization: LockNetService.Authorization( + key: key.identifier, + authentication: Authentication(key: key.secret) + ) + ) + + let (httpResponse, data) = try urlSession.synchronousDataTask(with: request.urlRequest()) + + guard httpResponse.statusCode == 200 + else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } + + guard let jsonData = data, + let response = try? jsonDecoder.decode(KeysResponse.self, from: jsonData) + else { throw LockNetService.Error.invalidResponse } + + let keys = try response.decrypt(with: key.secret, decoder: jsonDecoder) + return keys + } +} diff --git a/Sources/CoreLock/KeysResponse.swift b/Sources/CoreLock/KeysResponse.swift new file mode 100644 index 00000000..915fa539 --- /dev/null +++ b/Sources/CoreLock/KeysResponse.swift @@ -0,0 +1,47 @@ +// +// KeysResponse.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/16/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +public struct KeysResponse: Equatable { + + public let encryptedData: LockNetService.EncryptedData +} + +// MARK: - Codable + +extension KeysResponse: Codable { + + public init(from decoder: Decoder) throws { + self.encryptedData = try LockNetService.EncryptedData(from: decoder) + } + + public func encode(to encoder: Encoder) throws { + try encryptedData.encode(to: encoder) + } +} + +// MARK: - Encryption + +public extension KeysResponse { + + init(encrypt value: KeysList, + with key: KeyData, + encoder: JSONEncoder = JSONEncoder()) throws { + + let data = try encoder.encode(value) + self.encryptedData = try .init(encrypt: data, with: key) + } + + func decrypt(with key: KeyData, + decoder: JSONDecoder = JSONDecoder()) throws -> KeysList { + + let data = try encryptedData.decrypt(with: key) + return try decoder.decode(KeysList.self, from: data) + } +} diff --git a/Sources/CoreLock/ListEventsCharacteristic.swift b/Sources/CoreLock/ListEventsCharacteristic.swift index b548e571..148b5e8a 100644 --- a/Sources/CoreLock/ListEventsCharacteristic.swift +++ b/Sources/CoreLock/ListEventsCharacteristic.swift @@ -24,95 +24,14 @@ public struct ListEventsCharacteristic: TLVCharacteristic, Codable, Equatable { public let authentication: Authentication /// Fetch limit for events to view. - public let fetchRequest: FetchRequest? + public let fetchRequest: LockEvent.FetchRequest? public init(identifier: UUID, authentication: Authentication, - fetchRequest: FetchRequest? = nil) { + fetchRequest: LockEvent.FetchRequest? = nil) { self.identifier = identifier self.authentication = authentication self.fetchRequest = fetchRequest } } - -public extension ListEventsCharacteristic { - - struct FetchRequest: Codable, Equatable { - - /// The fetch offset of the fetch request. - public var offset: UInt8 - - /// The fetch limit of the fetch request. - public var limit: UInt8? - - /// The predicate of the fetch request. - public var predicate: Predicate? - - public init(offset: UInt8 = 0, - limit: UInt8? = nil, - predicate: Predicate? = nil) { - - self.offset = offset - self.limit = limit - self.predicate = predicate - } - } - - struct Predicate: Codable, Equatable { - - public var keys: [UUID]? - - public var start: Date? - - public var end: Date? - - public static var empty: Predicate { return .init(keys: nil, start: nil, end: nil) } - - public init(keys: [UUID]?, - start: Date? = nil, - end: Date? = nil) { - - self.keys = keys - self.start = start - self.end = end - } - } -} - -public extension Collection where Self.Element == LockEvent { - - func fetch(_ fetchRequest: ListEventsCharacteristic.FetchRequest) -> [LockEvent] { - - guard isEmpty == false else { return [] } - let limit = Int(fetchRequest.limit ?? .max) - let offset = Int(fetchRequest.offset) - let predicate = fetchRequest.predicate ?? .empty - return sorted(by: { $0.date > $1.date }) - .lazy - .suffix(from: offset) - .lazy - .prefix(limit) - .filter(predicate) - } - - func filter(_ predicate: ListEventsCharacteristic.Predicate) -> [LockEvent] { - - // don't filter - guard predicate != .empty else { return Array(self) } - var value = Array(self) - // filter by keys - if let keys = predicate.keys, - keys.isEmpty == false { - value.removeAll(where: { keys.contains($0.key) == false }) - } - // filter by date - if let startDate = predicate.start { - value.removeAll(where: { $0.date < startDate }) - } - if let endDate = predicate.end { - value.removeAll(where: { $0.date > endDate }) - } - return value - } -} diff --git a/Sources/CoreLock/LockAuthorizationStore.swift b/Sources/CoreLock/LockAuthorizationStore.swift new file mode 100644 index 00000000..29aeedfb --- /dev/null +++ b/Sources/CoreLock/LockAuthorizationStore.swift @@ -0,0 +1,113 @@ +// +// LockAuthorizationStore.swift +// +// +// Created by Alsey Coleman Miller on 10/16/19. +// + +import Foundation + +/// Lock Authorization Store +public protocol LockAuthorizationStore: class { + + var isEmpty: Bool { get } + + func add(_ key: Key, secret: KeyData) throws + + func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? + + func add(_ key: NewKey, secret: KeyData) throws + + func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? + + func removeKey(_ identifier: UUID) throws + + func removeNewKey(_ identifier: UUID) throws + + func removeAll() throws + + var list: KeysList { get } +} + +// MARK: - Supporting Types + +public final class InMemoryLockAuthorization: LockAuthorizationStore { + + public init() { } + + private var keys = [KeyEntry]() + + private var newKeys = [NewKeyEntry]() + + public var isEmpty: Bool { + + return keys.isEmpty && newKeys.isEmpty + } + + public func add(_ key: Key, secret: KeyData) throws { + + keys.append(KeyEntry(key: key, secret: secret)) + } + + public func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? { + + guard let keyEntry = keys.first(where: { $0.key.identifier == identifier }) + else { return nil } + + return (keyEntry.key, keyEntry.secret) + } + + public func add(_ key: NewKey, secret: KeyData) throws { + + newKeys.append(NewKeyEntry(newKey: key, secret: secret)) + } + + public func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? { + + guard let keyEntry = newKeys.first(where: { $0.newKey.identifier == identifier }) + else { return nil } + + return (keyEntry.newKey, keyEntry.secret) + } + + public func removeKey(_ identifier: UUID) throws { + + keys.removeAll(where: { $0.key.identifier == identifier }) + } + + public func removeNewKey(_ identifier: UUID) throws { + + newKeys.removeAll(where: { $0.newKey.identifier == identifier }) + } + + public func removeAll() throws { + + keys.removeAll() + newKeys.removeAll() + } + + public var list: KeysList { + + return KeysList( + keys: keys.map { $0.key }, + newKeys: newKeys.map { $0.newKey } + ) + } +} + +private extension InMemoryLockAuthorization { + + struct KeyEntry { + + let key: Key + + let secret: KeyData + } + + struct NewKeyEntry { + + let newKey: NewKey + + let secret: KeyData + } +} diff --git a/Sources/CoreLock/LockConfigurationStore.swift b/Sources/CoreLock/LockConfigurationStore.swift new file mode 100644 index 00000000..87ba52e0 --- /dev/null +++ b/Sources/CoreLock/LockConfigurationStore.swift @@ -0,0 +1,31 @@ +// +// LockConfigurationStore.swift +// +// +// Created by Alsey Coleman Miller on 10/16/19. +// + +import Foundation + +/// Lock Configuration Storage +public protocol LockConfigurationStore: class { + + var configuration: LockConfiguration { get } + + func update(_ configuration: LockConfiguration) throws +} + +// MARK: - Supporting Types + +public final class InMemoryLockConfigurationStore: LockConfigurationStore { + + public private(set) var configuration: LockConfiguration + + public init(configuration: LockConfiguration = LockConfiguration()) { + self.configuration = configuration + } + + public func update(_ configuration: LockConfiguration) throws { + self.configuration = configuration + } +} diff --git a/Sources/CoreLock/LockHardware.swift b/Sources/CoreLock/LockHardware.swift index 2b452ab8..b253b40b 100644 --- a/Sources/CoreLock/LockHardware.swift +++ b/Sources/CoreLock/LockHardware.swift @@ -34,8 +34,7 @@ public extension LockHardware { /// Empty / Null Lock Hardware information. static var empty: LockHardware { - - return LockHardware(model: "", hardwareRevision: "", serialNumber: "") + return .init(model: "", hardwareRevision: "", serialNumber: "") } } diff --git a/Sources/CoreLock/LockInformationRequest.swift b/Sources/CoreLock/LockInformationRequest.swift new file mode 100644 index 00000000..f529ab22 --- /dev/null +++ b/Sources/CoreLock/LockInformationRequest.swift @@ -0,0 +1,53 @@ +// +// LockInformationRequest.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/16/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// Lock Information Web Request +public struct LockInformationNetServiceRequest: Equatable { + + /// Lock server + public let server: URL +} + +public extension LockInformationNetServiceRequest { + + func urlRequest() -> URLRequest { + + // http://localhost:8080/info + let url = server.appendingPathComponent("info") + return URLRequest(url: url) + } +} + +public extension LockNetService.Client { + + /// Read the lock's information characteristic. + func readInformation(for server: LockNetService, + timeout: TimeInterval = LockNetService.defaultTimeout) throws -> LockNetService.LockInformation { + + log?("Read information for \(server.url.absoluteString)") + + let request = LockInformationNetServiceRequest(server: server.url).urlRequest() + + let (httpResponse, data) = try urlSession.synchronousDataTask(with: request) + + guard httpResponse.statusCode == 200 + else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } + + guard let jsonData = data, + let response = try? jsonDecoder.decode(LockNetService.LockInformation.self, from: jsonData) + else { throw LockNetService.Error.invalidResponse } + + return response + } +} diff --git a/Sources/CoreLock/LockInformationResponse.swift b/Sources/CoreLock/LockInformationResponse.swift new file mode 100644 index 00000000..275d0e9b --- /dev/null +++ b/Sources/CoreLock/LockInformationResponse.swift @@ -0,0 +1,43 @@ +// +// LockInformationResponse.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/16/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation + +public extension LockNetService { + + struct LockInformation: Equatable, Codable { + + /// Lock identifier + public let identifier: UUID + + /// Firmware build number + public let buildVersion: LockBuildVersion + + /// Firmware version + public let version: LockVersion + + /// Device state + public var status: LockStatus + + /// Supported lock actions + public let unlockActions: Set + + public init(identifier: UUID, + buildVersion: LockBuildVersion = .current, + version: LockVersion = .current, + status: LockStatus, + unlockActions: Set = [.default]) { + + self.identifier = identifier + self.buildVersion = buildVersion + self.version = version + self.status = status + self.unlockActions = unlockActions + } + } +} diff --git a/Sources/CoreLock/LockModel.swift b/Sources/CoreLock/LockModel.swift index 0cc6742d..50a8f6c3 100644 --- a/Sources/CoreLock/LockModel.swift +++ b/Sources/CoreLock/LockModel.swift @@ -14,7 +14,6 @@ public struct LockModel: RawRepresentable, Equatable, Hashable { public let rawValue: String public init(rawValue: String) { - self.rawValue = rawValue } } @@ -24,7 +23,6 @@ public struct LockModel: RawRepresentable, Equatable, Hashable { extension LockModel: CustomStringConvertible { public var description: String { - return rawValue } } @@ -34,7 +32,6 @@ extension LockModel: CustomStringConvertible { extension LockModel: ExpressibleByStringLiteral { public init(stringLiteral value: String) { - self.init(rawValue: value) } } @@ -53,15 +50,14 @@ public extension LockModel { // MARK: - Darwin #if os(macOS) + +public extension LockModel { - public extension LockModel { - - static var currentMac: LockModel { - - return LockModel(rawValue: UIDevice.current.model) - } + static var currentMac: LockModel { + return LockModel(rawValue: UIDevice.current.model) } - +} + #endif // MARK: - Codable diff --git a/Sources/CoreLock/LockNetService.swift b/Sources/CoreLock/LockNetService.swift new file mode 100644 index 00000000..c1270f38 --- /dev/null +++ b/Sources/CoreLock/LockNetService.swift @@ -0,0 +1,223 @@ +// +// LockNetService.swift +// +// +// Created by Alsey Coleman Miller on 10/16/19. +// + +import Foundation +import Bonjour + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +public struct LockNetService: Equatable, Hashable { + + public let identifier: UUID + + public let url: URL +} + +internal extension LockNetService { + + init(identifier: UUID, address: NetServiceAddress) { + + guard let url = URL(string: "http://" + address.description) + else { fatalError("Could not create URL from \(address)") } + + self.identifier = identifier + self.url = url + } +} + +public extension LockNetService { + + final class Client { + + // MARK: - Properties + + /// The log message handler. + public var log: ((String) -> ())? + + /// Bonjour Client + public let bonjour: NetServiceClient + + /// URL Session + public let urlSession: URLSession + + /// Whether the client is discovering net services. + private(set) var isDiscovering = false + + internal lazy var jsonDecoder = JSONDecoder() + + internal lazy var jsonEncoder = JSONEncoder() + + // MARK: - Initialization + + public init(bonjour: NetServiceClient, + urlSession: URLSession = .shared) { + + self.bonjour = bonjour + self.urlSession = urlSession + } + + // MARK: - Methods + + public func discover(duration: TimeInterval = 5.0, + timeout: TimeInterval = 10.0) throws -> Set { + + log?("Scanning for \(String(format: "%.2f", duration))s") + + isDiscovering = true + defer { isDiscovering = false } + + var foundServices = [UUID: Bonjour.Service](minimumCapacity: 1) + + let end = Date() + duration + try bonjour.discoverServices(of: .lock, in: .local, shouldContinue: { + Date() < end + }, foundService: { (service) in + guard service.type == .lock, + let identifier = UUID(uuidString: service.name) + else { return } + foundServices[identifier] = service + }) + + var locks = Set() + locks.reserveCapacity(foundServices.count) + + for (identifier, service) in foundServices { + + guard let addresses = try? bonjour.resolve(service, timeout: timeout), + let address = addresses.filter({ + switch $0.address { + case .ipv4: return true + case .ipv6: return false + } + }).first + else { continue } + + let lock = LockNetService( + identifier: identifier, + address: address + ) + + locks.insert(lock) + } + + log?("Found \(locks.count) devices") + + return locks + } + } +} + +// MARK: - Extensions + +public extension NetServiceType { + + static let lock = NetServiceType(rawValue: LockNetService.serviceType) +} + +public extension LockNetService { + + static let serviceType = "_lock._tcp." + + static var defaultTimeout: TimeInterval = 30.0 +} + +// MARK: - Supporting Types + +public extension LockNetService { + + enum Error: Swift.Error { + + case invalidURL + case statusCode(Int) + case invalidResponse + } +} + +public extension LockNetService { + + /// Lock authorization for Web API + struct Authorization: Equatable, Codable { + + /// Identifier of key making request. + public let key: UUID + + /// HMAC of key and nonce, and HMAC message + public let authentication: Authentication + } +} + +public extension LockNetService.Authorization { + + init(key: KeyCredentials) { + + self.init(key: key.identifier, authentication: Authentication(key: key.secret)) + } +} + +public extension LockNetService.Authorization { + + private static let jsonDecoder = JSONDecoder() + + private static let jsonEncoder = JSONEncoder() + + static let headerField: String = "Authorization" + + init?(header: String) { + + guard let data = Data(base64Encoded: header), + let authorization = try? type(of: self).jsonDecoder.decode(LockNetService.Authorization.self, from: data) + else { return nil } + + self = authorization + } + + var header: String { + let data = try! type(of: self).jsonEncoder.encode(self) + let base64 = data.base64EncodedString() + return base64 + } +} + +public extension LockNetService { + + struct EncryptedData: Equatable, Codable { + + internal enum CodingKeys: String, CodingKey { + + case initializationVector = "iv" + case encryptedData = "data" + } + + /// Crypto IV + public let initializationVector: InitializationVector + + /// Encrypted data + public let encryptedData: Data + } +} + +extension LockNetService.EncryptedData { + + init(encrypt data: Data, with key: KeyData) throws { + + do { + let (encryptedData, iv) = try CoreLock.encrypt(key: key.data, data: data) + self.initializationVector = iv + self.encryptedData = encryptedData + } + catch { throw AuthenticationError.encryptionError(error) } + } + + func decrypt(with key: KeyData) throws -> Data { + + // attempt to decrypt + do { return try CoreLock.decrypt(key: key.data, iv: initializationVector, data: encryptedData) } + catch { throw AuthenticationError.decryptionError(error) } + } +} diff --git a/Sources/CoreLock/URLSession.swift b/Sources/CoreLock/URLSession.swift new file mode 100644 index 00000000..33ac6ce8 --- /dev/null +++ b/Sources/CoreLock/URLSession.swift @@ -0,0 +1,47 @@ +// +// URLSession.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/16/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import Dispatch + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +internal extension URLSession { + + func synchronousDataTask(with request: URLRequest) throws -> (HTTPURLResponse, Data?) { + + var data: Data? + var response: URLResponse? + var error: Error? + + let semaphore = DispatchSemaphore(value: 0) + + let dataTask = self.dataTask(with: request) { + data = $0 + response = $1 + error = $2 + + semaphore.signal() + } + + dataTask.resume() + + _ = semaphore.wait(timeout: .distantFuture) + + if let error = error { + throw error + } + + guard let urlResponse = response as? HTTPURLResponse + else { fatalError("Invalid response: \(response?.description ?? "nil")") } + + return (urlResponse, data) + } +} diff --git a/Sources/CoreLock/UpdateRequest.swift b/Sources/CoreLock/UpdateRequest.swift new file mode 100644 index 00000000..8c3a4707 --- /dev/null +++ b/Sources/CoreLock/UpdateRequest.swift @@ -0,0 +1,60 @@ +// +// UpdateRequest.swift +// CoreLock +// +// Created by Alsey Coleman Miller on 10/18/19. +// + +import Foundation + +#if canImport(FoundationNetworking) +import FoundationNetworking +#endif + +/// Lock Software Update HTTP Request +public struct UpdateNetServiceRequest: Equatable { + + /// Lock server + public let server: URL + + /// Authorization header + public let authorization: LockNetService.Authorization +} + +// MARK: - URL Request + +public extension UpdateNetServiceRequest { + + func urlRequest() -> URLRequest { + + // http://localhost:8080/update + let url = server.appendingPathComponent("update") + var urlRequest = URLRequest(url: url) + urlRequest.addValue(authorization.header, forHTTPHeaderField: LockNetService.Authorization.headerField) + urlRequest.httpMethod = "POST" + return urlRequest + } +} + +// MARK: - Client + +public extension LockNetService.Client { + + /// Create new key. + func update(for server: LockNetService, + with key: KeyCredentials, + timeout: TimeInterval = LockNetService.defaultTimeout) throws { + + log?("Update software for \(server.url.absoluteString)") + + let request = UpdateNetServiceRequest( + server: server.url, + authorization: .init(key: key) + ).urlRequest() + + let (httpResponse, _) = try urlSession.synchronousDataTask(with: request) + + guard httpResponse.statusCode == 200 + else { throw LockNetService.Error.statusCode(httpResponse.statusCode) } + } +} diff --git a/Sources/CoreLock/Version.swift b/Sources/CoreLock/Version.swift index dab15823..8da964f7 100644 --- a/Sources/CoreLock/Version.swift +++ b/Sources/CoreLock/Version.swift @@ -27,7 +27,7 @@ public struct LockVersion: Equatable, Hashable { public extension LockVersion { - static var current: LockVersion { return LockVersion(major: 0, minor: 0, patch: 1) } + static var current: LockVersion { return LockVersion(major: 1, minor: 0, patch: 1) } } // MARK: - RawRepresentable diff --git a/Sources/CoreLockGATTServer/LockController.swift b/Sources/CoreLockGATTServer/LockGATTController.swift similarity index 86% rename from Sources/CoreLockGATTServer/LockController.swift rename to Sources/CoreLockGATTServer/LockGATTController.swift index 50385270..eda2794e 100644 --- a/Sources/CoreLockGATTServer/LockController.swift +++ b/Sources/CoreLockGATTServer/LockGATTController.swift @@ -17,7 +17,7 @@ import GATT import CoreLock /// Smart Lock GATT Server controller. -public final class LockController { +public final class LockGATTController { // MARK: - Properties @@ -25,11 +25,10 @@ public final class LockController { public let deviceInformationController: GATTDeviceInformationServiceController - public let lockServiceController: LockServiceController + public let lockServiceController: LockGATTServiceController // Lock hardware model public var hardware: LockHardware = .empty { - didSet { self.lockServiceController.hardware = hardware self.deviceInformationController.hardware = hardware @@ -44,7 +43,7 @@ public final class LockController { // load services self.deviceInformationController = try GATTDeviceInformationServiceController(peripheral: peripheral) - self.lockServiceController = try LockServiceController(peripheral: peripheral) + self.lockServiceController = try LockGATTServiceController(peripheral: peripheral) // set callbacks self.peripheral.willRead = { [unowned self] in self.willRead($0) } @@ -57,15 +56,10 @@ public final class LockController { private func willRead(_ request: GATTReadRequest) -> ATT.Error? { if lockServiceController.supportsCharacteristic(request.uuid) { - return lockServiceController.willRead(request) - } else if deviceInformationController.supportsCharacteristic(request.uuid) { - return deviceInformationController.willRead(request) - } else { - return nil } } @@ -73,15 +67,10 @@ public final class LockController { private func willWrite(_ request: GATTWriteRequest) -> ATT.Error? { if lockServiceController.supportsCharacteristic(request.uuid) { - return lockServiceController.willWrite(request) - } else if deviceInformationController.supportsCharacteristic(request.uuid) { - return deviceInformationController.willWrite(request) - } else { - return nil } } @@ -89,11 +78,8 @@ public final class LockController { private func didWrite(_ confirmation: GATTWriteConfirmation) { if lockServiceController.supportsCharacteristic(confirmation.uuid) { - lockServiceController.didWrite(confirmation) - } else if deviceInformationController.supportsCharacteristic(confirmation.uuid) { - deviceInformationController.didWrite(confirmation) } } @@ -102,7 +88,6 @@ public final class LockController { private extension GATTServiceController { func supportsCharacteristic(_ characteristicUUID: BluetoothUUID) -> Bool { - return characteristics.contains(characteristicUUID) } } diff --git a/Sources/CoreLockGATTServer/LockServiceController.swift b/Sources/CoreLockGATTServer/LockServiceController.swift index 90cbf7a9..2455bf3e 100644 --- a/Sources/CoreLockGATTServer/LockServiceController.swift +++ b/Sources/CoreLockGATTServer/LockServiceController.swift @@ -10,7 +10,7 @@ import Bluetooth import GATT import CoreLock -public final class LockServiceController : GATTServiceController { +public final class LockGATTServiceController : GATTServiceController { public static var service: BluetoothUUID { return Service.uuid } @@ -21,7 +21,7 @@ public final class LockServiceController : GATT // MARK: - Properties public let peripheral: Peripheral - + public var hardware: LockHardware = .empty { didSet { updateInformation() } } @@ -29,13 +29,13 @@ public final class LockServiceController : GATT public var configurationStore: LockConfigurationStore = InMemoryLockConfigurationStore() { didSet { updateInformation() } } - - public var setupSecret: KeyData = KeyData() - + public var authorization: LockAuthorizationStore = InMemoryLockAuthorization() { didSet { updateInformation() } } + public var setupSecret: KeyData = KeyData() + public var unlockDelegate: UnlockDelegate = UnlockSimulator() public var events: LockEventStore = InMemoryLockEvents() @@ -155,7 +155,13 @@ public final class LockServiceController : GATT public func willRead(_ request: GATTReadRequest) -> ATT.Error? { - return nil + switch request.handle { + case informationHandle: + print("Requested lock information") + return nil + default: + return nil + } } public func willWrite(_ request: GATTWriteRequest) -> ATT.Error? { @@ -600,62 +606,12 @@ public final class LockServiceController : GATT } } -/// Lock Configuration Storage -public protocol LockConfigurationStore { - - var configuration: LockConfiguration { get } - - func update(_ configuration: LockConfiguration) throws -} - -/// Lock Authorization Store -public protocol LockAuthorizationStore { - - var isEmpty: Bool { get } - - func add(_ key: Key, secret: KeyData) throws - - func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? - - func add(_ key: NewKey, secret: KeyData) throws - - func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? - - func removeKey(_ identifier: UUID) throws - - func removeNewKey(_ identifier: UUID) throws - - func removeAll() throws - - var list: KeysList { get } -} - -public protocol LockEventStore { - - func fetch(_ fetchRequest: ListEventsCharacteristic.FetchRequest) throws -> [LockEvent] - - func save(_ event: LockEvent) throws -} - /// Lock unlock manager public protocol UnlockDelegate { func unlock(_ action: UnlockAction) throws } -public final class InMemoryLockConfigurationStore: LockConfigurationStore { - - public private(set) var configuration: LockConfiguration - - public init(configuration: LockConfiguration = LockConfiguration()) { - self.configuration = configuration - } - - public func update(_ configuration: LockConfiguration) throws { - self.configuration = configuration - } -} - public struct UnlockSimulator: UnlockDelegate { public func unlock(_ action: UnlockAction) throws { @@ -663,97 +619,3 @@ public struct UnlockSimulator: UnlockDelegate { print("Simulate unlock with action \(action)") } } - -public final class InMemoryLockEvents: LockEventStore { - - public private(set) var events = [LockEvent]() - - public func fetch(_ fetchRequest: ListEventsCharacteristic.FetchRequest) throws -> [LockEvent] { - return events.fetch(fetchRequest) - } - - public func save(_ event: LockEvent) throws { - events.append(event) - } -} - -public final class InMemoryLockAuthorization: LockAuthorizationStore { - - public init() { } - - private var keys = [KeyEntry]() - - private var newKeys = [NewKeyEntry]() - - public var isEmpty: Bool { - - return keys.isEmpty && newKeys.isEmpty - } - - public func add(_ key: Key, secret: KeyData) throws { - - keys.append(KeyEntry(key: key, secret: secret)) - } - - public func key(for identifier: UUID) throws -> (key: Key, secret: KeyData)? { - - guard let keyEntry = keys.first(where: { $0.key.identifier == identifier }) - else { return nil } - - return (keyEntry.key, keyEntry.secret) - } - - public func add(_ key: NewKey, secret: KeyData) throws { - - newKeys.append(NewKeyEntry(newKey: key, secret: secret)) - } - - public func newKey(for identifier: UUID) throws -> (newKey: NewKey, secret: KeyData)? { - - guard let keyEntry = newKeys.first(where: { $0.newKey.identifier == identifier }) - else { return nil } - - return (keyEntry.newKey, keyEntry.secret) - } - - public func removeKey(_ identifier: UUID) throws { - - keys.removeAll(where: { $0.key.identifier == identifier }) - } - - public func removeNewKey(_ identifier: UUID) throws { - - newKeys.removeAll(where: { $0.newKey.identifier == identifier }) - } - - public func removeAll() throws { - - keys.removeAll() - newKeys.removeAll() - } - - public var list: KeysList { - - return KeysList( - keys: keys.map { $0.key }, - newKeys: newKeys.map { $0.newKey } - ) - } -} - -private extension InMemoryLockAuthorization { - - struct KeyEntry { - - let key: Key - - let secret: KeyData - } - - struct NewKeyEntry { - - let newKey: NewKey - - let secret: KeyData - } -} diff --git a/Sources/CoreLockWebServer/Credentials.swift b/Sources/CoreLockWebServer/Credentials.swift new file mode 100644 index 00000000..d6407b8f --- /dev/null +++ b/Sources/CoreLockWebServer/Credentials.swift @@ -0,0 +1,58 @@ +// +// Credentials.swift +// +// +// Created by Alsey Coleman Miller on 10/16/19. +// + +import Foundation +import CoreLock +import Kitura + +internal extension LockNetService.Authorization { + + init?(request: RouterRequest) { + + guard let authorizationHeader = request.headers[LockNetService.Authorization.headerField], + let authorization = LockNetService.Authorization(header: authorizationHeader) else { + return nil + } + + self = authorization + } +} + +internal extension LockWebServer { + + func authenticate(request: RouterRequest) throws -> (Key, KeyData)? { + + // authenticate + guard let authorization = LockNetService.Authorization(request: request) else { + log?("\(request.urlURL.path) Missing authentication") + return nil + } + + // validate key + guard let (key, secret) = try self.authorization.key(for: authorization.key) else { + log?("\(request.urlURL.path) Invalid key \(authorization.key)") + return nil + } + + // validate HMAC + guard authorization.authentication.isAuthenticated(with: secret) else { + log?("\(request.urlURL.path) Invalid HMAC") + return nil + } + + // guard against replay attacks + let timestamp = authorization.authentication.message.date + let now = Date() + guard timestamp <= now + authorizationTimeout, // cannot be used later for replay attacks + timestamp > now - authorizationTimeout else { // only valid for 5 seconds + log?("\(request.urlURL.path) Authentication expired \(timestamp) < \(now)") + return nil + } + + return (key, secret) + } +} diff --git a/Sources/CoreLockWebServer/Events.swift b/Sources/CoreLockWebServer/Events.swift new file mode 100644 index 00000000..4f8c6445 --- /dev/null +++ b/Sources/CoreLockWebServer/Events.swift @@ -0,0 +1,68 @@ +// +// Events.swift +// CoreLockWebServer +// +// Created by Alsey Coleman Miller on 10/22/19. +// + +import Foundation +import CoreLock +import Kitura + +internal extension LockWebServer { + + func addEventsRoute() { + + router.get("/event") { [unowned self] (request, response, next) in + do { + if let statusCode = try self.getEvents(request: request, response: response) { + _ = response.send(status: statusCode) + } + } + catch { + self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)") + dump(error) + _ = response.send(status: .internalServerError) + } + try response.end() + } + } + + private func getEvents(request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode? { + + // authenticate + guard let (key, secret) = try authenticate(request: request) else { + return .unauthorized + } + + log?("Key \(key.identifier) \(key.name) requested events list") + + var fetchRequest = LockEvent.FetchRequest() + + if let urlComponents = URLComponents(url: request.urlURL, resolvingAgainstBaseURL: true), + let queryItems = urlComponents.queryItems, + let queryFetchRequest = LockEvent.FetchRequest(queryItems: queryItems) { + var fetchDescription = "" + dump(fetchRequest, to: &fetchDescription) + log?(fetchDescription) + fetchRequest = queryFetchRequest + } + + // enforce permission, non-administrators can only view their own events. + if key.permission.isAdministrator == false { + var predicate = fetchRequest.predicate ?? .empty + predicate.keys = [key.identifier] + fetchRequest.predicate = predicate + } + + // execute fetch + let list = try events.fetch(fetchRequest) + + // encrypt + let eventsResponse = try EventsResponse(encrypt: list, with: secret, encoder: jsonEncoder) + + // respond + response.send(eventsResponse) + return nil + } +} diff --git a/Sources/CoreLockWebServer/Information.swift b/Sources/CoreLockWebServer/Information.swift new file mode 100644 index 00000000..6a290f90 --- /dev/null +++ b/Sources/CoreLockWebServer/Information.swift @@ -0,0 +1,36 @@ +// +// File.swift +// +// +// Created by Alsey Coleman Miller on 10/16/19. +// + +import Foundation +import CoreLock +import Kitura +import KituraContracts + +internal extension LockWebServer { + + func addLockInformationRoute() { + + router.get("/info", handler: getLockInformation) + } + + private func getLockInformation(completion: (LockNetService.LockInformation?, RequestError?) -> ()) { + + log?("Requested lock information") + + let status: LockStatus = authorization.isEmpty ? .setup : .unlock + + let identifier = configurationStore.configuration.identifier + + let information = LockNetService.LockInformation(identifier: identifier, + buildVersion: .current, + version: .current, + status: status, + unlockActions: [UnlockAction.default]) + + completion(information, nil) + } +} diff --git a/Sources/CoreLockWebServer/Permissions.swift b/Sources/CoreLockWebServer/Permissions.swift new file mode 100644 index 00000000..e00df4f7 --- /dev/null +++ b/Sources/CoreLockWebServer/Permissions.swift @@ -0,0 +1,177 @@ +// +// Permissions.swift +// +// +// Created by Alsey Coleman Miller on 10/16/19. +// + +import Foundation +import CoreLock +import Kitura + +internal extension LockWebServer { + + func addPermissionsRoute() { + + router.post("/key") { [unowned self] (request, response, next) in + do { + let statusCode = try self.createKey(request: request, response: response) + _ = response.send(status: statusCode) + } + catch { + self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)") + dump(error) + _ = response.send(status: .internalServerError) + } + try response.end() + } + + router.get("/key") { [unowned self] (request, response, next) in + do { + if let statusCode = try self.getKeys(request: request, response: response) { + _ = response.send(status: statusCode) + } + } + catch { + self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)") + dump(error) + _ = response.send(status: .internalServerError) + } + try response.end() + } + + router.delete("/key/:id") { [unowned self] (request, response, next) in + guard let identifierString = request.parameters["id"], + let identifier = UUID(uuidString: identifierString) else { + _ = response.send(status: .notFound) + try response.end() + return + } + do { + let statusCode = try self.deleteKey(identifier, type: .key, request: request, response: response) + _ = response.send(status: statusCode) + } + catch { + self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)") + dump(error) + _ = response.send(status: .internalServerError) + } + try response.end() + } + + router.delete("/newKey/:id") { [unowned self] (request, response, next) in + guard let identifierString = request.parameters["id"], + let identifier = UUID(uuidString: identifierString) else { + _ = response.send(status: .notFound) + try response.end() + return + } + do { + let statusCode = try self.deleteKey(identifier, type: .newKey, request: request, response: response) + _ = response.send(status: statusCode) + } + catch { + self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)") + dump(error) + _ = response.send(status: .internalServerError) + } + try response.end() + } + } + + private func getKeys(request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode? { + + // authenticate + guard let (key, secret) = try authenticate(request: request) else { + return .unauthorized + } + + // enforce permission + guard key.permission.isAdministrator else { + log?("Only lock owner and admins can view list of keys") + return .forbidden + } + + log?("Key \(key.identifier) \(key.name) requested keys list") + + // get list + let list = authorization.list + + // encrypt + let keysResponse = try KeysResponse(encrypt: list, with: secret, encoder: jsonEncoder) + + // respond + response.send(keysResponse) + return nil + } + + private func createKey(request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode { + + // authenticate + guard let (key, secret) = try authenticate(request: request) else { + return .unauthorized + } + + // enforce permission + guard key.permission.isAdministrator else { + log?("Only lock owner and admins can view list of keys") + return .forbidden + } + + // parse body + var data = Data() + _ = try request.read(into: &data) + let encryptedData = try jsonDecoder.decode(LockNetService.EncryptedData.self, from: data) + let newKeyRequest = try CreateNewKeyNetServiceRequest.decrypt( + encryptedData, + with: secret, + decoder: jsonDecoder + ) + let newKey = NewKey(request: newKeyRequest) + + log?("Create \(newKey.permission.type) key \"\(newKey.name)\" \(newKey.identifier)") + + try self.authorization.add(newKey, secret: newKeyRequest.secret) + + try events.save(.createNewKey(.init(key: key.identifier, newKey: newKey.identifier))) + + lockChanged?() + + return .created + } + + private func deleteKey(_ identifier: UUID, type: KeyType, request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode { + + // authenticate + guard let (key, _) = try authenticate(request: request) else { + return .unauthorized + } + + // enforce permission + guard key.permission.isAdministrator else { + log?("Only lock owner and admins can remove keys") + return .forbidden + } + + switch type { + case .key: + guard let (removeKey, _) = try authorization.key(for: identifier) + else { log?("Key \(identifier) does not exist"); return .notFound } + assert(removeKey.identifier == identifier) + try authorization.removeKey(removeKey.identifier) + case .newKey: + guard let (removeKey, _) = try authorization.newKey(for: identifier) + else { log?("New Key \(identifier) does not exist"); return .notFound } + assert(removeKey.identifier == identifier) + try authorization.removeNewKey(removeKey.identifier) + } + + log?("Key \(key.identifier) \(key.name) removed \(type) \(identifier)") + + try events.save(.removeKey(.init(key: key.identifier, removedKey: identifier, type: type))) + + lockChanged?() + + return .OK + } +} diff --git a/Sources/CoreLockWebServer/Server.swift b/Sources/CoreLockWebServer/Server.swift new file mode 100644 index 00000000..63ac31f1 --- /dev/null +++ b/Sources/CoreLockWebServer/Server.swift @@ -0,0 +1,82 @@ +// +// Server.swift +// +// +// Created by Alsey Coleman Miller on 10/16/19. +// + +import Foundation +import Dispatch +import CoreLock +import Kitura +import KituraNet + +#if os(Linux) +import NetService +#endif + +/// Lock Web Server +public final class LockWebServer { + + // MARK: - Properties + + public var log: ((String) -> ())? + + public var port: Int = 8080 + + public var hardware: LockHardware = .empty + + public var configurationStore: LockConfigurationStore = InMemoryLockConfigurationStore() + + public var authorization: LockAuthorizationStore = InMemoryLockAuthorization() + + public var authorizationTimeout: TimeInterval = 10.0 + + public var events: LockEventStore = InMemoryLockEvents() + + public var lockChanged: (() -> ())? + + public var update: (() -> ())? + + internal lazy var jsonEncoder = JSONEncoder() + + internal lazy var jsonDecoder = JSONDecoder() + + internal let router = Router() + + private var httpServer: HTTPServer? + + private var netService: NetService? + + // MARK: - Initialization + + public init() { + setupRouter() + } + + // MARK: - Methods + + private func setupRouter() { + + addLockInformationRoute() + addPermissionsRoute() + addEventsRoute() + addUpdateRoute() + } + + public func run() { + + // Bonjour + netService = NetService(domain: "local.", + type: LockNetService.serviceType, + name: configurationStore.configuration.identifier.uuidString, + port: Int32(port)) + + netService?.publish(options: []) + + // Kiture + httpServer = Kitura.addHTTPServer(onPort: port, with: router) + log?("Started HTTP Server on port \(port)") + Kitura.run() + } +} diff --git a/Sources/CoreLockWebServer/Update.swift b/Sources/CoreLockWebServer/Update.swift new file mode 100644 index 00000000..41c3e481 --- /dev/null +++ b/Sources/CoreLockWebServer/Update.swift @@ -0,0 +1,49 @@ +// +// Update.swift +// CoreLockWebServer +// +// Created by Alsey Coleman Miller on 10/19/19. +// + +import Foundation +import CoreLock +import Kitura + +internal extension LockWebServer { + + func addUpdateRoute() { + + router.post("/update") { [unowned self] (request, response, next) in + do { + let statusCode = try self.update(request: request, response: response) + _ = response.send(status: statusCode) + } + catch { + self.log?("\(request.urlURL.path) Internal server error. \(error.localizedDescription)") + dump(error) + _ = response.send(status: .internalServerError) + } + try response.end() + } + } + + private func update(request: RouterRequest, response: RouterResponse) throws -> HTTPStatusCode { + + // authenticate + guard let (key, _) = try authenticate(request: request) else { + return .unauthorized + } + + // enforce permission + guard key.permission.isAdministrator else { + log?("Only lock owner and admins can update") + return .forbidden + } + + log?("Key \(key.identifier) \(key.name) requested software update") + + update?() + + return .OK + } +} diff --git a/Sources/lockd/LockEventsFile.swift b/Sources/lockd/LockEventsFile.swift index 68b4380d..45c422f8 100644 --- a/Sources/lockd/LockEventsFile.swift +++ b/Sources/lockd/LockEventsFile.swift @@ -9,8 +9,8 @@ import Foundation import CoreLock import CoreLockGATTServer -/// Stores the lock evets in a JSON file. -public struct LockEventsFile: LockEventStore { +/// Stores the lock events in a JSON file. +public final class LockEventsFile: LockEventStore { typealias Events = [LockEvent] @@ -29,7 +29,7 @@ public struct LockEventsFile: LockEventStore { // MARK: - Methods - public func fetch(_ fetchRequest: ListEventsCharacteristic.FetchRequest) throws -> [LockEvent] { + public func fetch(_ fetchRequest: LockEvent.FetchRequest) throws -> [LockEvent] { return try load { $0.fetch(fetchRequest) } } diff --git a/Sources/lockd/main.swift b/Sources/lockd/main.swift index f43bc02b..c8890e5c 100644 --- a/Sources/lockd/main.swift +++ b/Sources/lockd/main.swift @@ -22,14 +22,17 @@ import Bluetooth import GATT import CoreLock import CoreLockGATTServer +import CoreLockWebServer #if os(Linux) typealias LinuxPeripheral = GATTPeripheral -var controller: LockController? +var controller: LockGATTController? #elseif os(macOS) -var controller: LockController? +var controller: LockGATTController? #endif +let webServer = LockWebServer() + var gpio: LockGPIOController? var advertiseTimer: Timer? let backgroundQueue = DispatchQueue(label: "com.colemancda.lockd") @@ -78,36 +81,54 @@ func run() throws { while peripheral.state != .poweredOn { sleep(1) } #endif + // load files let configurationStore = try LockConfigurationFile( url: URL(fileURLWithPath: "/opt/colemancda/lockd/config.json") ) + let authorization = try AuthorizationStoreFile( + url: URL(fileURLWithPath: "/opt/colemancda/lockd/data.json") + ) + let events = LockEventsFile( + url: URL(fileURLWithPath: "/opt/colemancda/lockd/events.json") + ) + let setupSecret = try LockSetupSecretFile( + createdAt: URL(fileURLWithPath: "/opt/colemancda/lockd/sharedSecret") + ) let lockIdentifier = configurationStore.configuration.identifier print("🔒 Lock \(lockIdentifier)") - // Intialize Smart Connect BLE Controller - controller = try LockController(peripheral: peripheral) - - // load files + // configure Smart Connect BLE Controller + controller = try LockGATTController(peripheral: peripheral) controller?.lockServiceController.configurationStore = configurationStore - controller?.lockServiceController.authorization = try AuthorizationStoreFile( - url: URL(fileURLWithPath: "/opt/colemancda/lockd/data.json") - ) - controller?.lockServiceController.setupSecret = try LockSetupSecretFile( - createdAt: URL(fileURLWithPath: "/opt/colemancda/lockd/sharedSecret") - ).sharedSecret - controller?.lockServiceController.events = LockEventsFile( - url: URL(fileURLWithPath: "/opt/colemancda/lockd/events.json") - ) + controller?.lockServiceController.authorization = authorization + controller?.lockServiceController.events = events + controller?.lockServiceController.setupSecret = setupSecret.sharedSecret + + // configure web server + webServer.authorization = authorization + webServer.configurationStore = configurationStore + webServer.events = events + webServer.log = { print("Web Server:", $0) } + webServer.update = { + DispatchQueue.global(qos: .userInitiated).async { + #if os(Linux) + system("/opt/colemancda/lockd/update.sh") + #else + print("Simulate software update") + #endif + } + } - // setup controller + // load hardware configuration if let hardware = try? JSONDecoder().decode(LockHardware.self, from: URL(fileURLWithPath: "/opt/colemancda/lockd/hardware.json")) { print("Running on hardware:") dump(hardware) controller?.hardware = hardware + webServer.hardware = hardware // load GPIO if let gpioController = hardware.gpioController() { @@ -130,7 +151,7 @@ func run() throws { try hostController.writeLocalName("Lock") // change advertisment for notifications - controller?.lockServiceController.lockChanged = { + func lockChanged() { backgroundQueue.asyncAfter(deadline: .now() + 2) { do { try hostController.setNotificationAdvertisement(rssi: 30) // FIXME: RSSI @@ -144,6 +165,9 @@ func run() throws { } } + controller?.lockServiceController.lockChanged = lockChanged + webServer.lockChanged = lockChanged + // make sure the device is always discoverable if #available(macOS 10.12, *) { advertiseTimer = .scheduledTimer(withTimeInterval: 30, repeats: true) { _ in @@ -158,6 +182,11 @@ func run() throws { } } + // start web server + DispatchQueue.global(qos: .userInitiated).async { + webServer.run() + } + // run main loop RunLoop.main.run() } diff --git a/Xcode/CoreLock.xcodeproj/project.pbxproj b/Xcode/CoreLock.xcodeproj/project.pbxproj index 983ef46c..778a8194 100644 --- a/Xcode/CoreLock.xcodeproj/project.pbxproj +++ b/Xcode/CoreLock.xcodeproj/project.pbxproj @@ -12,10 +12,26 @@ 6E0A32D722D8FCFB002EF9DE /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D422D8FCFB002EF9DE /* URL.swift */; }; 6E0A32D822D8FCFB002EF9DE /* URL.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D422D8FCFB002EF9DE /* URL.swift */; }; 6E0A32DA22D91E1D002EF9DE /* URLTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E0A32D922D91E1D002EF9DE /* URLTests.swift */; }; + 6E23152023576F7E00C363EC /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E23151F23576F7E00C363EC /* LockNetService.swift */; }; + 6E23152123576F7E00C363EC /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E23151F23576F7E00C363EC /* LockNetService.swift */; }; + 6E23152223576F7E00C363EC /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E23151F23576F7E00C363EC /* LockNetService.swift */; }; + 6E23152323576F7E00C363EC /* LockNetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E23151F23576F7E00C363EC /* LockNetService.swift */; }; + 6E4729EB235AE7B4007CBC07 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */; }; + 6E4729EC235AE7B4007CBC07 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */; }; + 6E4729ED235AE7B4007CBC07 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */; }; + 6E4729EE235AE7B4007CBC07 /* UpdateRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */; }; + 6E4729FE235B768E007CBC07 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */; }; + 6E4729FF235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */; }; + 6E472A00235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */; }; + 6E472A01235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */; }; 6E621244234721FF007D49BF /* Bluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = 6E621243234721FF007D49BF /* Bluetooth */; }; 6E62124823472217007D49BF /* Bluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = 6E62124723472217007D49BF /* Bluetooth */; }; 6E621266234723ED007D49BF /* Bluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = 6E621265234723ED007D49BF /* Bluetooth */; }; 6E62126A23472400007D49BF /* Bluetooth in Frameworks */ = {isa = PBXBuildFile; productRef = 6E62126923472400007D49BF /* Bluetooth */; }; + 6E87864B2357ACAB008624C1 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E87864A2357ACAB008624C1 /* KeysRequest.swift */; }; + 6E87864C2357ACAB008624C1 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E87864A2357ACAB008624C1 /* KeysRequest.swift */; }; + 6E87864D2357ACAB008624C1 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E87864A2357ACAB008624C1 /* KeysRequest.swift */; }; + 6E87864E2357ACAB008624C1 /* KeysRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E87864A2357ACAB008624C1 /* KeysRequest.swift */; }; 6E93D354231C624300119F65 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D351231C623B00119F65 /* Notification.swift */; }; 6E93D355231C624300119F65 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D351231C623B00119F65 /* Notification.swift */; }; 6E93D356231C624300119F65 /* Notification.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E93D351231C623B00119F65 /* Notification.swift */; }; @@ -40,6 +56,30 @@ 6EACDA9A231B353A000CF82A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EACDA98231B3539000CF82A /* Event.swift */; }; 6EACDA9B231B353A000CF82A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EACDA98231B3539000CF82A /* Event.swift */; }; 6EACDA9C231B353A000CF82A /* Event.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EACDA98231B3539000CF82A /* Event.swift */; }; + 6EBA70F6235790CB0005BEB7 /* Bonjour in Frameworks */ = {isa = PBXBuildFile; productRef = 6EBA70F5235790CB0005BEB7 /* Bonjour */; }; + 6EBA70F8235790D90005BEB7 /* Bonjour in Frameworks */ = {isa = PBXBuildFile; productRef = 6EBA70F7235790D90005BEB7 /* Bonjour */; }; + 6EBA70FA235790E10005BEB7 /* Bonjour in Frameworks */ = {isa = PBXBuildFile; productRef = 6EBA70F9235790E10005BEB7 /* Bonjour */; }; + 6EBA70FC235790E70005BEB7 /* Bonjour in Frameworks */ = {isa = PBXBuildFile; productRef = 6EBA70FB235790E70005BEB7 /* Bonjour */; }; + 6EBA70FE2357A7040005BEB7 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA70FD2357A7040005BEB7 /* URLSession.swift */; }; + 6EBA70FF2357A7040005BEB7 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA70FD2357A7040005BEB7 /* URLSession.swift */; }; + 6EBA71002357A7040005BEB7 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA70FD2357A7040005BEB7 /* URLSession.swift */; }; + 6EBA71012357A7040005BEB7 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA70FD2357A7040005BEB7 /* URLSession.swift */; }; + 6EBA71032357A7B40005BEB7 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */; }; + 6EBA71042357A7B40005BEB7 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */; }; + 6EBA71052357A7B40005BEB7 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */; }; + 6EBA71062357A7B40005BEB7 /* LockInformationRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */; }; + 6EBA71082357A9870005BEB7 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */; }; + 6EBA71092357A9870005BEB7 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */; }; + 6EBA710A2357A9870005BEB7 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */; }; + 6EBA710B2357A9870005BEB7 /* LockInformationResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */; }; + 6EBDB5CB235D5E3200F38CB0 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */; }; + 6EBDB5CC235D5E3200F38CB0 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */; }; + 6EBDB5CD235D5E3200F38CB0 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */; }; + 6EBDB5CE235D5E3200F38CB0 /* EventsRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */; }; + 6EBDB5D0235D7FF900F38CB0 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */; }; + 6EBDB5D1235D7FF900F38CB0 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */; }; + 6EBDB5D2235D7FF900F38CB0 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */; }; + 6EBDB5D3235D7FF900F38CB0 /* EventsResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */; }; 6ED81FE1231662E200B69520 /* CryptoSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FE0231662E200B69520 /* CryptoSwift */; }; 6ED81FE3231662E200B69520 /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FE2231662E200B69520 /* DarwinGATT */; }; 6ED81FE5231662E200B69520 /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FE4231662E200B69520 /* GATT */; }; @@ -52,6 +92,22 @@ 6ED81FF3231662FB00B69520 /* DarwinGATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FF2231662FB00B69520 /* DarwinGATT */; }; 6ED81FF5231662FB00B69520 /* GATT in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FF4231662FB00B69520 /* GATT */; }; 6ED81FF7231662FB00B69520 /* TLVCoding in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED81FF6231662FB00B69520 /* TLVCoding */; }; + 6EE1D6E12357C639004DD856 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E02357C639004DD856 /* KeysResponse.swift */; }; + 6EE1D6E22357C639004DD856 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E02357C639004DD856 /* KeysResponse.swift */; }; + 6EE1D6E32357C639004DD856 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E02357C639004DD856 /* KeysResponse.swift */; }; + 6EE1D6E42357C639004DD856 /* KeysResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E02357C639004DD856 /* KeysResponse.swift */; }; + 6EE1D6EA2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */; }; + 6EE1D6EB2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */; }; + 6EE1D6EC2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */; }; + 6EE1D6ED2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */; }; + 6EE1D6EF2357FAF3004DD856 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */; }; + 6EE1D6F02357FAF3004DD856 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */; }; + 6EE1D6F12357FAF3004DD856 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */; }; + 6EE1D6F22357FAF3004DD856 /* URLSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */; }; + 6EE1D79F235801BF004DD856 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D79E235801BF004DD856 /* EventStore.swift */; }; + 6EE1D7A0235801BF004DD856 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D79E235801BF004DD856 /* EventStore.swift */; }; + 6EE1D7A1235801BF004DD856 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D79E235801BF004DD856 /* EventStore.swift */; }; + 6EE1D7A2235801BF004DD856 /* EventStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EE1D79E235801BF004DD856 /* EventStore.swift */; }; 6EF1C65522C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */; }; 6EF1C65622C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */; }; 6EF1C65722C9AC9F005E9818 /* SetupCharacteristic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */; }; @@ -228,15 +284,28 @@ 52D6DA0F1BF000BD002C0205 /* CoreLock.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = CoreLock.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6E0A32D422D8FCFB002EF9DE /* URL.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URL.swift; sourceTree = ""; }; 6E0A32D922D91E1D002EF9DE /* URLTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLTests.swift; sourceTree = ""; }; + 6E23151F23576F7E00C363EC /* LockNetService.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockNetService.swift; sourceTree = ""; }; + 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UpdateRequest.swift; sourceTree = ""; }; + 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeleteKeyRequest.swift; sourceTree = ""; }; 6E60FF532121041400787DAA /* CryptoTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CryptoTests.swift; sourceTree = ""; }; 6E60FF542121041400787DAA /* GATTProfileTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GATTProfileTests.swift; sourceTree = ""; }; 6E60FF552121041400787DAA /* LinuxMain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LinuxMain.swift; sourceTree = ""; }; 6E60FF572121041400787DAA /* ServerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerTests.swift; sourceTree = ""; }; + 6E87864A2357ACAB008624C1 /* KeysRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysRequest.swift; sourceTree = ""; }; 6E93D351231C623B00119F65 /* Notification.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notification.swift; sourceTree = ""; }; 6E93D352231C623E00119F65 /* ListEventsCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ListEventsCharacteristic.swift; sourceTree = ""; }; 6E93D353231C624000119F65 /* EventsCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EventsCharacteristic.swift; sourceTree = ""; }; 6E9794C323301FC400B5C5C9 /* UUID.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UUID.swift; sourceTree = ""; }; 6EACDA98231B3539000CF82A /* Event.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Event.swift; sourceTree = ""; }; + 6EBA70FD2357A7040005BEB7 /* URLSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; + 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockInformationRequest.swift; sourceTree = ""; }; + 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockInformationResponse.swift; sourceTree = ""; }; + 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsRequest.swift; sourceTree = ""; }; + 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventsResponse.swift; sourceTree = ""; }; + 6EE1D6E02357C639004DD856 /* KeysResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeysResponse.swift; sourceTree = ""; }; + 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreateNewKeyRequest.swift; sourceTree = ""; }; + 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLSession.swift; sourceTree = ""; }; + 6EE1D79E235801BF004DD856 /* EventStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventStore.swift; sourceTree = ""; }; 6EF1C62E22C9AC9D005E9818 /* SetupCharacteristic.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SetupCharacteristic.swift; sourceTree = ""; }; 6EF1C62F22C9AC9D005E9818 /* LockConfiguration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = LockConfiguration.swift; sourceTree = ""; }; 6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = EncryptedData.swift; sourceTree = ""; }; @@ -286,6 +355,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6EBA70F6235790CB0005BEB7 /* Bonjour in Frameworks */, 6E621244234721FF007D49BF /* Bluetooth in Frameworks */, 6E9C95ED231149A5007C18FE /* GATT in Frameworks */, 6E9C95EA23114962007C18FE /* CryptoSwift in Frameworks */, @@ -298,6 +368,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6EBA70FA235790E10005BEB7 /* Bonjour in Frameworks */, 6E621266234723ED007D49BF /* Bluetooth in Frameworks */, 6ED81FED231662F100B69520 /* GATT in Frameworks */, 6ED81FE9231662F100B69520 /* CryptoSwift in Frameworks */, @@ -310,6 +381,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6EBA70FC235790E70005BEB7 /* Bonjour in Frameworks */, 6E62126A23472400007D49BF /* Bluetooth in Frameworks */, 6ED81FF5231662FB00B69520 /* GATT in Frameworks */, 6ED81FF1231662FB00B69520 /* CryptoSwift in Frameworks */, @@ -322,6 +394,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6EBA70F8235790D90005BEB7 /* Bonjour in Frameworks */, 6E62124823472217007D49BF /* Bluetooth in Frameworks */, 6ED81FE5231662E200B69520 /* GATT in Frameworks */, 6ED81FE1231662E200B69520 /* CryptoSwift in Frameworks */, @@ -427,6 +500,7 @@ 6EF1C64022C9AC9E005E9818 /* Crypto.swift */, 6EF1C65122C9AC9F005E9818 /* DateComponents.swift */, 6EF1C64B22C9AC9E005E9818 /* DeviceManager.swift */, + 6EBA70FD2357A7040005BEB7 /* URLSession.swift */, 6EF1C63022C9AC9D005E9818 /* EncryptedData.swift */, 6EF1C64622C9AC9E005E9818 /* GATTError.swift */, 6EF1C64322C9AC9E005E9818 /* GATTProfile.swift */, @@ -437,11 +511,19 @@ 6EF1C63322C9AC9D005E9818 /* ListKeysCharacteristic.swift */, 6E93D353231C624000119F65 /* EventsCharacteristic.swift */, 6E93D352231C623E00119F65 /* ListEventsCharacteristic.swift */, + 6EBDB5CA235D5E3200F38CB0 /* EventsRequest.swift */, + 6EBDB5CF235D7FF900F38CB0 /* EventsResponse.swift */, 6E93D351231C623B00119F65 /* Notification.swift */, 6EF1C62F22C9AC9D005E9818 /* LockConfiguration.swift */, 6EF1C65222C9AC9F005E9818 /* LockHardware.swift */, 6EF1C64822C9AC9E005E9818 /* LockInformationCharacteristic.swift */, 6EF1C64222C9AC9E005E9818 /* LockModel.swift */, + 6E23151F23576F7E00C363EC /* LockNetService.swift */, + 6EBA71022357A7B40005BEB7 /* LockInformationRequest.swift */, + 6EBA71072357A9870005BEB7 /* LockInformationResponse.swift */, + 6E87864A2357ACAB008624C1 /* KeysRequest.swift */, + 6EE1D6E02357C639004DD856 /* KeysResponse.swift */, + 6EE1D6E92357F1E9004DD856 /* CreateNewKeyRequest.swift */, 6EF1C64422C9AC9E005E9818 /* LockState.swift */, 6EF1C63422C9AC9D005E9818 /* NewKey.swift */, 6EF1C64E22C9AC9F005E9818 /* Permission.swift */, @@ -456,9 +538,13 @@ 6EF1C65322C9AC9F005E9818 /* UnlockAction.swift */, 6EF1C64722C9AC9E005E9818 /* UnlockCharacteristic.swift */, 6EF1C64922C9AC9E005E9818 /* Version.swift */, - 6E0A32D422D8FCFB002EF9DE /* URL.swift */, 6EACDA98231B3539000CF82A /* Event.swift */, + 6EE1D79E235801BF004DD856 /* EventStore.swift */, + 6E0A32D422D8FCFB002EF9DE /* URL.swift */, + 6EE1D6EE2357FAF2004DD856 /* URLSession.swift */, 6E9794C323301FC400B5C5C9 /* UUID.swift */, + 6E4729EA235AE7B3007CBC07 /* UpdateRequest.swift */, + 6E4729FD235B768E007CBC07 /* DeleteKeyRequest.swift */, ); path = CoreLock; sourceTree = ""; @@ -540,6 +626,7 @@ 6E9C95EE231149A5007C18FE /* DarwinGATT */, 6E9C95F1231149F5007C18FE /* TLVCoding */, 6E621243234721FF007D49BF /* Bluetooth */, + 6EBA70F5235790CB0005BEB7 /* Bonjour */, ); productName = SmartLock; productReference = 52D6D97C1BEFF229002C0205 /* CoreLock.framework */; @@ -565,6 +652,7 @@ 6ED81FEC231662F100B69520 /* GATT */, 6ED81FEE231662F100B69520 /* TLVCoding */, 6E621265234723ED007D49BF /* Bluetooth */, + 6EBA70F9235790E10005BEB7 /* Bonjour */, ); productName = "SmartLock-watchOS"; productReference = 52D6D9E21BEFFF6E002C0205 /* CoreLock.framework */; @@ -590,6 +678,7 @@ 6ED81FF4231662FB00B69520 /* GATT */, 6ED81FF6231662FB00B69520 /* TLVCoding */, 6E62126923472400007D49BF /* Bluetooth */, + 6EBA70FB235790E70005BEB7 /* Bonjour */, ); productName = "SmartLock-tvOS"; productReference = 52D6D9F01BEFFFBE002C0205 /* CoreLock.framework */; @@ -615,6 +704,7 @@ 6ED81FE4231662E200B69520 /* GATT */, 6ED81FE6231662E200B69520 /* TLVCoding */, 6E62124723472217007D49BF /* Bluetooth */, + 6EBA70F7235790D90005BEB7 /* Bonjour */, ); productName = "SmartLock-macOS"; productReference = 52D6DA0F1BF000BD002C0205 /* CoreLock.framework */; @@ -684,6 +774,7 @@ 6E9C95EB231149A5007C18FE /* XCRemoteSwiftPackageReference "GATT" */, 6E9C95F0231149F5007C18FE /* XCRemoteSwiftPackageReference "TLVCoding" */, 6E621242234721FF007D49BF /* XCRemoteSwiftPackageReference "Bluetooth" */, + 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */, ); productRefGroup = 52D6D97D1BEFF229002C0205 /* Products */; projectDirPath = ""; @@ -749,28 +840,40 @@ 6EF1C6CD22C9AC9F005E9818 /* TLV.swift in Sources */, 6EF1C6D922C9AC9F005E9818 /* Integer.swift in Sources */, 6E93D358231C624300119F65 /* ListEventsCharacteristic.swift in Sources */, + 6E23152023576F7E00C363EC /* LockNetService.swift in Sources */, 6EF1C69922C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */, 6EF1C6A522C9AC9F005E9818 /* LockModel.swift in Sources */, + 6E87864B2357ACAB008624C1 /* KeysRequest.swift in Sources */, 6EF1C69122C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */, + 6EBA70FE2357A7040005BEB7 /* URLSession.swift in Sources */, + 6E4729FE235B768E007CBC07 /* DeleteKeyRequest.swift in Sources */, 6EF1C68522C9AC9F005E9818 /* GitCommits.swift in Sources */, 6EF1C6DD22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */, + 6EBA71032357A7B40005BEB7 /* LockInformationRequest.swift in Sources */, 6EF1C69D22C9AC9F005E9818 /* Crypto.swift in Sources */, 6EF1C6E522C9AC9F005E9818 /* LockHardware.swift in Sources */, + 6EE1D6EF2357FAF3004DD856 /* URLSession.swift in Sources */, 6EF1C6B522C9AC9F005E9818 /* GATTError.swift in Sources */, 6E0A32D522D8FCFB002EF9DE /* URL.swift in Sources */, 6EF1C6A922C9AC9F005E9818 /* GATTProfile.swift in Sources */, 6EF1C6BD22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */, 6EF1C66122C9AC9F005E9818 /* SecureData.swift in Sources */, + 6E4729EB235AE7B4007CBC07 /* UpdateRequest.swift in Sources */, 6EF1C67122C9AC9F005E9818 /* Advertisement.swift in Sources */, 6EF1C6C922C9AC9F005E9818 /* DeviceManager.swift in Sources */, 6EF1C6D122C9AC9F005E9818 /* Authentication.swift in Sources */, 6EF1C66D22C9AC9F005E9818 /* NewKey.swift in Sources */, + 6EE1D6EA2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */, 6EF1C67922C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */, + 6EBA71082357A9870005BEB7 /* LockInformationResponse.swift in Sources */, 6EF1C69522C9AC9F005E9818 /* Key.swift in Sources */, 6EF1C68922C9AC9F005E9818 /* POSIXTime.swift in Sources */, 6EF1C68122C9AC9F005E9818 /* Central.swift in Sources */, 6EF1C6C522C9AC9F005E9818 /* Status.swift in Sources */, 6EF1C6C122C9AC9F005E9818 /* Version.swift in Sources */, + 6EE1D6E12357C639004DD856 /* KeysResponse.swift in Sources */, + 6EE1D79F235801BF004DD856 /* EventStore.swift in Sources */, + 6EBDB5D0235D7FF900F38CB0 /* EventsResponse.swift in Sources */, 6EACDA99231B353A000CF82A /* Event.swift in Sources */, 6E93D35C231C624300119F65 /* EventsCharacteristic.swift in Sources */, 6EF1C65D22C9AC9F005E9818 /* EncryptedData.swift in Sources */, @@ -783,6 +886,7 @@ 6EF1C66522C9AC9F005E9818 /* AuthenticationError.swift in Sources */, 6EF1C66922C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */, 6E93D354231C624300119F65 /* Notification.swift in Sources */, + 6EBDB5CB235D5E3200F38CB0 /* EventsRequest.swift in Sources */, 6EF1C6B922C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */, 6EF1C65922C9AC9F005E9818 /* LockConfiguration.swift in Sources */, 6EF1C6D522C9AC9F005E9818 /* Permission.swift in Sources */, @@ -801,28 +905,40 @@ 6EF1C6CF22C9AC9F005E9818 /* TLV.swift in Sources */, 6EF1C6DB22C9AC9F005E9818 /* Integer.swift in Sources */, 6E93D35A231C624300119F65 /* ListEventsCharacteristic.swift in Sources */, + 6E23152223576F7E00C363EC /* LockNetService.swift in Sources */, 6EF1C69B22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */, 6EF1C6A722C9AC9F005E9818 /* LockModel.swift in Sources */, + 6E87864D2357ACAB008624C1 /* KeysRequest.swift in Sources */, 6EF1C69322C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */, + 6EBA71002357A7040005BEB7 /* URLSession.swift in Sources */, + 6E472A00235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */, 6EF1C68722C9AC9F005E9818 /* GitCommits.swift in Sources */, 6EF1C6DF22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */, + 6EBA71052357A7B40005BEB7 /* LockInformationRequest.swift in Sources */, 6EF1C69F22C9AC9F005E9818 /* Crypto.swift in Sources */, 6EF1C6E722C9AC9F005E9818 /* LockHardware.swift in Sources */, + 6EE1D6F12357FAF3004DD856 /* URLSession.swift in Sources */, 6EF1C6B722C9AC9F005E9818 /* GATTError.swift in Sources */, 6E0A32D722D8FCFB002EF9DE /* URL.swift in Sources */, 6EF1C6AB22C9AC9F005E9818 /* GATTProfile.swift in Sources */, 6EF1C6BF22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */, 6EF1C66322C9AC9F005E9818 /* SecureData.swift in Sources */, + 6E4729ED235AE7B4007CBC07 /* UpdateRequest.swift in Sources */, 6EF1C67322C9AC9F005E9818 /* Advertisement.swift in Sources */, 6EF1C6CB22C9AC9F005E9818 /* DeviceManager.swift in Sources */, 6EF1C6D322C9AC9F005E9818 /* Authentication.swift in Sources */, 6EF1C66F22C9AC9F005E9818 /* NewKey.swift in Sources */, + 6EE1D6EC2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */, 6EF1C67B22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */, + 6EBA710A2357A9870005BEB7 /* LockInformationResponse.swift in Sources */, 6EF1C69722C9AC9F005E9818 /* Key.swift in Sources */, 6EF1C68B22C9AC9F005E9818 /* POSIXTime.swift in Sources */, 6EF1C68322C9AC9F005E9818 /* Central.swift in Sources */, 6EF1C6C722C9AC9F005E9818 /* Status.swift in Sources */, 6EF1C6C322C9AC9F005E9818 /* Version.swift in Sources */, + 6EE1D6E32357C639004DD856 /* KeysResponse.swift in Sources */, + 6EE1D7A1235801BF004DD856 /* EventStore.swift in Sources */, + 6EBDB5D2235D7FF900F38CB0 /* EventsResponse.swift in Sources */, 6EACDA9B231B353A000CF82A /* Event.swift in Sources */, 6E93D35E231C624300119F65 /* EventsCharacteristic.swift in Sources */, 6EF1C65F22C9AC9F005E9818 /* EncryptedData.swift in Sources */, @@ -835,6 +951,7 @@ 6EF1C66722C9AC9F005E9818 /* AuthenticationError.swift in Sources */, 6EF1C66B22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */, 6E93D356231C624300119F65 /* Notification.swift in Sources */, + 6EBDB5CD235D5E3200F38CB0 /* EventsRequest.swift in Sources */, 6EF1C6BB22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */, 6EF1C65B22C9AC9F005E9818 /* LockConfiguration.swift in Sources */, 6EF1C6D722C9AC9F005E9818 /* Permission.swift in Sources */, @@ -853,28 +970,40 @@ 6EF1C6D022C9AC9F005E9818 /* TLV.swift in Sources */, 6EF1C6DC22C9AC9F005E9818 /* Integer.swift in Sources */, 6E93D35B231C624300119F65 /* ListEventsCharacteristic.swift in Sources */, + 6E23152323576F7E00C363EC /* LockNetService.swift in Sources */, 6EF1C69C22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */, 6EF1C6A822C9AC9F005E9818 /* LockModel.swift in Sources */, + 6E87864E2357ACAB008624C1 /* KeysRequest.swift in Sources */, 6EF1C69422C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */, + 6EBA71012357A7040005BEB7 /* URLSession.swift in Sources */, + 6E472A01235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */, 6EF1C68822C9AC9F005E9818 /* GitCommits.swift in Sources */, 6EF1C6E022C9AC9F005E9818 /* SmartLockProfile.swift in Sources */, + 6EBA71062357A7B40005BEB7 /* LockInformationRequest.swift in Sources */, 6EF1C6A022C9AC9F005E9818 /* Crypto.swift in Sources */, 6EF1C6E822C9AC9F005E9818 /* LockHardware.swift in Sources */, + 6EE1D6F22357FAF3004DD856 /* URLSession.swift in Sources */, 6EF1C6B822C9AC9F005E9818 /* GATTError.swift in Sources */, 6E0A32D822D8FCFB002EF9DE /* URL.swift in Sources */, 6EF1C6AC22C9AC9F005E9818 /* GATTProfile.swift in Sources */, 6EF1C6C022C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */, 6EF1C66422C9AC9F005E9818 /* SecureData.swift in Sources */, + 6E4729EE235AE7B4007CBC07 /* UpdateRequest.swift in Sources */, 6EF1C67422C9AC9F005E9818 /* Advertisement.swift in Sources */, 6EF1C6CC22C9AC9F005E9818 /* DeviceManager.swift in Sources */, 6EF1C6D422C9AC9F005E9818 /* Authentication.swift in Sources */, 6EF1C67022C9AC9F005E9818 /* NewKey.swift in Sources */, + 6EE1D6ED2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */, 6EF1C67C22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */, + 6EBA710B2357A9870005BEB7 /* LockInformationResponse.swift in Sources */, 6EF1C69822C9AC9F005E9818 /* Key.swift in Sources */, 6EF1C68C22C9AC9F005E9818 /* POSIXTime.swift in Sources */, 6EF1C68422C9AC9F005E9818 /* Central.swift in Sources */, 6EF1C6C822C9AC9F005E9818 /* Status.swift in Sources */, 6EF1C6C422C9AC9F005E9818 /* Version.swift in Sources */, + 6EE1D6E42357C639004DD856 /* KeysResponse.swift in Sources */, + 6EE1D7A2235801BF004DD856 /* EventStore.swift in Sources */, + 6EBDB5D3235D7FF900F38CB0 /* EventsResponse.swift in Sources */, 6EACDA9C231B353A000CF82A /* Event.swift in Sources */, 6E93D35F231C624300119F65 /* EventsCharacteristic.swift in Sources */, 6EF1C66022C9AC9F005E9818 /* EncryptedData.swift in Sources */, @@ -887,6 +1016,7 @@ 6EF1C66822C9AC9F005E9818 /* AuthenticationError.swift in Sources */, 6EF1C66C22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */, 6E93D357231C624300119F65 /* Notification.swift in Sources */, + 6EBDB5CE235D5E3200F38CB0 /* EventsRequest.swift in Sources */, 6EF1C6BC22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */, 6EF1C65C22C9AC9F005E9818 /* LockConfiguration.swift in Sources */, 6EF1C6D822C9AC9F005E9818 /* Permission.swift in Sources */, @@ -905,28 +1035,40 @@ 6EF1C6CE22C9AC9F005E9818 /* TLV.swift in Sources */, 6EF1C6DA22C9AC9F005E9818 /* Integer.swift in Sources */, 6E93D359231C624300119F65 /* ListEventsCharacteristic.swift in Sources */, + 6E23152123576F7E00C363EC /* LockNetService.swift in Sources */, 6EF1C69A22C9AC9F005E9818 /* RemoveKeyCharacteristic.swift in Sources */, 6EF1C6A622C9AC9F005E9818 /* LockModel.swift in Sources */, + 6E87864C2357ACAB008624C1 /* KeysRequest.swift in Sources */, 6EF1C69222C9AC9F005E9818 /* ConfirmNewKeyCharacteristic.swift in Sources */, + 6EBA70FF2357A7040005BEB7 /* URLSession.swift in Sources */, + 6E4729FF235B768F007CBC07 /* DeleteKeyRequest.swift in Sources */, 6EF1C68622C9AC9F005E9818 /* GitCommits.swift in Sources */, 6EF1C6DE22C9AC9F005E9818 /* SmartLockProfile.swift in Sources */, + 6EBA71042357A7B40005BEB7 /* LockInformationRequest.swift in Sources */, 6EF1C69E22C9AC9F005E9818 /* Crypto.swift in Sources */, 6EF1C6E622C9AC9F005E9818 /* LockHardware.swift in Sources */, + 6EE1D6F02357FAF3004DD856 /* URLSession.swift in Sources */, 6EF1C6B622C9AC9F005E9818 /* GATTError.swift in Sources */, 6E0A32D622D8FCFB002EF9DE /* URL.swift in Sources */, 6EF1C6AA22C9AC9F005E9818 /* GATTProfile.swift in Sources */, 6EF1C6BE22C9AC9F005E9818 /* LockInformationCharacteristic.swift in Sources */, 6EF1C66222C9AC9F005E9818 /* SecureData.swift in Sources */, + 6E4729EC235AE7B4007CBC07 /* UpdateRequest.swift in Sources */, 6EF1C67222C9AC9F005E9818 /* Advertisement.swift in Sources */, 6EF1C6CA22C9AC9F005E9818 /* DeviceManager.swift in Sources */, 6EF1C6D222C9AC9F005E9818 /* Authentication.swift in Sources */, 6EF1C66E22C9AC9F005E9818 /* NewKey.swift in Sources */, + 6EE1D6EB2357F1E9004DD856 /* CreateNewKeyRequest.swift in Sources */, 6EF1C67A22C9AC9F005E9818 /* KeysCharacteristic.swift in Sources */, + 6EBA71092357A9870005BEB7 /* LockInformationResponse.swift in Sources */, 6EF1C69622C9AC9F005E9818 /* Key.swift in Sources */, 6EF1C68A22C9AC9F005E9818 /* POSIXTime.swift in Sources */, 6EF1C68222C9AC9F005E9818 /* Central.swift in Sources */, 6EF1C6C622C9AC9F005E9818 /* Status.swift in Sources */, 6EF1C6C222C9AC9F005E9818 /* Version.swift in Sources */, + 6EE1D6E22357C639004DD856 /* KeysResponse.swift in Sources */, + 6EE1D7A0235801BF004DD856 /* EventStore.swift in Sources */, + 6EBDB5D1235D7FF900F38CB0 /* EventsResponse.swift in Sources */, 6EACDA9A231B353A000CF82A /* Event.swift in Sources */, 6E93D35D231C624300119F65 /* EventsCharacteristic.swift in Sources */, 6EF1C65E22C9AC9F005E9818 /* EncryptedData.swift in Sources */, @@ -939,6 +1081,7 @@ 6EF1C66622C9AC9F005E9818 /* AuthenticationError.swift in Sources */, 6EF1C66A22C9AC9F005E9818 /* ListKeysCharacteristic.swift in Sources */, 6E93D355231C624300119F65 /* Notification.swift in Sources */, + 6EBDB5CC235D5E3200F38CB0 /* EventsRequest.swift in Sources */, 6EF1C6BA22C9AC9F005E9818 /* UnlockCharacteristic.swift in Sources */, 6EF1C65A22C9AC9F005E9818 /* LockConfiguration.swift in Sources */, 6EF1C6D622C9AC9F005E9818 /* Permission.swift in Sources */, @@ -1407,6 +1550,14 @@ kind = branch; }; }; + 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "git@github.com:PureSwift/Bonjour.git"; + requirement = { + branch = master; + kind = branch; + }; + }; /* End XCRemoteSwiftPackageReference section */ /* Begin XCSwiftPackageProductDependency section */ @@ -1450,6 +1601,26 @@ package = 6E9C95F0231149F5007C18FE /* XCRemoteSwiftPackageReference "TLVCoding" */; productName = TLVCoding; }; + 6EBA70F5235790CB0005BEB7 /* Bonjour */ = { + isa = XCSwiftPackageProductDependency; + package = 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */; + productName = Bonjour; + }; + 6EBA70F7235790D90005BEB7 /* Bonjour */ = { + isa = XCSwiftPackageProductDependency; + package = 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */; + productName = Bonjour; + }; + 6EBA70F9235790E10005BEB7 /* Bonjour */ = { + isa = XCSwiftPackageProductDependency; + package = 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */; + productName = Bonjour; + }; + 6EBA70FB235790E70005BEB7 /* Bonjour */ = { + isa = XCSwiftPackageProductDependency; + package = 6EBA70F4235790CB0005BEB7 /* XCRemoteSwiftPackageReference "Bonjour" */; + productName = Bonjour; + }; 6ED81FE0231662E200B69520 /* CryptoSwift */ = { isa = XCSwiftPackageProductDependency; package = 6E9C95E823114962007C18FE /* XCRemoteSwiftPackageReference "CryptoSwift" */; diff --git a/iOS/LockKit/Base.lproj/Intents.intentdefinition b/iOS/LockKit/Base.lproj/Intents.intentdefinition index 22882e47..f2beda58 100644 --- a/iOS/LockKit/Base.lproj/Intents.intentdefinition +++ b/iOS/LockKit/Base.lproj/Intents.intentdefinition @@ -9,11 +9,11 @@ INIntentDefinitionNamespace 9Yc029 INIntentDefinitionSystemVersion - 18G87 + 19A602 INIntentDefinitionToolsBuildVersion - 11M392q + 11A1027 INIntentDefinitionToolsVersion - 11.0 + 11.1 INIntents @@ -38,7 +38,7 @@ INIntentParameterCombinationSupportsBackgroundExecution INIntentParameterCombinationTitle - Unlock `${lock}` + Unlock ${lock} INIntentParameterCombinationTitleID O5ezH4 INIntentParameterCombinationUpdatesLinked @@ -53,10 +53,14 @@ INIntentParameterCombinationIsPrimary + INIntentParameterCombinationSubtitle + Unlock the specified lock. + INIntentParameterCombinationSubtitleID + bJj0q0 INIntentParameterCombinationSupportsBackgroundExecution INIntentParameterCombinationTitle - Unlock `${lock}` + Unlock ${lock} INIntentParameterCombinationTitleID dAVo2n @@ -94,7 +98,7 @@ INIntentParameterPromptDialogCustom INIntentParameterPromptDialogFormatString - You have ${count} locks matching ‘${lock}’. + You have ${count} locks matching ${lock}. INIntentParameterPromptDialogFormatStringID FwuKZ6 INIntentParameterPromptDialogType @@ -112,7 +116,7 @@ INIntentParameterPromptDialogCustom INIntentParameterPromptDialogFormatString - Just to confirm, you want to unlock ‘${lock}’? + Just to confirm, you want to unlock ${lock}? INIntentParameterPromptDialogFormatStringID FhxgkI INIntentParameterPromptDialogType @@ -135,7 +139,7 @@ INIntentParameterUnsupportedReasonCustom INIntentParameterUnsupportedReasonFormatString - `${displayName}` is not in range. Please move closer to ${displayName}. + ${displayName} is not in range. Please move closer to ${displayName}. INIntentParameterUnsupportedReasonFormatStringID bvi2l8 @@ -145,7 +149,7 @@ INIntentParameterUnsupportedReasonCustom INIntentParameterUnsupportedReasonFormatString - You do not have a key for `${displayName}`. + You do not have a key for ${displayName}. INIntentParameterUnsupportedReasonFormatStringID vARXDT @@ -155,7 +159,7 @@ INIntentParameterUnsupportedReasonCustom INIntentParameterUnsupportedReasonFormatString - You can only unlock `${displayName}` during the specified schedule. + You can only unlock ${displayName} during the specified schedule. INIntentParameterUnsupportedReasonFormatStringID wkobUh @@ -168,7 +172,7 @@ INIntentResponseCodeConciseFormatString - Unlocked ‘${lock}‘. + Unlocked ${lock}. INIntentResponseCodeConciseFormatStringID claxan INIntentResponseCodeFormatString diff --git a/iOS/LockKit/Controller/ContactsViewController.swift b/iOS/LockKit/Controller/ContactsViewController.swift index dae5457a..d2cad165 100644 --- a/iOS/LockKit/Controller/ContactsViewController.swift +++ b/iOS/LockKit/Controller/ContactsViewController.swift @@ -171,9 +171,8 @@ public final class ContactsViewController: TableViewController { let image: UIImage if let contactImage = managedObject.image.flatMap({ UIImage(data: $0) }) { image = contactImage - } else if #available(iOS 13, *), - let systemImage = UIImage(systemName: "person.crop.circle.fill") { - image = systemImage + } else if #available(iOS 13, *) { + image = UIImage(systemSymbol: .personCropCircleFill) } else { image = UIImage(permission: .admin) } diff --git a/iOS/LockKit/Controller/Extensions/ContextMenu.swift b/iOS/LockKit/Controller/Extensions/ContextMenu.swift index e7acfbf5..950e17af 100644 --- a/iOS/LockKit/Controller/Extensions/ContextMenu.swift +++ b/iOS/LockKit/Controller/Extensions/ContextMenu.swift @@ -8,6 +8,8 @@ import Foundation import UIKit +import IntentsUI +import SFSafeSymbols @available(iOSApplicationExtension 13.0, *) public extension UIViewController { @@ -24,22 +26,12 @@ public extension UIViewController { children: [] ) } - - let rename = UIAction(title: R.string.contextMenu.itemRename(), image: UIImage(systemName: "square.and.pencil")) { [weak self] (action) in - let alert = RenameActivity.viewController(for: lock) { _ in } - self?.present(alert, animated: true, completion: nil) - } - - let delete = UIAction(title: R.string.contextMenu.itemDelete(), image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] (action) in - let alert = DeleteLockActivity.viewController(for: lock) { _ in } - self?.present(alert, animated: true, completion: nil) - } - + var actions = [UIAction]() if cache.key.permission.isAdministrator { - let share = UIAction(title: R.string.contextMenu.itemShareKey(), image: UIImage(systemName: "square.and.arrow.up")) { [weak self] (action) in + let share = UIAction(title: R.string.contextMenu.itemShareKey(), image: UIImage(systemSymbol: .squareAndArrowUp)) { [weak self] (action) in let viewController = NewKeySelectPermissionViewController.fromStoryboard(with: lock) viewController.completion = { guard let (invitation, sender) = $0 else { @@ -54,7 +46,7 @@ public extension UIViewController { actions.append(share) - let manageKeys = UIAction(title: R.string.contextMenu.itemManage(), image: UIImage(systemName: "list.bullet")) { [weak self] (action) in + let manageKeys = UIAction(title: R.string.contextMenu.itemManage(), image: UIImage(systemSymbol: .listBullet)) { [weak self] (action) in let viewController = LockPermissionsViewController.fromStoryboard( with: lock, completion: { self?.dismiss(animated: true, completion: nil) } @@ -66,7 +58,44 @@ public extension UIViewController { actions.append(manageKeys) } + #if canImport(IntentsUI) && !targetEnvironment(macCatalyst) + if let delegate = self as? INUIAddVoiceShortcutViewControllerDelegate { + + let siri = UIAction(title: R.string.contextMenu.itemSiriShortcut(), image: UIImage(systemSymbol: .micFill)) { [weak self] (action) in + let siriViewController = INUIAddVoiceShortcutViewController( + unlock: lock, + cache: cache, + delegate: delegate + ) + self?.present(siriViewController, animated: true, completion: nil) + } + + actions.append(siri) + } + #endif + + let rename = UIAction(title: R.string.contextMenu.itemRename(), image: UIImage(systemSymbol: .squareAndPencil)) { [weak self] (action) in + let alert = RenameActivity.viewController(for: lock) { _ in } + self?.present(alert, animated: true, completion: nil) + } + actions.append(rename) + + if cache.key.permission.isAdministrator, + let viewController = self as? (UIViewController & ActivityIndicatorViewController) { + + let update = UIAction(title: R.string.activity.updateActivityTitle(), image: UIImage(systemSymbol: .squareAndArrowDown)) { (action) in + viewController.update(lock: lock) + } + + actions.append(update) + } + + let delete = UIAction(title: R.string.contextMenu.itemDelete(), image: UIImage(systemSymbol: .trash), attributes: .destructive) { [weak self] (action) in + let alert = DeleteLockActivity.viewController(for: lock) { _ in } + self?.present(alert, animated: true, completion: nil) + } + actions.append(delete) return UIMenu( diff --git a/iOS/LockKit/Controller/Extensions/Update.swift b/iOS/LockKit/Controller/Extensions/Update.swift new file mode 100644 index 00000000..877b6ecc --- /dev/null +++ b/iOS/LockKit/Controller/Extensions/Update.swift @@ -0,0 +1,46 @@ +// +// Update.swift +// LockKit +// +// Created by Alsey Coleman Miller on 10/20/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import UIKit +import CoreLock + +public extension ActivityIndicatorViewController where Self: UIViewController { + + func update(lock identifier: UUID) { + + guard let key = Store.shared.credentials(for: identifier) + else { assertionFailure(); return } + + let alert = UIAlertController(title: R.string.activity.updateActivityAlertTitle(), + message: R.string.activity.updateActivityAlertMessage(), + preferredStyle: .alert) + + alert.addAction(UIAlertAction(title: R.string.activity.updateActivityAlertCancel(), style: .cancel, handler: { _ in + + alert.dismiss(animated: true) { } + })) + + alert.addAction(UIAlertAction(title: R.string.activity.updateActivityAlertUpdate(), style: .`default`, handler: { [unowned self] _ in + + alert.dismiss(animated: true) { } + + self.performActivity(queue: .app, { + + let client = Store.shared.netServiceClient + + guard let netService = try client.discover(duration: 2.0, timeout: 10.0).first(where: { $0.identifier == identifier }) + else { throw LockError.notInRange(lock: identifier) } + + try client.update(for: netService, with: key, timeout: 30.0) + }) + })) + + self.present(alert, animated: true, completion: nil) + } +} diff --git a/iOS/LockKit/Controller/LockEventsViewController.swift b/iOS/LockKit/Controller/LockEventsViewController.swift index b0c2ecbb..22e9b272 100644 --- a/iOS/LockKit/Controller/LockEventsViewController.swift +++ b/iOS/LockKit/Controller/LockEventsViewController.swift @@ -140,8 +140,8 @@ public final class LockEventsViewController: TableViewController { private func reloadData() { - typealias FetchRequest = ListEventsCharacteristic.FetchRequest - typealias Predicate = ListEventsCharacteristic.Predicate + typealias FetchRequest = LockEvent.FetchRequest + typealias Predicate = LockEvent.Predicate // load keys if neccesary if needsKeys.isEmpty == false { @@ -167,42 +167,46 @@ public final class LockEventsViewController: TableViewController { let locks = self.locks let context = Store.shared.backgroundContext - if Store.shared.lockManager.central.state == .poweredOn { - performActivity(queue: .bluetooth, { [weak self] in - for lock in locks { - guard let device = try Store.shared.device(for: lock, scanDuration: 1.0) else { - if self?.lock == nil { - continue - } else { - throw CentralError.unknownPeripheral - } - } - let lastEventDate = try context.performErrorBlockAndWait { - try context.find(identifier: lock, type: LockManagedObject.self) - .flatMap { try $0.lastEvent(in: context)?.date } - } - let fetchRequest = FetchRequest( - offset: 0, - limit: nil, - predicate: Predicate( - keys: nil, - start: lastEventDate, - end: nil - ) + performActivity(queue: .app, { [weak self] in + guard let self = self else { return } + let expectsLock = self.lock != nil + for lock in locks { + + // fetch request + let lastEventDate = try context.performErrorBlockAndWait { + try context.find(identifier: lock, type: LockManagedObject.self) + .flatMap { try $0.lastEvent(in: context)?.date } + } + let fetchRequest = FetchRequest( + offset: 0, + limit: nil, + predicate: Predicate( + keys: nil, + start: lastEventDate, + end: nil ) - do { try Store.shared.listEvents(device, fetchRequest: fetchRequest) } - catch { - if self?.lock == nil { - continue - } else { - throw error - } + ) + // first try via Bonjour + if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lock }) { + + try Store.shared.listEvents(netService, fetchRequest: fetchRequest) + + } else if Store.shared.lockManager.central.state == .poweredOn, + let device = try DispatchQueue.bluetooth.sync(execute: { try Store.shared.device(for: lock, scanDuration: 2.0) }) { + + try DispatchQueue.bluetooth.sync { + _ = try Store.shared.listEvents(device, fetchRequest: fetchRequest) } + + } else if expectsLock { + throw LockError.notInRange(lock: lock) + } else { + continue } - }, completion: { (viewController, _) in - viewController.needsKeys.removeAll() - }) - } + } + }, completion: { (viewController, _) in + viewController.needsKeys.removeAll() + }) } private subscript (indexPath: IndexPath) -> EventManagedObject { diff --git a/iOS/LockKit/Controller/LockPermissionsViewController.swift b/iOS/LockKit/Controller/LockPermissionsViewController.swift index 2a24538e..c16d7c5d 100644 --- a/iOS/LockKit/Controller/LockPermissionsViewController.swift +++ b/iOS/LockKit/Controller/LockPermissionsViewController.swift @@ -77,13 +77,47 @@ public final class LockPermissionsViewController: UITableViewController { guard let lockIdentifier = self.lockIdentifier else { assertionFailure(); return } - - performActivity(queue: .bluetooth, { - guard let peripheral = try Store.shared.device(for: lockIdentifier, scanDuration: 1.0) - else { throw LockError.notInRange(lock: lockIdentifier) } - try Store.shared.listKeys(peripheral, notification: { (list, isComplete) in - mainQueue { [weak self] in self?.list = list } - }) + + performActivity(queue: .app, { + + // get lock key + guard let lockCache = Store.shared[lock: lockIdentifier] + else { throw LockError.noKey(lock: lockIdentifier) } + + guard lockCache.key.permission.isAdministrator + else { throw LockError.notAdmin(lock: lockIdentifier) } + + guard let keyData = Store.shared[key: lockCache.key.identifier] else { + assertionFailure("Missing from Keychain") + throw LockError.noKey(lock: lockIdentifier) + } + + let key = KeyCredentials( + identifier: lockCache.key.identifier, + secret: keyData + ) + + // attempt to load via Bonjour + let servers = (try? Store.shared.netServiceClient.discover(duration: 1.0, timeout: 3.0)) ?? [] + if let netService = servers.first(where: { $0.identifier == lockIdentifier }) { + let list = try Store.shared.netServiceClient.listKeys( + for: netService, + with: key, + timeout: 30.0 + ) + mainQueue { [weak self] in + self?.list = list + } + } else { + // attempt to load via Bluetooth + try DispatchQueue.bluetooth.sync { + guard let peripheral = try Store.shared.device(for: lockIdentifier, scanDuration: 1.0) + else { throw LockError.notInRange(lock: lockIdentifier) } + try Store.shared.listKeys(peripheral, notification: { (list, isComplete) in + mainQueue { [weak self] in self?.list = list } + }) + } + } }) } @@ -216,33 +250,42 @@ public final class LockPermissionsViewController: UITableViewController { alert.dismiss(animated: true, completion: nil) })) - alert.addAction(UIAlertAction(title: R.string.localizable.alertDelete(), style: .destructive, handler: { (UIAlertAction) in + alert.addAction(UIAlertAction(title: R.string.localizable.alertDelete(), style: .destructive, handler: { [weak self] (UIAlertAction) in alert.dismiss(animated: true) { } - self.showActivity() - - DispatchQueue.bluetooth.async { [weak self] in + self?.performActivity(queue: .app, { - do { - guard let peripheral = Store.shared[peripheral: lockIdentifier] - else { return } + // first try via BLE + if Store.shared.lockManager.central.state == .poweredOn, + let device = try DispatchQueue.bluetooth.sync(execute: { try Store.shared.device(for: lockIdentifier, scanDuration: 2.0) }) { - try LockManager.shared.removeKey(keyEntry.identifier, type: keyEntry.type, for: peripheral, with: key) - - } - catch { - mainQueue { - self?.showErrorAlert(error.localizedDescription) - self?.hideActivity(animated: false) + try DispatchQueue.bluetooth.sync { + try Store.shared.lockManager.removeKey( + keyEntry.identifier, + type: keyEntry.type, + for: device.scanData.peripheral, + with: key + ) } - return - } - mainQueue { - self?.list.remove(keyEntry.identifier, type: keyEntry.type) - self?.hideActivity(animated: true) + + } else if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lockIdentifier }) { + + // try via Bonjour + try Store.shared.netServiceClient.removeKey( + keyEntry.identifier, + type: keyEntry.type, + for: netService, + with: key, + timeout: 30.0 + ) + + } else { + throw LockError.notInRange(lock: lockIdentifier) } - } + }, completion: { (viewController, _) in + viewController.list.remove(keyEntry.identifier, type: keyEntry.type) + }) })) self.present(alert, animated: true, completion: nil) diff --git a/iOS/LockKit/Controller/LockViewController.swift b/iOS/LockKit/Controller/LockViewController.swift index 5f93867c..bfb9cb17 100644 --- a/iOS/LockKit/Controller/LockViewController.swift +++ b/iOS/LockKit/Controller/LockViewController.swift @@ -106,17 +106,16 @@ public final class LockViewController: UITableViewController { let activities = [ NewKeyActivity(), ManageKeysActivity(), - HomeKitEnableActivity(), RenameActivity(), UpdateActivity(), DeleteLockActivity { [unowned self] in self.navigationController?.popViewController(animated: true) }, - AddVoiceShortcutActivity() + AddSiriShortcutActivity() ] let lockItem = LockActivityItem(identifier: lockIdentifier) - let activityItems = [lockItem, lockItem.text, lockItem.image] as [Any] + let activityItems = [lockItem] as [Any] let activityViewController = UIActivityViewController( activityItems: activityItems, applicationActivities: activities diff --git a/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift b/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift index 7b48da21..38695f06 100644 --- a/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift +++ b/iOS/LockKit/Controller/Protocols/NewKeyViewController.swift @@ -47,7 +47,7 @@ public extension NewKeyViewController { self.showActivity() // add new key to lock - DispatchQueue.bluetooth.async { [weak self] in + DispatchQueue.app.async { [weak self] in guard let self = self else { return } @@ -59,27 +59,55 @@ public extension NewKeyViewController { let newKeySharedSecret = KeyData() + // file for sharing let newKeyInvitation = NewKey.Invitation( lock: lockIdentifier, key: newKey, secret: newKeySharedSecret ) + // for BLE / HTTP request + let newKeyRequest = CreateNewKeyRequest(key: newKey, secret: newKeySharedSecret) + do { - guard let peripheral = try Store.shared.device(for: lockIdentifier, scanDuration: 2.0) else { + // first try via BLE + if Store.shared.lockManager.central.state == .poweredOn, + let peripheral = try DispatchQueue.bluetooth.sync(execute: { try Store.shared.device(for: lockIdentifier, scanDuration: 2.0) }) { + + try DispatchQueue.bluetooth.sync { + try Store.shared.lockManager.createKey( + newKeyRequest, + for: peripheral.scanData.peripheral, + with: parentKey, + timeout: 30.0 + ) + } + + } else if let netService = try Store.shared.netServiceClient.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lockIdentifier }) { + + // try via Bonjour + try Store.shared.netServiceClient.createKey( + newKeyRequest, + for: netService, + with: parentKey, + timeout: 30.0 + ) + + } else { + // not in range mainQueue { self.hideActivity(animated: false) self.newKeyError(R.string.error.notInRange()) } return } - try LockManager.shared.createKey( - .init(key: newKey, secret: newKeySharedSecret), - for: peripheral.scanData.peripheral, - with: parentKey) + } catch { mainQueue { + #if DEBUG + dump(error) + #endif self.hideActivity(animated: false) self.newKeyError(error.localizedDescription) } @@ -123,6 +151,7 @@ public extension NewKeyViewController { private func newKeyError(_ error: String) { + log("⚠️ Unable to share key. \(error)") self.showErrorAlert(error, okHandler: { self.dismiss(animated: true, completion: nil) }, retryHandler: nil) } } diff --git a/iOS/LockKit/Localized Strings/en.lproj/ContextMenu.strings b/iOS/LockKit/Localized Strings/en.lproj/ContextMenu.strings index 4eb8cd69..5c791e63 100644 --- a/iOS/LockKit/Localized Strings/en.lproj/ContextMenu.strings +++ b/iOS/LockKit/Localized Strings/en.lproj/ContextMenu.strings @@ -10,3 +10,4 @@ "Item.Delete" = "Delete"; "Item.ShareKey" = "Share Key"; "Item.Manage" = "Manage"; +"Item.SiriShortcut" = "Add to Siri"; diff --git a/iOS/LockKit/Localized Strings/en.lproj/Error.strings b/iOS/LockKit/Localized Strings/en.lproj/Error.strings index 2d348d1e..24706a3a 100644 --- a/iOS/LockKit/Localized Strings/en.lproj/Error.strings +++ b/iOS/LockKit/Localized Strings/en.lproj/Error.strings @@ -8,6 +8,7 @@ "NotInRange" = "Lock not in range."; "NoKey" = "You don't have a key for this lock."; +"NotAdmin" = "You don't have administrator permissions for this lock."; "InvalidNewKeyFile" = "Invalid key file."; "ExistingKey" = "You already have a key for this lock."; "NewKeyExpired" = "Key expired."; diff --git a/iOS/LockKit/Localized Strings/es.lproj/ContextMenu.strings b/iOS/LockKit/Localized Strings/es.lproj/ContextMenu.strings index 4c6c66c8..571b0a92 100644 --- a/iOS/LockKit/Localized Strings/es.lproj/ContextMenu.strings +++ b/iOS/LockKit/Localized Strings/es.lproj/ContextMenu.strings @@ -10,3 +10,4 @@ "Item.Delete" = "Eliminar"; "Item.ShareKey" = "Compartir Llave"; "Item.Manage" = "Administrar"; +"Item.SiriShortcut" = "Agregar a Siri"; diff --git a/iOS/LockKit/Localized Strings/es.lproj/Error.strings b/iOS/LockKit/Localized Strings/es.lproj/Error.strings index 3c79a77c..306c8c1d 100644 --- a/iOS/LockKit/Localized Strings/es.lproj/Error.strings +++ b/iOS/LockKit/Localized Strings/es.lproj/Error.strings @@ -8,6 +8,7 @@ "NotInRange" = "La cerradura no se encuentra cerca"; "NoKey" = "No tiene una llave para esta cerradura."; +"NotAdmin" = "No tiene permisos de administrador para esta cerradura."; "InvalidNewKeyFile" = "Archivo invalido."; "ExistingKey" = "Usted ya cuenta con una llave para la cerradura."; "NewKeyExpired" = "La llave pendiente expiro."; diff --git a/iOS/LockKit/Model/Activity.swift b/iOS/LockKit/Model/Activity.swift index f7376227..d4df133d 100644 --- a/iOS/LockKit/Model/Activity.swift +++ b/iOS/LockKit/Model/Activity.swift @@ -187,8 +187,7 @@ public final class NewKeyActivity: UIActivity { public override func canPerform(withActivityItems activityItems: [Any]) -> Bool { guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first, - let lockCache = Store.shared[lock: lockItem.identifier], - Store.shared[peripheral: lockItem.identifier] != nil // Lock must be reachable + let lockCache = Store.shared[lock: lockItem.identifier] else { return false } // only owner and admin can share keys @@ -239,8 +238,7 @@ public final class ManageKeysActivity: UIActivity { public override func canPerform(withActivityItems activityItems: [Any]) -> Bool { guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first, - let lockCache = Store.shared[lock: lockItem.identifier], - Store.shared[peripheral: lockItem.identifier] != nil // Lock must be reachable + let lockCache = Store.shared[lock: lockItem.identifier] else { return false } return lockCache.key.permission.isAdministrator @@ -388,92 +386,6 @@ public final class RenameActivity: UIActivity { } } -/// Activity for enabling HomeKit. -public final class HomeKitEnableActivity: UIActivity { - - public override class var activityCategory: UIActivity.Category { return .action } - - private var item: LockActivityItem! - - public override var activityType: UIActivity.ActivityType? { - return LockActivity.homeKitEnable.activityType - } - - public override var activityTitle: String? { - return R.string.activity.homeKitEnableActivityTitle() - } - - public override var activityImage: UIImage? { - return R.image.activityHomeKit() - } - - public override func canPerform(withActivityItems activityItems: [Any]) -> Bool { - - guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first, - let lockCache = Store.shared[lock: lockItem.identifier], - Store.shared[peripheral: lockItem.identifier] != nil // Lock must be reachable - else { return false } - - guard lockCache.key.permission == .owner - else { return false } - - #if DEBUG - return true - #else - return false - #endif - } - - public override func prepare(withActivityItems activityItems: [Any]) { - - self.item = activityItems.compactMap({ $0 as? LockActivityItem }).first - } - - public override var activityViewController: UIViewController? { - - let lockItem = self.item! - - let alert = UIAlertController(title: R.string.activity.homeKitEnableActivityAlertTitle(), - message: R.string.activity.homeKitEnableActivityAlertMessage(), - preferredStyle: .alert) - - func enableHomeKit(_ enable: Bool = true) { - - guard let lockItem = self.item, - let lockCache = Store.shared[lock: lockItem.identifier], - let keyData = Store.shared[key: lockCache.key.identifier], - let peripheral = Store.shared[peripheral: lockItem.identifier] // Lock must be reachable - else { alert.dismiss(animated: true) { self.activityDidFinish(false) }; return } - - DispatchQueue.bluetooth.async { - - //do { try LockManager.shared.enableHomeKit(lockItem.identifier, key: (lockCache.keyIdentifier, keyData), enable: enable) } - - //catch { mainQueue { alert.showErrorAlert("\(error)"); self.activityDidFinish(false) }; return } - - mainQueue { alert.dismiss(animated: true) { self.activityDidFinish(true) } } - } - } - - alert.addAction(UIAlertAction(title:R.string.activity.homeKitEnableActivityAlertCancel(), style: .cancel, handler: { (UIAlertAction) in - - alert.dismiss(animated: true) { self.activityDidFinish(false) } - })) - - alert.addAction(UIAlertAction(title: R.string.activity.homeKitEnableActivityAlertYes(), style: .`default`, handler: { (UIAlertAction) in - - enableHomeKit() - })) - - alert.addAction(UIAlertAction(title: R.string.activity.homeKitEnableActivityAlertNo(), style: .`default`, handler: { (UIAlertAction) in - - enableHomeKit(false) - })) - - return alert - } -} - public final class UpdateActivity: UIActivity { public override class var activityCategory: UIActivity.Category { return .action } @@ -481,7 +393,7 @@ public final class UpdateActivity: UIActivity { private var item: LockActivityItem! public override var activityType: UIActivity.ActivityType? { - return LockActivity.homeKitEnable.activityType + return LockActivity.update.activityType } public override var activityTitle: String? { @@ -495,11 +407,10 @@ public final class UpdateActivity: UIActivity { public override func canPerform(withActivityItems activityItems: [Any]) -> Bool { guard let lockItem = activityItems.compactMap({ $0 as? LockActivityItem }).first, - let lockCache = Store.shared[lock: lockItem.identifier], - Store.shared[peripheral: lockItem.identifier] != nil // Lock must be reachable + let lockCache = Store.shared[lock: lockItem.identifier] else { return false } - guard lockCache.key.permission == .owner + guard lockCache.key.permission.isAdministrator else { return false } return true @@ -512,7 +423,7 @@ public final class UpdateActivity: UIActivity { public override var activityViewController: UIViewController? { - //let lockItem = self.item! + let lockItem = self.item! let alert = UIAlertController(title: R.string.activity.updateActivityAlertTitle(), message: R.string.activity.updateActivityAlertMessage(), @@ -524,8 +435,8 @@ public final class UpdateActivity: UIActivity { })) alert.addAction(UIAlertAction(title: R.string.activity.updateActivityAlertUpdate(), style: .`default`, handler: { (UIAlertAction) in - /* - let progressHUD = JGProgressHUD(style: .dark)! + + let progressHUD = JGProgressHUD(style: .dark) func showProgressHUD() { @@ -542,19 +453,40 @@ public final class UpdateActivity: UIActivity { } // fetch cache - guard let (lockCache, keyData) = Store.shared[lockItem.identifier] + guard let lockCache = Store.shared[lock: lockItem.identifier], + let keyData = Store.shared[key: lockCache.key.identifier] else { alert.dismiss(animated: true) { self.activityDidFinish(false) }; return } + let key = KeyCredentials(identifier: lockCache.key.identifier, secret: keyData) + showProgressHUD() - async { + DispatchQueue.app.async { + + let client = Store.shared.netServiceClient - do { try LockManager.shared.update(lockItem.identifier, key: (lockCache.keyIdentifier, keyData)) } + do { + guard let netService = try client.discover(duration: 1.0, timeout: 10.0).first(where: { $0.identifier == lockItem.identifier }) + else { throw LockError.notInRange(lock: lockItem.identifier) } + + try client.update(for: netService, with: key, timeout: 30.0) + } - catch { mainQueue { dismissProgressHUD(false); alert.showErrorAlert("\(error)"); self.activityDidFinish(false) }; return } + catch { + mainQueue { + dismissProgressHUD(false) + alert.showErrorAlert("\(error)") + self.activityDidFinish(false) + } + return + } - mainQueue { dismissProgressHUD(); alert.dismiss(animated: true) { self.activityDidFinish(true) } } - }*/ + mainQueue { + dismissProgressHUD() + self.activityDidFinish(true) + alert.dismiss(animated: true) { } + } + } })) return alert @@ -629,11 +561,11 @@ public final class ShareKeyCloudKitActivity: UIActivity { #if canImport(IntentsUI) import IntentsUI -public final class AddVoiceShortcutActivity: UIActivity { +public final class AddSiriShortcutActivity: UIActivity { public override class var activityCategory: UIActivity.Category { return .action } - private var item: LockActivityItem! + private var item: LockActivityItem? public override var activityType: UIActivity.ActivityType? { return LockActivity.addVoiceShortcut.activityType @@ -676,17 +608,16 @@ public final class AddVoiceShortcutActivity: UIActivity { else { return nil } guard let lockItem = self.item - else { fatalError() } + else { assertionFailure(); return nil } guard let lockCache = Store.shared[lock: lockItem.identifier] else { assertionFailure("Invalid lock"); return nil } - let intent = UnlockIntent(identifier: lockItem.identifier, cache: lockCache) - let shortcut = INShortcut.intent(intent) - let viewController = INUIAddVoiceShortcutViewController(shortcut: shortcut) - viewController.modalPresentationStyle = .formSheet - viewController.delegate = self - return viewController + return INUIAddVoiceShortcutViewController( + unlock: lockItem.identifier, + cache: lockCache, + delegate: self + ) #endif } } @@ -694,7 +625,7 @@ public final class AddVoiceShortcutActivity: UIActivity { // MARK: - INUIAddVoiceShortcutViewControllerDelegate @available(iOS 12, *) -extension AddVoiceShortcutActivity: INUIAddVoiceShortcutViewControllerDelegate { +extension AddSiriShortcutActivity: INUIAddVoiceShortcutViewControllerDelegate { public func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) { diff --git a/iOS/LockKit/Model/Error.swift b/iOS/LockKit/Model/Error.swift index 38168158..493ee713 100644 --- a/iOS/LockKit/Model/Error.swift +++ b/iOS/LockKit/Model/Error.swift @@ -17,6 +17,9 @@ public enum LockError: Error { /// No key for the specified lock. case noKey(lock: UUID) + /// Must be an administrator for the specified lock. + case notAdmin(lock: UUID) + /// Invalid QR code. case invalidQRCode @@ -57,6 +60,9 @@ extension LockError: CustomNSError { case let .noKey(lock: lock): userInfo[NSLocalizedDescriptionKey] = R.string.error.noKey() userInfo[UserInfoKey.lock.rawValue] = lock as NSUUID + case let .notAdmin(lock: lock): + userInfo[NSLocalizedDescriptionKey] = R.string.error.notAdmin() + userInfo[UserInfoKey.lock.rawValue] = lock as NSUUID case .invalidQRCode: userInfo[NSLocalizedDescriptionKey] = R.string.error.invalidQRCode() case .invalidNewKeyFile: diff --git a/iOS/LockKit/Model/Intent.swift b/iOS/LockKit/Model/Intent.swift index a4f95f53..3be2bb69 100644 --- a/iOS/LockKit/Model/Intent.swift +++ b/iOS/LockKit/Model/Intent.swift @@ -33,12 +33,6 @@ public extension UnlockIntent { self.__setImage(INImage(uiImage: UIImage(permission: cache.key.permission)), forParameterNamed: #keyPath(lock)) #endif } - - static var any: UnlockIntent { - let intent = UnlockIntent() - intent.suggestedInvocationPhrase = "Unlock" - return intent - } } @available(iOS 12, iOSApplicationExtension 12.0, watchOS 5.0, *) @@ -48,3 +42,22 @@ public extension IntentLock { self.init(identifier: identifier.uuidString, display: name, pronunciationHint: name) } } + +#if canImport(IntentsUI) && !targetEnvironment(macCatalyst) +import IntentsUI + +@available(iOSApplicationExtension 12.0, *) +public extension INUIAddVoiceShortcutViewController { + + convenience init(unlock lock: UUID, + cache: LockCache, + delegate: INUIAddVoiceShortcutViewControllerDelegate) { + + let intent = UnlockIntent(identifier: lock, cache: cache) + self.init(shortcut: .intent(intent)) + self.modalPresentationStyle = .formSheet + self.delegate = delegate + } +} + +#endif diff --git a/iOS/LockKit/Model/NetService.swift b/iOS/LockKit/Model/NetService.swift new file mode 100644 index 00000000..307e44bd --- /dev/null +++ b/iOS/LockKit/Model/NetService.swift @@ -0,0 +1,28 @@ +// +// NetService.swift +// LockKit +// +// Created by Alsey Coleman Miller on 10/16/19. +// Copyright © 2019 ColemanCDA. All rights reserved. +// + +import Foundation +import CoreLock +import Bonjour + +public typealias LockNetServiceClient = CoreLock.LockNetService.Client + +public extension LockNetServiceClient { + + static var shared: LockNetServiceClient { + return LockNetServiceCache.client + } +} + +private struct LockNetServiceCache { + + static let client = LockNetServiceClient( + bonjour: NetServiceClient(), + urlSession: .shared + ) +} diff --git a/iOS/LockKit/Model/Store.swift b/iOS/LockKit/Model/Store.swift index a24f8c9a..7838ea3f 100644 --- a/iOS/LockKit/Model/Store.swift +++ b/iOS/LockKit/Model/Store.swift @@ -142,6 +142,8 @@ public final class Store { public lazy var beaconController: BeaconController = .shared public lazy var spotlight: SpotlightController = .shared + + public lazy var netServiceClient: LockNetServiceClient = .shared #endif // BLE cache @@ -184,14 +186,18 @@ public final class Store { } set { - + let key = identifier.uuidString do { guard let data = newValue?.data else { - try keychain.remove(identifier.uuidString) + try keychain.remove(key) return } - try keychain.set(data, key: identifier.uuidString) - } catch { + if try keychain.contains(key) { + try keychain.remove(key) + } + try keychain.set(data, key: key) + } + catch { #if DEBUG print(error) #endif @@ -218,6 +224,14 @@ public final class Store { return true } + /// Get credentials from Keychain to authorize requests. + public func credentials(for lock: UUID) -> KeyCredentials? { + guard let cache = self[lock: lock], + let keyData = self[key: cache.key.identifier] + else { return nil } + return .init(identifier: cache.key.identifier, secret: keyData) + } + /// Forceably load cache. public func loadCache() { @@ -319,8 +333,8 @@ public final class Store { log("📶 Lock notification") guard preferences.monitorBluetoothNotifications else { return } // ignore notification - typealias FetchRequest = ListEventsCharacteristic.FetchRequest - typealias Predicate = ListEventsCharacteristic.Predicate + typealias FetchRequest = LockEvent.FetchRequest + typealias Predicate = LockEvent.Predicate let context = Store.shared.backgroundContext DispatchQueue.bluetooth.async { // scan for all locks @@ -556,7 +570,7 @@ public extension Store { @discardableResult func listEvents(_ lock: LockPeripheral, - fetchRequest: ListEventsCharacteristic.FetchRequest? = nil, + fetchRequest: LockEvent.FetchRequest? = nil, notification: @escaping ((EventsList, Bool) -> ()) = { _,_ in }) throws -> Bool { // get lock key @@ -604,6 +618,59 @@ public extension Store { } } +// MARK: - Bonjour Requests + +#if os(iOS) + +public extension Store { + + @discardableResult + func listEvents(_ lock: LockNetService, + fetchRequest: LockEvent.FetchRequest? = nil) throws -> Bool { + + // get lock key + guard let lockCache = self[lock: lock.identifier], + let keyData = self[key: lockCache.key.identifier] + else { return false } + + let key = KeyCredentials( + identifier: lockCache.key.identifier, + secret: keyData + ) + + let events = try netServiceClient.listEvents( + fetchRequest: fetchRequest, + for: lock, + with: key, + timeout: 30 + ) + + backgroundContext.commit { (context) in + try context.insert(events, for: lock.identifier) + } + + #if os(iOS) + if preferences.isCloudBackupEnabled { + DispatchQueue.cloud.async { [weak self] in + // upload to iCloud + do { + for event in events { + let value = LockEvent.Cloud(event: event, for: lock.identifier) + try self?.cloud.upload(value) + } + } catch { + log("⚠️ Could not upload latest events to iCloud: \(error.localizedDescription)") + } + } + } + #endif + + return true + } +} + +#endif + // MARK: - CloudKit Operations #if os(iOS) diff --git a/iOS/LockKit/View/LockTableViewCell.swift b/iOS/LockKit/View/LockTableViewCell.swift index d7e79e97..3a603265 100644 --- a/iOS/LockKit/View/LockTableViewCell.swift +++ b/iOS/LockKit/View/LockTableViewCell.swift @@ -13,9 +13,6 @@ public final class LockTableViewCell: UITableViewCell { // MARK: - IB Outlets - @available(*, deprecated, message: "Use `permissionView` instead") - @IBOutlet public private(set) weak var lockImageView: UIImageView! - @IBOutlet public private(set) weak var permissionView: PermissionIconView! @IBOutlet public private(set) weak var lockTitleLabel: UILabel! diff --git a/iOS/LockKit/View/LockTableViewCell.xib b/iOS/LockKit/View/LockTableViewCell.xib index 0ceca063..2dab5e1e 100644 --- a/iOS/LockKit/View/LockTableViewCell.xib +++ b/iOS/LockKit/View/LockTableViewCell.xib @@ -1,78 +1,96 @@ - + - - + - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - + + + + + - - - - - + + + + - + diff --git a/iOS/LockKit/en.lproj/Intents.strings b/iOS/LockKit/en.lproj/Intents.strings index 381556fc..5a369316 100644 --- a/iOS/LockKit/en.lproj/Intents.strings +++ b/iOS/LockKit/en.lproj/Intents.strings @@ -20,11 +20,11 @@ "Z5kgSL" = "Lock"; -"bvi2l8" = "`${displayName}` is not in range. Please move closer to ${displayName}."; +"bvi2l8" = "${displayName} is not in range. Please move closer to ${displayName}."; -"claxan" = "Unlocked ‘${lock}‘."; +"claxan" = "Unlocked ${lock}."; -"dAVo2n" = "Unlock `${lock}`"; +"dAVo2n" = "Unlock ${lock}"; "fjnXtW" = "Unlock"; @@ -32,7 +32,7 @@ "tnOjCJ" = "Lock"; -"vARXDT" = "You do not have a key for `${displayName}`."; +"vARXDT" = "You do not have a key for ${displayName}."; -"wkobUh" = "You can only unlock `${displayName}` during the specified schedule."; +"wkobUh" = "You can only unlock ${displayName} during the specified schedule."; diff --git a/iOS/LockKit/es.lproj/Intents.strings b/iOS/LockKit/es.lproj/Intents.strings index 1abc46a3..c34b5eb1 100644 --- a/iOS/LockKit/es.lproj/Intents.strings +++ b/iOS/LockKit/es.lproj/Intents.strings @@ -20,11 +20,11 @@ "Z5kgSL" = "Lock"; -"bvi2l8" = "`${displayName}` no esta en rango. Por favor acérquese a ${displayName}."; +"bvi2l8" = "${displayName} no esta en rango. Por favor acérquese a ${displayName}."; -"claxan" = "Abrío ‘${lock}‘."; +"claxan" = "Abrío ${lock}."; -"dAVo2n" = "Abre `${lock}`"; +"dAVo2n" = "Abre ${lock}"; "fjnXtW" = "Abre"; @@ -32,7 +32,7 @@ "tnOjCJ" = "Cerradura"; -"vARXDT" = "Usted no tiene una llave para `${displayName}`."; +"vARXDT" = "Usted no tiene una llave para ${displayName}."; -"wkobUh" = "Usted solo puede abrir `${displayName}` durante el horario especificado."; +"wkobUh" = "Usted solo puede abrir ${displayName} durante el horario especificado."; diff --git a/iOS/LockSwiftUI/PermissionScheduleView.swift b/iOS/LockSwiftUI/PermissionScheduleView.swift index ff1bfa60..4045ae40 100644 --- a/iOS/LockSwiftUI/PermissionScheduleView.swift +++ b/iOS/LockSwiftUI/PermissionScheduleView.swift @@ -9,6 +9,7 @@ import Foundation import SwiftUI import CoreLock +import SFSafeSymbols /// Permission Schedule View @available(iOS 13, *) @@ -65,7 +66,7 @@ public struct PermissionScheduleView: View { } private var checkmark: some View { - return Image(systemName: "checkmark") + return Image(systemSymbol: .checkmark) .foregroundColor(Color.orange) } diff --git a/iOS/Message/Info.plist b/iOS/Message/Info.plist index 2488dc4e..8c2d7cc0 100644 --- a/iOS/Message/Info.plist +++ b/iOS/Message/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Lock + Cerradura CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/iOS/SmartLock.xcodeproj/project.pbxproj b/iOS/SmartLock.xcodeproj/project.pbxproj index 441f3d57..daad1a28 100644 --- a/iOS/SmartLock.xcodeproj/project.pbxproj +++ b/iOS/SmartLock.xcodeproj/project.pbxproj @@ -228,6 +228,7 @@ 6E83BB4A233D906C00D6BA04 /* LockTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6EAADFB521229CBF0074CEDF /* LockTableViewCell.xib */; }; 6E8772CA231259DE003B9469 /* JGProgressHUD.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E8772C5231258CD003B9469 /* JGProgressHUD.framework */; platformFilter = ios; }; 6E8772CB231259DE003B9469 /* JGProgressHUD.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 6E8772C5231258CD003B9469 /* JGProgressHUD.framework */; platformFilter = ios; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 6E8786FB2357B199008624C1 /* NetService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E8786FA2357B199008624C1 /* NetService.swift */; }; 6E94EF75232F4A790028B745 /* CloudKit.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E94EF74232F4A790028B745 /* CloudKit.swift */; }; 6E9740412318C13D00634631 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6ED81FB32316618100B69520 /* Assets.xcassets */; }; 6E9740422318C14800634631 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6ED81FC72316618400B69520 /* Assets.xcassets */; }; @@ -321,6 +322,8 @@ 6ECD35C1233B4CB000E49357 /* NewKeyDocument.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECD35C0233B4CB000E49357 /* NewKeyDocument.swift */; }; 6ECD39A52336D369003E4AB4 /* ActivityIndicator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECD39A42336D369003E4AB4 /* ActivityIndicator.swift */; }; 6ECD6AEA23320D2C0007E7DA /* ContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ECD6AE923320D2C0007E7DA /* ContextMenu.swift */; }; + 6ED44E44235C4406002E8F54 /* SFSafeSymbols in Frameworks */ = {isa = PBXBuildFile; productRef = 6ED44E43235C4406002E8F54 /* SFSafeSymbols */; }; + 6ED44E46235C49AC002E8F54 /* Update.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED44E45235C49AC002E8F54 /* Update.swift */; }; 6ED81F972316292900B69520 /* BackgroundTasks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6ED81F962316292900B69520 /* BackgroundTasks.swift */; }; 6ED8207D231674E700B69520 /* IntentHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6E994AD8230E56DD0076DC1B /* IntentHandler.swift */; }; 6ED82092231676C100B69520 /* CoreLock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6E60005A2121164B00787DAA /* CoreLock.framework */; }; @@ -835,6 +838,7 @@ 6E83BAF72323843500FBC12E /* CollectionViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CollectionViewController.swift; sourceTree = ""; }; 6E83BB4E233D906C00D6BA04 /* LockKitSwiftUI.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = LockKitSwiftUI.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 6E8772BB231258CC003B9469 /* JGProgressHUD.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = JGProgressHUD.xcodeproj; path = ../Carthage/Checkouts/JGProgressHUD/JGProgressHUD.xcodeproj; sourceTree = ""; }; + 6E8786FA2357B199008624C1 /* NetService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetService.swift; sourceTree = ""; }; 6E94EF74232F4A790028B745 /* CloudKit.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CloudKit.swift; sourceTree = ""; }; 6E9794D323315EA000B5C5C9 /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/WatchOS.platform/Developer/SDKs/WatchOS6.0.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; }; 6E994A79230DE1700076DC1B /* CoreSpotlight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CoreSpotlight.swift; sourceTree = ""; }; @@ -906,6 +910,7 @@ 6ECD35C0233B4CB000E49357 /* NewKeyDocument.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NewKeyDocument.swift; sourceTree = ""; }; 6ECD39A42336D369003E4AB4 /* ActivityIndicator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityIndicator.swift; sourceTree = ""; }; 6ECD6AE923320D2C0007E7DA /* ContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenu.swift; sourceTree = ""; }; + 6ED44E45235C49AC002E8F54 /* Update.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Update.swift; sourceTree = ""; }; 6ED81F962316292900B69520 /* BackgroundTasks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundTasks.swift; sourceTree = ""; }; 6ED81FB12316617E00B69520 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Interface.storyboard; sourceTree = ""; }; 6ED81FB32316618100B69520 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -999,6 +1004,7 @@ 6E64B7EA231236E5002BC0A2 /* Rswift in Frameworks */, 6E64B7F4231247EF002BC0A2 /* QRCodeReader in Frameworks */, 6E64B7F1231240CC002BC0A2 /* JGProgressHUD in Frameworks */, + 6ED44E44235C4406002E8F54 /* SFSafeSymbols in Frameworks */, 6E5DF8B32319E3A50070787B /* OpenCombine in Frameworks */, 6EE5A10B2329FCA00000235C /* CloudKitCodable in Frameworks */, 6E64B7ED23123846002BC0A2 /* KeychainAccess in Frameworks */, @@ -1286,6 +1292,7 @@ 6E994A79230DE1700076DC1B /* CoreSpotlight.swift */, 6E73745E2325EA1600EC85B2 /* Event.swift */, 6E60004A212113DF00787DAA /* DeviceManager.swift */, + 6E8786FA2357B199008624C1 /* NetService.swift */, 6E806B97230F520400C6FF78 /* FileManager.swift */, 6E994AFC230E657A0076DC1B /* Intent.swift */, 6E806B93230F4F4700C6FF78 /* Keychain.swift */, @@ -1324,6 +1331,7 @@ 6E6000C321211CD400787DAA /* ErrorAlert.swift */, 6E6000C621211CD400787DAA /* PresentPopover.swift */, 6E6000C521211CD400787DAA /* UIAlertAction.swift */, + 6ED44E45235C49AC002E8F54 /* Update.swift */, 6ECD6AE923320D2C0007E7DA /* ContextMenu.swift */, ); path = Extensions; @@ -1774,6 +1782,7 @@ 6E64B7F3231247EF002BC0A2 /* QRCodeReader */, 6E5DF8B22319E3A50070787B /* OpenCombine */, 6EE5A10A2329FCA00000235C /* CloudKitCodable */, + 6ED44E43235C4406002E8F54 /* SFSafeSymbols */, ); productName = LockKit; productReference = 6E994A84230DFB6C0076DC1B /* LockKit.framework */; @@ -2151,6 +2160,7 @@ 6E64B7F2231247EF002BC0A2 /* XCRemoteSwiftPackageReference "QRCodeReader.swift" */, 6E5DF8B12319E3A50070787B /* XCRemoteSwiftPackageReference "OpenCombine" */, 6EE5A1092329FCA00000235C /* XCRemoteSwiftPackageReference "CloudKitCodable" */, + 6ED44E3C235C43EE002E8F54 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */, ); productRefGroup = 6E6000112121119400787DAA /* Products */; projectDirPath = ""; @@ -2838,12 +2848,14 @@ 6E994AFD230E657A0076DC1B /* Intent.swift in Sources */, 6E994AC5230DFDC60076DC1B /* NewKeyRecieveViewController.swift in Sources */, 6E50B0F4232243A2001014A8 /* UnlockEventManagedObject.swift in Sources */, + 6E8786FB2357B199008624C1 /* NetService.swift in Sources */, 6E08FB8B2316F9F600177026 /* WatchMessage.swift in Sources */, 6ECD39A52336D369003E4AB4 /* ActivityIndicator.swift in Sources */, 6E806BAB230F67D900C6FF78 /* LogViewController.swift in Sources */, 6E994AC1230DFDC60076DC1B /* LockViewController.swift in Sources */, 6E994ABB230DFDB60076DC1B /* AdaptiveNavigation.swift in Sources */, 6E806B94230F4F4700C6FF78 /* Keychain.swift in Sources */, + 6ED44E46235C49AC002E8F54 /* Update.swift in Sources */, 6ECD6AEA23320D2C0007E7DA /* ContextMenu.swift in Sources */, 6E6DB101232D7E8100481C36 /* CloudLock.swift in Sources */, 6E50B0EA2322424B001014A8 /* ScheduleManagedObject.swift in Sources */, @@ -4767,6 +4779,14 @@ kind = branch; }; }; + 6ED44E3C235C43EE002E8F54 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/piknotech/SFSafeSymbols.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 1.0.0; + }; + }; 6EE5A1092329FCA00000235C /* XCRemoteSwiftPackageReference "CloudKitCodable" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/colemancda/CloudKitCodable.git"; @@ -4838,6 +4858,11 @@ package = 6E83BADB233D906C00D6BA04 /* XCRemoteSwiftPackageReference "CloudKitCodable" */; productName = CloudKitCodable; }; + 6ED44E43235C4406002E8F54 /* SFSafeSymbols */ = { + isa = XCSwiftPackageProductDependency; + package = 6ED44E3C235C43EE002E8F54 /* XCRemoteSwiftPackageReference "SFSafeSymbols" */; + productName = SFSafeSymbols; + }; 6ED820992316777A00B69520 /* KeychainAccess */ = { isa = XCSwiftPackageProductDependency; package = 6E64B7EB23123846002BC0A2 /* XCRemoteSwiftPackageReference "KeychainAccess" */; diff --git a/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 33f0ed97..a6794efe 100644 --- a/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/iOS/SmartLock.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -10,6 +10,15 @@ "version": null } }, + { + "package": "Bonjour", + "repositoryURL": "git@github.com:PureSwift/Bonjour.git", + "state": { + "branch": "master", + "revision": "b3d28cc68b7fac11811848613633c90c8dd2e058", + "version": null + } + }, { "package": "CloudKitCodable", "repositoryURL": "https://github.com/colemancda/CloudKitCodable.git", @@ -24,7 +33,7 @@ "repositoryURL": "https://github.com/krzyzanowskim/CryptoSwift", "state": { "branch": "master", - "revision": "4ccfe9034a07af4b153657ba1cdac702ffcf8f2f", + "revision": "7c94c0bc9d222f9c88d9f6ed8f71926f372d30a8", "version": null } }, @@ -91,6 +100,15 @@ "version": null } }, + { + "package": "SFSafeSymbols", + "repositoryURL": "https://github.com/piknotech/SFSafeSymbols.git", + "state": { + "branch": null, + "revision": "d63bbbd0e70182362d33f108549968b69d0b859a", + "version": "1.0.0" + } + }, { "package": "TLVCoding", "repositoryURL": "https://github.com/PureSwift/TLVCoding.git", diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Intent.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Intent.xcscheme index af512881..2b953844 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Intent.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Intent.xcscheme @@ -30,7 +30,7 @@ @@ -62,7 +62,7 @@ @@ -80,7 +80,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/IntentUI.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/IntentUI.xcscheme index 20ffa69e..930e1378 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/IntentUI.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/IntentUI.xcscheme @@ -30,7 +30,7 @@ @@ -62,7 +62,7 @@ @@ -80,7 +80,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Message.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Message.xcscheme index 7c13e63a..f707e339 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Message.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Message.xcscheme @@ -30,7 +30,7 @@ @@ -62,7 +62,7 @@ @@ -80,7 +80,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme index 3e6734a0..33dda053 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/SmartLock.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -74,7 +74,7 @@ @@ -91,7 +91,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today macOS.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today macOS.xcscheme index e526beff..eb8849b7 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today macOS.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today macOS.xcscheme @@ -46,7 +46,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today.xcscheme index fd58b642..4f4e5453 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Today.xcscheme @@ -30,7 +30,7 @@ @@ -62,7 +62,7 @@ @@ -80,7 +80,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme index 3070f68c..ea9834d5 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Complication).xcscheme @@ -29,7 +29,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme index 49ce905c..edfa09f1 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch (Notification).xcscheme @@ -29,7 +29,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme index ffe21246..c321ffcf 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/Watch.xcscheme @@ -29,7 +29,7 @@ diff --git a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme index 2e87fb02..bb186056 100644 --- a/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme +++ b/iOS/SmartLock.xcodeproj/xcshareddata/xcschemes/WatchIntent.xcscheme @@ -43,7 +43,7 @@ diff --git a/iOS/SmartLock/AppDelegate.swift b/iOS/SmartLock/AppDelegate.swift index 1cab6127..e59e2e1a 100644 --- a/iOS/SmartLock/AppDelegate.swift +++ b/iOS/SmartLock/AppDelegate.swift @@ -76,6 +76,7 @@ final class AppDelegate: UIResponder, UIApplicationDelegate { // setup logging LockManager.shared.log = { log("🔒 LockManager: " + $0) } + LockNetServiceClient.shared.log = { log("🌐 NetService: " + $0) } BeaconController.shared.log = { log("📶 \(BeaconController.self): " + $0) } SpotlightController.shared.log = { log("🔦 \(SpotlightController.self): " + $0) } WatchController.shared.log = { log("⌚️ \(WatchController.self): " + $0) } diff --git a/iOS/SmartLock/Controller/KeysViewController.swift b/iOS/SmartLock/Controller/KeysViewController.swift index bd0ef621..bdeb50f9 100644 --- a/iOS/SmartLock/Controller/KeysViewController.swift +++ b/iOS/SmartLock/Controller/KeysViewController.swift @@ -8,6 +8,7 @@ import Foundation import UIKit +import IntentsUI import Bluetooth import DarwinGATT import GATT @@ -16,6 +17,7 @@ import LockKit import JGProgressHUD import OpenCombine import CloudKit +import SFSafeSymbols final class KeysViewController: UITableViewController { @@ -368,7 +370,7 @@ final class KeysViewController: UITableViewController { case let .key(identifier, _): return self?.menu(forLock: identifier) case .newKey: - let delete = UIAction(title: R.string.keysViewController.delete(), image: UIImage(systemName: "trash"), attributes: .destructive) { [weak self] (action) in + let delete = UIAction(title: R.string.keysViewController.delete(), image: UIImage(systemSymbol: .trash), attributes: .destructive) { [weak self] (action) in self?.delete(item) } return UIMenu( @@ -387,6 +389,21 @@ final class KeysViewController: UITableViewController { extension KeysViewController: TableViewActivityIndicatorViewController { } +// MARK: - INUIAddVoiceShortcutViewControllerDelegate + +@available(iOS 12, *) +extension KeysViewController: INUIAddVoiceShortcutViewControllerDelegate { + + public func addVoiceShortcutViewController(_ controller: INUIAddVoiceShortcutViewController, didFinishWith voiceShortcut: INVoiceShortcut?, error: Error?) { + + controller.dismiss(animated: true, completion: nil) + } + + public func addVoiceShortcutViewControllerDidCancel(_ controller: INUIAddVoiceShortcutViewController) { + controller.dismiss(animated: true, completion: nil) + } +} + // MARK: - UIDocumentPickerDelegate extension KeysViewController: UIDocumentPickerDelegate { diff --git a/iOS/SmartLock/Info.plist b/iOS/SmartLock/Info.plist index 19270abb..928f10ff 100644 --- a/iOS/SmartLock/Info.plist +++ b/iOS/SmartLock/Info.plist @@ -58,6 +58,11 @@ public.app-category.utilities LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsLocalNetworking + + NSBluetoothAlwaysUsageDescription Bluetooth is used to scan for nearby locks in the background. NSBluetoothPeripheralUsageDescription diff --git a/iOS/SmartLock/Localizable Strings/es.lproj/InfoPlist.strings b/iOS/SmartLock/Localizable Strings/es.lproj/InfoPlist.strings index fde39bc6..2ca3b890 100644 --- a/iOS/SmartLock/Localizable Strings/es.lproj/InfoPlist.strings +++ b/iOS/SmartLock/Localizable Strings/es.lproj/InfoPlist.strings @@ -6,7 +6,7 @@ Copyright © 2019 ColemanCDA. All rights reserved. */ -"CFBundleDisplayName" = "Lock"; +"CFBundleDisplayName" = "Cerradura"; "NSBluetoothAlwaysUsageDescription" = "Bluetooth es usado para escanear Cerrarudas cercanas en segundo plano."; "NSBluetoothPeripheralUsageDescription" = "Se necesita Bluetooth para conectarse a su Cerradura Inteligente."; "NSCameraUsageDescription" = "Se necesita la camara para instalar la Cerradura Inteligente."; diff --git a/iOS/Today macOS/Info.plist b/iOS/Today macOS/Info.plist index c7f3f358..10e4e1ba 100644 --- a/iOS/Today macOS/Info.plist +++ b/iOS/Today macOS/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Lock + Cerradura CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/iOS/Today macOS/en.lproj/InfoPlist.strings b/iOS/Today macOS/en.lproj/InfoPlist.strings index dd798eda..df7e9f5a 100644 --- a/iOS/Today macOS/en.lproj/InfoPlist.strings +++ b/iOS/Today macOS/en.lproj/InfoPlist.strings @@ -1,3 +1,3 @@ /* Display name and description for this extension. */ -"CFBundleDisplayName" = "Lock"; +"CFBundleDisplayName" = "Cerradura"; "com.apple.notificationcenter.widget.description" = "Widget for controlling your locks."; diff --git a/iOS/Today/Base.lproj/MainInterface.storyboard b/iOS/Today/Base.lproj/MainInterface.storyboard index 82595eb9..18a9bc72 100644 --- a/iOS/Today/Base.lproj/MainInterface.storyboard +++ b/iOS/Today/Base.lproj/MainInterface.storyboard @@ -1,48 +1,33 @@ - + - - + - + - - - + + + - - - - - - - - - - - - - - - - - - - + + + + + + + + - - - - - - + + + - + diff --git a/iOS/Today/Info.plist b/iOS/Today/Info.plist index 89bcbe0d..1debbec9 100644 --- a/iOS/Today/Info.plist +++ b/iOS/Today/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Lock + Cerradura CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier diff --git a/iOS/Today/TodayViewController.swift b/iOS/Today/TodayViewController.swift index e1457f6c..abc8bed8 100644 --- a/iOS/Today/TodayViewController.swift +++ b/iOS/Today/TodayViewController.swift @@ -16,20 +16,19 @@ import DarwinGATT import CoreLock import LockKit import OpenCombine +import Combine -final class TodayViewController: UIViewController, NCWidgetProviding { - - // MARK: - IB Outlets - - @IBOutlet private(set) weak var tableView: UITableView! +final class TodayViewController: UITableViewController { // MARK: - Properties - private(set) var items: [Item] = [.noNearbyLocks] + private(set) var items: [Item] = [.loading] { + didSet { tableView.reloadData() } + } - private var peripheralsObserver: AnyCancellable? - private var informationObserver: AnyCancellable? - private var locksObserver: AnyCancellable? + private(set) var isScanning = true { + didSet { configureView() } + } @available(iOS 10.0, *) private lazy var selectionFeedbackGenerator: UISelectionFeedbackGenerator = { @@ -38,6 +37,13 @@ final class TodayViewController: UIViewController, NCWidgetProviding { return feedbackGenerator }() + private var peripheralsObserver: OpenCombine.AnyCancellable? + private var informationObserver: OpenCombine.AnyCancellable? + private var locksObserver: OpenCombine.AnyCancellable? + @available(iOS 13.0, *) + private lazy var updateTableViewSubject = Combine.PassthroughSubject() + private var updateTableViewObserver: AnyObject? // Combine.AnyCancellable + // MARK: - Loading override func viewDidLoad() { @@ -55,35 +61,32 @@ final class TodayViewController: UIViewController, NCWidgetProviding { tableView.register(LockTableViewCell.self) tableView.rowHeight = UITableView.automaticDimension tableView.estimatedRowHeight = 60 + tableView.tableFooterView = UIView() // Set Logging LockManager.shared.log = { log("🔒 LockManager: " + $0) } BeaconController.shared.log = { log("📶 \(BeaconController.self): " + $0) } - // scan beacons - BeaconController.shared.scanBeacons() - - // Observe changes + // observe model changes peripheralsObserver = Store.shared.peripherals.sink { [weak self] _ in - mainQueue { self?.configureView() } + self?.locksChanged() } informationObserver = Store.shared.lockInformation.sink { [weak self] _ in - mainQueue { self?.configureView() } + self?.locksChanged() } locksObserver = Store.shared.locks.sink { [weak self] _ in - mainQueue { self?.configureView() } + self?.locksChanged() + } + + if #available(iOS 13.0, *) { + updateTableViewObserver = updateTableViewSubject + .delay(for: 1.0, scheduler: DispatchQueue.main) + .sink(receiveValue: { [weak self] in self?.configureView() }) } // update UI configureView() - // scan for locks - if Store.shared.lockInformation.value.isEmpty { - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - self?.scan() - } - } - if #available(iOS 10.0, *) { selectionFeedbackGenerator.prepare() } @@ -97,39 +100,32 @@ final class TodayViewController: UIViewController, NCWidgetProviding { } } - // MARK: - NCWidgetProviding - - func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) { - - log("☀️ Update Widget Data") - - // load updated lock information - Store.shared.loadCache() - - if #available(iOS 10.0, *) { - selectionFeedbackGenerator.prepare() + override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { + super.viewWillTransition(to: size, with: coordinator) + + let updatedVisibleCellCount = cellsToDisplay + let currentVisibleCellCount = self.tableView.visibleCells.count + let cellCountDifference = updatedVisibleCellCount - currentVisibleCellCount + + // If the number of visible cells has changed, animate them in/out along with the resize animation. + if cellCountDifference != 0 { + coordinator.animate(alongsideTransition: { [unowned self] (UIViewControllerTransitionCoordinatorContext) in + self.tableView.performBatchUpdates({ [unowned self] in + // Build an array of IndexPath objects representing the rows to be inserted or deleted. + let range = (1...abs(cellCountDifference)) + let indexPaths = range.map { IndexPath(row: $0, section: 0) } + + // Animate the insertion or deletion of the rows. + if cellCountDifference > 0 { + self.tableView.insertRows(at: indexPaths, with: .fade) + } else { + self.tableView.deleteRows(at: indexPaths, with: .fade) + } + }, completion: nil) + }, completion: nil) } - - // Perform any setup necessary in order to update the view. - - // If an error is encountered, use NCUpdateResult.Failed - // If there's no update required, use NCUpdateResult.NoData - // If there's an update, use NCUpdateResult.NewData - - scan { completionHandler($0 ? .newData : .failed) } - } - - @available(iOSApplicationExtension 10.0, *) - func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { - - log("☀️ Widget Display Mode changed \(activeDisplayMode.debugDescription) \(maxSize)") - - - } - - func widgetMarginInsets(forProposedMarginInsets defaultMarginInsets: UIEdgeInsets) -> UIEdgeInsets { - return .zero } + // MARK: - Methods @@ -139,11 +135,16 @@ final class TodayViewController: UIViewController, NCWidgetProviding { get { return items[indexPath.row] } } + private func locksChanged() { + if #available(iOS 13.0, *) { + updateTableViewSubject.send() + } else { + mainQueue { [weak self] in self?.configureView() } + } + } + private func configureView() { - // load updated lock information - Store.shared.loadCache() - let locks = Store.shared.peripherals.value.values .lazy .sorted { $0.scanData.rssi < $1.scanData.rssi } @@ -155,29 +156,26 @@ final class TodayViewController: UIViewController, NCWidgetProviding { .flatMap { (identifier: information.identifier, cache: $0) } } - let oldItems = self.items if locks.isEmpty { - items = [.noNearbyLocks] + items = [isScanning ? .loading : .noNearbyLocks] } else { items = locks.map { .lock($0.identifier, $0.cache) } } - // reload table view - if oldItems != items { - tableView.reloadData() - } + // Show expanded view for multiple devices + extensionContext?.widgetLargestAvailableDisplayMode = items.count > 1 ? .expanded : .compact } private func scan(_ completion: ((Bool) -> ())? = nil) { - // load updated lock information - Store.shared.loadCache() + self.isScanning = true // scan beacons BeaconController.shared.scanBeacons() // scan for devices DispatchQueue.bluetooth.async { + defer { mainQueue { self.isScanning = false } } do { try Store.shared.scan(duration: 1.0) } catch { log("⚠️ Could not scan: \(error.localizedDescription)") @@ -193,6 +191,16 @@ final class TodayViewController: UIViewController, NCWidgetProviding { let item = self[indexPath] switch item { + case .loading: + cell.lockTitleLabel.text = "Loading..." + cell.lockDetailLabel.text = nil + cell.activityIndicatorView.isHidden = false + if cell.activityIndicatorView.isAnimating == false { + cell.activityIndicatorView.startAnimating() + } + cell.permissionView.isHidden = true + cell.selectionStyle = .none + cell.accessoryType = .none case .noNearbyLocks: cell.lockTitleLabel.text = "No Nearby Locks" cell.lockDetailLabel.text = nil @@ -212,6 +220,14 @@ final class TodayViewController: UIViewController, NCWidgetProviding { } } + private var cellsToDisplay: Int { + if extensionContext?.widgetActiveDisplayMode == .compact { + return 1 + } else { + return items.count + } + } + private func select(_ item: Item) { if #available(iOSApplicationExtension 10.0, *) { @@ -219,6 +235,8 @@ final class TodayViewController: UIViewController, NCWidgetProviding { } switch item { + case .loading: + break case .noNearbyLocks: scan() case let .lock(identifier, cache): @@ -239,17 +257,17 @@ final class TodayViewController: UIViewController, NCWidgetProviding { // MARK: - UITableViewDataSource -extension TodayViewController: UITableViewDataSource { +extension TodayViewController { - func numberOfSections(in tableView: UITableView) -> Int { + override func numberOfSections(in tableView: UITableView) -> Int { return 1 } - func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { - return items.count + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return min(cellsToDisplay, items.count) } - func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { guard let cell = tableView.dequeueReusableCell(LockTableViewCell.self, for: indexPath) else { fatalError("Could not dequeue resusable cell \(LockTableViewCell.self)") } @@ -260,16 +278,16 @@ extension TodayViewController: UITableViewDataSource { // MARK: - UITableViewDelegate -extension TodayViewController: UITableViewDelegate { +extension TodayViewController { - func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { defer { tableView.deselectRow(at: indexPath, animated: true) } let item = self[indexPath] select(item) } - func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { + override func tableView(_ tableView: UITableView, accessoryButtonTappedForRowWith indexPath: IndexPath) { let item = self[indexPath] guard case let .lock(identifier, _) = item @@ -277,6 +295,63 @@ extension TodayViewController: UITableViewDelegate { let url = LockURL.unlock(lock: identifier) self.extensionContext?.open(url.rawValue, completionHandler: nil) } + + override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { + + let activeDisplayMode = extensionContext?.widgetActiveDisplayMode ?? .compact + switch activeDisplayMode { + case .compact: + return LockTableViewCell.todayCellHeight + case .expanded: + return LockTableViewCell.standardCellHeight + @unknown default: + assertionFailure("Unexpected value \(activeDisplayMode.rawValue) for activeDisplayMode.") + return LockTableViewCell.todayCellHeight + } + } +} + +// MARK: - NCWidgetProviding + +extension TodayViewController: NCWidgetProviding { + + func widgetPerformUpdate(completionHandler: (@escaping (NCUpdateResult) -> Void)) { + + log("☀️ Update Widget Data") + + // load updated lock information + Store.shared.loadCache() + + if #available(iOS 10.0, *) { + selectionFeedbackGenerator.prepare() + } + + // Perform any setup necessary in order to update the view. + + // If an error is encountered, use NCUpdateResult.Failed + // If there's no update required, use NCUpdateResult.NoData + // If there's an update, use NCUpdateResult.NewData + + scan { completionHandler($0 ? .newData : .failed) } + } + + @available(iOSApplicationExtension 10.0, *) + func widgetActiveDisplayModeDidChange(_ activeDisplayMode: NCWidgetDisplayMode, withMaximumSize maxSize: CGSize) { + + log("☀️ Widget Display Mode changed \(activeDisplayMode.debugDescription) \(maxSize)") + + switch activeDisplayMode { + case .compact: + // The compact view is a fixed size. + preferredContentSize = maxSize + case .expanded: + // Dynamically calculate the height of the cells for the extended height. + let height = CGFloat(items.count) * LockTableViewCell.standardCellHeight + preferredContentSize = CGSize(width: maxSize.width, height: min(height, maxSize.height)) + @unknown default: + assertionFailure("Unexpected value \(activeDisplayMode.rawValue) for activeDisplayMode.") + } + } } // MARK: - Supporting Types @@ -285,11 +360,21 @@ extension TodayViewController { enum Item: Equatable { + case loading case noNearbyLocks case lock(UUID, LockCache) } } +// MARK: - Extensions + +internal extension LockTableViewCell { + + // Heights for the two styles of cell display. + static let todayCellHeight: CGFloat = 110 + static let standardCellHeight: CGFloat = 75 +} + @available(iOSApplicationExtension 10.0, *) extension NCWidgetDisplayMode { diff --git a/iOS/Watch Extension/Controller/InterfaceController.swift b/iOS/Watch Extension/Controller/InterfaceController.swift index 31165e29..b624611b 100644 --- a/iOS/Watch Extension/Controller/InterfaceController.swift +++ b/iOS/Watch Extension/Controller/InterfaceController.swift @@ -159,7 +159,6 @@ final class InterfaceController: WKInterfaceController { log("Selected lock \(item.identifier)") - donateUnlockIntent(for: item.identifier) unlock(lock: item.identifier, peripheral: item.peripheral) } diff --git a/iOS/Watch Extension/Controller/Unlock.swift b/iOS/Watch Extension/Controller/Unlock.swift index 49243e2c..cb87d3db 100644 --- a/iOS/Watch Extension/Controller/Unlock.swift +++ b/iOS/Watch Extension/Controller/Unlock.swift @@ -33,27 +33,3 @@ public extension ActivityInterface where Self: WKInterfaceController { } } -public extension WKInterfaceController { - - /// Donate Siri Shortcut to unlock the specified lock. - func donateUnlockIntent(for lock: UUID) { - - guard let lockCache = Store.shared[lock: lock] else { - assertionFailure("Invalid lock \(lock)") - return - } - - if #available(watchOS 5.0, *) { - let intent = UnlockIntent(identifier: lock, cache: lockCache) - let interaction = INInteraction(intent: intent, response: nil) - interaction.donate { error in - if let error = error { - log("⚠️ Donating intent failed with error \(error.localizedDescription)") - #if DEBUG - print(error) - #endif - } - } - } - } -} diff --git a/iOS/Watch/Info.plist b/iOS/Watch/Info.plist index 602edf49..4d5f25e1 100644 --- a/iOS/Watch/Info.plist +++ b/iOS/Watch/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Lock + Cerradura CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier